InMemoryEventBus.publish and PayloadJobsEventBus.publish called
descriptor.schema.parse(payload) but discarded the result, so handlers
and enqueued jobs received the raw input — zod defaults, catches,
transforms and strips never applied. Both bus templates now fan out the
parsed value; regression tests added to both test templates and the
events snapshot hashes regenerated (template-tree sha over
hbs-stripped paths — only the four touched files differ).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
next.js and react-internal.js are plain re-exports of base, so no
rules-of-hooks checking was active anywhere despite three React apps
and core-ui. Wire eslint-plugin-react-hooks in base.js scoped to
**/*.tsx (non-React packages untouched). Fixes the one violation it
surfaced: web-tanstack's root route called Route.useLoaderData inside
an anonymous component callback — extracted to a named RootComponent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProductionUseCase<I, O, M> only demanded Instrumented + Captured (+
Audited for mutating-with-audits). The boot assertion additionally
requires __analyzed for non-empty analyticsEvents and __rateLimited for
non-empty rateLimit, so the type-level gate under-promised what boot
enforces. The slot now derives both from the manifest entry; the
feature-scoped requiresConsent brand stays boot-only (documented).
Also make IAnalytics extend AnalyticsProtocol from
core-shared/di/bind-protocols so narrowing the ctx protocol fails
typecheck in core-analytics instead of drifting silently.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- turbo/generators/config.ts used require() inside the reader generator
action (no-require-imports); use the top-level node:fs imports
- auth authentication.service.ts used a literal self-assertion
("users" as "users"); prefer-as-const
- apps/cms lacked web-next's next-env.d.ts triple-slash-reference
override, and the committed next-env.d.ts now references
.next/types/routes.d.ts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The base config documented (and carried allowlist off-blocks for) the
OTel-SDK import restriction but the no-restricted-imports pattern group
only covered @sentry/*. Add @opentelemetry/sdk-*, exporter-*,
instrumentation-*, resources and semantic-conventions to the restricted
group, and extend the off-block for core-audit's trace-id enrichment
test, which legitimately builds an in-memory sdk-trace-base tracer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parseManifestUseCases returning null for a manifest that exists made
the cross-feature gate silently skip that feature. findUnparseableManifests
now runs before the empty-graph early exit (an unparseable manifest
contributes zero events and would otherwise pass as nothing-to-check)
and any hit fails the run. Reader-closure from the downstream fork is
not ported: this tree has no reads: manifests or ./reader exports yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
usecase-must-be-wired returned {} when the manifest parsed to null,
silently disabling the error-level gate when the manifest existed but
could not be read. It now reports unparseableManifest on Program in
that case; a genuinely missing manifest stays a no-op (that is
feature-must-have-manifest's job).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A manifest written as `{...} satisfies FeatureManifest` (or the combined
`as const satisfies` idiom) parsed to null, silently no-oping the
error-level conformance rules. unwrapAsConst now strips TSAsExpression
and TSSatisfiesExpression in a loop. The file also carried a verbatim
second copy of its own parser for parseManifestFully; both public entry
points now share one implementation. The template's field set (audits,
rateLimit, requiresConsent) is preserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The core-package trpc removal e2e was only stripping @repo/core-trpc
from the two app package.json files. blog, marketing-pages, and
navigation also depend on core-trpc for their ./ui hooks, so pnpm
install in the simulated post-removal state failed to resolve the
workspace dep. Strip the dep from every package that references it.
Replace NotImplementedError stubs in AuthenticationService with working
implementations: createSession signs a HS256 JWT using Payload's instance
secret, validateSession verifies and decodes the token then looks up the
user, invalidateSession returns a blank cookie with maxAge 0. No external
JWT dependency — uses Node crypto HMAC directly.
Also clarify withAudit/withAnalytics comments: the wrappers intentionally
delegate recording to the use case body (only it knows which fields to
extract), so the TODO was misleading.
- Add ReadOnly<F> phantom brand to core-shared/conformance (compile-time
enforcement that readers only wrap non-mutating use cases)
- Add isReadOnly runtime predicate for boot-time assertReaderPurity
- Scaffold pnpm turbo gen reader: creates integrations/readers/ with
interface, implementation, test, barrel, and adds ./reader export
subpath to package.json
Add reads field to UseCaseManifest, update CLAUDE.md with Q0-Q3 rules,
add ./reader subpath to AGENTS.md exports table, and cascade reader
conventions through conformance quickref, adding-a-feature guide, and
scaffolding guide. Moves gen reader from deferred to planned.
Introduce readers as a fourth cross-feature mechanism alongside events,
jobs, and realtime. Readers solve synchronous domain queries across
verticals (e.g. permission checks) where Payload relationTo gives raw
data but the answer requires business-rule evaluation by the owning
feature.
- Define rules Q0-Q3 (query-only, contract public, read-only, no cycles)
- Reader wraps existing use cases via ReadOnly<F> brand enforcement
- Lives under integrations/readers/ with ./reader export subpath
- Manifest reads: ["auth"] field for conformance gate visibility
- Update glossary with Reader, reads, ReadOnly<F> terms
Replace the boolean bound flag with a cached promise so concurrent
callers (layout + page server components) await the same binding
operation. Prevents DI resolution before containers are populated.
Pages import feature server components and pass only route-derived
props (slug, id). All data fetching, prefetch, and hydration is owned
by the feature's .server.tsx component.
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.
- Add USE_DEV_SEED=false branch to bindAll dispatcher
- Use dotenv-cli to inject root .env into all Turbo tasks
- Add globalDependencies for .env cache invalidation
- Split globals.css into globals.css + theme.css so Storybook can
import design tokens without the @import "tailwindcss" directive
that breaks Vite's preview module loading
- Prepend @tailwindcss/vite plugin for correct processing order
- Add @source to scan core-ui components for utility class generation
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.
Bring docs/architecture/ in line with the current repo:
- feature-conformance-explainer.html: drop the "proposed / not yet
implemented" framing — the system is shipped. Four enforcement points
become five (adds `pnpm fallow` as the whole-codebase audit). Manifest
playground shows `coverage`, `analyticsEvents`, `rateLimit`,
`requiresConsent`. Milestone / anchor / open-question sections kept
but marked historical.
- agent-first-workflow-and-conformance.md: four → five enforcement
layers; layer table gains the Fallow row.
- di-explainer.html: bind-production sample rewritten to show
wireUseCase() + assertFeatureConformance() + the full wrapper stack
(span → capture → audit? → analytics? → consent? → rateLimit?).
- data-flow-explainer.html: same bind-production refresh for the
data-flow narrative.
- audit-and-compliance-explainer.html: AuditAction enum 6 → 10 values
(CONSENT_GRANT / WITHDRAW / RESTRICT / UNRESTRICT);
BindProductionContext example gains analytics, consentFactory,
rateLimit.
- vertical-feature-spec.md: §5 layout lists the 8 optional cores plus
core-testing; §9.5 hedges the turbo.json snippet against the live
file; §10.4 drops the dated "360 tests" metric for the ADR-020
coverage architecture; §11 gains a historical lead-in pointing at
docs/decisions/ as the canonical 25-ADR set.
Align the architecture docs with the current repo:
- Boundary matrix: feature may depend on core, feature, tooling — a
feature may import another feature's public exports. overview.md,
dependency-flow.md, and vertical-feature-spec.md all said the stale
`feature -> core, tooling`.
- Optional-core lists completed with core-analytics, core-consent,
core-dsr; tooling list completed with core-testing.
- Package count corrected to the accurate 19-package breakdown.
- BindContext table gained analytics, consentFactory, rateLimit.
Deeper drift (the HTML explainers, vertical-feature-spec §5/§9.5/§11)
is tracked in the local .tmp/ working note, not yet addressed.
In-session skill counterparts to `pnpm work decompose --execute` and
`pnpm work dispatch --execute`. They dispatch sub-agents — a decomposer
for /work-decompose; separate worktree-isolated implementer and
read-only reviewer sub-agents for /work-dispatch's implement-review loop.
Single source of truth: the skills read `.sandcastle/*.prompt.md` at
dispatch time and never copy them — the prompt files stay authoritative
for both Sandcastle and the skills, so the role definitions cannot
drift. `.sandcastle/` and `pnpm work` are untouched; this is additive.
turbo.json's boundary config already allows `feature -> feature`, and
the cross-feature event system depends on it — a consumer must import
the publisher's event contract from `@repo/<publisher>`. But the ESLint
boundaries config, ADR-010, and AGENTS.md still declared
`feature -> [core, tooling]`, contradicting turbo.json and the shipped
code (marketing-pages imports @repo/auth).
Align all three to turbo.json: a feature may import another feature's
published public exports. Internals stay sealed by the `exports` map,
and cross-feature behaviour still flows through IEventBus.
The core-package e2e tests cover each generator in isolation. None
exercised generators running in sequence — so the feature template
shipping without an integrations/cms/index.ts barrel went unnoticed
until `gen job` failed against a scaffolded feature.
This test scaffolds a feature, then runs `gen job` against it. `gen job`
throws at its `<gen:job-tasks>` anchor assertion if the barrel is
missing, so the composition gap now fails loudly.
Two CLAUDE.md conventions had no mechanical gate, so both drifted:
entity models shipped without sibling tests, and feature test files
imported src modules via `../` instead of the `@/` alias.
- `entity-must-have-test` — every entities/models/<x>.ts needs a sibling
<x>.test.ts (errors and barrels excluded).
- `no-relative-parent-import-in-tests` — feature test files must import
src via `@/`, not `../`. Scoped to feature packages; core packages are
governed by their own generator templates.
Both register at warn level, bringing the conformance rule count to 15.
The rule only matched bus.publish("string-literal", ...), but the
canonical pattern that `gen event` prescribes is
bus.publish(eventDescriptor, payload) — an imported identifier, never a
literal. The rule therefore never fired on real code, which is how the
auth signUp publish drifted from its manifest undetected.
Add `_event-ast.js`: resolves a `bus.publish(<identifier>)` argument by
following the import to the event-contract file and extracting the name
from either `defineEvent("...", schema)` or an inline `{ name }` object.
Unresolvable arguments are skipped, so the rule never false-positives.
bind-production.types.test.ts imported feature.manifest and the sign-in
use case with `../` parent paths. Test files import src modules via the
`@/` alias per the repo convention — this clears the way for the new
no-relative-parent-import-in-tests rule.
Each core-package e2e test's `onTestFinished` cleanup `rmSync`-es a temp
clone containing a full installed `node_modules`. That delete can exceed
vitest's 10s default hook timeout under load, failing 2-8 of the 8 e2e
tests per run. Set `hookTimeout: 120_000` to match the per-test timeout.
The feature generator emitted no `src/integrations/cms/index.ts`, but
`gen job` and `gen event consume` both `assertAnchors` on that file's
`<gen:job-tasks>` anchor — so they failed on any freshly-scaffolded
feature. Add a `cms/index.ts.hbs` template carrying the anchor, wire the
add action into the generator, and export `./cms` from the package.json
template to match all five existing features.
container.test.ts imported cross-directory modules with `../...` paths.
Test files use the `@/` alias for src imports per the repo convention;
this was the outlier vs auth/media/navigation.
cookie.ts was the only entity model in the feature packages without a
sibling test. Covers minimal/full attributes, invalid sameSite, and a
missing required field.
The signUp use case calls `bus.publish(userSignedUpEvent, ...)` but its
manifest entry declared `publishes: []`. The conformance event graph was
blind to the entire auth -> marketing-pages welcome-email flow as a
result. Declaring "auth.user.signed-up" makes `pnpm conformance` surface
the event and its publisher.
Storybook's `stories` glob was emptied when core-ui became an optional
scaffoldable package. With core-ui present in the repo, wire the glob
back so its six stories are discovered — the manual post-scaffold step
the `gen core-package ui` generator already documents.
The analytics, consent, and dsr optional-core generators lacked the
byte-identical reconstruction e2e tests the other five optional cores
already have. Add the three tests and their snapshots.
Adding three more tests exposed a latent flaw in the suite: each test
does a full `pnpm install` in a temp clone, and running all eight in
parallel saturated vitest's workers (RPC timeout) while the never-cleaned
temp dirs filled the disk. Run the suite sequentially via
`fileParallelism: false` and remove each temp clone with `onTestFinished`;
also exclude the local `.pnpm-store` from the clone.
The Socket supply-chain filter (ADR-023) was added after the initial
library-trace backfill, leaving the 36 traces dated 2026-05-14 without
the socketRisk filter-results field the trace schema now expects.
Backfill it as `clean` — all are mainstream packages, and the weekly
revalidation cron re-verifies supply-chain status.
The root devDependency carried a "*" wildcard, and ADR-019 documented a
nonexistent ^2.73.0 pin — the package's installed version is 0.5.10.
Pin to ^0.5.10 and correct ADR-019 to match.
The implemented IAnalytics refined the signature ADR-024 sketched:
events are attributed to the user set by identify() (the standard SDK
model) rather than passed per track() call, the attribute parameter is
named `attributes` consistently, and AnalyticsUser stays id-only with
traits riding identify()'s second argument. The code is the more
idiomatic contract and docs/guides/analytics.md already matched it —
update the ADR's interface block, manifest example, and PII-boundary
section to describe what shipped.