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

@@ -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) {