From 54958170ab9346c5df07be0e457bf419a712d7f5 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Sat, 9 May 2026 12:15:23 +0200 Subject: [PATCH] docs(plan): core-package generator + template slimming (8 phases, ~50 tasks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementation plan for the design at docs/superpowers/specs/2026-05-09-core- package-generator-design.md. Eight phases (numbered Phase 0..7 in the plan; Phase 0 is the read-first orientation, Phases 1-7 implement spec phases 0-5 plus a final cross-doc sweep). Phase 1 ships BindContext with bounded generics over protocol types in core- shared. Phase 2 ships the generator framework (entry, prompt, dispatch table — empty). Phases 3-6 each capture one optional package as a verbatim template, ship the generator action, and remove the package from main with byte-identical reconstruction verified via a snapshot. Phase 7 is the final cross-doc sweep (template-tiers.md, README updates, HTML explainer pass). Companion ADR will be assigned at implementation time (expected ADR-017). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-09-core-package-generator.md | 2316 +++++++++++++++++ 1 file changed, 2316 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-09-core-package-generator.md diff --git a/docs/superpowers/plans/2026-05-09-core-package-generator.md b/docs/superpowers/plans/2026-05-09-core-package-generator.md new file mode 100644 index 0000000..a445deb --- /dev/null +++ b/docs/superpowers/plans/2026-05-09-core-package-generator.md @@ -0,0 +1,2316 @@ +# Core-package generator + template slimming — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Slim the default monorepo template to a minimal kernel by removing four optional core packages (`core-realtime`, `core-events`, `core-trpc`, `core-ui`) from `main`, and add a `pnpm turbo gen core-package` generator that scaffolds any of them back byte-identical to the current shipped versions. + +**Architecture:** Six sequential phases. Phase 0 introduces a `BindContext` object in `core-shared` with bounded generics over minimal protocol types — feature binders take a single `ctx` arg, optional packages keep their full interfaces but `extends`-link to the protocols. Phase 1 ships the generator framework (entry, prompt, dispatch table — empty). Phases 2-5 each: capture a package's source as a verbatim template, ship the generator action that scaffolds the package + edits consuming-app config + patches `core-eslint/base.js` for any rules to re-add, then **remove** the package from `main`. Phase 6 is a final cross-doc sweep + an end-to-end "scaffold everything from slim main" acceptance test. + +**Tech Stack:** TypeScript, Node 22, plop (via `@turbo/gen`), Vitest, ESLint, Turborepo, Inversify, Next.js, Payload CMS, Vite (for `apps/web-tanstack`), Storybook. + +**Spec:** `docs/superpowers/specs/2026-05-09-core-package-generator-design.md` — read first, especially §4 (Phase 0 BindContext), §6 (per-package lifecycle), §6.2 (ESLint rule placement). + +**Phase numbering:** the spec numbers work phases 0-5 (BindContext, framework, realtime, events, trpc, ui). This plan keeps the realtime-plan convention of using "Phase 0 — Read first" as the orientation preamble, so plan phases shift up by one. Mapping: + +| Plan | Spec | +|---|---| +| Phase 0 (Read first) | (orientation; not in spec) | +| Phase 1 (BindContext refactor) | spec Phase 0 | +| Phase 2 (Generator framework) | spec Phase 1 | +| Phase 3 (core-realtime) | spec Phase 2 | +| Phase 4 (core-events) | spec Phase 3 | +| Phase 5 (core-trpc) | spec Phase 4 | +| Phase 6 (core-ui) | spec Phase 5 | +| Phase 7 (Final cross-doc sweep) | spec §3 trailing "+ a final cross-doc sweep" | + +--- + +## Phase 0 — Read first + +- [ ] **Step 1: Read the spec end-to-end** + +Open `docs/superpowers/specs/2026-05-09-core-package-generator-design.md`. The plan maps to its sections roughly 1:1. §4 (Phase 0 details), §6.1 (per-package variations table), §7 (testing), and §8 (doc/HTML updates) are load-bearing references. + +- [ ] **Step 2: Skim the existing generators** + +Read `turbo/generators/config.ts` (top to bottom; ~1000 lines). Note the existing patterns: `setGenerator` shape, `prompts`, `actions`, plop `add` / `modify` types, `assertAnchors` from `lib/anchor-validate.js`, the Handlebars helper `eq`. Phase 1 reuses all of these. + +- [ ] **Step 3: Read CLAUDE.md "Read first" docs** + +Confirm familiarity with: +- `AGENTS.md` — package map, boundary rules +- `docs/architecture/dependency-flow.md` — current binder shapes (will change in Phase 0) +- `docs/architecture/vertical-feature-spec.md` — feature package conventions +- ADR-008 (per-feature DI containers), ADR-014 (instrumentation), ADR-015 (events/jobs), ADR-016 (realtime) + +--- + +## Phase 1 — `BindContext` refactor + +**Goal:** Decouple feature binders from optional package interfaces by introducing a single `ctx` arg parameterized over minimal protocol types defined in `core-shared`. Optional package interfaces remain in their packages but `extends`-link to the protocols. + +**Files touched in this phase:** +- Create: `packages/core-shared/src/di/bind-protocols.ts` +- Create: `packages/core-shared/src/di/bind-protocols.test.ts` +- Create: `packages/core-shared/src/di/bind-context.ts` +- Create: `packages/core-shared/src/di/index.ts` +- Modify: `packages/core-shared/src/index.ts` (add `./di` export) +- Modify: `packages/core-shared/package.json` (add `./di` subpath export) +- Modify: `packages/core-events/src/event-bus.interface.ts` (extends EventBusProtocol) +- Modify: `packages/core-realtime/src/realtime-broadcaster.interface.ts` (extends RealtimeBroadcasterProtocol) +- Modify: `packages/core-realtime/src/realtime-handler-registry.ts` (extends RealtimeRegistryProtocol) +- Modify: All 5 feature `bind-production.ts` and `bind-dev-seed.ts` (10 files) +- Modify: `apps/web-next/src/server/bind-production.ts` (aggregator) +- Modify: `apps/web-next/src/server/bind-production.test.ts` +- Modify: `packages//src/di/bind-dev-seed.test.ts` (5 files) +- Modify: `docs/architecture/dependency-flow.md`, `docs/architecture/vertical-feature-spec.md`, `CLAUDE.md`, `AGENTS.md`, per-feature `AGENTS.md` +- Modify: `docs/architecture/di-explainer.html`, `docs/architecture/data-flow-explainer.html` + +### Task 1.1: Create protocol types in core-shared (TDD) + +**Files:** +- Create: `packages/core-shared/src/di/bind-protocols.ts` +- Create: `packages/core-shared/src/di/bind-protocols.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `packages/core-shared/src/di/bind-protocols.test.ts`: + +```ts +import { describe, it, expectTypeOf } from "vitest"; +import type { + EventBusProtocol, + RealtimeBroadcasterProtocol, + RealtimeRegistryProtocol, +} from "./bind-protocols"; + +describe("EventBusProtocol", () => { + it("requires publish(event, payload) and subscribe(event, consumer, handler)", () => { + type Bus = EventBusProtocol; + expectTypeOf().toBeFunction(); + expectTypeOf().toBeFunction(); + }); +}); + +describe("RealtimeBroadcasterProtocol", () => { + it("requires broadcast(channel, payload)", () => { + type Rt = RealtimeBroadcasterProtocol; + expectTypeOf().toBeFunction(); + }); +}); + +describe("RealtimeRegistryProtocol", () => { + it("requires register, registerChannel, listChannels", () => { + type Reg = RealtimeRegistryProtocol; + expectTypeOf().toBeFunction(); + expectTypeOf().toBeFunction(); + expectTypeOf().toBeFunction(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @repo/core-shared test src/di/bind-protocols.test.ts` +Expected: FAIL — Cannot find module `./bind-protocols` + +- [ ] **Step 3: Create the protocol types** + +Create `packages/core-shared/src/di/bind-protocols.ts`: + +```ts +/** + * Minimal protocol surfaces used by feature binders to interact with optional + * cross-cutting infrastructure (event bus, realtime broadcaster, realtime + * handler registry). Lives in `core-shared` so `BindContext` can reference + * these unconditionally — features depend on `core-shared`, never on the + * optional packages directly. + * + * The optional packages' full interfaces (`IEventBus`, `IRealtimeBroadcaster`, + * `IRealtimeHandlerRegistry`) `extends` these — typechecks fail if a refactor + * narrows the protocol surface in a way the full interface would lose. + */ + +export type EventBusProtocol = { + publish(event: { name: string }, payload: T): Promise; + subscribe( + event: { name: string }, + consumer: string, + handler: (payload: T) => Promise, + ): void; +}; + +export type RealtimeBroadcasterProtocol = { + broadcast( + channel: { name: string; key?: unknown }, + payload: T, + ): Promise; +}; + +export type RealtimeRegistryProtocol = { + register(entry: { descriptor: unknown; handler: unknown }): void; + registerChannel(descriptor: unknown): void; + listChannels(): unknown[]; +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @repo/core-shared test src/di/bind-protocols.test.ts` +Expected: PASS — 3 tests + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-shared/src/di/bind-protocols.ts packages/core-shared/src/di/bind-protocols.test.ts +git commit -m "feat(core-shared): bind-protocols (event bus / realtime / realtime registry)" +``` + +### Task 1.2: Create BindContext type + +**Files:** +- Create: `packages/core-shared/src/di/bind-context.ts` + +- [ ] **Step 1: Write `bind-context.ts`** + +Create `packages/core-shared/src/di/bind-context.ts`: + +```ts +import type { SanitizedConfig } from "payload"; +import type { ITracer, ILogger } from "../instrumentation"; +import type { IJobQueue } from "../jobs"; +import type { + EventBusProtocol, + RealtimeBroadcasterProtocol, + RealtimeRegistryProtocol, +} from "./bind-protocols"; + +/** Always-present fields. Feature binders rely on these unconditionally. */ +export type BindContextBase = { + tracer: ITracer; + logger: ILogger; +}; + +/** + * Optional cross-cutting deps. Generics let the app aggregator narrow the + * shape to full interfaces (`IEventBus`, `IRealtimeBroadcaster`, etc.); feature + * binders see only the protocol surface, which is enough for the methods they + * call. When an optional core package is absent the corresponding generic + * defaults to its protocol type, and `ctx.bus` / `ctx.realtime` are undefined + * at runtime. + */ +export type BindContext< + Bus extends EventBusProtocol = EventBusProtocol, + Realtime extends RealtimeBroadcasterProtocol = RealtimeBroadcasterProtocol, + RealtimeReg extends RealtimeRegistryProtocol = RealtimeRegistryProtocol, +> = BindContextBase & { + bus?: Bus; + queue?: IJobQueue; + realtime?: Realtime; + realtimeRegistry?: RealtimeReg; +}; + +/** Production binders also receive the resolved Payload config. */ +export type BindProductionContext< + Bus extends EventBusProtocol = EventBusProtocol, + Realtime extends RealtimeBroadcasterProtocol = RealtimeBroadcasterProtocol, + RealtimeReg extends RealtimeRegistryProtocol = RealtimeRegistryProtocol, +> = BindContext & { + config: SanitizedConfig; +}; +``` + +- [ ] **Step 2: Run typecheck to confirm** + +Run: `pnpm --filter @repo/core-shared typecheck` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add packages/core-shared/src/di/bind-context.ts +git commit -m "feat(core-shared): BindContext + BindProductionContext types" +``` + +### Task 1.3: Add `./di` subpath export + +**Files:** +- Create: `packages/core-shared/src/di/index.ts` +- Modify: `packages/core-shared/package.json` + +- [ ] **Step 1: Create the barrel** + +Create `packages/core-shared/src/di/index.ts`: + +```ts +export * from "./bind-protocols"; +export * from "./bind-context"; +``` + +- [ ] **Step 2: Add subpath export to package.json** + +In `packages/core-shared/package.json`, add to the `exports` object: + +```json +"./di": "./src/di/index.ts", +"./di/bind-protocols": "./src/di/bind-protocols.ts", +"./di/bind-context": "./src/di/bind-context.ts" +``` + +(Place these alphabetically among the existing `./jobs`, `./payload`, `./trpc/...` entries.) + +- [ ] **Step 3: Verify resolution** + +Run: `pnpm --filter @repo/core-shared typecheck` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add packages/core-shared/src/di/index.ts packages/core-shared/package.json +git commit -m "feat(core-shared): expose ./di subpath export" +``` + +### Task 1.4: Wire IEventBus to extend EventBusProtocol + +**Files:** +- Modify: `packages/core-events/src/event-bus.interface.ts` + +- [ ] **Step 1: Update interface** + +Read `packages/core-events/src/event-bus.interface.ts`. Add the `extends` clause: + +```ts +import type { EventBusProtocol } from "@repo/core-shared/di/bind-protocols"; +// ... existing imports ... + +export interface IEventBus extends EventBusProtocol { + // existing method declarations +} +``` + +- [ ] **Step 2: Verify** + +Run: `pnpm --filter @repo/core-events typecheck` +Expected: PASS — if it fails, IEventBus's existing method signatures don't structurally match the protocol; reconcile by either tightening the protocol (only if all impls already conform) or relaxing the IEventBus signatures (preferred — features should depend on the smaller surface). + +- [ ] **Step 3: Commit** + +```bash +git add packages/core-events/src/event-bus.interface.ts +git commit -m "feat(core-events): IEventBus extends EventBusProtocol" +``` + +### Task 1.5: Wire IRealtimeBroadcaster to extend protocol + +**Files:** +- Modify: `packages/core-realtime/src/realtime-broadcaster.interface.ts` + +- [ ] **Step 1: Update interface** + +Read `packages/core-realtime/src/realtime-broadcaster.interface.ts`. Add: + +```ts +import type { RealtimeBroadcasterProtocol } from "@repo/core-shared/di/bind-protocols"; + +export interface IRealtimeBroadcaster extends RealtimeBroadcasterProtocol { + // existing method declarations +} +``` + +- [ ] **Step 2: Verify** + +Run: `pnpm --filter @repo/core-realtime typecheck` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add packages/core-realtime/src/realtime-broadcaster.interface.ts +git commit -m "feat(core-realtime): IRealtimeBroadcaster extends RealtimeBroadcasterProtocol" +``` + +### Task 1.6: Wire IRealtimeHandlerRegistry to extend protocol + +**Files:** +- Modify: `packages/core-realtime/src/realtime-handler-registry.ts` + +- [ ] **Step 1: Update interface** + +Read `packages/core-realtime/src/realtime-handler-registry.ts`. Find the `interface IRealtimeHandlerRegistry` declaration. Add the `extends`: + +```ts +import type { RealtimeRegistryProtocol } from "@repo/core-shared/di/bind-protocols"; + +export interface IRealtimeHandlerRegistry extends RealtimeRegistryProtocol { + // existing method declarations +} +``` + +- [ ] **Step 2: Verify** + +Run: `pnpm --filter @repo/core-realtime typecheck` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add packages/core-realtime/src/realtime-handler-registry.ts +git commit -m "feat(core-realtime): IRealtimeHandlerRegistry extends RealtimeRegistryProtocol" +``` + +### Task 1.7: Migrate auth binders to ctx arg + +**Files:** +- Modify: `packages/auth/src/di/bind-production.ts` +- Modify: `packages/auth/src/di/bind-dev-seed.ts` + +- [ ] **Step 1: Read existing binder** + +Open `packages/auth/src/di/bind-production.ts`. Note the current 7-arg positional signature. + +- [ ] **Step 2: Refactor `bindProductionAuth` signature** + +Change from: + +```ts +export function bindProductionAuth( + config: SanitizedConfig, + tracer: ITracer, + logger: ILogger, + bus: IEventBus, + queue: IJobQueue, + realtime: IRealtimeBroadcaster, + realtimeRegistry: IRealtimeHandlerRegistry, +): void { + // body +} +``` + +To: + +```ts +import type { BindProductionContext } from "@repo/core-shared/di"; + +export function bindProductionAuth(ctx: BindProductionContext): void { + const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx; + // body unchanged otherwise + // optional usages become guarded: + bus?.subscribe(/* ... */); + realtimeRegistry?.register(/* ... */); +} +``` + +Drop the now-unused positional imports (`IEventBus`, `IRealtimeBroadcaster`, etc. used only for type signatures — keep imports if the body still references them inside guard blocks). + +- [ ] **Step 3: Refactor `bindDevSeedAuth` signature** + +Open `packages/auth/src/di/bind-dev-seed.ts`. Change from positional 6-arg (`tracer, logger, bus, queue, realtime, realtimeRegistry`) to: + +```ts +import type { BindContext } from "@repo/core-shared/di"; + +export async function bindDevSeedAuth(ctx: BindContext): Promise { + const { tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx; + // body unchanged; same guards as above +} +``` + +- [ ] **Step 4: Verify** + +Run: `pnpm --filter @repo/auth typecheck` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add packages/auth/src/di/bind-production.ts packages/auth/src/di/bind-dev-seed.ts +git commit -m "refactor(auth): bindProductionAuth + bindDevSeedAuth take BindContext arg" +``` + +### Task 1.8: Migrate blog binders + +**Files:** +- Modify: `packages/blog/src/di/bind-production.ts` +- Modify: `packages/blog/src/di/bind-dev-seed.ts` + +Apply the same pattern as Task 1.7. Read each file, convert the positional signature to single-arg `ctx: BindProductionContext` (production) / `ctx: BindContext` (dev-seed), destructure inside, guard optional usages. + +- [ ] **Step 1: Read + refactor `packages/blog/src/di/bind-production.ts`** + +Same pattern as Task 1.7 step 2. + +- [ ] **Step 2: Read + refactor `packages/blog/src/di/bind-dev-seed.ts`** + +Same pattern as Task 1.7 step 3. + +- [ ] **Step 3: Verify** + +Run: `pnpm --filter @repo/blog typecheck` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add packages/blog/src/di/bind-production.ts packages/blog/src/di/bind-dev-seed.ts +git commit -m "refactor(blog): binders take BindContext arg" +``` + +### Task 1.9: Migrate marketing-pages binders + +**Files:** +- Modify: `packages/marketing-pages/src/di/bind-production.ts` +- Modify: `packages/marketing-pages/src/di/bind-dev-seed.ts` + +Same pattern as Task 1.7. + +- [ ] **Step 1: Read + refactor both binder files** (same shape as Task 1.7) +- [ ] **Step 2: `pnpm --filter @repo/marketing-pages typecheck` → PASS** +- [ ] **Step 3: Commit** + +```bash +git add packages/marketing-pages/src/di/bind-production.ts packages/marketing-pages/src/di/bind-dev-seed.ts +git commit -m "refactor(marketing-pages): binders take BindContext arg" +``` + +### Task 1.10: Migrate navigation binders + +**Files:** +- Modify: `packages/navigation/src/di/bind-production.ts` +- Modify: `packages/navigation/src/di/bind-dev-seed.ts` + +Same pattern. + +- [ ] **Step 1: Read + refactor both binder files** +- [ ] **Step 2: `pnpm --filter @repo/navigation typecheck` → PASS** +- [ ] **Step 3: Commit** + +```bash +git add packages/navigation/src/di/bind-production.ts packages/navigation/src/di/bind-dev-seed.ts +git commit -m "refactor(navigation): binders take BindContext arg" +``` + +### Task 1.11: Migrate media binders + +**Files:** +- Modify: `packages/media/src/di/bind-production.ts` +- Modify: `packages/media/src/di/bind-dev-seed.ts` + +Same pattern. + +- [ ] **Step 1: Read + refactor both binder files** +- [ ] **Step 2: `pnpm --filter @repo/media typecheck` → PASS** +- [ ] **Step 3: Commit** + +```bash +git add packages/media/src/di/bind-production.ts packages/media/src/di/bind-dev-seed.ts +git commit -m "refactor(media): binders take BindContext arg" +``` + +### Task 1.12: Migrate web-next aggregator (`bind-production.ts`) + +**Files:** +- Modify: `apps/web-next/src/server/bind-production.ts` + +- [ ] **Step 1: Read aggregator end-to-end** + +Open `apps/web-next/src/server/bind-production.ts`. Note where each `bindProduction` and `bindDevSeed` is called. They currently get 7 (production) / 6 (dev-seed) positional args. + +- [ ] **Step 2: Build BindProductionContext once at the top of `bindAllProduction`** + +Replace the per-call positional args with a single `ctx`: + +```ts +import type { BindProductionContext } from "@repo/core-shared/di"; +import type { IEventBus } from "@repo/core-events"; +import type { + IRealtimeBroadcaster, + IRealtimeHandlerRegistry, +} from "@repo/core-realtime"; + +export async function bindAllProduction(deps: BindAllDeps): Promise { + if (bound) return; + bound = true; + const { tracer, logger } = resolveInstrumentation(); + const { bus, queue } = await resolveEventsAndJobsProduction(); + const resolvedConfig = await config; + const { realtime, realtimeRegistry } = deps; + + const ctx: BindProductionContext< + IEventBus, + IRealtimeBroadcaster, + IRealtimeHandlerRegistry + > = { + config: resolvedConfig, + tracer, + logger, + bus, + queue, + realtime, + realtimeRegistry, + }; + + bindProductionAuth(ctx); + bindProductionBlog(ctx); + bindProductionMarketingPages(ctx); + bindProductionNavigation(ctx); + bindProductionMedia(ctx); + maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger); + bindRealtimeBridge(bus, realtime); +} +``` + +- [ ] **Step 3: Same for `bindAllDevSeed`** + +```ts +export async function bindAllDevSeed(deps: BindAllDeps): Promise { + if (bound) return; + bound = true; + const { tracer, logger } = resolveInstrumentation(); + const { bus, queue } = resolveEventsAndJobsDevSeed(); + const { realtime, realtimeRegistry } = deps; + + const ctx: BindContext = { + tracer, + logger, + bus, + queue, + realtime, + realtimeRegistry, + }; + + await bindDevSeedAuth(ctx); + await bindDevSeedBlog(ctx); + await bindDevSeedMarketingPages(ctx); + await bindDevSeedNavigation(ctx); + await bindDevSeedMedia(ctx); + maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger); + bindRealtimeBridge(bus, realtime); +} +``` + +- [ ] **Step 4: Verify** + +Run: `pnpm --filter @repo/web-next typecheck` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add apps/web-next/src/server/bind-production.ts +git commit -m "refactor(web-next): aggregator builds BindContext once" +``` + +### Task 1.13: Update web-next bind-production tests + +**Files:** +- Modify: `apps/web-next/src/server/bind-production.test.ts` + +- [ ] **Step 1: Read existing tests** + +Open `apps/web-next/src/server/bind-production.test.ts`. The existing tests assert binders are called with positional args (e.g., `expect(args[3]).toBeInstanceOf(PayloadJobsEventBus)`). After Task 1.12, binders receive a single `ctx` arg. + +- [ ] **Step 2: Update assertions to ctx-shape** + +Replace positional-index assertions with property-based ones: + +```ts +// Was: +const args = vi.mocked(bindProductionAuth).mock.calls[0]!; +expect(args[3]).toBeInstanceOf(PayloadJobsEventBus); // bus +expect(args[4]).toBeInstanceOf(PayloadJobQueue); // queue + +// Now: +const ctx = vi.mocked(bindProductionAuth).mock.calls[0]![0]; +expect(ctx.bus).toBeInstanceOf(PayloadJobsEventBus); +expect(ctx.queue).toBeInstanceOf(PayloadJobQueue); +``` + +Update all such assertions throughout the file. The realtime-ping env-gate tests added in `f801345` continue to work as-is since they read from the registry, not from binder args. + +- [ ] **Step 3: Run tests** + +Run: `pnpm --filter @repo/web-next test src/server/bind-production.test.ts` +Expected: PASS — all tests (24 currently) + +- [ ] **Step 4: Commit** + +```bash +git add apps/web-next/src/server/bind-production.test.ts +git commit -m "test(web-next): bind-production tests assert ctx-shape arg" +``` + +### Task 1.14: Update per-feature bind-dev-seed tests + +**Files:** +- Modify: `packages/auth/src/di/bind-dev-seed.test.ts` +- Modify: `packages/blog/src/di/bind-dev-seed.test.ts` +- Modify: `packages/marketing-pages/src/di/bind-dev-seed.test.ts` +- Modify: `packages/navigation/src/di/bind-dev-seed.test.ts` +- Modify: `packages/media/src/di/bind-dev-seed.test.ts` + +- [ ] **Step 1: Read each file** + +For each, find calls to `bindDevSeed(...)` with positional args. + +- [ ] **Step 2: Convert each test to construct ctx** + +Replace patterns like: + +```ts +await bindDevSeedAuth(tracer, logger, bus, queue, realtime, realtimeRegistry); +``` + +With: + +```ts +await bindDevSeedAuth({ tracer, logger, bus, queue, realtime, realtimeRegistry }); +``` + +- [ ] **Step 3: Run all 5 feature test suites** + +```bash +pnpm --filter @repo/auth test +pnpm --filter @repo/blog test +pnpm --filter @repo/marketing-pages test +pnpm --filter @repo/navigation test +pnpm --filter @repo/media test +``` +Expected: All PASS. + +- [ ] **Step 4: Commit (one combined commit since they're mechanical)** + +```bash +git add packages/{auth,blog,marketing-pages,navigation,media}/src/di/bind-dev-seed.test.ts +git commit -m "test: bind-dev-seed tests pass ctx object across all 5 features" +``` + +### Task 1.15: Run all gates + +- [ ] **Step 1: Run lint, typecheck, test, boundaries from repo root** + +```bash +pnpm lint +pnpm typecheck +pnpm test +pnpm turbo boundaries +``` +Expected: All clean (lint may have pre-existing warnings about turbo.json env-vars — unchanged from before). + +If any feature's test fails, re-read its `bind-dev-seed.test.ts` against the latest binder shape and reconcile. + +- [ ] **Step 2: No commit yet** — Phase 0 commits are already in place; this is just the verification gate. + +### Task 1.16: Update Phase 0 docs + +**Files:** +- Modify: `docs/architecture/dependency-flow.md` +- Modify: `docs/architecture/vertical-feature-spec.md` +- Modify: `CLAUDE.md` +- Modify: `AGENTS.md` +- Modify: `packages//AGENTS.md` (5 files) + +- [ ] **Step 1: Update `docs/architecture/dependency-flow.md`** + +Find the section describing binder signatures. Replace any `bindProductionX(config, tracer, logger, bus, queue, realtime, realtimeRegistry)` example with the single-arg `bindProductionX(ctx: BindProductionContext)` form. Add a paragraph explaining: required fields (tracer, logger, config-when-production), optional fields (bus, queue, realtime, realtimeRegistry), why optional (corresponds to optional core packages). + +- [ ] **Step 2: Update `docs/architecture/vertical-feature-spec.md`** + +Same — replace positional binder examples with ctx-arg examples. + +- [ ] **Step 3: Update `CLAUDE.md` "Key Conventions" binder rules** + +Find the bullets mentioning `bindProductionX(config, tracer, logger, bus, queue, realtime, realtimeRegistry)`. Replace with the new single-arg shape. Add a new convention bullet: + +> **Binders take a `ctx` arg from `core-shared/di`.** Required: `tracer`, `logger`, plus `config` for production. Optional: `bus`, `queue`, `realtime`, `realtimeRegistry` (correspond to optional core packages — guard with `?.` when used). + +- [ ] **Step 4: Update root `AGENTS.md`** + +Find the binder convention reference; align with CLAUDE.md. + +- [ ] **Step 5: Update each feature's `AGENTS.md`** + +For each of `packages/{auth,blog,marketing-pages,navigation,media}/AGENTS.md`, find binder examples and update to the ctx form. + +- [ ] **Step 6: Commit** + +```bash +git add docs/architecture/dependency-flow.md docs/architecture/vertical-feature-spec.md CLAUDE.md AGENTS.md packages/*/AGENTS.md +git commit -m "docs: BindContext binder shape across architecture + per-feature AGENTS" +``` + +### Task 1.17: Update HTML explainers (Phase 0) + +**Files:** +- Modify: `docs/architecture/di-explainer.html` +- Modify: `docs/architecture/data-flow-explainer.html` + +- [ ] **Step 1: Open `di-explainer.html` in a browser** (or read the HTML directly) + +Find any visualization showing positional binder args (boxes labeled "config / tracer / logger / bus / queue / realtime / realtimeRegistry" feeding into a feature). Replace with a single "ctx" box that fans into the feature. + +- [ ] **Step 2: Update `data-flow-explainer.html`** + +Same — find binder-arg flows and consolidate into a single ctx flow. + +- [ ] **Step 3: Verify visually** + +Open each in a browser. Confirm the visualization is coherent (no orphaned arrows, labels match). + +- [ ] **Step 4: Commit** + +```bash +git add docs/architecture/di-explainer.html docs/architecture/data-flow-explainer.html +git commit -m "docs(html): di + data-flow explainers reflect BindContext" +``` + +### Task 1.18: Phase 0 verification gate + +- [ ] **Step 1: Final cross-cutting test sweep** + +```bash +pnpm lint +pnpm typecheck +pnpm test +pnpm turbo boundaries +``` +Expected: All green. Phase 0 is complete. + +--- + +## Phase 2 — Generator framework + +**Goal:** Ship the `pnpm turbo gen core-package` entry, prompt, and dispatch table — but no templates yet. Phase 3+ each register their template entry. + +**Files touched:** +- Modify: `turbo/generators/config.ts` +- Create: `turbo/generators/templates/core-package/.gitkeep` +- Create: `turbo/generators/lib/core-package-utils.ts` +- Create: `turbo/generators/lib/core-package-utils.test.ts` +- Modify: `turbo/generators/config.test.ts` (or create it if absent) +- Modify: `CLAUDE.md` (Quick Start), `AGENTS.md` +- Create: `docs/scaffolding/core-package-generator.md` + +### Task 2.1: Add the generator entry + dispatch table (TDD) + +**Files:** +- Modify: `turbo/generators/config.ts` +- Modify: `turbo/generators/config.test.ts` + +- [ ] **Step 1: Read existing config.test.ts** (if present) or skim other generator registrations. + +- [ ] **Step 2: Write the failing test for the registration shape** + +Add to `turbo/generators/config.test.ts` (create if absent — copy boilerplate from existing test files in the repo): + +```ts +import { describe, it, expect } from "vitest"; +import generator from "./config"; + +describe("core-package generator", () => { + it("is registered with an empty choices list initially", () => { + const captured: Array<{ name: string; def: unknown }> = []; + const plopMock = { + setHelper: () => {}, + setGenerator: (name: string, def: unknown) => captured.push({ name, def }), + } as unknown as Parameters[0]; + generator(plopMock); + const corePkg = captured.find((c) => c.name === "core-package"); + expect(corePkg).toBeDefined(); + const def = corePkg!.def as { prompts: { name: string; choices: unknown[] }[] }; + expect(def.prompts[0]!.name).toBe("name"); + expect(def.prompts[0]!.choices).toEqual([]); + }); +}); +``` + +- [ ] **Step 3: Run the test → FAIL** + +Run: `pnpm --filter ... test turbo/generators/config.test.ts` (use the workspace path that owns the generators package). +Expected: FAIL — `core-package` not registered. + +- [ ] **Step 4: Add the registration to `turbo/generators/config.ts`** + +After the existing `setGenerator("realtime", ...)` block, add: + +```ts +const CORE_PACKAGE_GENERATORS: Record< + string, + () => PlopTypes.ActionType[] +> = { + // Phases 3-6 each insert one entry here. +}; + +plop.setGenerator("core-package", { + description: "Scaffold an optional core package (realtime, events, trpc, ui)", + prompts: [ + { + type: "list", + name: "name", + message: "Which optional core package?", + choices: [], + }, + ], + actions: (answers) => { + const a = answers as { name: string }; + const handler = CORE_PACKAGE_GENERATORS[a.name]; + if (!handler) { + throw new Error(`No generator for core-package '${a.name}'`); + } + return handler(); + }, +}); +``` + +- [ ] **Step 5: Run the test → PASS** + +- [ ] **Step 6: Commit** + +```bash +git add turbo/generators/config.ts turbo/generators/config.test.ts +git commit -m "feat(generators): add core-package entry + dispatch table (empty)" +``` + +### Task 2.2: Create core-package-utils helper module + +**Files:** +- Create: `turbo/generators/lib/core-package-utils.ts` +- Create: `turbo/generators/lib/core-package-utils.test.ts` + +- [ ] **Step 1: Write tests for the utility functions** + +Create `turbo/generators/lib/core-package-utils.test.ts`: + +```ts +import { describe, it, expect } from "vitest"; +import { existsSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + assertOptionalPackageNotPresent, + addToTranspilePackages, +} from "./core-package-utils"; + +describe("assertOptionalPackageNotPresent", () => { + it("throws if packages// exists", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const pkgRoot = join(tmp, "packages", "core-foo"); + require("node:fs").mkdirSync(pkgRoot, { recursive: true }); + expect(() => assertOptionalPackageNotPresent("core-foo", tmp)).toThrow( + /already exists/, + ); + }); + + it("returns silently if absent", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + expect(() => assertOptionalPackageNotPresent("core-foo", tmp)).not.toThrow(); + }); +}); + +describe("addToTranspilePackages", () => { + it("inserts package name alphabetically into transpilePackages array", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const cfgPath = join(tmp, "next.config.mjs"); + writeFileSync( + cfgPath, + `const nextConfig = {\n transpilePackages: [\n "@repo/core-cms",\n "@repo/core-shared",\n ],\n};`, + ); + addToTranspilePackages(cfgPath, "@repo/core-realtime"); + const result = readFileSync(cfgPath, "utf8"); + expect(result).toMatch(/"@repo\/core-cms",\s*"@repo\/core-realtime",\s*"@repo\/core-shared"/); + }); + + it("is idempotent — duplicate insertion is a no-op", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const cfgPath = join(tmp, "next.config.mjs"); + writeFileSync( + cfgPath, + `const nextConfig = {\n transpilePackages: ["@repo/core-realtime"],\n};`, + ); + addToTranspilePackages(cfgPath, "@repo/core-realtime"); + const result = readFileSync(cfgPath, "utf8"); + expect(result.match(/@repo\/core-realtime/g)?.length).toBe(1); + }); +}); +``` + +- [ ] **Step 2: Run tests → FAIL** + +Run: `pnpm --filter test` +Expected: FAIL — module not found. + +- [ ] **Step 3: Implement `core-package-utils.ts`** + +```ts +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Throws if a core package directory already exists. Used as the first action + * in every per-package generator so re-running is safe. + */ +export function assertOptionalPackageNotPresent( + name: string, + cwd: string = process.cwd(), +): void { + const pkgRoot = join(cwd, "packages", name); + if (existsSync(pkgRoot)) { + throw new Error( + `packages/${name}/ already exists — refusing to scaffold (delete it first if intentional)`, + ); + } +} + +/** + * Inserts a package name into the transpilePackages array of a Next.js config + * file, preserving alphabetical order. Idempotent. + */ +export function addToTranspilePackages( + nextConfigPath: string, + pkgName: string, +): void { + const source = readFileSync(nextConfigPath, "utf8"); + if (source.includes(`"${pkgName}"`)) return; // idempotent + const updated = source.replace( + /(transpilePackages:\s*\[\s*)([\s\S]*?)(\s*\])/, + (_match, open: string, body: string, close: string) => { + const entries = body + .split(",") + .map((e) => e.trim()) + .filter(Boolean); + entries.push(`"${pkgName}"`); + entries.sort(); + const formatted = entries.map((e) => ` ${e}`).join(",\n"); + return `${open}\n${formatted},${close}`; + }, + ); + writeFileSync(nextConfigPath, updated); +} +``` + +- [ ] **Step 4: Run tests → PASS** + +- [ ] **Step 5: Commit** + +```bash +git add turbo/generators/lib/core-package-utils.ts turbo/generators/lib/core-package-utils.test.ts +git commit -m "feat(generators): core-package-utils (assertNotPresent + addToTranspilePackages)" +``` + +### Task 2.3: Create empty templates directory + scaffolding doc + +**Files:** +- Create: `turbo/generators/templates/core-package/.gitkeep` +- Create: `docs/scaffolding/core-package-generator.md` +- Modify: `CLAUDE.md` (Quick Start) +- Modify: `AGENTS.md` + +- [ ] **Step 1: Create the placeholder + doc** + +```bash +mkdir -p turbo/generators/templates/core-package +touch turbo/generators/templates/core-package/.gitkeep +``` + +Create `docs/scaffolding/core-package-generator.md`: + +```markdown +# Core-package generator + +`pnpm turbo gen core-package` scaffolds an optional core package back into +a slimmed template. Each name maps to a verbatim copy of the package as it +shipped at the time the generator was added. + +## Usage + +```bash +pnpm turbo gen core-package +# → Which optional core package? (use arrow keys) +# ❯ realtime +# events +# trpc +# ui +``` + +The generator emits the package files, updates consuming-app config (e.g. +`apps/web-next/next.config.mjs` `transpilePackages`), patches +`packages/core-eslint/base.js` to re-add any package-specific lint rules, +then prints the manual app/server wiring needed to bring the package fully +online. + +## Available templates + +| Name | Description | Phase added | +|---|---|---| +| `realtime` | Socket.IO realtime layer (ADR-016) | Phase 3 | +| `events` | Cross-feature event bus + Payload jobs adapter (ADR-015) | Phase 4 | +| `trpc` | tRPC server setup | Phase 5 | +| `ui` | Design-system package | Phase 6 | + +## Verifying an existing project + +If your project already has a core-* package and you want to verify the +generator's template hasn't drifted from the shipped source, use the +byte-identical reconstruction snapshot: + +```bash +git stash -u +pnpm turbo gen core-package +git diff packages/core-/ +# Expect: zero diff (modulo .hbs strip + trailing-newline normalization) +``` + +Snapshots live at `turbo/generators/__snapshots__/core-package/.snapshot.json`. +``` + +- [ ] **Step 2: Add Quick Start line in CLAUDE.md** + +In the Quick Start code block at the top of `CLAUDE.md`, add after the existing `pnpm turbo gen` lines: + +```bash +pnpm turbo gen core-package # Scaffold an optional core package (see docs/scaffolding/core-package-generator.md) +``` + +- [ ] **Step 3: Add to AGENTS.md** the `core-package` generator alongside the existing four (in whatever section enumerates available generators). + +- [ ] **Step 4: Commit** + +```bash +git add turbo/generators/templates/core-package/.gitkeep docs/scaffolding/core-package-generator.md CLAUDE.md AGENTS.md +git commit -m "docs: core-package generator reference + Quick Start entry" +``` + +### Task 2.4: Phase 2 verification gate + +- [ ] **Step 1: Run all gates** + +```bash +pnpm lint && pnpm typecheck && pnpm test && pnpm turbo boundaries +``` +Expected: green. + +- [ ] **Step 2: Smoke-test the generator (manual)** + +```bash +pnpm turbo gen core-package +# Pick "realtime" (or any name) +# Expected: error "No generator for core-package 'realtime'" +``` + +This is the desired state — phases 3-6 add the entries. + +--- + +## Phase 3 — core-realtime template + scaffold + remove + +**Goal:** Capture the current `packages/core-realtime/` source as a verbatim template, ship the generator action that scaffolds it back + re-adds ESLint rules, then remove core-realtime from `main` and verify byte-identical reconstruction. + +**Files touched (template capture):** +- Create: `turbo/generators/templates/core-package/realtime/**/*.hbs` (all files mirror `packages/core-realtime/**`) +- Create: `turbo/generators/__snapshots__/core-package/realtime.snapshot.json` +- Create: `turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.js.hbs` +- Create: `turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.test.js.hbs` +- Create: `turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.js.hbs` +- Create: `turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.test.js.hbs` + +**Files touched (generator wiring):** +- Modify: `turbo/generators/config.ts` (push to dispatch table + choices list + emit helpers) +- Modify: `turbo/generators/lib/core-package-utils.ts` (add `addBoundariesEntry`, `splicePluginRulesAt`, `splicePluginImportsAt`) + +**Files touched (anchors in core-eslint/base.js):** +- Modify: `packages/core-eslint/base.js` (add `// ` and `// ` anchor comments) +- Modify: `packages/core-eslint/anchors.test.js` (assert anchors present) + +**Files touched (slimming):** +- Delete: `packages/core-realtime/` (entire directory) +- Modify: `apps/web-next/next.config.mjs` (remove `@repo/core-realtime` from `transpilePackages`) +- Modify: `apps/web-next/server.ts` (strip realtime bootstrap) +- Modify: `apps/web-next/src/server/bind-production.ts` (drop realtime fields from ctx; remove `maybeRegisterRealtimePing`, `bindRealtimeBridge`) +- Modify: `apps/web-next/package.json` (remove `@repo/core-realtime` dep) +- Modify: `packages/core-eslint/base.js` (strip rule files imports + plugin block; anchors remain) +- Delete: `packages/core-eslint/rules/no-direct-socket-io.js` + `.test.js` +- Delete: `packages/core-eslint/rules/no-realtime-handler-reexport.js` + `.test.js` +- Modify: each feature's `package.json` (remove `@repo/core-realtime` dep) +- Modify: `apps/web-next/src/__tests__/realtime-ping.test.ts` (delete file — moves into the template) +- Modify: ADR-016 (add Status header) +- Modify: `docs/guides/realtime.md` (add prerequisite note) + +### Task 3.1: Add anchors to core-eslint/base.js (TDD) + +**Files:** +- Modify: `packages/core-eslint/base.js` +- Modify: `packages/core-eslint/anchors.test.js` + +- [ ] **Step 1: Add anchor assertion to `anchors.test.js`** + +Open `packages/core-eslint/anchors.test.js`. Add to its assertions: + +```js +expect(baseSource).toContain("// "); +expect(baseSource).toContain("// "); +``` + +- [ ] **Step 2: Run test → FAIL** + +Run: `pnpm --filter @repo/core-eslint test anchors.test.js` +Expected: FAIL — anchors not present. + +- [ ] **Step 3: Add the anchors to `base.js`** + +In `packages/core-eslint/base.js`: + +- Above the realtime rule imports (around line 7), add: `// ` +- Above the realtime rules block (around line 174), add: `// ` + +- [ ] **Step 4: Run test → PASS** + +- [ ] **Step 5: Commit** + +```bash +git add packages/core-eslint/base.js packages/core-eslint/anchors.test.js +git commit -m "feat(core-eslint): anchors for realtime rules splice" +``` + +### Task 3.2: Capture core-realtime as template files + +**Files:** ~22 `.hbs` files under `turbo/generators/templates/core-package/realtime/` + +- [ ] **Step 1: Mirror the package tree** + +```bash +mkdir -p turbo/generators/templates/core-package/realtime/src +cp -r packages/core-realtime/AGENTS.md turbo/generators/templates/core-package/realtime/AGENTS.md.hbs +cp -r packages/core-realtime/eslint.config.js turbo/generators/templates/core-package/realtime/eslint.config.js.hbs +cp -r packages/core-realtime/package.json turbo/generators/templates/core-package/realtime/package.json.hbs +cp -r packages/core-realtime/tsconfig.json turbo/generators/templates/core-package/realtime/tsconfig.json.hbs +cp -r packages/core-realtime/turbo.json turbo/generators/templates/core-package/realtime/turbo.json.hbs +cp -r packages/core-realtime/vitest.config.ts turbo/generators/templates/core-package/realtime/vitest.config.ts.hbs +# Then mirror src/* (each file gets a .hbs sibling) +for f in packages/core-realtime/src/*; do + base=$(basename "$f") + cp "$f" "turbo/generators/templates/core-package/realtime/src/$base.hbs" +done +``` + +- [ ] **Step 2: Verify the file list** + +```bash +find turbo/generators/templates/core-package/realtime -type f | sort +``` +Expected: 6 top-level `.hbs` + 22 `src/*.hbs` ≈ 28 files. Cross-check against `packages/core-realtime/` shape. + +- [ ] **Step 3: Commit** + +```bash +git add turbo/generators/templates/core-package/realtime +git commit -m "feat(generators): capture core-realtime as verbatim template files" +``` + +### Task 3.3: Capture realtime ESLint rule files as templates + +**Files:** 4 `.hbs` files under `turbo/generators/templates/core-package/realtime-eslint-rules/` + +- [ ] **Step 1: Copy rule files** + +```bash +mkdir -p turbo/generators/templates/core-package/realtime-eslint-rules +cp packages/core-eslint/rules/no-direct-socket-io.js turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.js.hbs +cp packages/core-eslint/rules/no-direct-socket-io.test.js turbo/generators/templates/core-package/realtime-eslint-rules/no-direct-socket-io.test.js.hbs +cp packages/core-eslint/rules/no-realtime-handler-reexport.js turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.js.hbs +cp packages/core-eslint/rules/no-realtime-handler-reexport.test.js turbo/generators/templates/core-package/realtime-eslint-rules/no-realtime-handler-reexport.test.js.hbs +``` + +- [ ] **Step 2: Commit** + +```bash +git add turbo/generators/templates/core-package/realtime-eslint-rules +git commit -m "feat(generators): capture realtime ESLint rules as template files" +``` + +### Task 3.4: Create the realtime byte-identical snapshot + +**Files:** +- Create: `turbo/generators/__snapshots__/core-package/realtime.snapshot.json` +- Create: `turbo/generators/lib/snapshot.ts` +- Create: `turbo/generators/lib/snapshot.test.ts` + +- [ ] **Step 1: Write the snapshot helper test** + +Create `turbo/generators/lib/snapshot.test.ts`: + +```ts +import { describe, it, expect } from "vitest"; +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { computeSnapshot } from "./snapshot"; + +describe("computeSnapshot", () => { + it("returns sorted file paths + sha256 hashes", () => { + const tmp = mkdtempSync(join(tmpdir(), "snapshot-")); + mkdirSync(join(tmp, "src")); + writeFileSync(join(tmp, "package.json"), `{ "name": "x" }\n`); + writeFileSync(join(tmp, "src", "index.ts"), `export {};\n`); + const snap = computeSnapshot(tmp); + expect(snap).toEqual([ + { path: "package.json", sha256: expect.any(String) }, + { path: "src/index.ts", sha256: expect.any(String) }, + ]); + }); +}); +``` + +- [ ] **Step 2: Run test → FAIL** + +- [ ] **Step 3: Implement `computeSnapshot`** + +Create `turbo/generators/lib/snapshot.ts`: + +```ts +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { join, relative } from "node:path"; + +export type SnapshotEntry = { path: string; sha256: string }; + +/** + * Recursively collect all files under root, sorted by relative path, with + * sha256 of post-normalized contents (LF line endings, single trailing + * newline). Used by the byte-identical reconstruction test. + */ +export function computeSnapshot(root: string): SnapshotEntry[] { + const out: SnapshotEntry[] = []; + walk(root, root, out); + out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)); + return out; +} + +function walk(root: string, dir: string, out: SnapshotEntry[]): void { + for (const name of readdirSync(dir)) { + const full = join(dir, name); + const stat = statSync(full); + if (stat.isDirectory()) { + if (name === "node_modules" || name === ".turbo") continue; + walk(root, full, out); + } else if (stat.isFile()) { + const raw = readFileSync(full, "utf8"); + const normalized = raw.replace(/\r\n/g, "\n").replace(/\n*$/, "\n"); + const sha = createHash("sha256").update(normalized).digest("hex"); + out.push({ path: relative(root, full).replace(/\\/g, "/"), sha256: sha }); + } + } +} +``` + +- [ ] **Step 4: Run test → PASS** + +- [ ] **Step 5: Generate the realtime snapshot from current source** + +```bash +mkdir -p turbo/generators/__snapshots__/core-package +pnpm exec tsx -e ' + import { computeSnapshot } from "./turbo/generators/lib/snapshot"; + import { writeFileSync } from "node:fs"; + const snap = computeSnapshot("./packages/core-realtime"); + writeFileSync( + "./turbo/generators/__snapshots__/core-package/realtime.snapshot.json", + JSON.stringify(snap, null, 2) + "\n", + ); +' +``` + +- [ ] **Step 6: Commit** + +```bash +git add turbo/generators/lib/snapshot.ts turbo/generators/lib/snapshot.test.ts turbo/generators/__snapshots__/core-package/realtime.snapshot.json +git commit -m "feat(generators): byte-identical snapshot machinery + realtime snapshot" +``` + +### Task 3.5: Add `splicePluginImportsAt` + `splicePluginRulesAt` helpers (TDD) + +**Files:** +- Modify: `turbo/generators/lib/core-package-utils.ts` +- Modify: `turbo/generators/lib/core-package-utils.test.ts` + +- [ ] **Step 1: Add tests** + +Append to `core-package-utils.test.ts`: + +```ts +describe("splicePluginRulesAt", () => { + it("inserts rule block at the named anchor", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const path = join(tmp, "base.js"); + writeFileSync( + path, + `before\n// \nafter\n`, + ); + splicePluginRulesAt(path, "realtime-rules", "INSERTED_BLOCK"); + const result = readFileSync(path, "utf8"); + expect(result).toContain("// \nINSERTED_BLOCK\nafter"); + }); + + it("is idempotent — re-inserting same block at anchor is a no-op", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const path = join(tmp, "base.js"); + writeFileSync( + path, + `// \nINSERTED_BLOCK\nrest\n`, + ); + splicePluginRulesAt(path, "realtime-rules", "INSERTED_BLOCK"); + const result = readFileSync(path, "utf8"); + expect(result.match(/INSERTED_BLOCK/g)?.length).toBe(1); + }); +}); +``` + +- [ ] **Step 2: Run → FAIL** (function not exported) + +- [ ] **Step 3: Implement** + +Append to `core-package-utils.ts`: + +```ts +/** + * Inserts a code block immediately after a `// ` anchor in a file. + * Idempotent: refuses to insert if the exact block already follows the anchor. + */ +export function splicePluginRulesAt( + filePath: string, + anchorName: string, + block: string, +): void { + const source = readFileSync(filePath, "utf8"); + const anchor = `// `; + const idx = source.indexOf(anchor); + if (idx === -1) { + throw new Error(`Anchor ${anchor} not found in ${filePath}`); + } + const after = source.slice(idx + anchor.length); + if (after.trimStart().startsWith(block.trim())) return; // idempotent + const updated = + source.slice(0, idx + anchor.length) + "\n" + block + after; + writeFileSync(filePath, updated); +} + +/** + * Inserts an import line immediately after the matching anchor. Idempotent. + */ +export function splicePluginImportsAt( + filePath: string, + anchorName: string, + importLine: string, +): void { + splicePluginRulesAt(filePath, anchorName, importLine); +} +``` + +- [ ] **Step 4: Run → PASS** + +- [ ] **Step 5: Commit** + +```bash +git add turbo/generators/lib/core-package-utils.ts turbo/generators/lib/core-package-utils.test.ts +git commit -m "feat(generators): splicePluginRulesAt + splicePluginImportsAt helpers" +``` + +### Task 3.5b: Add `addBoundariesEntry` and `emitTemplateTree` helpers (TDD) + +**Files:** +- Modify: `turbo/generators/lib/core-package-utils.ts` +- Modify: `turbo/generators/lib/core-package-utils.test.ts` + +- [ ] **Step 1: Write tests** + +Append to `core-package-utils.test.ts`: + +```ts +describe("addBoundariesEntry", () => { + it("inserts entry before the packages/core-* wildcard", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const path = join(tmp, "base.js"); + writeFileSync( + path, + `"boundaries/elements": [ + { type: "core-composition", pattern: "packages/core-cms" }, + { type: "core", pattern: "packages/core-*" }, + { type: "feature", pattern: "packages/!(core-*)" }, + ],`, + ); + addBoundariesEntry(path, "packages/core-realtime", { mode: "folder" }); + const result = readFileSync(path, "utf8"); + // The new entry must appear BEFORE the packages/core-* wildcard + const newIdx = result.indexOf(`pattern: "packages/core-realtime"`); + const wildcardIdx = result.indexOf(`pattern: "packages/core-*"`); + expect(newIdx).toBeGreaterThan(0); + expect(newIdx).toBeLessThan(wildcardIdx); + }); + + it("is idempotent — re-inserting same entry is a no-op", () => { + const tmp = mkdtempSync(join(tmpdir(), "core-pkg-")); + const path = join(tmp, "base.js"); + writeFileSync( + path, + `"boundaries/elements": [ + { type: "core", pattern: "packages/core-realtime", mode: "folder" }, + { type: "core", pattern: "packages/core-*" }, + ],`, + ); + addBoundariesEntry(path, "packages/core-realtime", { mode: "folder" }); + const result = readFileSync(path, "utf8"); + expect(result.match(/packages\/core-realtime/g)?.length).toBe(1); + }); +}); + +describe("emitTemplateTree", () => { + it("produces an `add` plop action per .hbs file in the template directory", () => { + // emitTemplateTree reads from turbo/generators/templates/ — the test uses a + // temp template directory injected via the `templatesRoot` arg. + const tmpTemplates = mkdtempSync(join(tmpdir(), "tpl-")); + mkdirSync(join(tmpTemplates, "core-package", "demo", "src"), { recursive: true }); + writeFileSync(join(tmpTemplates, "core-package", "demo", "package.json.hbs"), "{}"); + writeFileSync(join(tmpTemplates, "core-package", "demo", "src", "index.ts.hbs"), "export {};"); + const actions = emitTemplateTree("core-package/demo", "packages/demo", { templatesRoot: tmpTemplates }); + expect(actions).toHaveLength(2); + expect(actions[0]!.type).toBe("add"); + const paths = actions.map((a) => (a as { path: string }).path); + expect(paths).toContain("packages/demo/package.json"); + expect(paths).toContain("packages/demo/src/index.ts"); + }); +}); +``` + +- [ ] **Step 2: Run → FAIL** (helpers not exported) + +- [ ] **Step 3: Implement both helpers in `core-package-utils.ts`** + +```ts +import { readdirSync, statSync, mkdirSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; + +/** + * Inserts a `{ type, pattern, ...opts }` entry into the boundaries/elements + * array of `core-eslint/base.js`, placed immediately BEFORE the + * `packages/core-*` wildcard so its more-specific match wins. Idempotent. + */ +export function addBoundariesEntry( + baseJsPath: string, + packagePath: string, + opts: { mode?: "folder" } = {}, +): void { + const source = readFileSync(baseJsPath, "utf8"); + if (source.includes(`pattern: "${packagePath}"`)) return; // idempotent + const wildcardLine = source.match( + /(\s*\{\s*type:\s*"core",\s*pattern:\s*"packages\/core-\*"[^}]*\},)/, + ); + if (!wildcardLine) { + throw new Error(`packages/core-* wildcard not found in ${baseJsPath}`); + } + const modeFragment = opts.mode ? `, mode: "${opts.mode}"` : ""; + const newEntry = ` { type: "core", pattern: "${packagePath}"${modeFragment} },\n`; + const updated = source.replace(wildcardLine[0], `\n${newEntry}${wildcardLine[1]}`); + writeFileSync(baseJsPath, updated); +} + +/** + * Walks turbo/generators/templates// recursively. For each .hbs + * file, returns a plop `add` action that emits the file (without .hbs + * extension) at /. The actions are sorted so + * directory creation is deterministic. + */ +export function emitTemplateTree( + srcPrefix: string, + destPrefix: string, + opts: { templatesRoot?: string } = {}, +): PlopTypes.AddAction[] { + const root = + opts.templatesRoot ?? join(__dirname, "..", "templates"); + const srcRoot = join(root, srcPrefix); + const out: PlopTypes.AddAction[] = []; + walkHbs(srcRoot, srcRoot, srcPrefix, destPrefix, out); + out.sort((a, b) => a.path.localeCompare(b.path)); + return out; +} + +function walkHbs( + topRoot: string, + dir: string, + srcPrefix: string, + destPrefix: string, + out: PlopTypes.AddAction[], +): void { + for (const name of readdirSync(dir)) { + const full = join(dir, name); + if (statSync(full).isDirectory()) { + walkHbs(topRoot, full, srcPrefix, destPrefix, out); + continue; + } + if (!name.endsWith(".hbs")) continue; + const rel = relative(topRoot, full).replace(/\.hbs$/, ""); + out.push({ + type: "add", + path: join(destPrefix, rel).replace(/\\/g, "/"), + templateFile: join("templates", srcPrefix, relative(topRoot, full)), + } as PlopTypes.AddAction); + } +} +``` + +- [ ] **Step 4: Run → PASS** + +- [ ] **Step 5: Commit** + +```bash +git add turbo/generators/lib/core-package-utils.ts turbo/generators/lib/core-package-utils.test.ts +git commit -m "feat(generators): addBoundariesEntry + emitTemplateTree helpers" +``` + +### Task 3.6: Wire the realtime entry into the dispatch table + +**Files:** +- Modify: `turbo/generators/config.ts` +- Modify: `turbo/generators/config.test.ts` + +- [ ] **Step 1: Add an emit-test for the realtime action** + +Append to `turbo/generators/config.test.ts`: + +```ts +describe("core-package realtime", () => { + it("emits actions covering package files, transpilePackages, and ESLint rules", () => { + // Capture the actions returned by the realtime entry. + const captured: Array<{ name: string; def: PlopTypes.PlopGeneratorConfig }> = []; + const plop = { + setHelper: () => {}, + setGenerator: (n: string, d: unknown) => captured.push({ name: n, def: d as PlopTypes.PlopGeneratorConfig }), + } as unknown as PlopTypes.NodePlopAPI; + generator(plop); + const corePkg = captured.find((c) => c.name === "core-package")!.def; + const actions = (corePkg.actions as (a: { name: string }) => PlopTypes.ActionType[])( + { name: "realtime" }, + ); + // Expectations: at least one assertNotPresent guard, multiple `add` actions, and a transpilePackages action. + expect(actions.length).toBeGreaterThan(20); // 28 files + extras + }); +}); +``` + +- [ ] **Step 2: Run → FAIL** (no entry registered) + +- [ ] **Step 3: Implement the realtime entry** + +In `turbo/generators/config.ts`, populate `CORE_PACKAGE_GENERATORS["realtime"]`: + +```ts +import { + assertOptionalPackageNotPresent, + addToTranspilePackages, + splicePluginRulesAt, + splicePluginImportsAt, + addBoundariesEntry, + emitTemplateTree, +} from "./lib/core-package-utils.js"; + +const REALTIME_RULE_IMPORTS = `import noDirectSocketIO from "./rules/no-direct-socket-io.js"; +import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";`; + +const REALTIME_RULE_BLOCK = ` { + files: ["**/*.{ts,tsx,mjs,cjs,js}"], + plugins: { + "repo-rules": { + rules: { + "no-direct-socket-io": noDirectSocketIO, + "no-realtime-handler-reexport": noRealtimeHandlerReexport, + }, + }, + }, + rules: { + "repo-rules/no-direct-socket-io": "error", + "repo-rules/no-realtime-handler-reexport": "error", + }, + },`; + +CORE_PACKAGE_GENERATORS["realtime"] = () => [ + () => assertOptionalPackageNotPresent("core-realtime"), + ...emitTemplateTree("core-package/realtime", "packages/core-realtime"), + ...emitTemplateTree( + "core-package/realtime-eslint-rules", + "packages/core-eslint/rules", + ), + () => addToTranspilePackages("apps/web-next/next.config.mjs", "@repo/core-realtime"), + () => addBoundariesEntry("packages/core-realtime", { mode: "folder" }), + () => splicePluginImportsAt( + "packages/core-eslint/base.js", + "realtime-rules-imports", + REALTIME_RULE_IMPORTS, + ), + () => splicePluginRulesAt( + "packages/core-eslint/base.js", + "realtime-rules", + REALTIME_RULE_BLOCK, + ), + () => printRealtimeNextSteps(), +]; +``` + +Add the realtime name to the `choices` array in the prompt. + +`emitTemplateTree(srcPrefix, destPrefix)` is a helper that recursively expands all `.hbs` files in the template directory into plop `add` actions. Implement it in `core-package-utils.ts` if not already done: + +```ts +export function emitTemplateTree( + srcPrefix: string, + destPrefix: string, +): PlopTypes.AddAction[] { + // Walk turbo/generators/templates//, build an `add` action per .hbs file + // mapping to / + const out: PlopTypes.AddAction[] = []; + const root = join(__dirname, "..", "templates", srcPrefix); + // ... recursive walk, push plop add actions + return out; +} +``` + +(Implement the walk; mirror existing pattern in config.ts where templates are inlined.) + +`addBoundariesEntry(pkgPath, opts)` patches `packages/core-eslint/base.js` to add a boundaries entry before the `packages/core-*` wildcard. Implement similarly to `splicePluginRulesAt`. + +`printRealtimeNextSteps()` returns a custom plop action that prints the manual-wiring text. Mirror existing `printHandlerNextSteps` in config.ts. + +- [ ] **Step 4: Run → PASS** + +- [ ] **Step 5: Commit** + +```bash +git add turbo/generators/config.ts turbo/generators/config.test.ts turbo/generators/lib/core-package-utils.ts +git commit -m "feat(generators): wire realtime entry into core-package dispatch table" +``` + +### Task 3.7: Generator e2e test (scaffold core-realtime into a tmp workspace) + +**Files:** +- Create: `turbo/generators/__tests__/core-package-realtime.e2e.test.ts` + +- [ ] **Step 1: Write the e2e test** + +```ts +import { describe, it, expect } from "vitest"; +import { mkdtempSync, cpSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { execSync } from "node:child_process"; +import { join } from "node:path"; +import { computeSnapshot } from "../lib/snapshot"; +import expectedSnapshot from "../__snapshots__/core-package/realtime.snapshot.json"; + +describe("e2e: core-package realtime", () => { + it("byte-identical reconstruction matches snapshot", { timeout: 60_000 }, () => { + const tmp = mkdtempSync(join(tmpdir(), "e2e-")); + cpSync(process.cwd(), tmp, { + recursive: true, + filter: (src) => !src.includes("node_modules") && !src.includes(".turbo") && !src.includes("packages/core-realtime"), + }); + execSync(`cd ${tmp} && pnpm install`, { stdio: "ignore" }); + execSync(`cd ${tmp} && pnpm turbo gen core-package --args realtime`, { stdio: "ignore" }); + const result = computeSnapshot(join(tmp, "packages/core-realtime")); + expect(result).toEqual(expectedSnapshot); + }); +}); +``` + +- [ ] **Step 2: Run the test → PASS** (only after Phase 3.6 entry is wired) + +- [ ] **Step 3: Commit** + +```bash +git add turbo/generators/__tests__/core-package-realtime.e2e.test.ts +git commit -m "test(generators): e2e byte-identical reconstruction of core-realtime" +``` + +### Task 3.8: Remove core-realtime from main + +**Files:** see "Files touched (slimming)" list above. + +- [ ] **Step 1: Strip realtime from `apps/web-next/server.ts`** + +Open `apps/web-next/server.ts`. Remove all imports from `@repo/core-realtime`, `socket.io`, `socket.io-client`. Remove the `IOServer` construction, the authenticator, the `SocketIORealtimeServer.start()` call. The file collapses to a vanilla Next custom server (or replace entirely with `next start` if all custom-server logic was realtime-related). + +- [ ] **Step 2: Strip realtime from `apps/web-next/src/server/bind-production.ts`** + +Drop the `realtime`, `realtimeRegistry` fields from `BindAllDeps`. Remove `maybeRegisterRealtimePing` and `bindRealtimeBridge`. Update the `BindProductionContext<...>` generic args to drop the realtime params (revert to default protocol types or to `BindProductionContext`). + +- [ ] **Step 3: Strip dependency declarations** + +Edit each of these `package.json` files to remove the `"@repo/core-realtime": "workspace:*"` line: +- `apps/web-next/package.json` +- `packages/auth/package.json` +- `packages/blog/package.json` +- `packages/marketing-pages/package.json` +- `packages/navigation/package.json` +- `packages/media/package.json` + +- [ ] **Step 4: Strip transpile entry** + +In `apps/web-next/next.config.mjs`, remove the `"@repo/core-realtime"` line from `transpilePackages`. + +- [ ] **Step 5: Strip the boundaries entry** + +In `packages/core-eslint/base.js`, remove the line: +```js +{ type: "core", pattern: "packages/core-realtime", mode: "folder" }, +``` + +- [ ] **Step 6: Strip the realtime ESLint rule registration** + +In `packages/core-eslint/base.js`, between the anchors: +- After `// `: remove the two `import` lines. +- After `// `: remove the entire `repo-rules` plugin block. +- Anchors themselves remain. + +- [ ] **Step 7: Delete the rule files** + +```bash +rm packages/core-eslint/rules/no-direct-socket-io.js +rm packages/core-eslint/rules/no-direct-socket-io.test.js +rm packages/core-eslint/rules/no-realtime-handler-reexport.js +rm packages/core-eslint/rules/no-realtime-handler-reexport.test.js +``` + +- [ ] **Step 8: Delete the realtime e2e test (it lives in the template now)** + +```bash +rm apps/web-next/src/__tests__/realtime-ping.test.ts +``` + +- [ ] **Step 9: Delete the package** + +```bash +rm -rf packages/core-realtime +``` + +- [ ] **Step 10: Re-install + run gates** + +```bash +pnpm install # rewrites lockfile +pnpm lint && pnpm typecheck && pnpm test && pnpm turbo boundaries +``` +Expected: All green. The `IRealtimeBroadcaster` etc. type imports in feature binders went away (Phase 1 made them optional via protocol defaults). + +- [ ] **Step 11: Commit (large commit — slimming)** + +```bash +git add -A +git commit -m "refactor: remove core-realtime from main (scaffoldable via gen core-package realtime)" +``` + +### Task 3.9: Update Phase 3 docs + HTML + +**Files:** +- Modify: `docs/decisions/adr-016-realtime-layer.md` (Status header) +- Modify: `docs/guides/realtime.md` (prerequisite note) +- Modify: `docs/architecture/data-flow-explainer.html` (mark realtime as conditional) + +- [ ] **Step 1: Add Status header to ADR-016** + +At the top of `docs/decisions/adr-016-realtime-layer.md`, after the title, insert: + +```markdown +**Status:** Optional — scaffold via `pnpm turbo gen core-package realtime`. The package is not in the default template; this ADR documents the design that the generator emits. +``` + +- [ ] **Step 2: Add prerequisite note to realtime guide** + +At the top of `docs/guides/realtime.md`: + +```markdown +> **Prerequisite:** This guide assumes `@repo/core-realtime` is present. If you started from the slim template, run `pnpm turbo gen core-package realtime` first. +``` + +- [ ] **Step 3: Update `data-flow-explainer.html`** + +Find any solid arrows representing realtime flows. Convert to dashed lines + add an "optional" tag. (If the explainer doesn't have realtime arrows yet, no change needed.) + +- [ ] **Step 4: Commit** + +```bash +git add docs/decisions/adr-016-realtime-layer.md docs/guides/realtime.md docs/architecture/data-flow-explainer.html +git commit -m "docs: realtime now optional — Status headers + conditional HTML rendering" +``` + +### Task 3.10: Phase 3 verification gate + +- [ ] **Step 1: Final gate sweep** + +```bash +pnpm lint && pnpm typecheck && pnpm test && pnpm turbo boundaries +``` +Expected: green. + +- [ ] **Step 2: Run the e2e test directly** + +```bash +pnpm exec vitest run turbo/generators/__tests__/core-package-realtime.e2e.test.ts +``` +Expected: byte-identical reconstruction PASS. + +--- + +## Phase 4 — core-events template + scaffold + remove + +**Goal:** Same lifecycle as Phase 3 for `core-events`. Differences: events ESLint rules are inline `no-restricted-syntax` blocks in `base.js` (no separate rule files), and the boundaries entry is not needed (the wildcard catches it). + +### Task 4.1: Add events anchor to base.js (TDD) + +**Files:** +- Modify: `packages/core-eslint/base.js` (add `// ` anchor) +- Modify: `packages/core-eslint/anchors.test.js` (assert anchor) + +- [ ] **Step 1: Add anchor assertion → run → FAIL** +- [ ] **Step 2: Add anchor in base.js around the existing `no-restricted-syntax` block for E1 + J** +- [ ] **Step 3: Run → PASS** +- [ ] **Step 4: Commit** + +```bash +git add packages/core-eslint/base.js packages/core-eslint/anchors.test.js +git commit -m "feat(core-eslint): anchor for events rules splice" +``` + +### Task 4.2: Capture core-events as template files + +**Files:** `turbo/generators/templates/core-package/events/**/*.hbs` + +- [ ] **Step 1: Mirror the package tree** + +```bash +mkdir -p turbo/generators/templates/core-package/events/src +cp packages/core-events/{AGENTS.md,eslint.config.js,package.json,tsconfig.json,turbo.json,vitest.config.ts} turbo/generators/templates/core-package/events/ +# Rename to .hbs: +for f in turbo/generators/templates/core-package/events/*; do + [[ -f "$f" && "$f" != *.hbs ]] && mv "$f" "$f.hbs" +done +for f in packages/core-events/src/*; do + base=$(basename "$f") + cp "$f" "turbo/generators/templates/core-package/events/src/$base.hbs" +done +``` + +- [ ] **Step 2: Commit** + +```bash +git add turbo/generators/templates/core-package/events +git commit -m "feat(generators): capture core-events as verbatim template files" +``` + +### Task 4.3: Generate the events snapshot + +- [ ] **Step 1: Use the snapshot helper from Task 3.4** + +```bash +pnpm exec tsx -e ' + import { computeSnapshot } from "./turbo/generators/lib/snapshot"; + import { writeFileSync } from "node:fs"; + const snap = computeSnapshot("./packages/core-events"); + writeFileSync( + "./turbo/generators/__snapshots__/core-package/events.snapshot.json", + JSON.stringify(snap, null, 2) + "\n", + ); +' +``` + +- [ ] **Step 2: Commit** + +```bash +git add turbo/generators/__snapshots__/core-package/events.snapshot.json +git commit -m "feat(generators): events byte-identical snapshot" +``` + +### Task 4.4: Wire events entry + e2e test + +**Files:** +- Modify: `turbo/generators/config.ts` (push `events` entry; add to choices) +- Create: `turbo/generators/__tests__/core-package-events.e2e.test.ts` + +- [ ] **Step 1: Write the events emit test (in config.test.ts) + e2e test (separate file)** + +Mirror the realtime pattern from Tasks 3.6-3.7. The events entry is simpler: + +```ts +const EVENTS_RULE_BLOCK = ``; + +CORE_PACKAGE_GENERATORS["events"] = () => [ + () => assertOptionalPackageNotPresent("core-events"), + ...emitTemplateTree("core-package/events", "packages/core-events"), + () => addToTranspilePackages("apps/web-next/next.config.mjs", "@repo/core-events"), + () => splicePluginRulesAt( + "packages/core-eslint/base.js", + "events-rules", + EVENTS_RULE_BLOCK, + ), + () => printEventsNextSteps(), +]; +``` + +- [ ] **Step 2: Run tests → PASS** + +- [ ] **Step 3: Commit** + +```bash +git add turbo/generators/config.ts turbo/generators/config.test.ts turbo/generators/__tests__/core-package-events.e2e.test.ts +git commit -m "feat(generators): wire events entry + e2e test" +``` + +### Task 4.5: Remove core-events from main + +**Files (slimming):** +- Strip `bus`, `queue` from each feature's `package.json` deps if `core-events` is the source. Confirm: events brings `IEventBus`; if features only use it via `ctx.bus`, the dep was probably for type imports only and goes away when binders are protocol-driven. +- Delete `packages/core-events/` +- Strip `@repo/core-events` from `apps/web-next/next.config.mjs` `transpilePackages` +- Update `apps/web-next/src/server/bind-production.ts` — drop `bus, queue` construction, drop the `IEventBus` import + generic arg +- Strip the events `no-restricted-syntax` block from `core-eslint/base.js` (anchor remains) +- Update `bind-production.test.ts` if any test exercises `bus`/`queue` + +- [ ] **Step 1: Strip from app aggregator** +- [ ] **Step 2: Strip ESLint block** +- [ ] **Step 3: Strip dependencies from feature package.json files (search for `@repo/core-events`)** +- [ ] **Step 4: Strip from next.config.mjs transpilePackages** +- [ ] **Step 5: Delete `packages/core-events`** +- [ ] **Step 6: Update `bind-production.test.ts`** +- [ ] **Step 7: `pnpm install` + run all gates → green** +- [ ] **Step 8: Commit** + +```bash +git add -A +git commit -m "refactor: remove core-events from main (scaffoldable via gen core-package events)" +``` + +### Task 4.6: Update Phase 4 docs + HTML + +- [ ] **Step 1: ADR-015 Status header** +- [ ] **Step 2: `docs/guides/events-and-jobs.md` prerequisite note** +- [ ] **Step 3: `data-flow-explainer.html` events arrows → dashed/conditional** +- [ ] **Step 4: Commit** + +```bash +git add docs/decisions/adr-015-events-and-jobs.md docs/guides/events-and-jobs.md docs/architecture/data-flow-explainer.html +git commit -m "docs: events now optional — Status headers + conditional HTML rendering" +``` + +### Task 4.7: Phase 4 verification gate + +- [ ] **Step 1: Final gates green + e2e test passes** + +--- + +## Phase 5 — core-trpc template + scaffold + remove + +**Goal:** Same lifecycle for `core-trpc`. Differences: no ESLint rules (skip the rule splice steps); two apps (web-next + web-tanstack) need tRPC root mount removal. + +### Task 5.1: Capture core-trpc as template + +```bash +mkdir -p turbo/generators/templates/core-package/trpc/src +cp packages/core-trpc/{AGENTS.md,eslint.config.js,package.json,tsconfig.json,turbo.json,vitest.config.ts} turbo/generators/templates/core-package/trpc/ +for f in turbo/generators/templates/core-package/trpc/*; do + [[ -f "$f" && "$f" != *.hbs ]] && mv "$f" "$f.hbs" +done +for f in packages/core-trpc/src/*; do + base=$(basename "$f") + cp "$f" "turbo/generators/templates/core-package/trpc/src/$base.hbs" +done +git add turbo/generators/templates/core-package/trpc +git commit -m "feat(generators): capture core-trpc as verbatim template files" +``` + +### Task 5.2: Generate the trpc snapshot + +```bash +pnpm exec tsx -e ' + import { computeSnapshot } from "./turbo/generators/lib/snapshot"; + import { writeFileSync } from "node:fs"; + writeFileSync( + "./turbo/generators/__snapshots__/core-package/trpc.snapshot.json", + JSON.stringify(computeSnapshot("./packages/core-trpc"), null, 2) + "\n", + ); +' +git add turbo/generators/__snapshots__/core-package/trpc.snapshot.json +git commit -m "feat(generators): trpc byte-identical snapshot" +``` + +### Task 5.3: Wire trpc entry + e2e test + +```ts +CORE_PACKAGE_GENERATORS["trpc"] = () => [ + () => assertOptionalPackageNotPresent("core-trpc"), + ...emitTemplateTree("core-package/trpc", "packages/core-trpc"), + () => addToTranspilePackages("apps/web-next/next.config.mjs", "@repo/core-trpc"), + () => printTrpcNextSteps(), // includes manual app-mount instructions +]; +``` + +`printTrpcNextSteps()` prints copy-paste-ready instructions for both apps' tRPC root mounting (web-next: `app/api/trpc/[trpc]/route.ts`; web-tanstack: equivalent). + +- [ ] **Step 1: Implement entry + e2e test** +- [ ] **Step 2: Run e2e → PASS** +- [ ] **Step 3: Commit** + +```bash +git add turbo/generators/config.ts turbo/generators/__tests__/core-package-trpc.e2e.test.ts +git commit -m "feat(generators): wire trpc entry + e2e test" +``` + +### Task 5.4: Remove core-trpc from main + +- [ ] **Step 1: Strip tRPC root mount from `apps/web-next/app/api/trpc/[trpc]/route.ts` (delete file or convert to a stub)** +- [ ] **Step 2: Strip tRPC from `apps/web-tanstack/src/routes/__root.tsx`** + +The current `__root.tsx` wraps `` with ``. After removal, simplify to: + +```tsx +import { Outlet, createRootRoute } from "@tanstack/react-router"; + +export const Route = createRootRoute({ + component: () => , +}); +``` + +Also remove the `@repo/core-trpc/tanstack` import and any feature-router calls in `apps/web-tanstack/src/routes/index.tsx` that use tRPC. + +- [ ] **Step 3: Strip `@repo/core-trpc` from `apps/web-next/package.json` and `apps/web-tanstack/package.json` dependencies** +- [ ] **Step 4: Strip `@repo/core-trpc` from `apps/web-next/next.config.mjs` `transpilePackages`** (web-tanstack has no vite/next config to update) +- [ ] **Step 5: Delete `packages/core-trpc/`** +- [ ] **Step 6: `pnpm install` + run gates → green** +- [ ] **Step 7: Commit** + +```bash +git add -A +git commit -m "refactor: remove core-trpc from main (scaffoldable via gen core-package trpc)" +``` + +### Task 5.5: Phase 5 docs + +- [ ] **Step 1: Add prerequisite notes wherever feature `AGENTS.md` mentions tRPC integration** +- [ ] **Step 2: Mark tRPC layer as conditional in `data-flow-explainer.html`** +- [ ] **Step 3: Commit** + +```bash +git add packages/*/AGENTS.md docs/architecture/data-flow-explainer.html +git commit -m "docs: trpc now optional — prerequisite notes + conditional HTML" +``` + +### Task 5.6: Phase 5 verification gate + +- [ ] **Step 1: Gates green + e2e PASS** + +--- + +## Phase 6 — core-ui template + scaffold + remove + +**Goal:** Same lifecycle. Differences: third app (Storybook) consumes core-ui. + +### Task 6.1: Capture core-ui as template + +```bash +mkdir -p turbo/generators/templates/core-package/ui/src +# (mirror the structure — core-ui is a React-library shape; preserve src tree) +git add turbo/generators/templates/core-package/ui +git commit -m "feat(generators): capture core-ui as verbatim template files" +``` + +### Task 6.2: Generate the ui snapshot + wire entry + e2e test + +```ts +CORE_PACKAGE_GENERATORS["ui"] = () => [ + () => assertOptionalPackageNotPresent("core-ui"), + ...emitTemplateTree("core-package/ui", "packages/core-ui"), + () => addToTranspilePackages("apps/web-next/next.config.mjs", "@repo/core-ui"), + () => printUiNextSteps(), +]; +``` + +`apps/web-tanstack` currently has no `vite.config.ts` (TanStack Start dev/build scripts are placeholders). When Tanstack Start gains a real config in a future plan, the `printUiNextSteps()` block instructs the user to add `@repo/core-ui` to `optimizeDeps.include` (or equivalent) at that point. Storybook's `apps/storybook` consumption of core-ui is also handled via printed next-steps — registering stories in Storybook's main config is project-specific and resists templating. + +- [ ] **Step 1: Capture template + snapshot** +- [ ] **Step 2: Wire entry** +- [ ] **Step 3: e2e test** +- [ ] **Step 4: Run → PASS** +- [ ] **Step 5: Commit** + +```bash +git add turbo/generators/config.ts turbo/generators/templates/core-package/ui turbo/generators/__snapshots__/core-package/ui.snapshot.json turbo/generators/__tests__/core-package-ui.e2e.test.ts +git commit -m "feat(generators): wire ui entry + capture template + e2e test" +``` + +### Task 6.3: Remove core-ui from main + +- [ ] **Step 1: Strip `@repo/core-ui` import sites in apps + storybook** +- [ ] **Step 2: Strip Storybook stories list registrations** +- [ ] **Step 3: Strip from package.json deps in web-next, web-tanstack, storybook** +- [ ] **Step 4: Strip from next.config.mjs transpilePackages + vite.config.ts** +- [ ] **Step 5: Delete `packages/core-ui/`** +- [ ] **Step 6: `pnpm install` + gates → green** +- [ ] **Step 7: Commit** + +```bash +git add -A +git commit -m "refactor: remove core-ui from main (scaffoldable via gen core-package ui)" +``` + +### Task 6.4: Phase 6 docs + verification + +- [ ] **Step 1: Docs + HTML conditional rendering for ui layer** +- [ ] **Step 2: Gates green** +- [ ] **Step 3: Commit** + +```bash +git add packages/core-ui/README.md # if it survives +git commit -m "docs: ui now optional — prerequisite + conditional HTML" +``` + +--- + +## Phase 7 — Final cross-doc sweep + acceptance + +### Task 7.1: Add `docs/architecture/template-tiers.md` + +**Files:** +- Create: `docs/architecture/template-tiers.md` + +- [ ] **Step 1: Write the doc** + +Create with this content: + +```markdown +# Template tiers + +This template ships in three tiers: + +## Must-have (always present) + +- `@repo/core-shared` — protocol types, `BindContext`, instrumentation interfaces, jobs interface, tRPC primitives, payload helpers +- `@repo/core-eslint` — flat-config preset + repo-rules ESLint plugin + boundaries +- `@repo/core-typescript` — tsconfig presets (base, react-library, nextjs) +- `@repo/core-testing` — vitest helpers, factories, mocks, contracts +- `@repo/core-cms` — Payload config base +- `@repo/core-api` — top-level tRPC router root (mounts feature routers) + +Plus all 5 feature packages: auth, blog, marketing-pages, navigation, media. + +## Optional (scaffolded on demand) + +| Package | Generator | ADR | Guide | +|---|---|---|---| +| core-realtime | `pnpm turbo gen core-package realtime` | ADR-016 | docs/guides/realtime.md | +| core-events | `pnpm turbo gen core-package events` | ADR-015 | docs/guides/events-and-jobs.md | +| core-trpc | `pnpm turbo gen core-package trpc` | (none) | (none) | +| core-ui | `pnpm turbo gen core-package ui` | (none) | (none) | + +## Why optional + +Each optional package addresses a specific need (realtime delivery, cross-feature events, tRPC, design system). Projects that don't need them get a slimmer template. The generator emits byte-identical copies of the packages as they shipped — see `turbo/generators/__snapshots__/core-package/.snapshot.json` for the canonical content hashes. +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/architecture/template-tiers.md +git commit -m "docs: template-tiers reference (must-have + optional + generator commands)" +``` + +### Task 7.2: Update README.md (project root) + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Add an "Optional packages" section near the Quick Start** + +```markdown +## Optional packages + +The default template includes the must-have core packages and all 5 feature packages. Four core packages are optional and scaffold on demand: + +```bash +pnpm turbo gen core-package realtime # Socket.IO realtime layer (ADR-016) +pnpm turbo gen core-package events # Cross-feature events + Payload jobs (ADR-015) +pnpm turbo gen core-package trpc # tRPC server setup +pnpm turbo gen core-package ui # Design system +``` + +See `docs/architecture/template-tiers.md` for the full tier list. +``` + +- [ ] **Step 2: Commit** + +```bash +git add README.md +git commit -m "docs(readme): Optional packages section + scaffold commands" +``` + +### Task 7.3: HTML explainers final review + +- [ ] **Step 1: Open `docs/architecture/di-explainer.html` and `data-flow-explainer.html`** + +Verify all 4 optional packages have consistent dashed/conditional treatment. Any solid arrow into a removed package layer is a bug — fix. + +- [ ] **Step 2: Verify legend explains the conditional/dashed convention** + +Add a legend if missing. + +- [ ] **Step 3: Commit any fixes** + +```bash +git add docs/architecture/*.html +git commit -m "docs(html): final pass on conditional rendering for optional packages" +``` + +### Task 7.4: Re-check all "Read first" lists + +- [ ] **Step 1: `grep -rn "Read first" CLAUDE.md AGENTS.md packages/*/AGENTS.md`** + +Confirm every link still resolves (no references to deleted package READMEs that would 404). + +- [ ] **Step 2: Fix any broken refs** + +- [ ] **Step 3: Commit if changes** + +```bash +git add CLAUDE.md AGENTS.md packages/*/AGENTS.md +git commit -m "docs: refresh Read first lists after optional-package extraction" +``` + +### Task 7.5: End-to-end acceptance test (manual) + +- [ ] **Step 1: Run from a clean clone of slim main** + +```bash +git clone /tmp/test-project +cd /tmp/test-project +pnpm install +pnpm turbo gen core-package realtime +pnpm turbo gen core-package events +pnpm turbo gen core-package trpc +pnpm turbo gen core-package ui +pnpm install +pnpm lint && pnpm typecheck && pnpm test && pnpm turbo boundaries +``` +Expected: all green; resulting tree byte-identical to pre-slim main (modulo lockfile order). + +- [ ] **Step 2: Commit summary memory note (project memory) describing the verified end-state** + +(No commit to repo — write to user memory if desired.) + +### Task 7.6: Final verification gate + +- [ ] **Step 1: All gates green** +- [ ] **Step 2: All e2e tests passing** +- [ ] **Step 3: Plan complete** + +--- + +## Notes for the executing agent + +- Phases 0-7 are sequenced. Don't start Phase 2 until Phase 1's gates are green; don't start Phase 3 until Phase 2 is in place. +- Phases 3-6 follow identical lifecycles. After completing Phase 3, the pattern is established — phases 4-6 should move faster. +- The "byte-identical reconstruction" promise depends on snapshots being captured at the moment the templates are extracted. If any other commit lands between Task 3.4 (snapshot capture) and Task 3.7 (e2e test) that touches `packages/core-realtime/`, regenerate the snapshot before proceeding. +- ESLint rules: realtime has separate rule files; events has inline `no-restricted-syntax` blocks. Trpc and ui have neither. The phase-specific tasks call out what to do. +- Doc updates per phase are non-negotiable. The HTML explainers in particular get tweaked on every phase — keep them honest. +- Use `pnpm turbo gen core-package` from the repo root (or any subdirectory — turbo finds the generators). +- The `printNextSteps` print actions for each package are content-heavy. Mirror the existing `printHandlerNextSteps` pattern in `turbo/generators/config.ts` for output formatting.