docs(adr): rename ADR-012 — drop Lazar; update title + content + cross-refs

- Rename docs/decisions/adr-012-lazar-conformance.md → adr-012-feature-conventions.md
- Strip "Lazar", "Plan 8/9/10/11", "refactor-logs" refs from all ADRs,
  architecture docs, HTML explainers, and feature/core AGENTS.md files
- Update all incoming links in docs/, packages/*/AGENTS.md, HTML explainers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 10:07:37 +02:00
parent 06da37f723
commit 841655573b
18 changed files with 420 additions and 435 deletions

View File

@@ -12,7 +12,7 @@ core-events is scaffolded. `IJobQueue` (in `@repo/core-shared/jobs`) and the
## Context
Until this ADR the monorepo had no shared mechanism for *cross-feature* communication or *deferred* work. Two separate gaps:
Until this ADR the monorepo had no shared mechanism for _cross-feature_ communication or _deferred_ work. Two separate gaps:
1. **Cross-feature reactions** — when `auth` creates a user, `marketing-pages` wants to send a welcome email. Direct imports between feature packages are blocked by ESLint boundaries (R20). Without a bus, the only options were to merge the features or to leak a use-case import through `core-api`. Both compromise the vertical-slice property.
2. **Background jobs** — heavyweight side effects (email send, image processing, periodic cleanups) belong off the request path. The repo had no contract for "enqueue and run later." Payload's job system sits in `apps/cms` but feature packages had no abstraction over it.
@@ -27,7 +27,7 @@ The architecture's vendor-isolation principle (R40) — feature packages must no
- **E0 — Events are for cross-feature decoupling, not internal flow control.** In-feature reactions are direct use-case calls. The bus is for crossing feature boundaries.
- **E1 — Event contracts are public; handlers are private.** The publisher's `events/<x>.event.ts` is exported from the feature root barrel. The consumer's `events/handlers/on-<publisher>-<event>.handler.ts` is private to the consumer's `bind-*` files and never re-exported. Custom rule `core-eslint/rules/no-handler-reexport` blocks accidental exports.
- **J0 — Jobs are for *deferred* work, not abstraction.** Synchronous code stays synchronous. A job exists only when something must run off the request path (latency, retries, cron).
- **J0 — Jobs are for _deferred_ work, not abstraction.** Synchronous code stays synchronous. A job exists only when something must run off the request path (latency, retries, cron).
A second custom ESLint rule, `no-direct-payload-jobs`, blocks `payload.jobs.queue(...)` outside `core-shared/jobs/`. Feature packages enqueue through `IJobQueue` only.
@@ -72,7 +72,7 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
## Alternatives considered
- **Single package containing both interfaces.** Rejected — `PayloadJobsEventBus` depends on `IJobQueue`. If `IJobQueue` lived in `core-events`, every feature that uses *only* jobs (no events) would still pull `core-events` transitively. The split keeps the dependency graph minimal.
- **Single package containing both interfaces.** Rejected — `PayloadJobsEventBus` depends on `IJobQueue`. If `IJobQueue` lived in `core-events`, every feature that uses _only_ jobs (no events) would still pull `core-events` transitively. The split keeps the dependency graph minimal.
- **Synchronous in-process events without a queue layer.** Rejected for production — Payload's job system gives durability, retries, and observability for free; events that flow through it gain those properties at no extra cost.
- **Vendor-coupled events (e.g., direct `payload.jobs.queue`).** Rejected — would re-couple feature packages to Payload, violating R40's vendor-isolation principle.
- **Event contracts as ad-hoc TypeScript types instead of `EventDescriptor` + Zod.** Rejected — the descriptor's `name` field is the wire format the production bus uses to route to `__events.*` task slugs. Without a single source of truth, publisher and consumer can disagree at runtime. Zod gives runtime payload validation cheaply.
@@ -82,6 +82,7 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
## Consequences
**Positive:**
- Cross-feature event flows that span vertical slices without violating boundaries.
- Background work has a single contract (`IJobQueue`) that swaps from in-memory to Payload-durable per environment.
- Vendor-swappable: replacing Payload means writing one new `IJobQueue` adapter.
@@ -89,6 +90,7 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
- The proof-of-life flow (sign-up → welcome email) ships green in both dev-seed and production wiring; the dev-seed path is fully exercised by `apps/web-next/src/__tests__/sign-up-welcome-email.test.ts`.
**Negative:**
- Two queue implementations means dev-seed handlers register via `queue.register(slug, ...)` while production relies on Payload tasks resolving from the per-feature container. The dispatch story differs by environment; the abstraction hides it but it's a real surface.
- `InMemoryEventBus` is synchronous; `PayloadJobsEventBus` is asynchronous and at-least-once. Subscribers must be idempotent.
- Six anchor comments in every feature is more visual noise than the average reader expects. Mitigated by the CI guard (so they can't drift accidentally) and the generators (so contributors don't need to know they exist).
@@ -114,4 +116,3 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
- ADR-008 — per-feature DI containers
- ADR-010 — Turborepo boundaries
- ADR-014 — Instrumentation & Sentry logging
- Plan 10 spec — events-and-jobs-design.md