feat(conformance): wire cross-feature reader pattern into docs and schema

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.
This commit is contained in:
danijel-lf
2026-05-28 20:55:34 +02:00
parent d4ce68d738
commit b97e6105d3
8 changed files with 33 additions and 11 deletions

View File

@@ -23,6 +23,7 @@ pnpm turbo gen feature # Scaffold a new feature package
pnpm turbo gen event # Scaffold an event contract or handler
pnpm turbo gen job # Scaffold a background job
pnpm turbo gen realtime # Scaffold a realtime channel or handler
pnpm turbo gen reader # Scaffold a cross-feature reader
pnpm turbo gen core-package # Scaffold an optional core package
pnpm turbo gen core-ui-component # Scaffold an atomic-design component
docker compose up -d # Start PostgreSQL
@@ -64,7 +65,7 @@ Turborepo + pnpm monorepo organized by vertical features. Each feature (`auth`,
## Conformance system
Every feature has a `src/feature.manifest.ts` declaring its use cases, audits, publishes, consumes, required cores, `rateLimit?: RateLimitBudget[]` (when applicable, for per-use-case rate-limit budgets), and (when applicable) `requiresConsent: ConsentCategory[]` for features that gate behaviour behind user consent. Drift is caught at five latencies:
Every feature has a `src/feature.manifest.ts` declaring its use cases, audits, publishes, consumes, reads (cross-feature reader deps), required cores, `rateLimit?: RateLimitBudget[]` (when applicable, for per-use-case rate-limit budgets), and (when applicable) `requiresConsent: ConsentCategory[]` for features that gate behaviour behind user consent. Drift is caught at five latencies:
| Layer | Latency | Catches |
| -------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
@@ -74,7 +75,7 @@ Every feature has a `src/feature.manifest.ts` declaring its use cases, audits, p
| **CI drift gate** (`pnpm conformance`) | ~120s | orphan event consumers across features |
| **Fallow** (`pnpm fallow`) | ~3060s | dead exports / unused files; duplicate code; circular deps; complexity hotspots; AI-change audit drift |
The fifteen conformance ESLint rules: `feature-must-have-manifest` (error), `usecase-must-have-test-file` (error), `required-cores-installed` (error), `usecase-must-be-wired` (error), `no-undeclared-event-publish` (warn), `no-undeclared-audit` (warn), `no-undeclared-analytics-event` (warn), `pii-declaration-must-be-complete` (warn), `component-must-have-story` (warn), `component-must-have-test` (warn), `atomic-tier-import-direction` (warn), `no-undeclared-consent-check` (warn), `no-undeclared-rate-limit` (warn), `entity-must-have-test` (warn), `no-relative-parent-import-in-tests` (warn). Fallow runs as a fifth layer, post-ESLint, whole-codebase.
The sixteen conformance ESLint rules: `feature-must-have-manifest` (error), `usecase-must-have-test-file` (error), `required-cores-installed` (error), `usecase-must-be-wired` (error), `no-undeclared-event-publish` (warn), `no-undeclared-audit` (warn), `no-undeclared-analytics-event` (warn), `no-undeclared-reader` (warn), `pii-declaration-must-be-complete` (warn), `component-must-have-story` (warn), `component-must-have-test` (warn), `atomic-tier-import-direction` (warn), `no-undeclared-consent-check` (warn), `no-undeclared-rate-limit` (warn), `entity-must-have-test` (warn), `no-relative-parent-import-in-tests` (warn). Fallow runs as a fifth layer, post-ESLint, whole-codebase.
See `docs/architecture/agent-first-workflow-and-conformance.md` for the full design and `docs/guides/conformance-quickref.md` for the day-to-day reference.
@@ -124,9 +125,16 @@ See `docs/guides/coverage.md` for the cookbook and ADR-020 for the full rational
- **Realtime is for state delivery, not for replacing tRPC (R0)** Persistent request/response operations belong on tRPC procedures. Use realtime when the server needs to push without a request or the data is too high-frequency for HTTP
- **Realtime channel descriptors are exported; handlers are private (R1)** A feature's `realtime/<name>.channel.ts` is re-exported from the root barrel; `realtime/handlers/*.handler.ts` is wired only in bind-\* files and never re-exported (ESLint-enforced via `no-realtime-handler-reexport`)
- **`socket.io` lives in `@repo/core-realtime` only (R2)** Feature packages MUST NOT import `socket.io` or `socket.io-client`. ESLint rule `no-direct-socket-io` enforces this; allowlist covers `core-realtime/src/socket-io-*.ts` and `apps/*/server.ts`
- **Cross-feature domain queries go through readers (Q0)** When a use case needs another vertical's domain-evaluated answer on the request path (e.g., permission check), use a reader (`I<Feature>Reader`). For raw data joins, use Payload `relationTo`. For reactions/side effects, use the event bus
- **Reader contracts are public; implementations are private (Q1)** The owning feature exports `I<Feature>Reader` from `./reader` subpath (`integrations/readers/`). The implementation (`<Feature>Reader`) is internal, constructed by the binder. Consumers import the type only
- **Readers are strictly read-only; cross-feature writes go through events (Q2)** A reader may only wrap use cases declared `mutates: false`. Enforced by `ReadOnly<F>` brand at compile time and `assertReaderPurity` at boot time
- **Reader cycles are a design error (Q3)** If Feature A reads from Feature B and vice versa, the boundaries are wrong. Break via: (a) UI composition at app layer, (b) event for one direction, (c) merge the features
- **Readers wrap existing use cases, not repositories** The reader is a thin facade; if the domain logic doesn't exist as a use case yet, create the use case first (manifest-first). No `MockReader` needed same class works in dev-seed because the use cases beneath it are backed by mock repos
- **Manifest `reads` field** Use cases that query another feature's reader declare `reads: ["<feature>"]` in `feature.manifest.ts`. Verified by `assertFeatureConformance` at boot and `no-undeclared-reader` ESLint rule
- **Binders return readers; `bindAll()` threads them** `bindProductionAuth(ctx)` returns `{ reader: IAuthReader }`. `bindAll()` passes it: `bindProductionBlog(ctx, { authReader: authResult.reader })`. Ordering in `bindAll()` is explicit owning feature first, consumers after
- **Manifest-first ordering** for any new use case, the workflow is **(1) manifest entry** **(2) contracts** (`xInputSchema`, `xOutputSchema`, `IXUseCase`) **(3) tests (red)** **(4) implementation (green)**. The generator emits the manifest + a self-asserting `bind-production.ts` so new features are conformance-compliant by default
- **Self-asserting `bindProductionX(ctx)`** every feature's bind-production calls `assertFeatureConformance(container, manifest, symbols, ctx)` at its tail. `pnpm dev` refuses to boot on drift
- **`pnpm conformance`** cross-feature event-closure check; fails CI on orphan consumers
- **`pnpm conformance`** cross-feature event-closure and reader-closure check; fails CI on orphan consumers or unresolvable `reads` entries
- **New runtime dependencies require a library trace** adding a runtime dependency to a feature- or core-tier package requires a trace at `docs/library-decisions/<date>-<name>.md` produced by the `/evaluate-library` skill; see ADR-022 and `docs/guides/adding-a-library.md`
- **CI security + supply-chain enforcement** Renovate for bumps + Action SHA pinning, Socket for supply-chain behavior, weekly trace revalidation, CodeQL + audit signatures + gitleaks. See ADR-023 + `docs/guides/ci-security.md`