refactor(docs): strip residual Phase/Plan setup-history references

Final sweep for setup-process bookkeeping not caught by template-reset-v1.
ADRs drop Plan-N qualifiers; spec collapses the historical 11-phase
migration table; scaffolding guide drops "Phase added" column; comment
prefixes referencing R-numbers in test describes / eslint inline comments
are normalized. Architecture-level rule IDs (R40, R52, E0, J0, etc.) are
preserved where they serve as stable cross-references in ADRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 10:28:31 +02:00
parent 318dc05b6e
commit 2edc76002a
13 changed files with 155 additions and 137 deletions

View File

@@ -33,7 +33,7 @@ mock/real drift.
run against every implementation (Mock + Payload). Eliminates the
class of bug where the mock and the real impl drift apart.
5. **Tests in core-* packages and apps** — composition smoke tests
5. **Tests in core-\* packages and apps** — composition smoke tests
(appRouter, payloadConfig, bind-production, providers).
6. **Storybook test-runner** — every story executed as a smoke test.
@@ -60,7 +60,7 @@ mock/real drift.
- New package to maintain (small, mostly stable surface).
- Coverage thresholds may fail builds initially; we add tests to cross
threshold as part of Plan 7.
threshold as features land.
- Sequence shuffle may surface latent flakes; we fix as found.
- Templates for new features now require writing tests first; this is
by design.

View File

@@ -94,7 +94,7 @@ Clean Architecture pattern, with four intentional divergences (§Adaptations bel
| --------------- | ------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------ |
| DI library | `@evyweb/ioctopus` | `inversify` | Already integrated; equivalent expressive power via `.toDynamicValue()`. |
| DI scope | One global `ApplicationContainer` | One per feature (`authContainer`, `blogContainer`, …) | Vertical-feature isolation (ADR-008). |
| Test placement | `tests/unit/...` mirror | Colocated `*.test.{ts,tsx}` | Established by ADR-011 / Plan 7; clearer per-file ownership. |
| Test placement | `tests/unit/...` mirror | Colocated `*.test.{ts,tsx}` | Established by ADR-011; clearer per-file ownership. |
| Instrumentation | Sentry/observability service wrapping | Not adopted | Out of scope; revisit when observability becomes a requirement. |
`InputParseError` is also duplicated per feature (~6 lines × 5
@@ -161,7 +161,6 @@ beats DRY for a class this small.
## References
- Reference repo: https://github.com/nikolovlazar/nextjs-clean-architecture
- Prior ADRs: ADR-006 (vertical-feature-packages), ADR-008 (per-feature DI containers), ADR-011 (TDD foundation)
## Update — 2026-05-06

View File

@@ -94,16 +94,14 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
- 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).
- `void bus; void queue;` lines linger in feature binders that haven't yet wired any event/job (Phase 6 placeholder so `no-unused-vars` passes). Cosmetic; removed naturally as features adopt the system.
- `void bus; void queue;` lines linger in feature binders that haven't yet wired any event/job (placeholder so `no-unused-vars` passes). Cosmetic; removed naturally as features adopt the system.
## Notes from execution
## Implementation notes
- **Plan paused at the Phase 5/6 seam, not at a failure.** Phases 15 (additive — new packages, new lint rules, new anchor comments) finished cleanly. Phases 69 (invasive — every binder signature, every app's bindAll, generators, proof-of-life, docs) ran in a continuation session.
- **`InMemoryJobQueue.register()` was already in scope at Task 5** (`feat(core-shared/jobs): InMemoryJobQueue with register()`) so Task 34 Step 3's "add register" sub-step was already complete by the time bindAll-wiring landed.
- **Auth is username-based, not email-based.** The spec's example contract had `email`; this ADR's `userSignedUpEvent` schema does keep `email`, but `signUpUseCase` synthesizes `${username}@example.local` to satisfy the contract. The proof-of-life flows record this synthesized email — the realism of the address is incidental to the cross-feature plumbing being verified.
- **`apps/auth/src/di/module.ts` (the default-fallback DI module) gains `new InMemoryEventBus()` per `.toDynamicValue()` resolution.** Real cross-feature wiring runs through `bindProductionAuth` / `bindDevSeedAuth` where the bindAll-resolved bus is shared; the module's per-resolution bus is acceptable because the module is a default-mock fallback, not a runtime path.
- **`@repo/auth` and `@repo/marketing-pages` exports were extended** for the e2e test: `./di/container`, `./di/symbols`, plus `marketing-pages` exposes `./services/mailer` and `./services/recording-mailer`. Containers and symbols being public is consistent with the binders already being public.
- **Generator-level fixes folded in during Phase 8:** dropped publisher prompt's `when` clause (Plop `--args` cannot bypass conditional prompts); switched event-task template to `TaskConfig<{ input; output }>` shape (runtime slugs aren't keys of `TypedJobs['tasks']`); registered a custom Handlebars `eq` helper for the void/typed branch in `gen job`'s template.
- **Generator-level fixes:** dropped publisher prompt's `when` clause (Plop `--args` cannot bypass conditional prompts); switched event-task template to `TaskConfig<{ input; output }>` shape (runtime slugs aren't keys of `TypedJobs['tasks']`); registered a custom Handlebars `eq` helper for the void/typed branch in `gen job`'s template.
## Out of scope (deferred)

View File

@@ -25,7 +25,7 @@ The vendor-isolation principle from ADR-014 and ADR-015 carries over without mod
**3. Four auth checkpoints, pure function authorization.** The connect handler (gate 1) reads the session cookie, calls `IRealtimeAuthenticator.authenticate()`, and attaches `{ userId, roles } | null` to `socket.data.user`. Channel subscribe (gate 2) matches the requested name against registered descriptors (template-aware for `"notifications.user.{userId}"`-style channels), calls `authorize(descriptor, params, user)`, and on success calls `socket.join("ch:<name>")`. Inbound message (gate 3) re-validates schema and re-applies `authorize` as defense-in-depth, then invokes the wrapped handler with `ctx = { userId, roles }`. Broadcast (gate 4) has no gate — `io.to("ch:<name>").emit(...)` fans out to whoever cleared gate 2; subscribe is the single source of truth. `authorize` is a pure function with no DB hit.
**4. Hybrid bus-bridge / direct-broadcast model.** The existing `IEventBus` from ADR-015 is reused as a *third consumer* in a bridge pattern: a `bindRealtimeBridge(bus, broadcaster, allowlist)` step in `bindAll()` subscribes to allowlisted bus events and forwards them onto realtime channels. The bridge allowlist ships empty in v1; the first entries land with the dashboard PR. Direct broadcast (feature use case adds `realtime: IRealtimeBroadcaster` to its factory deps and calls `realtime.broadcast(channel, payload)`) is the primary path; the bridge is for cases where bus events already exist and realtime is additive.
**4. Hybrid bus-bridge / direct-broadcast model.** The existing `IEventBus` from ADR-015 is reused as a _third consumer_ in a bridge pattern: a `bindRealtimeBridge(bus, broadcaster, allowlist)` step in `bindAll()` subscribes to allowlisted bus events and forwards them onto realtime channels. The bridge allowlist ships empty in v1; the first entries land with the dashboard PR. Direct broadcast (feature use case adds `realtime: IRealtimeBroadcaster` to its factory deps and calls `realtime.broadcast(channel, payload)`) is the primary path; the bridge is for cases where bus events already exist and realtime is additive.
**5. Custom Node server for `apps/web-next`.** `apps/web-next/server.ts` replaces `next start` / `next dev` as the boot entry. Both Next.js and Socket.IO share one http server on port 3000. The `bindAll()` dispatcher gains two new resolution steps: `resolveRealtime()` (picks `InMemoryRealtimeBroadcaster` vs `SocketIORealtimeBroadcaster` by env) and the bridge wiring call. `bindAll(deps?)` is optional — callers may pass pre-constructed broadcaster/registry instances (the server does) or omit them and receive `InMemoryRealtimeBroadcaster` defaults (page-handler callers, existing tests). A `bound` guard ensures the Noop defaults are never silently accepted in production.
@@ -59,6 +59,7 @@ Handlers are wrapped in the same `withSpan(tracer, { op: "realtime-handler" }, w
## Consequences
**Positive:**
- Server-push to connected browser tabs without polling, across any feature on demand.
- Vendor-swappable: replacing Socket.IO means writing one new adapter pair (`IRealtimeBroadcaster` + `IRealtimeServer`).
- The existing `IEventBus` is reused as the bridge source; features that already publish events get realtime fan-out for free via one `allowlist` entry.
@@ -66,14 +67,15 @@ Handlers are wrapped in the same `withSpan(tracer, { op: "realtime-handler" }, w
- `RecordingRealtimeBroadcaster` in `core-testing` gives use-case tests a drop-in broadcaster that records calls, mirroring `RecordingEventBus`.
**Negative:**
- `bindProductionX` / `bindDevSeedX` now take seven arguments `(config, tracer, logger, bus, queue, realtime, realtimeRegistry)`. Future expansion may warrant collapsing to a single `BindContext` object; deferred.
- The custom Node server for `apps/web-next` means `next start` / `next dev` are no longer sufficient entry points. The CMS and TanStack apps still use their existing runtimes until they need realtime.
- `InMemoryRealtimeBroadcaster` has no room/socket model — it stores all broadcasts in a flat array. Sufficient for unit testing; insufficient for integration tests that assert specific sockets received a broadcast. The `realtime-ping` integration test uses a real `SocketIORealtimeServer` in-process.
## Notes from execution
- **`RecordingRealtimeBroadcaster` scope-type alias widened during Phase 6.** The spec's local type alias `RealtimeChannelDescriptor<TName, TSchema>` in `core-testing` was widened to handle the discriminated-union shape of the actual descriptor correctly. The recorded-broadcast entries use `{ channel: string; payload: unknown }` to avoid tying the recording type to the exact generic parameters.
- **`IRealtimeHandlerRegistry` gained `registerChannel` / `listChannels` during Phase 9.** The original spec had only `register` / `getInboundDescriptor` / `list`. `registerChannel` and `listChannels` were added to support outbound-only channel subscription: gate 2 (subscribe authorization) iterates `listChannels` + `register`ed descriptors independently of inbound handler registration, separating the "is this a known channel?" check from "does this channel have an inbound handler?"
- **`RecordingRealtimeBroadcaster` scope-type alias widened.** The spec's local type alias `RealtimeChannelDescriptor<TName, TSchema>` in `core-testing` was widened to handle the discriminated-union shape of the actual descriptor correctly. The recorded-broadcast entries use `{ channel: string; payload: unknown }` to avoid tying the recording type to the exact generic parameters.
- **`IRealtimeHandlerRegistry` gained `registerChannel` / `listChannels`.** The original spec had only `register` / `getInboundDescriptor` / `list`. `registerChannel` and `listChannels` were added to support outbound-only channel subscription: gate 2 (subscribe authorization) iterates `listChannels` + `register`ed descriptors independently of inbound handler registration, separating the "is this a known channel?" check from "does this channel have an inbound handler?"
- **`bindAll(deps?)` is optional with `InMemoryRealtimeBroadcaster` defaults.** Existing page-handler callers that invoke `bindAll()` with no args continue to work without modification. A `bound` guard ensures that in production, where `bindAll()` is always called from `server.ts` with explicit `SocketIORealtimeBroadcaster` / `RealtimeHandlerRegistry` args, the in-memory fallback is never silently wired.
## Out of scope (deferred)
@@ -93,7 +95,7 @@ Items surfaced by the final branch review that were intentionally not landed in
3. **`matchChannelTemplate` placeholders cannot contain dots** (`packages/core-realtime/src/channel-template.ts:14-17` uses `([^.]+)`). Fine for UUID-style identifiers; document the constraint in `defineRealtimeChannel`'s JSDoc when the first non-UUID key shape arrives.
4. **`SocketIORealtimeServer` swallows handler errors with bare `catch {}`** (`packages/core-realtime/src/socket-io-realtime-server.ts:108-117`). Wrapped handlers (`withCapture`) already record the error; unwrapped handlers lose it. Adding a server-injected logger that records "handler_error for channel X" would help debug connection-level issues — defer until a debugging incident actually motivates it.
5. **`bindAll(deps?: Partial<BindAllDeps>)` permits a half-populated deps object** that mixes a real broadcaster with a fresh registry (or vice versa). In practice no caller does this, but the type doesn't enforce all-or-nothing semantics. Tighten to `deps?: BindAllDeps` (full or absent) when the next consumer lands.
6. **AGENTS.md anchor count phrasing.** AGENTS.md says "three fixed `// <gen:realtime-*>` anchor comments per feature." There are three *kinds* but four placements (the handlers anchor lives in both `bind-production.ts` and `bind-dev-seed.ts`). Tighten to "three anchor kinds across both bind files" when the AGENTS.md is next touched.
6. **AGENTS.md anchor count phrasing.** AGENTS.md says "three fixed `// <gen:realtime-*>` anchor comments per feature." There are three _kinds_ but four placements (the handlers anchor lives in both `bind-production.ts` and `bind-dev-seed.ts`). Tighten to "three anchor kinds across both bind files" when the AGENTS.md is next touched.
## Related