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.
11 KiB
Conformance system — quick reference
Day-to-day reference for the manifest-first workflow. For design rationale see docs/architecture/agent-first-workflow-and-conformance.md and the interactive feature-conformance-explainer.html.
The manifest
Every feature has one at src/feature.manifest.ts:
import { defineFeature } from "@repo/core-shared/conformance";
export const fooManifest = defineFeature({
name: "foo",
requiredCores: [],
useCases: {
getThing: { mutates: false, audits: [], publishes: [], consumes: [] },
createThing: {
mutates: true,
audits: ["thing.created"],
publishes: ["foo.thing-created"],
consumes: [],
reads: ["auth"], // cross-feature reader dependency
},
},
realtimeChannels: [],
jobs: [],
} as const);
export type FooManifest = typeof fooManifest;
Field reference:
| Field | Type | Meaning |
|---|---|---|
name |
string literal | Feature name (kebab-case, matches package name) |
requiredCores |
string[] | Optional cores this feature requires (e.g. ["audit", "events"]) |
useCases.<name>.mutates |
boolean | True for create/update/delete; drives whether __audited brand is required |
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 |
rateLimit |
RateLimitBudget[] | Rate-limit budgets this feature's use cases enforce; drives withRateLimit wrapping + no-undeclared-rate-limit |
Re-export from src/index.ts:
export { fooManifest, type FooManifest } from "./feature.manifest";
bindProductionX self-assertion
Every feature's bind-production.ts calls the assertion at the tail:
import { assertFeatureConformance } from "@repo/core-shared/conformance";
import { fooManifest } from "../feature.manifest";
export function bindProductionFoo(ctx: BindProductionContext): void {
// ... bind use cases, wrapped with withSpan + withCapture + (if mutating + audits) withAudit ...
assertFeatureConformance(
fooContainer,
fooManifest,
{
getThing: FOO_SYMBOLS.IGetThingUseCase,
createThing: FOO_SYMBOLS.ICreateThingUseCase,
},
ctx,
);
}
The symbol map declares which container symbol each manifest use-case key resolves to.
The five gates
| Gate | When it fires | What it catches | Severity |
|---|---|---|---|
tsc |
on save | forgotten wrappers; manifest-derived slot type rejects unwrapped factory | error |
eslint |
on save / pnpm lint |
manifest ↔ code drift; missing sibling test; missing manifest | error or warn |
pnpm dev |
at boot | binding lost its runtime brand; manifest declares more than wired | throws synchronously |
pnpm conformance |
CI | orphan event consumers across features | exits non-zero |
pnpm fallow |
~30–60s | unused exports/files, dupes, circular deps, complexity, AI-change audit | warn (currently) |
ESLint rules
| Rule | Severity | What it does |
|---|---|---|
conformance/feature-must-have-manifest |
error | Use-case files require a sibling manifest |
conformance/usecase-must-have-test-file |
error | Every *.use-case.ts has a sibling *.use-case.test.ts |
conformance/required-cores-installed |
error | Manifest's requiredCores must exist as core-<name> packages in pnpm-workspace.yaml |
conformance/no-undeclared-event-publish |
warn | bus.publish("X") literal must match the manifest's publishes for the use case |
conformance/no-undeclared-audit |
warn | auditLog.record({ type: "X" }) literal must match the manifest's audits |
conformance/usecase-must-be-wired |
error | Every manifest use case must be bound via wireUseCase({ name: "<key>" }) in bind-production.ts / bind-dev-seed.ts |
conformance/no-undeclared-analytics-event |
warn | analytics.track("X") literal must match the manifest's analyticsEvents for the use case |
conformance/pii-declaration-must-be-complete |
warn | custom.pii blocks in Payload config files must declare all required fields: category, purpose, exportable, restrictable |
conformance/component-must-have-story |
warn | Every component file under src/ must have a sibling .stories.tsx file |
conformance/component-must-have-test |
warn | Every component file under src/ must have a sibling .test.tsx file |
conformance/atomic-tier-import-direction |
warn | Atomic-design import direction must flow downward (atoms ← molecules ← organisms ← templates ← pages); no upward imports |
conformance/no-undeclared-consent-check |
warn | consent.isGranted("X") literal in a use-case file must match a category declared in manifest.requiresConsent; warns if declared categories are never checked |
conformance/no-undeclared-rate-limit |
warn | rateLimit.consume(budgetName, ...) call in a use-case file must match a budget name declared in manifest.rateLimit; warns if declared budgets are never consumed |
Workflow ordering for new use cases
- Manifest — add the use case to
feature.manifest.tswith emptyaudits/publishes/consumes - Contracts — export
xInputSchema,xOutputSchema,IXUseCasefrom the use-case file (factory body throws "not implemented") - Tests (red) — write the test importing the contracts; verify it fails
- Implementation (green) — fill the factory body until tests pass
For the fast path: pnpm turbo gen feature <name> scaffolds steps 1 + 2 in a single command.
Common drift patterns and the gate that catches them
- Forgot
withSpanat bind time → tsc TS2322 +usecase-must-be-wiredESLint error + boot assertion + bind-production smoke test - Manifest entry has no
wireUseCasecall in either binder →usecase-must-be-wiredESLint error + boot assertion + bind-production smoke test - Manifest declares
audits: ["X"]but factory doesn't callauditLog.record({type:"X"})→ no automatic catch yet; future story - Factory calls
bus.publish("Y")but manifest doesn't declare it →conformance/no-undeclared-event-publish(warn) - Feature has use cases but no manifest →
conformance/feature-must-have-manifest(error) - Manifest references
requiredCores: ["X"]but nocore-Xpackage exists →conformance/required-cores-installed(error) - One feature consumes
Ybut no feature publishesY→pnpm conformanceorphan check (CI gate) - Factory calls
analytics.track("X")but manifest doesn't declare it inanalyticsEvents→conformance/no-undeclared-analytics-event(warn); add the event slug to the manifest or remove the call
Pinning down a drift
When a gate fires, the error message tells you what to run. For example:
Feature blog has use cases but no feature.manifest.ts. Run 'pnpm turbo gen feature blog' or scaffold the manifest manually at packages/blog/src/feature.manifest.ts.
That's the "fix" line — follow it.
Fallow audit for AI changes
When you (the agent) finish a task and are about to commit, run:
pnpm fallow:audit
This runs fallow audit --base main, comparing your branch's diff against main. If your change adds dead exports, dupes, or complexity hotspots, fallow tells you exactly what and where. Fix or accept (with --gate flag to ignore inherited findings).
This is the catch-all for whole-codebase drift the per-file gates can't see.
For the deeper design rationale see docs/architecture/agent-first-workflow-and-conformance.md and the interactive feature-conformance-explainer.html.