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

@@ -193,9 +193,12 @@ Readers delegate to use cases that are already wrapped with `withSpan` and `with
## Out of scope (deferred)
1. **`pnpm turbo gen reader` generator.** Will scaffold `integrations/readers/` + `./reader` export + manifest `reads` field. Deferred until the pattern is exercised with a real reader.
2. **ESLint rule `no-undeclared-reader`.** Follows the `no-undeclared-event-publish` pattern. Deferred until the manifest schema is extended.
3. **Contract evolution / versioning for readers.** Same as event contracts (ADR-015 §deferred-3) — no migration story for breaking reader interface changes yet.
1. **ESLint rule `no-undeclared-reader`.** Follows the `no-undeclared-event-publish` pattern. Deferred until the first reader is exercised.
2. **Contract evolution / versioning for readers.** Same as event contracts (ADR-015 §deferred-3) — no migration story for breaking reader interface changes yet.
## Planned
1. **`pnpm turbo gen reader` generator.** Will scaffold `integrations/readers/` + `./reader` export subpath + interface + implementation + test. Follows the `gen event` Plop pattern with anchor protocol.
## Related

View File

@@ -242,7 +242,7 @@ The server-side push interface in `@repo/core-realtime`. Use cases call `broadca
A consumer's reaction to an inbound client message on a channel. Lives at `packages/<feature>/src/realtime/handlers/*.handler.ts`. **Always private** — never re-exported (rule R1, enforced by `no-realtime-handler-reexport`).
**Reader** (`I<Feature>Reader`):
A synchronous, read-only cross-feature query contract. Exported by the owning feature from `./reader` subpath; implementation is private. Wraps the feature's existing use cases (only those declared `mutates: false`). Lives at `packages/<feature>/src/integrations/readers/`. See ADR-026.
A synchronous, read-only cross-feature query contract. Exported by the owning feature from `./reader` subpath; implementation is private. Wraps the feature's existing use cases (only those declared `mutates: false`). Lives at `packages/<feature>/src/integrations/readers/`.
_Use when:_ you need another vertical's **domain-evaluated** answer on the request path (e.g., "does this user have the editor role?"). **Don't use for raw data lookups** — that's Payload `relationTo`. **Don't use for side effects** — that's the event bus (rule Q2).
_Avoid:_ confusing readers with repositories (repositories are inward-facing data access; readers are outward-facing domain query contracts).
@@ -429,7 +429,7 @@ The Renovate-triggered re-walk of `evaluate-library` when a runtime dep's major
- **"feature"** — always a vertical feature package; never a CMS-collection field or a generic capability.
- **"service"** — a DI-injected port (e.g. `IAuthenticationService`); not a Kubernetes service, Payload collection, or generic "service object".
- **"reader"** — always `I<Feature>Reader` (cross-feature synchronous domain query, ADR-026); not a file reader, stream reader, or CQRS read model.
- **"reader"** — always `I<Feature>Reader` (cross-feature synchronous domain query); not a file reader, stream reader, or CQRS read model.
- **"handler"** — qualify by context: **event handler** (cross-feature) | **realtime handler** (inbound socket message) | **task handler** (Payload job).
- **"schema"** — qualify: **Zod schema** (input/output contracts) | **Payload collection schema** (CMS field definitions).
- **"config"** — qualify: **Payload config** | **Next config** | **Vitest config** | **TS config**.

View File

@@ -29,7 +29,7 @@ per-use-case patterns below.
For any new use case, follow these four steps in order:
1. **Manifest entry** — declare the use case in `src/feature.manifest.ts` with its `mutates` flag and (initially empty) `audits` / `publishes` / `consumes` arrays.
1. **Manifest entry** — declare the use case in `src/feature.manifest.ts` with its `mutates` flag and (initially empty) `audits` / `publishes` / `consumes` / `reads` arrays.
2. **Contracts** — export `xInputSchema`, `xOutputSchema`, and the `IXUseCase` type alias from the use-case file. Factory body starts as `throw new Error("not implemented")`.
3. **Tests (red)** — write the failing test that exercises the contract via the factory + a mock repository.
4. **Implementation (green)** — fill the factory body until the tests pass.
@@ -55,6 +55,7 @@ Every feature package owns:
| `di/` | `symbols.ts` + `module.ts` + `container.ts` + `bind-production.ts` |
| `integrations/api/` | `procedures.ts` (feature error map) + `router.ts` (uses `xProcedure.input(xInputSchema)`) |
| `integrations/cms/` | Payload collection/global configs |
| `integrations/readers/` | `I<Feature>Reader` interface + implementation (when feature exposes cross-feature queries) |
| `ui/` | Query builders and future React components (behind `./ui` subpath) |
| `__factories__/` | Test data factories |
| `__contracts__/` | Contract suites shared by mock and real repository tests |

View File

@@ -21,6 +21,7 @@ export const fooManifest = defineFeature({
audits: ["thing.created"],
publishes: ["foo.thing-created"],
consumes: [],
reads: ["auth"], // cross-feature reader dependency
},
},
realtimeChannels: [],
@@ -40,6 +41,7 @@ Field reference:
| `useCases.<name>.audits` | string[] | Audit event types this use case emits via `auditLog.record({ type: "X" })` |
| `useCases.<name>.publishes` | string[] | Cross-feature events this use case publishes via `bus.publish("X")` |
| `useCases.<name>.consumes` | string[] | Cross-feature events this use case consumes (via an event handler) |
| `useCases.<name>.reads` | string[] | Other features whose readers this use case queries (e.g. `["auth"]`) |
| `realtimeChannels` | string[] | Realtime channels this feature owns |
| `jobs` | string[] | Job slugs this feature enqueues |
| `requiresConsent` | ConsentCategory[] | Consent categories feature use cases require; drives `withConsent` wrapping + `no-undeclared-consent-check` |

View File

@@ -111,6 +111,7 @@ pnpm turbo gen event consume # consumer handler + Payload event-task
pnpm turbo gen job # background job + TaskConfig
pnpm turbo gen realtime channel # realtime channel descriptor (ADR-016)
pnpm turbo gen realtime handler # inbound realtime handler (ADR-016)
pnpm turbo gen reader # cross-feature reader interface + implementation
```
The event/job generators insert at six fixed `// <gen:*>` anchor comments. Generated features include four of them automatically (the `// <gen:job-tasks>` location is in `integrations/cms/index.ts`, which is manually authored as part of the post-scaffold wiring); pre-existing features were retrofitted in ADR-015.
@@ -127,4 +128,5 @@ The realtime generators insert at three additional fixed `// <gen:realtime-*>` a
- `docs/decisions/adr-013-input-output-unification.md` — schemas-in-use-case + presenter
- `docs/decisions/adr-014-instrumentation-sentry.md` — span + capture wiring
- `docs/decisions/adr-015-events-and-jobs.md` — cross-feature events + background jobs
- `docs/decisions/adr-026-cross-feature-readers.md` — cross-feature synchronous readers
- `docs/decisions/adr-016-realtime-layer.md` — Socket.IO realtime channels + handlers