docs(core-shared): add glossary entries + update conformance refs for rate-limit and security headers
- Add IRateLimit, RateLimited, withRateLimit, SBOM, SecurityHeadersConfig, buildSecurityHeaders, nonce (CSP) entries to docs/glossary.md - Bump conformance rule count 12 → 13 in CLAUDE.md; add rateLimit manifest field to description; add no-undeclared-rate-limit to rule list - Add rateLimit manifest field row + no-undeclared-rate-limit (13th rule) to docs/guides/conformance-quickref.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ pnpm dev # Start all dev servers
|
|||||||
pnpm build # Build all packages
|
pnpm build # Build all packages
|
||||||
pnpm test # Run all tests
|
pnpm test # Run all tests
|
||||||
pnpm typecheck # TypeScript across all packages
|
pnpm typecheck # TypeScript across all packages
|
||||||
pnpm lint # ESLint (incl. 12 conformance/* rules)
|
pnpm lint # ESLint (incl. 13 conformance/* rules)
|
||||||
pnpm conformance # Cross-feature event closure
|
pnpm conformance # Cross-feature event closure
|
||||||
pnpm fallow # Whole-codebase: dead exports, dupes, complexity
|
pnpm fallow # Whole-codebase: dead exports, dupes, complexity
|
||||||
pnpm fallow:audit # AI-change audit (run before commits)
|
pnpm fallow:audit # AI-change audit (run before commits)
|
||||||
@@ -62,7 +62,7 @@ Turborepo + pnpm monorepo organized by vertical features. Each feature (`auth`,
|
|||||||
|
|
||||||
## Conformance system
|
## Conformance system
|
||||||
|
|
||||||
Every feature has a `src/feature.manifest.ts` declaring its use cases, audits, publishes, consumes, required cores, 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, 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 |
|
| Layer | Latency | Catches |
|
||||||
| -------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
| -------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
@@ -72,7 +72,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 |
|
| **CI drift gate** (`pnpm conformance`) | ~120s | orphan event consumers across features |
|
||||||
| **Fallow** (`pnpm fallow`) | ~30–60s | dead exports / unused files; duplicate code; circular deps; complexity hotspots; AI-change audit drift |
|
| **Fallow** (`pnpm fallow`) | ~30–60s | dead exports / unused files; duplicate code; circular deps; complexity hotspots; AI-change audit drift |
|
||||||
|
|
||||||
The twelve 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). Fallow runs as a fifth layer, post-ESLint, whole-codebase.
|
The thirteen 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). 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ A commit-body trailer (`Release-As: 0.5.0`) that overrides release-please's auto
|
|||||||
**CHANGELOG.md**:
|
**CHANGELOG.md**:
|
||||||
A per-package, release-please-managed changelog (one at repo root for the template; one at `packages/<feature>/CHANGELOG.md` for each feature). Do not edit manually — release-please regenerates from commit history.
|
A per-package, release-please-managed changelog (one at repo root for the template; one at `packages/<feature>/CHANGELOG.md` for each feature). Do not edit manually — release-please regenerates from commit history.
|
||||||
|
|
||||||
|
**SBOM** (Software Bill of Materials):
|
||||||
|
A machine-readable inventory of all direct and transitive package dependencies attached as a JSON file to each GitHub release. Generated in CycloneDX JSON format via `@cyclonedx/cyclonedx-npm` in the `release-please.yml` CI workflow (runs only when a release is actually cut). Named `sbom-<tag>.cdx.json` and uploaded as a GitHub release asset. Enables downstream supply-chain scanning and NTIA minimum-elements compliance. Not committed to the repository.
|
||||||
|
_Avoid:_ confusing SBOM (a release asset produced by CI) with library traces (developer-authored, per-decision records in `docs/library-decisions/`).
|
||||||
|
|
||||||
## Cross-feature mechanisms
|
## Cross-feature mechanisms
|
||||||
|
|
||||||
**Event bus** (`IEventBus`):
|
**Event bus** (`IEventBus`):
|
||||||
@@ -283,6 +287,16 @@ _Avoid:_ applying `withConsent` in test files — tests inject mocks directly in
|
|||||||
**Rate-limit** (`core-shared/rate-limit`):
|
**Rate-limit** (`core-shared/rate-limit`):
|
||||||
Fourth conformance channel after audit/analytics/consent. `IRateLimit` interface (`consume`/`reset`) + `withRateLimit` wrapper + `RateLimited` brand + `rateLimit: { window, budget }` manifest field. Default budgets in manifest, runtime overrides via `ctx.rateLimit` config. Consumer wires Redis/Upstash impl. See ADR-025.
|
Fourth conformance channel after audit/analytics/consent. `IRateLimit` interface (`consume`/`reset`) + `withRateLimit` wrapper + `RateLimited` brand + `rateLimit: { window, budget }` manifest field. Default budgets in manifest, runtime overrides via `ctx.rateLimit` config. Consumer wires Redis/Upstash impl. See ADR-025.
|
||||||
|
|
||||||
|
**`IRateLimit`**:
|
||||||
|
The vendor-neutral rate-limit interface in `@repo/core-shared/rate-limit/`. Two methods: `consume(budgetName, key, weight?)` → `RateLimitDecision` (fields: `allowed: boolean`, `remaining: number`, `resetAt: Date`) and `reset(budgetName, key)`. Two implementations: `InMemoryRateLimit` (dev/test) and a consumer-wired production backend (Redis/Upstash). Feature packages receive it via `ctx.rateLimit` at DI bind time; declared in the manifest's `rateLimit` field.
|
||||||
|
|
||||||
|
**`RateLimited`**:
|
||||||
|
The phantom-type brand `RateLimited<F>` attached by `withRateLimit(rateLimit, fn)` at DI bind time. Signals that the wrapped use case's declared `RateLimitBudget[]` entries will be enforced before execution. TypeScript rejects binding an unwrapped factory to a `RateLimited`-typed DI symbol when the manifest declares `rateLimit`. The boot assertion verifies the brand at runtime.
|
||||||
|
_Avoid:_ applying `withRateLimit` in test files — tests inject mocks directly and do not go through the DI wrappers.
|
||||||
|
|
||||||
|
**`withRateLimit`**:
|
||||||
|
The wrapper composer that attaches the `RateLimited` brand at DI bind time. Signature: `withRateLimit(rateLimit: IRateLimit, fn)`. Part of the six-wrapper composition chain — see the combined `withSpan / withCapture / …` entry above. Outermost position is `withSpan`; `withRateLimit` sits between `withAnalytics` and the inner factory.
|
||||||
|
|
||||||
**PII inventory**:
|
**PII inventory**:
|
||||||
Personal-data declaration at the Payload field level via `custom.pii = { category, purpose, retention, exportable, restrictable }`. Generator emits `compliance/data-map.yml`. DSR cascade walks this at runtime. See ADR-025.
|
Personal-data declaration at the Payload field level via `custom.pii = { category, purpose, retention, exportable, restrictable }`. Generator emits `compliance/data-map.yml`. DSR cascade walks this at runtime. See ADR-025.
|
||||||
|
|
||||||
@@ -307,6 +321,18 @@ The server-side scrubbing of email/passwords/tokens/cookies/auth/IPs at the OTel
|
|||||||
**Rule 0**:
|
**Rule 0**:
|
||||||
The `bindAll()` decision that selects OTel+Sentry instrumentation when a DSN env var is present, otherwise the Noop chain. Orthogonal to `USE_DEV_SEED` / `NODE_ENV` (those control repo bindings, not instrumentation).
|
The `bindAll()` decision that selects OTel+Sentry instrumentation when a DSN env var is present, otherwise the Noop chain. Orthogonal to `USE_DEV_SEED` / `NODE_ENV` (those control repo bindings, not instrumentation).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
**`SecurityHeadersConfig`**:
|
||||||
|
The TypeScript type accepted by `buildSecurityHeaders()`. Fields: `mode` (`"prod"` | `"dev"`), optional `nonce` (per-request CSP nonce string), and optional allowlist arrays `allowedConnectOrigins`, `allowedImgOrigins`, `allowedFontOrigins` (absolute URLs). Lives at `@repo/core-shared/security`.
|
||||||
|
|
||||||
|
**`buildSecurityHeaders()`**:
|
||||||
|
The pure function in `@repo/core-shared/security` that accepts a `SecurityHeadersConfig` and returns a `Record<string, string>` of HTTP security headers: `Strict-Transport-Security`, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, and `Content-Security-Policy`. In `"prod"` mode the CSP uses `strict-dynamic` + nonce; in `"dev"` mode it relaxes to `unsafe-inline`/`unsafe-eval` to allow HMR tooling.
|
||||||
|
|
||||||
|
**`nonce`** (CSP context):
|
||||||
|
A per-request cryptographically random token (typically 16+ bytes, base64-encoded) threaded into the `Content-Security-Policy` header's `script-src` directive as `'nonce-<value>'`. Required when using `strict-dynamic` CSP to permit specific server-rendered inline `<script>` tags (e.g. Next.js bootstrap chunk, Sentry SDK loader) without `unsafe-inline`. Passed to `buildSecurityHeaders({ nonce })` and set as the `nonce` attribute on matching `<script>` elements.
|
||||||
|
_Avoid:_ reusing the same nonce across requests — it must be regenerated per HTTP response to preserve the security guarantee.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
**PRD**:
|
**PRD**:
|
||||||
|
|||||||
@@ -32,17 +32,18 @@ export type FooManifest = typeof fooManifest;
|
|||||||
|
|
||||||
Field reference:
|
Field reference:
|
||||||
|
|
||||||
| Field | Type | Meaning |
|
| Field | Type | Meaning |
|
||||||
| --------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- |
|
| --------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| `name` | string literal | Feature name (kebab-case, matches package name) |
|
| `name` | string literal | Feature name (kebab-case, matches package name) |
|
||||||
| `requiredCores` | string[] | Optional cores this feature requires (e.g. `["audit", "events"]`) |
|
| `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>.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>.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>.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>.consumes` | string[] | Cross-feature events this use case consumes (via an event handler) |
|
||||||
| `realtimeChannels` | string[] | Realtime channels this feature owns |
|
| `realtimeChannels` | string[] | Realtime channels this feature owns |
|
||||||
| `jobs` | string[] | Job slugs this feature enqueues |
|
| `jobs` | string[] | Job slugs this feature enqueues |
|
||||||
| `requiresConsent` | ConsentCategory[] | Consent categories feature use cases require; drives `withConsent` wrapping + `no-undeclared-consent-check` |
|
| `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`:
|
Re-export from `src/index.ts`:
|
||||||
|
|
||||||
@@ -87,20 +88,21 @@ The symbol map declares which container symbol each manifest use-case key resolv
|
|||||||
|
|
||||||
## ESLint rules
|
## ESLint rules
|
||||||
|
|
||||||
| Rule | Severity | What it does |
|
| Rule | Severity | What it does |
|
||||||
| ---------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `conformance/feature-must-have-manifest` | error | Use-case files require a sibling manifest |
|
| `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/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/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-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/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/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/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/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-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/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/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-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
|
## Workflow ordering for new use cases
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user