@trpc/client resolved to 11.16.0 in some packages while @trpc/server
resolved to 11.17.0 in others, tripping the exact-version peer check.
Update every @trpc/* range in-range so the whole workspace resolves to
one version (11.18.0); install is peer-warning-free again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
core-trpc imported AppRouter (type-only) from core-api, which depends
on the features, which depend on core-trpc — a cycle that killed every
turbo graph walk (lint/build/CI) and an illegal core -> core-composition
boundary edge. The tRPC context is now router-agnostic (AnyTRPCRouter);
useTRPC takes the router type as a generic and each feature exports a
type-only app-slice (e.g. BlogAppSlice) mirroring its mount key, so UI
hooks keep full procedure typing without touching the composition layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getPageBySlug returned undefined while the feature already mapped
PageNotFoundError to NOT_FOUND and blog throws for missing slugs (B11).
Unify on the throwing contract: use case throws, controller narrows to
a non-optional presenter, the server component catches the domain error
to render its not-found state, and the router now surfaces NOT_FOUND.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm audit --prod --audit-level=critical (the new CI gate) failed on
vitest <3.2.6 (critical, arbitrary file read/execute via the UI
server). Bump vitest + @vitest/coverage-v8 in-range across the
workspace and add @vitest/coverage-v8 to every package that runs
vitest but lacked it (core-audit, core-cms, core-eslint, core-testing,
core-trpc, apps/cms, web-next, web-tanstack, turbo/generators) so
'pnpm test -- --coverage' works in every package. No compound test
scripts exist, so the vitest-last pass-through concern does not apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server components (.server.tsx) resolve controllers from DI, prefetch
data, and wrap client components in HydrationBoundary. Client components
(.client.tsx) use hooks for hydration + background refetch. Barrel
exports server components under clean names — consumers never see the
server/client split.
Webpack (Next.js transpilePackages) resolves from .ts source directly
and cannot find .js files. The bind-production binders already used
extensionless imports; align bind-dev-seed to match. Also fixes the
turbo gen feature template so new features are consistent.
Each feature's bind-production.ts already ends with
assertFeatureConformance(), but that check only fired on `pnpm dev`.
Adds a smoke test per feature that invokes the binder with a Noop ctx,
so missing __instrumented / __captured / __audited brands now fail
under `pnpm test` (and therefore in CI), closing the gap between the
TypeScript brand layer and the boot-time runtime assertion.
Closes the user's ask: versioning + a changelog generated on merging
to main, building on the just-mandated Conventional Commits substrate
(CLAUDE.md Key Conventions).
Architecture: ADR-021. Cookbook: docs/guides/releasing.md.
Initial state — six tracked packages at v0.1.0:
- . -> template-vertical (tag: template-v...)
- packages/auth -> @repo/auth (tag: auth-v...)
- packages/blog -> @repo/blog (tag: blog-v...)
- packages/media -> @repo/media (tag: media-v...)
- packages/marketing-pages -> @repo/marketing-pages (tag: marketing-pages-v...)
- packages/navigation -> @repo/navigation (tag: navigation-v...)
Core packages, tooling, and apps are NOT independently versioned
(ADR-021 rationale: core bumps cascade; apps aren't consumables;
surfacing them would create noise without information).
Configuration:
- release-please-config.json - 6 tracked packages, hybrid scope,
pre-1.0 conservative bump policy
(feat: -> patch, feat!: -> minor),
conventional-commit type mapping
- .release-please-manifest.json - baseline 0.1.0 for all 6 packages
- .github/workflows/release-please.yml - googleapis/release-please-
action@v4 on push to main,
concurrency-gated, write
permissions for the rolling PR
Workflow: on every push to main, release-please scans commits since
the last release tag PER PACKAGE (using commit-path, not the
conventional-commit scope), updates a single rolling release PR with
version bumps + per-package CHANGELOG entries. Merging that PR cuts
per-package tags + GitHub releases.
CHANGELOG files seeded at v0.1.0 baseline:
- CHANGELOG.md (root)
- packages/<feature>/CHANGELOG.md (5 features)
Subsequent versions are appended by release-please from commit
history. Do not edit manually.
Visibility surfaces updated (every agent entry point):
- CLAUDE.md Read First + new "Versioning is hybrid" Key Conventions
bullet (with bump policy summary)
- AGENTS.md preamble - new "Releases:" callout alongside Commits
- docs/glossary.md - new Releasing section with 8 terms (Conventional
Commits, release-please, Hybrid versioning, Tag prefix, Rolling
release PR, Bump targeting, Pre-1.0 bump policy, Release-As trailer,
CHANGELOG.md)
- docs/README.md - guides tree updated with releasing.md
- .claude/hooks/session-start.sh - one-line release reminder
- .claude/hooks/prompt-context.sh - new keyword group for
release/version/bump/semver/tag prompts
Package.json version bumps:
- root: name "template" -> "template-vertical", version "0.1.0"
- packages/auth, blog, media, marketing-pages, navigation: "0.0.0" -> "0.1.0"
Root rename rationale: release-please tags use the package-name + the
component prefix; "template-vertical" matches the repo identity (and
the user's question preview).
First release-please PR after this lands will sweep all subsequent
post-baseline commits into 0.1.1 / 0.2.0 bumps as appropriate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the per-layer threshold gaps surfaced by the 2026-05-13 PRD
audit. After this commit all five features pass their declared
100%/100%/95%/100% bands on entities + use-cases + controllers.
media (was: missing @vitest/coverage-v8 + missing vitest config block +
one controller at 86.66% lines / 75% branches)
- Added @vitest/coverage-v8 dev dep
- Applied the standard helper-driven vitest config
- Declared the coverage section in feature.manifest.ts
- Added 2 tests to list-media.controller.test.ts covering the
InputParseError branch (unknown fields + invalid limit)
- Now: 16 files / 80 tests / 97.12% / controllers 100%
marketing-pages (was: get-site-settings.controller at 93.54% lines /
90.9% branches)
- Added 1 test to get-site-settings.controller.test.ts covering the
InputParseError branch on unknown fields
- Now: 22 files / 68 tests / 95.66% / controllers 100%
navigation (was: entities/errors/common.ts at 50% function hits +
get-header.controller at 86.66% lines / 80% branches)
- Root cause: InputParseError class never instantiated in any test
- Added 2 tests to get-header.controller.test.ts covering the
InputParseError branch + verifying the Zod cause is preserved.
One test exercises both gap files at once (controller throws,
InputParseError class is constructed).
- Wired navigation/vitest.config.ts through the shared helper
- Declared the coverage section in feature.manifest.ts
- Now: 11 files / 45 tests / 98.04% / entities + controllers 100%
All 5 features now drive thresholds from the manifest via the helper.
The duplication problem the keystone eliminates is gone.
Repo-wide via `pnpm coverage:aggregate`:
- statements 95.87% (lh 2994 / lf 3123)
- branches 88.91% (brh 433 / brf 487)
- functions 100% (fnh 142 / fnf 142)
- lines 95.87%
`pnpm coverage:diff -- --base HEAD~1` reports status: pass.
coverage/summary.json refreshed in the same commit so the trend
captures the post-unification state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates blog and marketing-pages to the same pattern auth landed in
f7baa8b: vitest config consumes vitestThresholdsFromBands(
DEFAULT_COVERAGE_BANDS) instead of the duplicated hand-written
thresholds block, and each manifest declares its coverage section
explicitly.
Verified semantically identical to the previous hand-written
thresholds — the same numbers come out the other end of the helper.
No new regressions:
- blog: 89 tests, 96.33% overall, all bands green
- marketing-pages: 67 tests, 95.28% overall — controllers/ shows a
real 93.54% lines / 90.9% branches gap that has been there since
before this refactor (the previous hand-written threshold was the
same 100%/95%). This is one of the L0 unification work items
listed in the PRD's findings; capturing here as the third feature
with real test gaps (navigation, media, marketing-pages).
Three of five features now drive their vitest thresholds from the
manifest helper: auth ✓, blog ✓, marketing-pages ✓. Navigation and
media stay on the legacy config until L0 unification closes their
test gaps (touching them would expose the same failures and add
nothing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename docs/decisions/adr-012-lazar-conformance.md → adr-012-feature-conventions.md
- Strip "Lazar", "Plan 8/9/10/11", "refactor-logs" refs from all ADRs,
architecture docs, HTML explainers, and feature/core AGENTS.md files
- Update all incoming links in docs/, packages/*/AGENTS.md, HTML explainers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AGENTS.md bind-production code block: shows the slim default state
(no @repo/core-events / @repo/core-realtime imports, BindProductionContext
with no generic args) with a comment pointing to the scaffold workflow.
The previous block showed a fully-wired post-scaffold state without
signaling that none of those packages exist in main.
- bind-protocols.test.ts: top-of-file comment clarifies what these tests
actually verify (protocol shapes have required methods) vs what the
spec text might suggest (full assignability of optional packages'
interfaces — that's verified by the e2e reconstruction tests, not here).
- core-package-generator.md: drops two stale "Until Phases 3-6 land"
parentheticals — the phases shipped.
- config.test.ts: extends the choices assertion to cover all 4 names
(realtime, events, trpc, ui).
- marketing-pages bind-* comments: reverse the inverted optional/required
language. queue (IJobQueue) is from core-shared and always present;
bus is the optional one (from @repo/core-events when scaffolded).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- di-explainer.html: update stale JS data strings from positional-arg
form (bindAllProduction(config), bindProductionBlog(config,tracer,...))
to ctx-arg form (bindAllProduction(deps), bindProductionBlog(ctx)) with
a note on ctx shape.
- auth/sign-up.use-case: change bus param from IEventBus to
EventBusProtocol|undefined; guard bus.publish with if(bus) so the use
case is safe when core-events is absent (Phase 3+).
- auth/bind-production + bind-dev-seed: drop as IEventBus cast and unused
IEventBus import; ctx.bus is now passed directly (typed as
EventBusProtocol|undefined).
- marketing-pages/bind-production + bind-dev-seed: drop as IJobQueue cast
and unused IJobQueue import; wrap event-handler DI block in if(queue)
guard so the handler is only bound when core-jobs is wired.
- core-shared/src/index.ts: add `export * from "./di"` as the plan
specified (subpath export alone is no longer the only access path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Six per-package AGENTS.md tables and the di-explainer wiring trace all
showed pre-ADR-014 binder shapes. Refreshed to the post-ADR-015
reality:
- Per-package "Public exports" tables now show the
(config, tracer, logger, bus, queue) production signature and the
(tracer, logger, bus, queue) dev-seed companion. auth and
marketing-pages also list their newly-public ./di/container,
./di/symbols, and (marketing-pages only) ./services/mailer +
./services/recording-mailer subpaths.
- di-explainer's wiring trace adds the resolveEventsAndJobs* step in
bindAll(), the (config, tracer, logger, bus, queue) binder
signature, and the // <gen:event-handlers> / // <gen:jobs>
injection sites.
No code change; pre-existing AGENTS.md staleness predates ADR-014 +
ADR-015 — this commit catches both up.
Three issues uncovered by the full pnpm typecheck/test/boundaries pass
and resolved here:
- core-testing was importing IEventBus / IJobQueue from core-events /
core-shared, creating two boundary violations (tooling → core) and a
build-graph cycle. Inlined the type aliases (mirroring how
RecordingTracer / RecordingLogger handle ITracer / ILogger).
recording-event-bus.test.ts replaces defineEvent() with an inline
descriptor literal so no runtime import is needed either. core-events
and core-shared are removed from core-testing dependencies.
- turbo.json: typecheck and test no longer dependsOn ^typecheck / ^build.
Each package's tsc / vitest resolves cross-package types via
node_modules independently, and dropping the topological dep avoids the
spurious cycle warning that appeared once core-testing started
importing core-events / core-shared.
- turbo.json: feature.dependencies.allow gains "feature". Cross-feature
event flow (ADR-015) requires a consumer feature to import the
publisher's event contract directly. The dangerous form (importing
the publisher's handler/use-case/repo) is still blocked by E1's
no-handler-reexport ESLint rule and the missing public exports.
- TaskConfig<"slug-string"> → TaskConfig<{ input; output }> in the gen
job task template (and the shipped send-welcome-email.task.ts) since
runtime-generated slugs aren't keys of TypedJobs['tasks'].
End-to-end proof-of-life. bindAllDevSeed wires InMemoryEventBus +
InMemoryJobQueue across all features. signUpController publishes
auth.user.signed-up; marketing-pages' handler enqueues
marketing-pages.send-welcome-email; the in-memory queue dispatches
the wrapped job which records on RecordingMailerService.
Adds ./di/container, ./di/symbols, ./services/mailer, and
./services/recording-mailer to auth + marketing-pages exports so
the e2e test can resolve from the per-feature containers without
deep-importing.
sendWelcomeEmailJob takes IMailerService, validates the typed input
(userId + email), and delegates to mailer.sendWelcome. The
onAuthUserSignedUpHandler now takes IJobQueue and enqueues
"marketing-pages.send-welcome-email" with the event payload.
Both binders bind a RecordingMailerService at the IMailerService
symbol (production placeholder until a real adapter ships) and pass
mailer / queue into the wrapped factories. Dev-seed additionally
queue.register()s the slug so the in-memory queue dispatches via
the wrapped job; production relies on the generated Payload event-task
to resolve the wrapped handler from the container.
Generated handler + Payload event-task via gen event consume,
threaded through symbols / both binders / cms re-export at the
configured anchors. bus.subscribe wires the in-memory delivery in
dev-seed; the __events.auth.user.signed-up.marketing-pages Payload
task closes the production-bus loop.
Also fixes two generator-level issues found during Phase 8:
- Drop publisher prompt's `when` clause so --args can supply the
4th positional argument (Plop limitation: --args cannot bypass
conditional prompts). Validate runs only in consume mode.
- Switch event-task.ts.hbs from TaskConfig<"slug-string"> to
TaskConfig<{ input: ...; output: object }> since runtime-generated
event slugs are not keys of TypedJobs['tasks'].
Plan 10 documented R44 (capture at originating-throw layer) but only the
R43 repo leg was wired. captureException had zero call sites in any
controller or use-case body. This commit closes the gap.
Mechanism:
- Extract __sentryReported flag helpers into core-shared/instrumentation/
reported-flag.ts. SentryLogger switches to importing them; RecordingLogger
carries an inlined copy (tooling → core boundary disallows the import).
- Add withCapture(logger, tags, fn) higher-order wrapper paralleling
withSpan. On throw: capture-with-tags, mark, re-throw. Bail if the flag
was already set — covers the bubbled-from-repo case so each error
surfaces in the logger exactly once with the inner-most layer's tags.
- Apply withSpan(withCapture(factory)) in every feature's bind-production
and bind-dev-seed: auth (3 use cases × 3 controllers), blog (3×3),
marketing-pages (2×2), navigation (1×1), media (3×3). Span is outermost
so the errored span timing reflects the capture-and-rethrow.
- RecordingLogger.captureException now also honours the flag — test
capture counts stay honest when both repo and outer layer wrap.
Tests:
- packages/core-shared/src/instrumentation/with-capture.test.ts —
4 cases covering success, capture-on-throw, mark-on-capture, no-double
via the flag.
- packages/blog/tests/r44-no-double-capture.test.ts — 3 cases: repo throw
→ 1 capture with repo tags; controller parse fail → 1 capture with
controller tags; success → 0 captures.
Verification: pnpm test 26/26, pnpm lint 15/15, pnpm typecheck 14/14.
Docs: ADR-014 and the refactor log gain a "Post-merge follow-up" section
recording the gap, the fix, and the underlying lesson (don't describe
intent as shipped state — grep first).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two flat-config blocks to core-eslint/base.js: (1) repo-wide
no-restricted-imports for @sentry/* with the R40 message, (2) an
allowlist override for the only paths permitted to import the Sentry
SDK directly — core-shared/instrumentation/sentry/**, the bind-sentry
DI files, the no-sentry test guards, and apps' instrumentation* /
next.config / vite.config / sentry.*.config files. Patterns use
**/-prefix so they match whether ESLint runs from the repo root or
from inside a sub-package.
Also adds the standard `argsIgnorePattern: "^_"` config (used
throughout the repo) and a Node-globals override for *.mjs/*.cjs/*.js
and *.config.{ts,tsx} so withSentryConfig in next.config.mjs lints
clean. Required adding `globals` as a core-eslint dep.
Adds .github/workflows/sentry-pii-guard.yml — a lightweight CI step
that fails any PR introducing `sendDefaultPii: true` (R31). Excludes
node_modules / dist / .next / .turbo from the grep so vendored SDK
JSDoc examples don't false-positive.
Pre-existing lint nits cleared as part of getting `pnpm lint` green:
- core-testing define-contract-suite.test.ts: void the unused
receivedTracer (mirrors the next test's pattern)
- marketing-pages bind-dev-seed.ts: drop unused MockSiteSettingsRepository
import
- marketing-pages get-site-settings.use-case.ts: drop the now-redundant
eslint-disable for `_input`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the canonical blog pattern landed earlier on this branch.
Per feature:
- src/__seeds__/dev.ts — lazy buildDev<Entities>() function using the
feature's existing factory for sensible defaults
- src/di/bind-dev-seed.ts — bindDevSeed<Feature>() async function that
rebinds the repo symbol(s) to a populated MockXRepository via
.toConstantValue
- src/di/bind-dev-seed.test.ts — 3+ tests per feature (populates,
reachable by id/slug, idempotent)
- package.json — adds ./di/bind-dev-seed subpath export
Tests + use cases continue to construct mocks directly; the seed never
runs from a *.test.ts path. App boot wiring (USE_DEV_SEED env branch)
follows in a separate commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each per-feature AGENTS.md now reflects the post-Plan-9 layout:
entity/error paths, public-API split (./ui), use-case schemas, presenter
pattern, feature-scoped tRPC error map, and feature-specific
errors-to-codes table.
core-testing/AGENTS.md gains a Plan 9 test-patterns section documenting
R25 (output validation), R26 (router error mapping), R27/R28
(presenter shape) test obligations.
auth: documents real PayloadUsersRepository + AuthenticationService and
the deferred session methods.
media: documents the full Clean Architecture scaffold introduced in
Plan 8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec reviewer caught a systemic gap during Task 6 (navigation) review:
Plan 8 left every feature's domain error class with a constructor that
didn't set this.name = '<ClassName>'. Plan 9 spec R6 requires it.
Fixed across all 10 error files:
- auth: AuthenticationError, UnauthenticatedError, UnauthorizedError, InputParseError
- blog: ArticleNotFoundError, InputParseError
- marketing-pages: PageNotFoundError, InputParseError
- navigation: HeaderNotFoundError, InputParseError
- media: MediaNotFoundError, InputParseError
Functionally a no-op — defineErrorMiddleware uses instanceof, not name —
but ensures correct serialization, stack traces, and JSON inspection.
Refactor log: §7
Spec: R6
All 5 features (auth, blog, marketing-pages, navigation; media has no
entities yet) now follow Lazar's pattern:
- entities/<x>.ts → entities/models/<x>.ts
- entities/errors.ts → entities/errors/<domain>.ts + errors/common.ts
Updates all import paths across factories, contracts, tests, use cases,
controllers, repositories, integrations, and src/index.ts exports.
navigation divergence: had no errors.ts; errors/header.ts +
errors/common.ts added as new forward-looking stubs.
Refactor log: docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md
Spec: §5, §9.3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the decision to add @repo/core-testing, factories, contract
suites, vitest safety defaults, coverage thresholds, Storybook
test-runner, and CI as one cohesive TDD foundation. Per-feature
AGENTS.md gains a Tests section pointing to factories, contract suite,
and the canonical test commands.
Spec: §7.4, §7.5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Deprecate mockPayloadModule with throw guard (hoisting incompatible)
- Replace `as never` with stubPayloadConfig in payload-articles test (consistency)
- Tighten pages contract to use toHaveLength (exact assertions)
- Header contract: define CONTRACT_HEADER_SEED, assert item count + order
Reviewer: superpowers:code-reviewer (Task 4 of Plan 7).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>