docs(architecture): refresh explainers and spec to the shipped system
Some checks failed
CI / typecheck + lint + boundaries + test + build (push) Has been cancelled
CI / Playwright e2e (push) Has been cancelled
CI / Storybook smoke tests + visual regression (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Coverage snapshot / snapshot (push) Has been cancelled
Release Please / release-please (push) Has been cancelled
Sentry PII guard (R31) / pii-guard (push) Has been cancelled
Mutation testing (nightly) / mutate (push) Has been cancelled
Library trace revalidation (weekly) / revalidate (push) Has been cancelled

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.
This commit is contained in:
2026-05-23 14:06:03 +02:00
parent b455ae8018
commit 0748f9e5ed
6 changed files with 251 additions and 51 deletions

View File

@@ -400,7 +400,7 @@ The reviewer agent **explicitly checks the task's `Out of scope` section against
## Conformance system integration
The four enforcement layers (detailed in [`feature-conformance-explainer.html`](./feature-conformance-explainer.html)):
The five enforcement layers (detailed in [`feature-conformance-explainer.html`](./feature-conformance-explainer.html)):
| Layer | Latency | Catches |
| ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------- |
@@ -408,6 +408,7 @@ The four enforcement layers (detailed in [`feature-conformance-explainer.html`](
| AST-aware ESLint | <1s | manifest code drift; undeclared `bus.publish` / `auditLogger.log`; required cores not installed |
| Boot assertion (`assertConformance`) | ~3s | binding type-casts that hid unwrapped factories; manifests edited without rebinding |
| CI drift gate (`pnpm conformance`) | ~120s | orphan event consumers; scaffold drift from generator; required-cores workspace mismatch |
| Fallow audit (`pnpm fallow`) | ~3060s | whole-codebase dead exports, duplicate code, circular deps, complexity hotspots |
### How conformance interacts with tasks
@@ -417,7 +418,7 @@ When a task adds an audit emission (e.g. `audits: ["user.created"]`):
2. The binding's branded slot type _now_ demands `Audited<F>` TS2322 if the wrapper is missing
3. Agent adds `withAudit(...)` in `bind-production.ts` TS goes quiet
4. Agent adds `auditLogger.log(...)` in the use-case factory ESLint goes quiet
5. Pre-commit `pnpm conformance` confirms all four layers pass
5. Pre-commit `pnpm conformance` confirms all five layers pass
6. PR submitted
Each step gives sub-second feedback. The agent's iteration loop is dominated by think + write, not by waiting for feedback.
@@ -653,7 +654,7 @@ These are explicitly deferred until the tier that needs them:
This design is "done" when:
- [ ] `docs/work/` exists with templates, `_state.json` schema, README
- [ ] Conformance system v1 is implemented through all four enforcement layers
- [ ] Conformance system v1 is implemented through all five enforcement layers
- [ ] All five feature packages have manifests
- [ ] All three apps run `assertConformance` at boot
- [ ] `pnpm conformance` is a CI gate

View File

@@ -983,7 +983,11 @@ footer .colophon {
| <span class="s">"UPDATE"</span>
| <span class="s">"DELETE"</span>
| <span class="s">"EXPORT"</span>
| <span class="s">"PERMISSION_CHANGE"</span>;
| <span class="s">"PERMISSION_CHANGE"</span>
| <span class="s">"CONSENT_GRANT"</span>
| <span class="s">"CONSENT_WITHDRAW"</span>
| <span class="s">"RESTRICT"</span>
| <span class="s">"UNRESTRICT"</span>;
<span class="c">/**
* `from_where` fragment per DPA. IP truncated to /24 (IPv4) or /48 (IPv6)
@@ -1381,6 +1385,7 @@ footer .colophon {
<span class="k">const</span> <span class="n">ctx</span>: <span class="t">BindProductionContext</span> = {
<span class="n">tracer</span>, <span class="n">logger</span>, <span class="n">config</span>, <span class="n">bus</span>, <span class="n">queue</span>, <span class="n">realtime</span>, <span class="n">realtimeRegistry</span>,
<span class="h"><span class="n">auditLog</span></span>, <span class="c">// ← NEW</span>
<span class="n">analytics</span>, <span class="n">consentFactory</span>, <span class="n">rateLimit</span>, <span class="c">// optional (ADR-024 + ADR-025)</span>
};
<span class="k">await</span> <span class="n">bindProductionAuth</span>(<span class="n">ctx</span>);

View File

@@ -1703,16 +1703,56 @@ footer .colophon {
<h3>blog/di/<em>bind-production.ts</em></h3>
<p>Called from each app's bootstrap (<code>apps/web-next/src/server/bind-production.ts</code>) with the <code>ctx</code> object built once by the aggregator. <code>BindProductionContext</code> is imported from <code>@repo/core-shared/di</code>.</p>
<pre class="code" data-lang="typescript // packages/blog/src/di/bind-production.ts"><span class="k">export function</span> <span class="n">bindProductionBlog</span>(<span class="n">ctx</span>: <span class="t">BindProductionContext</span>): <span class="t">void</span> {
<span class="c">// bus, realtime, realtimeRegistry, auditLog are optional — present only when the corresponding optional package is scaffolded</span>
<span class="k">const</span> { <span class="n">config</span>, <span class="n">tracer</span>, <span class="n">logger</span>, <span class="n">bus</span>, <span class="n">queue</span>, <span class="n">realtime</span>, <span class="n">realtimeRegistry</span>, <span class="n">auditLog</span> } = <span class="n">ctx</span>;
<span class="c">// bus, realtime, realtimeRegistry, auditLog are optional — present only when</span>
<span class="c">// the corresponding optional package is scaffolded</span>
<span class="k">const</span> { <span class="n">config</span>, <span class="n">tracer</span>, <span class="n">logger</span>, <span class="n">bus</span>, <span class="n">queue</span> } = <span class="n">ctx</span>;
<span class="c">// 1) Swap the mock repo for the real Payload-backed impl</span>
<span class="k">if</span> (<span class="n">blogContainer</span>.<span class="n">isBound</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>)) {
<span class="n">blogContainer</span>.<span class="n">unbind</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>);
}
<span class="n">blogContainer</span>
.<span class="n">bind</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>)
.<span class="n">toConstantValue</span>(<span class="k">new</span> <span class="t">ArticlesRepository</span>(<span class="n">config</span>));
<span class="c">// Use cases + controllers stay untouched.</span>
<span class="c">// They'll resolve through the new repo automatically.</span>
<span class="k">const</span> <span class="n">repo</span> = <span class="k">new</span> <span class="t">ArticlesRepository</span>(<span class="n">config</span>, <span class="n">tracer</span>, <span class="n">logger</span>);
<span class="n">blogContainer</span>.<span class="n">bind</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>).<span class="n">toConstantValue</span>(<span class="n">repo</span>);
<span class="c">// 2) Wire each use case via wireUseCase — composes</span>
<span class="c">// withSpan → withCapture → withAudit → withAnalytics → withConsent → withRateLimit → factory(deps)</span>
<span class="c">// and binds the brand-stacked closure to the symbol. One call per use case.</span>
<span class="k">const</span> <span class="n">wrappedGetArticles</span> = <span class="n">wireUseCase</span>({
<span class="n">container</span>: <span class="n">blogContainer</span>,
<span class="n">symbol</span>: <span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesUseCase</span>,
<span class="n">factory</span>: <span class="n">getArticlesUseCase</span>,
<span class="n">deps</span>: [<span class="n">repo</span>],
<span class="n">feature</span>: <span class="s">"blog"</span>, <span class="n">layer</span>: <span class="s">"use-case"</span>, <span class="n">name</span>: <span class="s">"getArticles"</span>,
<span class="n">tracer</span>, <span class="n">logger</span>,
});
<span class="c">// ... wireUseCase calls for the other use cases ...</span>
<span class="c">// 3) Wrap each controller — controllers still use withSpan + withCapture manually,</span>
<span class="c">// because they take the already-wired use case as their dep.</span>
<span class="k">if</span> (<span class="n">blogContainer</span>.<span class="n">isBound</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesController</span>)) {
<span class="n">blogContainer</span>.<span class="n">unbind</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesController</span>);
}
<span class="n">blogContainer</span>.<span class="n">bind</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesController</span>).<span class="n">toConstantValue</span>(
<span class="n">withSpan</span>(<span class="n">tracer</span>, { <span class="n">name</span>: <span class="s">"blog.getArticles"</span>, <span class="n">op</span>: <span class="s">"controller"</span> },
<span class="n">withCapture</span>(<span class="n">logger</span>, { <span class="n">feature</span>: <span class="s">"blog"</span>, <span class="n">layer</span>: <span class="s">"controller"</span>, <span class="n">name</span>: <span class="s">"blog.getArticles"</span> },
<span class="n">getArticlesController</span>(<span class="n">wrappedGetArticles</span>),
),
),
);
<span class="c">// bus + queue are passed through; generated event/job handlers consume them at the anchors below.</span>
<span class="k">void</span> <span class="n">bus</span>; <span class="k">void</span> <span class="n">queue</span>;
<span class="c">// &lt;gen:event-handlers&gt;</span>
<span class="c">// &lt;gen:jobs&gt;</span>
<span class="c">// 4) Self-asserting tail — refuses to boot if any use-case binding is missing a required brand</span>
<span class="c">// (withSpan / withCapture / withAudit / withAnalytics / withConsent / withRateLimit) or drifts from the manifest.</span>
<span class="n">assertFeatureConformance</span>(
<span class="n">blogContainer</span>,
<span class="n">blogManifest</span>,
{ <span class="n">getArticles</span>: <span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesUseCase</span>, <span class="c">/* … */</span> },
<span class="n">ctx</span>,
);
}</pre>
</div>
</div>

View File

@@ -756,7 +756,7 @@ footer .colophon {
<div class="cast-tag">file 04 · prod swap</div>
<h3>bind-production.ts</h3>
<p class="role">Replaces the mock repository binding with a real Payload-backed one at app boot.</p>
<p>Exports <code>bindProductionBlog(ctx: BindProductionContext)</code>. Destructures <code>{ config, tracer, logger, bus, queue, realtime, realtimeRegistry }</code> from <code>ctx</code>. Function body: <code>blogContainer.unbind(symbol)</code> if already bound, then <code>.bind(symbol).toConstantValue(new ArticlesRepository(config, tracer, logger))</code>. Use cases and controllers are wrapped via <code>withSpan(withCapture(factory(deps)))</code> at bind time so they inherit instrumentation without changing their factory bodies. The optional <code>bus</code> and <code>queue</code> fields come from the app's <code>resolveEventsAndJobs*</code> step (ADR-015) and feed the <code>// &lt;gen:event-handlers&gt;</code> / <code>// &lt;gen:jobs&gt;</code> injection sites. <code>BindProductionContext</code> is imported from <code>@repo/core-shared/di</code> — features never import the optional packages directly for the binder signature.</p>
<p>Exports <code>bindProductionBlog(ctx: BindProductionContext)</code>. Destructures the ctx (always: <code>config, tracer, logger</code>; optional: <code>bus, queue, realtime, realtimeRegistry, metrics, auditLog, analytics, consentFactory, rateLimit</code>). Function body: <code>blogContainer.unbind(symbol)</code> if already bound, then <code>.bind(symbol).toConstantValue(new ArticlesRepository(config, tracer, logger))</code>. Use cases are wired via <code>wireUseCase({...})</code> from <code>@repo/core-shared/conformance</code> — which composes <code>withSpanwithCapture → withAudit? → withAnalytics? → withConsent? → withRateLimit? → factory(deps)</code> (each <code>?</code> wrapper applies only when the manifest declares that channel). Controllers are wrapped manually with <code>withSpan(withCapture(...))</code>. The file ends with <code>assertFeatureConformance(container, manifest, symbolMap, ctx)</code> — the boot gate that refuses to start on manifest ↔ binding drift. The optional <code>bus</code> and <code>queue</code> fields come from the app's <code>resolveEventsAndJobs*</code> step (ADR-015) and feed the <code>// &lt;gen:event-handlers&gt;</code> / <code>// &lt;gen:jobs&gt;</code> injection sites. <code>BindProductionContext</code> is imported from <code>@repo/core-shared/di</code> — features never import the optional packages directly for the binder signature.</p>
<div class="runs-when"><strong>When it runs:</strong> called from app boot (<code>apps/web-next/src/server/bind-production.ts</code>) when <code>USE_DEV_SEED ≠ "true"</code> AND Payload config is resolvable.</div>
</div>
@@ -924,15 +924,59 @@ footer .colophon {
<div class="step-num">06</div>
<div class="step-body">
<div class="when">App boot · per feature</div>
<h4>Each binder <em>swaps the repo binding</em>.</h4>
<p>Whichever binder runs, it does the same thing for the repository symbol: <code>unbind</code> the old binding, <code>bind</code> a new <code>.toConstantValue(impl)</code>. Use case and controller bindings stay untouched — they keep their <code>.toDynamicValue</code> closures and will pick up the new repo on next resolve.</p>
<h4>Each binder <em>swaps the repo binding</em>, then re-wires use cases + controllers.</h4>
<p>The binder destructures <code>ctx</code>, unbinds + rebinds the repository as <code>.toConstantValue(impl)</code>, then re-wires every use case through <code>wireUseCase({ ... })</code> (which composes <code>withSpan → withCapture → withAudit? → withAnalytics? → withConsent? → withRateLimit? → factory(deps)</code> and binds the result). Controllers are wrapped with <code>withSpan(withCapture(...))</code> at bind time. The binder ends with <code>assertFeatureConformance(container, manifest, symbolMap, ctx)</code>, which refuses to boot if any use-case binding is missing a brand the manifest required.</p>
</div>
<pre class="code" data-lang="typescript // packages/blog/src/di/bind-production.ts (or bind-dev-seed.ts)"><span class="k">if</span> (<span class="n">blogContainer</span>.<span class="n">isBound</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>)) {
<span class="n">blogContainer</span>.<span class="n">unbind</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>);
}
<span class="n">blogContainer</span>
.<span class="n">bind</span>(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>)
.<span class="n">toConstantValue</span>(<span class="k">new</span> <span class="t">ArticlesRepository</span>(<span class="n">config</span>)); <span class="c">// or new MockArticlesRepository() with seed data</span></pre>
<pre class="code" data-lang="typescript // packages/auth/src/di/bind-production.ts (illustrative excerpt)"><span class="k">export function</span> <span class="n">bindProductionAuth</span>(<span class="n">ctx</span>: <span class="t">BindProductionContext</span>): <span class="t">void</span> {
<span class="k">const</span> { <span class="n">config</span>, <span class="n">tracer</span>, <span class="n">logger</span>, <span class="n">bus</span>, <span class="n">consentFactory</span> } = <span class="n">ctx</span>;
<span class="c">// 1. Swap the repository binding.</span>
<span class="k">if</span> (<span class="n">authContainer</span>.<span class="n">isBound</span>(<span class="n">AUTH_SYMBOLS</span>.<span class="n">IUsersRepository</span>)) {
<span class="n">authContainer</span>.<span class="n">unbind</span>(<span class="n">AUTH_SYMBOLS</span>.<span class="n">IUsersRepository</span>);
}
<span class="k">const</span> <span class="n">repo</span> = <span class="k">new</span> <span class="t">UsersRepository</span>(<span class="n">config</span>, <span class="n">tracer</span>, <span class="n">logger</span>);
<span class="n">authContainer</span>.<span class="n">bind</span>&lt;<span class="t">IUsersRepository</span>&gt;(<span class="n">AUTH_SYMBOLS</span>.<span class="n">IUsersRepository</span>).<span class="n">toConstantValue</span>(<span class="n">repo</span>);
<span class="c">// 2. Wire each use case through the conformance helper.</span>
<span class="c">// wireUseCase composes withSpan → withCapture → withAudit? → withAnalytics?</span>
<span class="c">// → withConsent? → withRateLimit? → factory(deps), then binds the result.</span>
<span class="k">const</span> <span class="n">wrappedSignIn</span> = <span class="n">wireUseCase</span>({
<span class="n">container</span>: <span class="n">authContainer</span>,
<span class="n">symbol</span>: <span class="n">AUTH_SYMBOLS</span>.<span class="n">ISignInUseCase</span>,
<span class="n">factory</span>: <span class="n">signInUseCase</span>,
<span class="n">deps</span>: [<span class="n">repo</span>, <span class="n">authService</span>, <span class="n">ctx</span>.<span class="n">rateLimit</span> ?? <span class="k">new</span> <span class="t">NoopRateLimit</span>()],
<span class="n">feature</span>: <span class="s">"auth"</span>, <span class="n">layer</span>: <span class="s">"use-case"</span>, <span class="n">name</span>: <span class="s">"signIn"</span>,
<span class="n">tracer</span>, <span class="n">logger</span>,
<span class="n">rateLimit</span>: <span class="n">ctx</span>.<span class="n">rateLimit</span> ?? <span class="k">new</span> <span class="t">NoopRateLimit</span>(), <span class="c">// manifest declares rateLimit</span>
});
<span class="k">const</span> <span class="n">wrappedSignUp</span> = <span class="n">wireUseCase</span>({
<span class="n">container</span>: <span class="n">authContainer</span>,
<span class="n">symbol</span>: <span class="n">AUTH_SYMBOLS</span>.<span class="n">ISignUpUseCase</span>,
<span class="n">factory</span>: <span class="n">signUpUseCase</span>,
<span class="n">deps</span>: [<span class="n">repo</span>, <span class="n">authService</span>, <span class="n">bus</span>, <span class="n">consentFactory</span>],
<span class="n">feature</span>: <span class="s">"auth"</span>, <span class="n">layer</span>: <span class="s">"use-case"</span>, <span class="n">name</span>: <span class="s">"signUp"</span>,
<span class="n">tracer</span>, <span class="n">logger</span>,
});
<span class="c">// 3. Wrap controllers with span + capture at bind time.</span>
<span class="n">authContainer</span>
.<span class="n">bind</span>(<span class="n">AUTH_SYMBOLS</span>.<span class="n">ISignInController</span>)
.<span class="n">toConstantValue</span>(
<span class="n">withSpan</span>(<span class="n">tracer</span>, { <span class="n">name</span>: <span class="s">"auth.signIn"</span>, <span class="n">op</span>: <span class="s">"controller"</span> },
<span class="n">withCapture</span>(<span class="n">logger</span>, { <span class="n">feature</span>: <span class="s">"auth"</span>, <span class="n">layer</span>: <span class="s">"controller"</span>, <span class="n">name</span>: <span class="s">"auth.signIn"</span> },
<span class="n">signInController</span>(<span class="n">wrappedSignIn</span>),
),
),
);
<span class="c">// 4. Boot-time conformance check — refuses to start on missing brands.</span>
<span class="n">assertFeatureConformance</span>(
<span class="n">authContainer</span>,
<span class="n">authManifest</span>,
{ <span class="n">signIn</span>: <span class="n">AUTH_SYMBOLS</span>.<span class="n">ISignInUseCase</span>, <span class="n">signUp</span>: <span class="n">AUTH_SYMBOLS</span>.<span class="n">ISignUpUseCase</span> },
<span class="n">ctx</span>,
);
}</pre>
</div>
<div class="lifecycle-stage">
@@ -1204,29 +1248,61 @@ footer .colophon {
└─ required: tracer, logger, config | optional: metrics, bus, queue, realtime, realtimeRegistry, auditLog
└─ bindProductionX(ctx) ← single ctx object passed to each feature binder
└─ feature container also binds TRACER + LOGGER
└─ withSpan(withCapture(...)) at every use case + controller
└─ wireUseCase({...}) at every use case:
withSpan → withCapture → withAudit? → withAnalytics? → withConsent? → withRateLimit? → factory(deps)
(the ? wrappers apply only when the manifest declares that channel)
└─ withSpan(withCapture(...)) at every controller
└─ assertFeatureConformance(container, manifest, symbolMap, ctx) at the tail
└─ // &lt;gen:event-handlers&gt; / // &lt;gen:jobs&gt; / // &lt;gen:realtime-handlers&gt; / // &lt;gen:audit-hooks&gt; injection sites</code></pre>
<p class="trace-p"><strong>Why per-feature containers also get the binding:</strong> repository classes resolve TRACER/LOGGER through the container; controllers and use cases receive instrumentation via the bind-time wrapper instead.</p>
</div>
</div>
<h3 class="trace-h3" style="margin-top: 36px;">Two wrappers, applied as a sandwich</h3>
<p class="trace-p"><code>withSpan</code> and <code>withCapture</code> are higher-order functions that take a <code>(args) =&gt; Promise&lt;R&gt;</code> and return the same shape. The binders compose them: <code>withSpan(withCapture(factory(deps)))</code>. Span is outermost so an errored span's timing reflects the capture-and-rethrow.</p>
<h3 class="trace-h3" style="margin-top: 36px;">The wrapper stack, applied as a sandwich</h3>
<p class="trace-p">Each wrapper is a higher-order function taking <code>(args) =&gt; Promise&lt;R&gt;</code> and returning the same shape. For use cases, <code>wireUseCase</code> composes them in this order (outermost → innermost) and binds the result:</p>
<pre class="trace-tree"><code>withSpan
└─ withCapture
└─ withAudit? ← only if manifest declares `audits`
└─ withAnalytics? ← only if manifest declares `analyticsEvents`
└─ withConsent? ← only if manifest declares `requiresConsent`
└─ withRateLimit? ← only if manifest declares `rateLimit`
└─ factory(deps)</code></pre>
<p class="trace-p"><code>withSpan</code> is always outermost so an errored span's timing reflects the capture-and-rethrow. The four optional wrappers (<code>withAudit</code>, <code>withAnalytics</code>, <code>withConsent</code>, <code>withRateLimit</code>) are conditionally inserted by <code>wireUseCase</code> based on the use case's manifest entry — a use case without any of those declarations gets the plain <code>withSpan(withCapture(factory(deps)))</code> sandwich. Controllers always use just <code>withSpan(withCapture(...))</code>.</p>
<table class="capture-rules">
<thead>
<tr><th>Wrapper</th><th>What it does</th><th>Where it fires</th></tr>
<tr><th>Wrapper</th><th>What it does</th><th>Applied when</th></tr>
</thead>
<tbody>
<tr>
<td><code>withSpan(tracer, opts, fn)</code></td>
<td>Calls <code>tracer.startSpan(opts, () =&gt; fn(...))</code>. Pure delegation — no error handling of its own; status-on-error logic lives in the tracer impl.</td>
<td>Around every use case + controller, at DI bind time</td>
<td>Always — every use case + every controller</td>
</tr>
<tr>
<td><code>withCapture(logger, tags, fn)</code></td>
<td>On throw: checks <code>__sentryReported</code>; if not set, calls <code>logger.captureException(err, { tags })</code>, marks the flag, re-throws. If already set, just re-throws.</td>
<td>Around every use case + controller, inside the span wrapper</td>
<td>Always — every use case + every controller, inside the span wrapper</td>
</tr>
<tr>
<td><code>withAudit</code></td>
<td>Forwarding wrapper that attaches the <code>__audited</code> brand so the conformance gate sees the binding is audit-aware. Automated audit recording from manifest declarations is reserved.</td>
<td>Use case only, when <code>manifest.useCases[x].audits</code> is non-empty</td>
</tr>
<tr>
<td><code>withAnalytics</code></td>
<td>Forwarding wrapper that attaches the <code>__analyzed</code> brand. Automated analytics recording from manifest declarations is reserved.</td>
<td>Use case only, when <code>manifest.useCases[x].analyticsEvents</code> is declared</td>
</tr>
<tr>
<td><code>withConsent</code></td>
<td>Gates execution on the consent categories declared by the manifest; throws / no-ops when consent is missing.</td>
<td>Use case only, when <code>manifest.useCases[x].requiresConsent</code> is declared</td>
</tr>
<tr>
<td><code>withRateLimit</code></td>
<td>Innermost of the optional wrappers — consults the rate-limit budgets and rejects before <code>factory(deps)</code> runs.</td>
<td>Use case only, when <code>manifest.useCases[x].rateLimit</code> is declared</td>
</tr>
</tbody>
</table>

View File

@@ -729,7 +729,7 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
</div>
<div class="title-block">
<h1 class="title">Feature<br><em>Conformance</em></h1>
<p class="lede">A four-layer feedback system for AI agents writing features. The manifest is the source of truth; the compiler, the editor, the dev server, and CI are the agent's correction signal — fast, structured, layered.</p>
<p class="lede">A five-layer feedback system for AI agents writing features. The manifest is the source of truth; the compiler, the editor, the dev server, CI, and the fallow audit are the agent's correction signal — fast, structured, layered.</p>
</div>
<div class="contents-row">
<span class="contents-label">Contents</span>
@@ -737,7 +737,7 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
<li><a href="#drift"><span class="num">01</span>Drift</a></li>
<li><a href="#agent-loop"><span class="num">02</span>Agent Loop</a></li>
<li><a href="#manifest"><span class="num">03</span>Manifest</a></li>
<li><a href="#layers"><span class="num">04</span>Four Layers</a></li>
<li><a href="#layers"><span class="num">04</span>Five Layers</a></li>
<li><a href="#mistakes"><span class="num">05</span>Mistakes</a></li>
<li><a href="#composition"><span class="num">06</span>Composition</a></li>
<li><a href="#milestones"><span class="num">07</span>Build Order</a></li>
@@ -765,7 +765,7 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
<div class="boundary-card">
<h3>Every convention is a <em>promise</em>. Every promise needs a <em>watchman</em>.</h3>
<p>This repo already enforces some promises: <em class="h">no-handler-reexport</em>, <em class="h">no-direct-socket-io</em>, the PII grep gate in CI. Those are watchmen. The question is how to scale the watchman model so that every new core capability — audit, events, realtime, observability — gets the same enforcement quality, and so that the resulting signals are sharp enough for an <em class="h">AI agent</em> to read, understand, and act on without human triage.</p>
<p>The answer below is a single declarative primitive (a feature manifest) read by four independent enforcement layers, each catching what the layer above missed. The earlier the layer fires, the tighter the agent's correction loop.</p>
<p>The answer below is a single declarative primitive (a feature manifest) read by five independent enforcement layers, each catching what the layer above missed. The earlier the layer fires, the tighter the agent's correction loop.</p>
</div>
</div>
</section>
@@ -777,7 +777,7 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
<span class="section-num">§ 02</span>
<div>
<h2 class="section-title">Built for the <em>agent loop</em>.</h2>
<p class="section-blurb">An AI agent writing a new feature does not have intuition, taste, or memory of last quarter's incident. It has exactly what the toolchain emits — diagnostics, exit codes, stack traces, and the contents of error messages. Every design choice in the four layers below is shaped by that single fact: the system's output is the agent's correction signal.</p>
<p class="section-blurb">An AI agent writing a new feature does not have intuition, taste, or memory of last quarter's incident. It has exactly what the toolchain emits — diagnostics, exit codes, stack traces, and the contents of error messages. Every design choice in the five layers below is shaped by that single fact: the system's output is the agent's correction signal.</p>
</div>
</div>
@@ -803,7 +803,7 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
<div style="border: 1px solid var(--rule-strong); background: var(--paper-3); padding: 24px 26px; border-radius: 4px; border-left: 4px solid var(--accent);">
<div style="font-family: 'JetBrains Mono', monospace; font-size: 10.5px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--accent); margin-bottom: 10px;">Loop property</div>
<h3 style="font-family: 'Fraunces', serif; font-variation-settings: 'opsz' 22, 'SOFT' 30, 'wght' 460; font-size: 20px; line-height: 1.1; margin: 0 0 12px; letter-spacing: -0.005em;">Layered</h3>
<p style="font-family: 'Fraunces', serif; font-variation-settings: 'opsz' 14, 'SOFT' 40; font-size: 14px; line-height: 1.5; color: var(--ink-2); margin: 0;">Four checks at four moments. If one is silenced or skipped, another catches it. No silent passes — exactly one layer must complain.</p>
<p style="font-family: 'Fraunces', serif; font-variation-settings: 'opsz' 14, 'SOFT' 40; font-size: 14px; line-height: 1.5; color: var(--ink-2); margin: 0;">Five checks at five moments. If one is silenced or skipped, another catches it. No silent passes — exactly one layer must complain.</p>
</div>
</div>
@@ -844,18 +844,37 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
</div>
</div>
<pre class="code" data-lang="packages/features/auth/src/feature.manifest.ts"><span class="k">import</span> { defineFeature } <span class="k">from</span> <span class="s">"@repo/core-shared/conformance"</span>;
<pre class="code" data-lang="packages/auth/src/feature.manifest.ts"><span class="k">import</span> { defineFeature } <span class="k">from</span> <span class="s">"@repo/core-shared/conformance"</span>;
<span class="k">export const</span> <span class="t">authManifest</span> = <span class="t">defineFeature</span>({
<span class="n">name</span>: <span class="s">"auth"</span>,
<span class="n">requiredCores</span>: [<span class="s">"audit"</span>, <span class="s">"events"</span>], <span class="c">// hard deps</span>
<span class="n">useCases</span>: {
<span class="n">signIn</span>: { <span class="n">mutates</span>: <span class="b">false</span>, <span class="n">audits</span>: [], <span class="n">publishes</span>: [], <span class="n">consumes</span>: [] },
<span class="n">signUp</span>: { <span class="n">mutates</span>: <span class="b">true</span>, <span class="n">audits</span>: [<span class="s">"user.created"</span>], <span class="n">publishes</span>: [<span class="s">"auth.signed-up"</span>], <span class="n">consumes</span>: [] },
<span class="n">signOut</span>: { <span class="n">mutates</span>: <span class="b">true</span>, <span class="n">audits</span>: [<span class="s">"session.ended"</span>], <span class="n">publishes</span>: [], <span class="n">consumes</span>: [] },
<span class="n">signIn</span>: {
<span class="n">mutates</span>: <span class="b">false</span>, <span class="n">audits</span>: [], <span class="n">publishes</span>: [], <span class="n">consumes</span>: [],
<span class="n">rateLimit</span>: [ <span class="c">// optional: per-use-case budgets</span>
{ <span class="n">name</span>: <span class="s">"ip"</span>, <span class="n">window</span>: <span class="s">"1m"</span>, <span class="n">budget</span>: <span class="b">5</span> },
{ <span class="n">name</span>: <span class="s">"account"</span>, <span class="n">window</span>: <span class="s">"1h"</span>, <span class="n">budget</span>: <span class="b">10</span> },
],
},
<span class="n">signUp</span>: {
<span class="n">mutates</span>: <span class="b">true</span>, <span class="n">audits</span>: [<span class="s">"user.created"</span>], <span class="n">publishes</span>: [<span class="s">"auth.signed-up"</span>], <span class="n">consumes</span>: [],
<span class="n">analyticsEvents</span>: [<span class="s">"auth.signup.completed"</span>], <span class="c">// optional: declares withAnalytics emissions</span>
},
<span class="n">signOut</span>: { <span class="n">mutates</span>: <span class="b">true</span>, <span class="n">audits</span>: [<span class="s">"session.ended"</span>], <span class="n">publishes</span>: [], <span class="n">consumes</span>: [] },
},
<span class="n">realtimeChannels</span>: [],
<span class="n">jobs</span>: [<span class="s">"auth.welcome-email"</span>],
<span class="n">coverage</span>: { <span class="c">// optional: ADR-020 coverage bands</span>
<span class="n">bands</span>: {
<span class="n">baseline</span>: { <span class="n">statements</span>: <span class="b">80</span>, <span class="n">branches</span>: <span class="b">75</span>, <span class="n">functions</span>: <span class="b">80</span>, <span class="n">lines</span>: <span class="b">80</span> },
<span class="n">entities</span>: { <span class="n">statements</span>: <span class="b">100</span>, <span class="n">branches</span>: <span class="b">100</span>, <span class="n">functions</span>: <span class="b">100</span>, <span class="n">lines</span>: <span class="b">100</span> },
<span class="s">"use-cases"</span>: { <span class="n">statements</span>: <span class="b">100</span>, <span class="n">branches</span>: <span class="b">95</span>, <span class="n">functions</span>: <span class="b">100</span>, <span class="n">lines</span>: <span class="b">100</span> },
<span class="n">controllers</span>: { <span class="n">statements</span>: <span class="b">100</span>, <span class="n">branches</span>: <span class="b">95</span>, <span class="n">functions</span>: <span class="b">100</span>, <span class="n">lines</span>: <span class="b">100</span> },
},
<span class="n">mutationTargets</span>: [<span class="s">"entities"</span>, <span class="s">"use-cases"</span>], <span class="c">// L3 mutation testing surface</span>
},
<span class="c">// requiresConsent?: [...] — optional: consent categories gated by withConsent</span>
} <span class="k">as const</span>);</pre>
<div class="playground">
@@ -963,14 +982,14 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
</div>
</section>
<!-- ═════════════ §03 Four enforcement points ═════════════ -->
<!-- ═════════════ §04 Five enforcement points ═════════════ -->
<section id="layers">
<div class="frame">
<div class="section-head">
<span class="section-num">§ 04</span>
<div>
<h2 class="section-title">Four <em>enforcement</em> points.</h2>
<p class="section-blurb">Each layer runs in a different process at a different moment, and so each layer sees something the others don't. The compiler sees types but not runtime calls. ESLint sees the AST but not bindings. The dev server sees the wired container but not what's missing. CI sees the whole repo. Composed, they catch nearly every drift class.</p>
<h2 class="section-title">Five <em>enforcement</em> points.</h2>
<p class="section-blurb">Each layer runs in a different process at a different moment, and so each layer sees something the others don't. The compiler sees types but not runtime calls. ESLint sees the AST but not bindings. The dev server sees the wired container but not what's missing. CI sees the whole repo. Fallow sees the whole codebase — dead exports, duplicates, circular deps, complexity hotspots that none of the other four are shaped to catch. Composed, they catch nearly every drift class.</p>
</div>
</div>
@@ -981,6 +1000,7 @@ footer.colophon ul li a { font-family: "JetBrains Mono", monospace; font-size: 1
<button data-layer="eslint" aria-pressed="false">editor</button>
<button data-layer="boot" aria-pressed="false">dev server</button>
<button data-layer="ci" aria-pressed="false">ci</button>
<button data-layer="fallow" aria-pressed="false">fallow</button>
</div>
</div>
@@ -1039,7 +1059,7 @@ bind&lt;<span class="t">ISignUpUseCase</span>&gt;(SYMBOL).toDynamicValue&lt;
<span class="section-num">§ 05</span>
<div>
<h2 class="section-title">A catalog of <em>mistakes</em>.</h2>
<p class="section-blurb">A working enforcement system is best judged by the mistakes it catches and where. Below is a matrix of common drift patterns mapped to the four layers — click any row to see the actual error message each layer surfaces. The earlier the catch, the cheaper the fix; the rightmost catch is the last line of defence.</p>
<p class="section-blurb">A working enforcement system is best judged by the mistakes it catches and where. Below is a matrix of common drift patterns mapped to the first four layers — click any row to see the actual error message each layer surfaces. (Fallow, the fifth layer, catches a different class of drift — accretion rather than contradiction — and is covered in §04.) The earlier the catch, the cheaper the fix; the rightmost catch is the last line of defence.</p>
</div>
</div>
@@ -1122,7 +1142,7 @@ bind&lt;<span class="t">ISignUpUseCase</span>&gt;(SYMBOL).toDynamicValue&lt;
<span class="section-num">§ 07</span>
<div>
<h2 class="section-title">Build <em>order</em>.</h2>
<p class="section-blurb">Four independently shippable milestones. Build them in this order because each is small on its own, and each catches mistakes the next milestone otherwise has to handle. Stop after any milestone and the remainder still works as a manual checklist.</p>
<p class="section-blurb"><em>Historical — this was the original build plan; the work is complete.</em> Four independently shippable milestones. Built in this order because each was small on its own, and each catches mistakes the next milestone otherwise has to handle. Stop after any milestone and the remainder still works as a manual checklist.</p>
</div>
</div>
@@ -1167,7 +1187,7 @@ bind&lt;<span class="t">ISignUpUseCase</span>&gt;(SYMBOL).toDynamicValue&lt;
<span class="section-num">§ 08</span>
<div>
<h2 class="section-title">Anchor points <em>already here</em>.</h2>
<p class="section-blurb">Most of the foundation for this system already exists in <code>template-vertical</code>. The list below shows what's in place (green), what's partially there (amber), and what needs to be built fresh (red). The plan is to extend existing muscle, not introduce a parallel mechanism.</p>
<p class="section-blurb"><em>Historical — written before the system was built; the &ldquo;missing&rdquo; pieces below have since been built on top of the green anchors.</em> Most of the foundation for this system already existed in <code>template-vertical</code>. The list below shows what was in place (green), what was partially there (amber), and what needed to be built fresh (red). The plan was to extend existing muscle, not introduce a parallel mechanism — and that's what shipped.</p>
</div>
</div>
@@ -1224,14 +1244,14 @@ bind&lt;<span class="t">ISignUpUseCase</span>&gt;(SYMBOL).toDynamicValue&lt;
</div>
</section>
<!-- ═════════════ §09 Beyond the four ═════════════ -->
<!-- ═════════════ §09 Beyond the five ═════════════ -->
<section id="extensions">
<div class="frame">
<div class="section-head">
<span class="section-num">§ 09</span>
<div>
<h2 class="section-title">Beyond the <em>four</em>.</h2>
<p class="section-blurb">Two natural extensions came out of design conversations — sharper AST machinery, and a separate story for code conventions. Both lean on the same four-layer chassis; neither requires a new tool.</p>
<h2 class="section-title">Beyond the <em>five</em>.</h2>
<p class="section-blurb">Two natural extensions came out of design conversations — sharper AST machinery, and a separate story for code conventions. Both lean on the same five-layer chassis; neither requires a new tool.</p>
</div>
</div>
@@ -1277,7 +1297,7 @@ bind&lt;<span class="t">ISignUpUseCase</span>&gt;(SYMBOL).toDynamicValue&lt;
<span class="section-num">§ 10</span>
<div>
<h2 class="section-title">Open <em>questions</em>.</h2>
<p class="section-blurb">Decisions to make before the spec is written. None block starting milestone i, but the answers shape how the manifest is shaped and which layer carries which check.</p>
<p class="section-blurb"><em>Historical — these were the open questions before the spec was written; all five have since been answered in code. Kept here for the reasoning trail.</em> Decisions to make before the spec is written. None block starting milestone i, but the answers shape how the manifest is shaped and which layer carries which check.</p>
</div>
</div>
@@ -1333,7 +1353,7 @@ bind&lt;<span class="t">ISignUpUseCase</span>&gt;(SYMBOL).toDynamicValue&lt;
<div>
<h4>Colophon</h4>
<p>Fraunces (display + body) &amp; JetBrains Mono (code) over Linseed Mill.</p>
<p style="margin-top: 12px;">An explainer of a proposed enforcement system. Not yet implemented. Read alongside the ADRs.</p>
<p style="margin-top: 12px;">Shipped — this page now reflects the live system. <code>defineFeature</code>, <code>wireUseCase</code>, <code>assertFeatureConformance</code>, the fifteen ESLint rules, and the <code>pnpm conformance</code> / <code>pnpm fallow</code> gates are all in the repo. Read alongside the ADRs.</p>
</div>
<div>
<h4>Sibling explainers</h4>
@@ -1719,6 +1739,52 @@ bind&lt;<span class="t">ISignUpUseCase</span>&gt;(SYMBOL).toDynamicValue&lt;
}
}`,
},
fallow: {
title: 'Whole-codebase <em>fallow audit</em>',
latency: '~3060s',
tag: 'post-eslint',
heading: 'The view that <em>no single rule</em> can take.',
body: `
<p>The four layers above all read a single file, a single binding, or the cross-feature event graph. None of them are shaped to ask the corpus-level question: <em>which exports nobody imports? which functions are duplicated? which imports form a cycle? which file is now too complex to safely touch?</em> Fallow is that fifth eye.</p>
<p><code>pnpm fallow</code> wraps <code>knip</code> (dead exports / unused files), <code>jscpd</code> (duplicate code), <code>madge</code> (circular deps), and a complexity scan into one whole-codebase pass that runs after ESLint. <code>pnpm fallow:audit</code> is the AI-change audit variant — run it before committing agent-authored work to catch drift the per-file rules can't see.</p>
<ul>
<li>Catches what shape-rules miss: <em>accretion</em>, not <em>contradiction</em>.</li>
<li>Runs post-ESLint locally; gated in CI; surfaced as a pre-commit advisory.</li>
<li>Slow enough that it doesn't fire on save — fast enough to run before every commit.</li>
</ul>`,
catches: 'dead exports &amp; unused files; duplicate code blocks; circular module dependencies; complexity hotspots; AI-change audit drift.',
codeLang: 'pnpm fallow — composed audit',
code: `<span class="c">// scripts/fallow.ts — orchestrates the whole-codebase audit.</span>
<span class="k">import</span> { runKnip } <span class="k">from</span> <span class="s">"./fallow/knip"</span>; <span class="c">// dead exports + files</span>
<span class="k">import</span> { runJscpd } <span class="k">from</span> <span class="s">"./fallow/jscpd"</span>; <span class="c">// duplicate blocks</span>
<span class="k">import</span> { runMadge } <span class="k">from</span> <span class="s">"./fallow/madge"</span>; <span class="c">// circular deps</span>
<span class="k">import</span> { runComplex } <span class="k">from</span> <span class="s">"./fallow/complexity"</span>; <span class="c">// hotspots</span>
<span class="k">const</span> findings = [
...<span class="k">await</span> <span class="h">runKnip</span>(),
...<span class="k">await</span> <span class="h">runJscpd</span>(),
...<span class="k">await</span> <span class="h">runMadge</span>(),
...<span class="k">await</span> <span class="h">runComplex</span>(),
];
<span class="k">if</span> (findings.length &gt; 0) {
<span class="k">for</span> (<span class="k">const</span> f <span class="k">of</span> findings) <span class="h">report</span>(f);
process.exit(1);
}
<span class="c">// Sample output an agent reads:</span>
<span class="err">fallow: knip</span>
<span class="b">packages/auth/src/repositories/legacy.repository.ts</span>
Export 'findByLegacyId' is declared but never imported.
<span class="err">fallow: jscpd</span>
<span class="b">packages/blog/src/use-cases/list-articles.usecase.ts:18-44</span>
<span class="b">packages/media/src/use-cases/list-media.usecase.ts:22-48</span>
27-line duplicate block. Extract or accept.
<span class="err">fallow: madge</span>
Cycle: features/auth → core-shared → features/auth (via di/bind-production)`,
},
};
function show(layerKey) {

View File

@@ -111,12 +111,19 @@ repo/
storybook/ # unchanged; updates imports from @repo/ui → @repo/core-ui
packages/
# ─── CORE (foundation, tagged "core") ───
# ─── CORE — must-have (tagged "core" / "core-composition") ───
core-shared/ # generic primitives (no business knowledge)
core-cms/ # Payload composition only (aggregates feature cms exports)
core-api/ # tRPC composition only (aggregates feature api exports)
# ─── CORE — optional (scaffold via `pnpm turbo gen core-package <name>`) ───
core-trpc/ # frontend tRPC platform (client, providers per framework)
core-ui/ # design-system primitives (atoms/molecules/templates)
core-events/ # in-memory + Payload-backed event bus + job queue (ADR-015)
core-realtime/ # Socket.IO broadcaster + handler registry (ADR-016)
core-audit/ # DPA-compliant audit logging (ADR-018)
core-analytics/ # product analytics capture channel (ADR-024)
core-consent/ # consent + cookie banner (ADR-025)
core-dsr/ # data-subject-rights — export/delete/rectify/restrict (ADR-025)
# ─── FEATURES (business capabilities, tagged "feature") ───
auth/ # Users collection + sign-in/up/out
@@ -126,8 +133,9 @@ repo/
navigation/ # header global
# ─── TOOLING (tagged "tooling") ───
core-eslint/
core-typescript/
core-eslint/ # ESLint preset + the 15 conformance rules + boundary rules
core-typescript/ # tsconfig presets (base, react-library, nextjs)
core-testing/ # factories, contract suites, recording test doubles
docs/
architecture/
@@ -135,7 +143,7 @@ repo/
dependency-flow.md # rewritten
vertical-feature-spec.md # copy of source spec
decisions/
adr-001 … adr-009 # five existing + four new (see §10)
adr-001 … adr-NNN # 25 ADRs at time of writing — see §11 and `docs/decisions/`
guides/
adding-a-feature.md # rewritten
testing-strategy.md # rewritten
@@ -469,6 +477,8 @@ A feature may import another feature's **public exports** — its `@repo/<featur
### 9.5 Root `turbo.json` (unchanged concept)
The snippet below shows the original task shape. The live `turbo.json` has evolved — additional tasks (`conformance`, `fallow`, `boundaries`, `test:stories`, `build-storybook`), tweaks to `dependsOn` (`test` and `typecheck` no longer depend on `^build`), and the `boundaries.tags` block enforcing the dependency matrix (see §9.2). See the actual root `turbo.json` for the authoritative shape; the principle below is unchanged.
```json
{
"tasks": {
@@ -543,7 +553,7 @@ No shared `initializeContainer()` / `destroyContainer()`.
### 10.4 Actual test coverage
- **360 tests across 26 packages** (`pnpm test` green as of 2026-05-06)
- `pnpm test` runs green across every workspace package. Coverage thresholds are declared per-feature in `feature.manifest.ts` (ADR-020) — `entities` and `use-cases` at 100% statements/branches/functions/lines, `controllers` at 100/95/100/100, with a baseline of 80/75/80/80 elsewhere. The four coverage layers (L0 thresholds → L1 diff coverage → L2 aggregate trend → L3 mutation) are documented in `docs/guides/coverage.md`.
Key coverage areas:
@@ -583,6 +593,8 @@ Identity presenters do not require R27/R28 tests. Void-output controllers (e.g.,
## 11. Docs + ADR strategy
> **Historical.** This section captures the ADR strategy at the time of the vertical-feature refactor — 5 existing ADRs (001005), 4 new ADRs (006009), and the 2 post-spec ADRs (012013). The **canonical, current ADR set** lives in `docs/decisions/` and now spans **25 ADRs** — adding boundaries (010), TDD foundation (011), instrumentation + OpenTelemetry (014, 017), events / realtime / audit (015, 016, 018), Sandcastle (019), coverage (020), hybrid versioning (021), library policy + CI security (022, 023), product analytics (024), and the EU compliance baseline (025). Read `docs/decisions/` for the authoritative list; the tables below are preserved as the refactor's original record.
### 11.1 Existing ADRs
| File | Action | Notes |