docs(arch): add §05 'Tradeoffs by part' to data-flow explainer

Each folder/file in the feature anatomy gets its own card with:
- Brief one-line description
- Two-column pros/cons list (3-4 items each)

Covers 22 parts grouped into 21 cards: entities/models, entities/errors,
application/repositories interfaces, application/services interfaces,
application/use-cases, infrastructure real repo, infrastructure mock
repo, infrastructure services, interface-adapters/controllers, di/
(symbols, module, container, bind-production, bind-dev-seed),
integrations/api/procedures, integrations/api/router, integrations/cms,
ui, __factories__, __contracts__, __seeds__, src/index.ts.

Each card uses the same paper/cream palette as the rest of the page;
pros/cons differentiated by color (forest-green + / oxblood −) and a
JetBrains-Mono leading character.

Verdict section renumbered to §06; contents nav updated to 6-column
grid.
This commit is contained in:
2026-05-06 19:59:16 +02:00
parent 8f34daca36
commit b90570b62f

View File

@@ -1123,6 +1123,136 @@ ul.role-jobs > li code {
.role-factory { width: 100%; } .role-factory { width: 100%; }
} }
/* ─── Tradeoffs by part ──────────────────────────────────────────────── */
.tradeoffs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 22px;
margin-top: 8px;
}
.tradeoff-card {
border: 1px solid var(--rule-strong);
background: var(--paper-3);
padding: 26px 30px;
border-radius: 4px;
display: flex;
flex-direction: column;
}
.tradeoff-card .tag {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 10px;
word-break: break-all;
}
.tradeoff-card h3 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 30, "SOFT" 30, "wght" 440;
font-size: 24px;
line-height: 1.1;
letter-spacing: -0.005em;
margin: 0 0 10px;
}
.tradeoff-card .blurb {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 14, "SOFT" 40;
font-size: 14px;
line-height: 1.5;
color: var(--ink-2);
margin: 0 0 18px;
font-style: italic;
}
.tradeoff-card .blurb code {
font-family: "JetBrains Mono", monospace;
font-style: normal;
font-size: 12px;
background: var(--paper-2);
padding: 1px 5px;
border-radius: 2px;
}
.pc-cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 22px;
padding-top: 14px;
border-top: 1px dashed var(--rule);
flex: 1;
}
.pc-cols h5 {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
margin: 0 0 10px;
font-weight: 500;
}
.pc-cols .pros h5 { color: var(--ok); }
.pc-cols .cons h5 { color: var(--accent); }
.pc-cols ul {
list-style: none;
padding: 0;
margin: 0;
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 14, "SOFT" 40;
font-size: 13.5px;
line-height: 1.5;
color: var(--ink);
}
.pc-cols li {
position: relative;
padding-left: 18px;
margin-bottom: 8px;
text-wrap: pretty;
}
.pc-cols .pros li::before {
content: "+";
position: absolute;
left: 0;
top: 0;
color: var(--ok);
font-family: "JetBrains Mono", monospace;
font-weight: 600;
font-size: 14px;
}
.pc-cols .cons li::before {
content: "";
position: absolute;
left: 0;
top: 0;
color: var(--accent);
font-family: "JetBrains Mono", monospace;
font-weight: 600;
font-size: 14px;
}
.pc-cols code {
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
background: var(--paper-2);
padding: 0 4px;
border-radius: 2px;
}
@media (max-width: 1100px) {
.tradeoffs-grid { grid-template-columns: 1fr; }
.pc-cols { grid-template-columns: 1fr; gap: 14px; }
}
/* ─── Verdict ────────────────────────────────────────────────────────── */ /* ─── Verdict ────────────────────────────────────────────────────────── */
.verdict-block { .verdict-block {
@@ -1297,12 +1427,13 @@ footer .colophon {
<div class="contents-row"> <div class="contents-row">
<div class="contents-label">Contents</div> <div class="contents-label">Contents</div>
<ol class="contents"> <ol class="contents" style="grid-template-columns: repeat(6, 1fr);">
<li><a href="#anatomy"><span class="num">01</span>Feature anatomy</a></li> <li><a href="#anatomy"><span class="num">01</span>Feature anatomy</a></li>
<li><a href="#flow"><span class="num">02</span>Request flow</a></li> <li><a href="#flow"><span class="num">02</span>Request flow</a></li>
<li><a href="#di"><span class="num">03</span>Dependency injection</a></li> <li><a href="#di"><span class="num">03</span>Dependency injection</a></li>
<li><a href="#contracts"><span class="num">04</span>Mocks, contracts &amp; factories</a></li> <li><a href="#contracts"><span class="num">04</span>Mocks, contracts &amp; factories</a></li>
<li><a href="#verdict"><span class="num">05</span>The verdict</a></li> <li><a href="#tradeoffs"><span class="num">05</span>Tradeoffs by part</a></li>
<li><a href="#verdict"><span class="num">06</span>The verdict</a></li>
</ol> </ol>
</div> </div>
</header> </header>
@@ -1768,10 +1899,416 @@ footer .colophon {
</div> </div>
</section> </section>
<!-- ─── 05 ─ VERDICT ─────────────────────────────────────────────────── --> <!-- ─── 05 ─ TRADEOFFS BY PART ──────────────────────────────────────── -->
<section id="verdict" style="border-bottom: 0;"> <section id="tradeoffs">
<div class="section-head"> <div class="section-head">
<div class="section-num">§ 05</div> <div class="section-num">§ 05</div>
<div>
<h2 class="section-title">Tradeoffs <em>by part</em>.</h2>
<p class="section-blurb">Every layer in the feature anatomy gives you something and costs you something. This is the honest accounting — what each folder or file buys, what it asks in return. Read this when you're deciding whether to add a piece, not all at once.</p>
</div>
</div>
<div class="tradeoffs-grid">
<div class="tradeoff-card">
<div class="tag">entities/models/</div>
<h3>Entity Zod schemas + types</h3>
<p class="blurb">One file per entity (<code>article.ts</code>, <code>user.ts</code>): a Zod schema and the inferred TypeScript type.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>One source of truth: schema and type from a single declaration via <code>z.infer</code></li>
<li>Runtime validation available anywhere the schema is imported (output schemas, parser-transforms)</li>
<li>Pure-domain — zero framework knowledge, infinitely portable</li>
<li>Test factories build against this same shape, can't drift</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Adds Zod as a feature-level dep where a plain TS interface would suffice</li>
<li>Schema can drift from Payload's actual collection shape — must keep in sync manually</li>
<li>Overkill for entities that never get parsed at runtime</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">entities/errors/</div>
<h3>Domain error classes</h3>
<p class="blurb">One file per error domain (<code>article.ts</code>, <code>auth.ts</code>) plus <code>common.ts</code> for <code>InputParseError</code>.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Domain errors carry meaning — <code>ArticleNotFoundError</code> beats a generic <code>Error</code> by miles</li>
<li><code>defineErrorMiddleware</code> matches by <code>instanceof</code> and translates to <code>TRPCError</code> codes</li>
<li>Per-feature ownership — auth's errors don't leak into blog</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Every constructor must set <code>this.name</code> (R6) — easy to forget, was the systemic Plan-9 fix-up</li>
<li><code>InputParseError</code> is duplicated per feature (~6 lines × 5) — by design, but feels redundant</li>
<li>Adding a new error class = update the feature's <code>procedures.ts</code> map too</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">application/repositories/</div>
<h3>Repository interfaces</h3>
<p class="blurb"><code>&lt;x&gt;.repository.interface.ts</code> — TypeScript interface, no implementation, no Zod.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Use cases depend on the contract, not a concrete class — testable, swappable</li>
<li>Mock and real impl share the interface — TypeScript catches drift at compile time</li>
<li>Plain TypeScript — no extra deps, no runtime cost</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>One more file per repo (interface + real + mock = 3 minimum)</li>
<li>Method names get written twice (interface + each impl) — refactors touch both</li>
<li>Wider repo signatures vs narrower use-case schemas can feel duplicative</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">application/services/</div>
<h3>Service interfaces</h3>
<p class="blurb"><code>&lt;x&gt;.service.interface.ts</code> — interface for non-data-access boundaries (auth, email, …).</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Same testability win as repos — stub the boundary, inject the stub</li>
<li>Lets stateful behavior (sessions, password hashing) sit behind a clean contract</li>
<li>Use cases don't import auth lib internals — they call methods on an interface</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Easy to over-create services for thin one-method wrappers</li>
<li>The "service" name is fuzzy — can become a junk drawer of "anything that's not a repo"</li>
<li>Most features don't need any (only auth has one today)</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">application/use-cases/</div>
<h3>Factory-function use cases</h3>
<p class="blurb">One file per verb-noun (<code>get-articles.use-case.ts</code>): input + output schemas + factory.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Single source of truth for I/O contracts (R1) — schema lives here, controllers and routers import</li>
<li><code>xOutputSchema.parse(...)</code> at the end of the body catches malformed repo returns at the layer that owns the contract</li>
<li>Factory function = trivially testable; tests construct mocks and inject directly</li>
<li>One concern per file; clear single responsibility</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>~30 lines of file overhead per use case (input + output + types + factory)</li>
<li>Identity output schemas (<code>z.array(articleSchema)</code>) feel ceremonial when they don't add validation</li>
<li>Many files per feature (blog has 3, media has 3, marketing-pages has 2)</li>
<li><code>.parse()</code> on every call has measurable cost on hot paths (negligible in practice)</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">infrastructure/repositories/&lt;x&gt;.repository.ts</div>
<h3>Real (Payload-backed) repository</h3>
<p class="blurb">Constructor takes <code>SanitizedConfig</code>, methods call <code>getPayload({ config })</code>, map to domain.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>All Payload knowledge lives in one place per repo — easy to swap CMS later</li>
<li>Constructor injection keeps the feature boundary clean (no <code>core-cms</code> dep)</li>
<li>Class names without <code>Payload</code> prefix — DI swaps mock ↔ real cleanly</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Per-method <code>getPayload({ config })</code> is repetitive</li>
<li><code>toDomain</code> mappers are easy to forget for new fields → silent shape drift</li>
<li>Tests need <code>vi.mock("payload")</code> + Payload stub setup — more ceremony than testing the mock impl</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">infrastructure/repositories/&lt;x&gt;.repository.mock.ts</div>
<h3>Mock repository (sibling of real)</h3>
<p class="blurb">In-memory implementation. The default DI binding; also injected directly in unit tests.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Dev mode runs without Payload booted — <code>pnpm dev</code> just works</li>
<li>Direct test injection — no DI ceremony for use-case / controller tests</li>
<li>Same interface as real impl; contract suite proves behavioral parity</li>
<li>Fast and deterministic — no setup, no fixtures, no I/O</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Without the contract suite, mock can silently diverge from real (e.g. ID-uniqueness assumptions)</li>
<li>Doubles the repo file count (one real + one mock per repo)</li>
<li>Easy to give the mock "extra" behavior the real impl can't match (false confidence)</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">infrastructure/services/</div>
<h3>Real + mock services</h3>
<p class="blurb">Same dual-impl pattern as repositories. Auth has one (real <code>AuthenticationService</code> + mock).</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Lets stateful boundary code (session creation, password hashing) be unit-tested with a fake</li>
<li>Real impl can defer hard parts as <code>NotImplementedError</code> while the mock fully works (auth's session methods do this today)</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Adds a directory tree most features don't need</li>
<li>"Service" abstraction can hide what's actually being mocked — repos are clearer</li>
<li>Deferred-real-impl pattern is honest but technical-debt-shaped</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">interface-adapters/controllers/</div>
<h3>Factory controllers + presenter</h3>
<p class="blurb">One file per use case. Receives <code>unknown</code>, <code>safeParse</code>s, calls use case, runs presenter.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Transport-agnostic — same controller works from tRPC, CLI, server actions, cron</li>
<li>Owns input parsing — <code>InputParseError</code> is the controller's responsibility, never the use case's</li>
<li>Co-located <code>function presenter</code> means view-shape transforms live next to the wire</li>
<li>One controller per use case — clear single responsibility, easy to test</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Schema runs twice on the tRPC path (procedure <code>.input</code> + controller <code>safeParse</code>) — defense in depth has a cost</li>
<li>Identity presenters feel ceremonial when no transform is needed (R11 always-present rule)</li>
<li>Per-use-case files multiply — blog has 3, marketing-pages has 2</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">di/symbols.ts</div>
<h3>The address book</h3>
<p class="blurb">Plain object of <code>Symbol.for("blog:I…")</code> keys. One per binding the container holds.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li><code>Symbol.for</code> namespacing prevents cross-feature collisions</li>
<li>Type-erased keys let the container index without forcing eager class imports</li>
<li>Const object — every binding has a compile-time-checked key</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Adding a use case = update three places: factory, symbols, module binding</li>
<li>Symbols carry no type info at runtime — bind/get must agree on the type parameter (footgun)</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">di/module.ts</div>
<h3>Default binding registry</h3>
<p class="blurb"><code>ContainerModule</code> with all repository, service, use-case, controller bindings.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Declarative — every binding visible in one block; easy to audit</li>
<li><code>.toDynamicValue</code> is what makes factory functions work as DI bindings</li>
<li>Loaded once at module construction; nothing runs per-request</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Verbose — each <code>.toDynamicValue((ctx) =&gt; factoryFn(ctx.container.get(...)))</code> repeats boilerplate</li>
<li>Imports every concrete class + factory in the feature — large surface area in one file</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">di/container.ts</div>
<h3>The singleton</h3>
<p class="blurb">Three lines: <code>reflect-metadata</code>, <code>new Container({ defaultScope: "Singleton" })</code>, <code>load(Module)</code>.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Singleton scope = automatic caching; subsequent <code>.get()</code> calls reuse the closure</li>
<li>One-line file — almost no maintenance</li>
<li>Per-feature container = vertical isolation; no cross-feature DI coupling (ADR-008)</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Module-level singleton = global state; tests must <code>unbindAll()</code> + reload to start fresh</li>
<li><code>import "reflect-metadata"</code> is a side-effect import — easy to forget when scaffolding new files</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">di/bind-production.ts</div>
<h3>Production binder</h3>
<p class="blurb"><code>bindProduction&lt;F&gt;(config)</code> — unbinds the mock, rebinds the real Payload-backed impl.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Decouples Payload config from the feature package — boundary stays clean</li>
<li>Idempotent (<code>isBound</code> guard) — safe to call multiple times</li>
<li>Only the repo binding swaps; use cases + controllers stay put and pick up the new repo automatically</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Easy to forget to call when wiring a new app — silent fallback to mock in production</li>
<li>One per feature × app — boilerplate compounds with feature count</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">di/bind-dev-seed.ts</div>
<h3>Dev-seed binder</h3>
<p class="blurb"><code>bindDevSeed&lt;F&gt;()</code> — unbinds the empty mock, rebinds a populated mock (post-Plan-9).</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Dev mode shows realistic data without Payload running — design review, storybook, offline work all just work</li>
<li>Reuses the same factory the tests use — no separate fixture system</li>
<li>Symmetric with <code>bind-production</code> — one mental model, two binders</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Yet another file per feature</li>
<li>Seed data drifts from real Payload shape over time — needs occasional refresh</li>
<li>Fourth place to update when entity schemas change (after schema, factory, repo)</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">integrations/api/procedures.ts</div>
<h3>Feature-scoped tRPC procedure</h3>
<p class="blurb"><code>xProcedure = t.procedure.use(defineErrorMiddleware([...]))</code> — owns the feature's error-to-code map.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Feature owns its error → <code>TRPCError</code> mapping — no central registry, no <code>core-shared</code> coupling</li>
<li>Adding an error class = one tuple in this file; type system guides you</li>
<li><code>defineErrorMiddleware</code> in <code>core-shared</code> is plumbing only — boundary stays clean</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Adds a fifth file to <code>integrations/api/</code> — a feature with two procedures has the same overhead as one with ten</li>
<li>The <code>InputParseError → BAD_REQUEST</code> tuple is dormant on the tRPC path (tRPC's own zod parse fires first) — feels theatrical</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">integrations/api/router.ts</div>
<h3>tRPC router slice</h3>
<p class="blurb">One file per feature, composed into <code>core-api</code>'s <code>appRouter</code> via the <code>./api</code> export.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li><code>xProcedure.input(xInputSchema)</code> — schemas imported from the use-case file, never redefined</li>
<li>One slice per feature — easy to add or remove a feature from the API</li>
<li>Container is the only thing the router knows about — controllers are resolved, not imported</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Router files grow proportional to procedure count — blog already has three</li>
<li>Routers can't be tested without DI involvement — <code>createCaller</code> goes through the container</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">integrations/cms/</div>
<h3>Payload collections + globals</h3>
<p class="blurb">Collection / global definitions exposed via the <code>./cms</code> export, composed into <code>core-cms</code>.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Each collection lives with its feature — no central <code>cms-core</code> grab-bag</li>
<li>Hooks for the feature's domain logic (revalidation, slugify) are co-located</li>
<li><code>core-cms</code> just composes; features stay independently versionable</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Coupling to Payload's API surface — swapping CMS later is non-trivial</li>
<li>Collection schema can drift from the entity Zod schema — two places to keep in sync</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">ui/</div>
<h3>Frontend public surface</h3>
<p class="blurb"><code>ui/index.ts</code> exports query builders + components. Apps import via <code>@repo/&lt;feature&gt;/ui</code>.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Apps depend on the feature's UI surface explicitly — clear separation from contracts</li>
<li>Query builders sit next to the data they query — refactoring is local</li>
<li>Components for feature-specific UI (e.g. <code>ArticleCard</code>) live with the feature, not in <code>core-ui</code></li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Features without UI today (auth, media) still need a placeholder <code>export {}</code></li>
<li>React Query API surface coupling — swapping client lib touches every feature</li>
<li>The <code>./ui</code> subpath is a fifth public-API entry to maintain per feature</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">__factories__/</div>
<h3>Test data factories</h3>
<p class="blurb">One factory per entity. Sequence-counter defaults; <code>.build({ overrides })</code>; <code>.reset()</code> in <code>beforeEach</code>.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Tests stop drowning in inline fixtures — only override what the assertion cares about</li>
<li>Sequence-driven defaults = deterministic IDs across runs</li>
<li>Shared between unit tests, contract suites, and <code>__seeds__/</code> — one source of "valid entity"</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Factory defaults can become outdated as entity schema evolves — Zod tightens, factory still hands out old shape, tests pass but production fails</li>
<li>Forgetting <code>.reset()</code> in <code>beforeEach</code> causes flaky test ordering</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">__contracts__/</div>
<h3>Repository contract suite</h3>
<p class="blurb">A portable test suite parameterized by <code>buildSubject</code>. Run against mock + real impl.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Catches mock / real divergence at unit-test time, not in production</li>
<li>The behavioral contract is a literal artifact, not folklore</li>
<li>Twelve <code>it()</code> blocks run twice — twenty-four assertions for the price of writing twelve</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>~50 lines of test boilerplate per repository</li>
<li>Adding a method to the interface means writing the contract test before either impl can pass</li>
<li>Easy to over-specify and lock implementation details (e.g. assert exact ID format the mock happens to produce)</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">__seeds__/</div>
<h3>Dev-seed data</h3>
<p class="blurb"><code>buildDev&lt;Entities&gt;()</code> — uses the feature's factory; consumed by <code>bind-dev-seed</code>.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>App-level realistic data without Payload running — storybook, design review, offline dev</li>
<li>Lazy function = side-effect-free at module load (factory sequence not advanced on import)</li>
<li>Reuses factory defaults — no parallel fixture system</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>The <code>__</code> prefix borrows from the test convention but this folder is reachable from runtime via DI — slightly mislabeled</li>
<li>Seed entities can rot — refreshes when entity schema or domain model changes</li>
<li>Yet another place where realistic data lives — tests, factories, seeds, real Payload all need to agree</li>
</ul></div>
</div>
</div>
<div class="tradeoff-card">
<div class="tag">src/index.ts (root)</div>
<h3>Public contract surface</h3>
<p class="blurb">The feature's <code>.</code> export — types, errors, schemas, IUseCase aliases, router type, constants.</p>
<div class="pc-cols">
<div class="pros"><h5>Pros</h5><ul>
<li>Single file lists everything the feature exposes — easy to audit, easy to enforce</li>
<li>Clean split from <code>./ui</code>: contracts here, UI artifacts there</li>
<li>Type aliases (<code>IXUseCase</code>, <code>IXController</code>) decouple consumers from the impl</li>
</ul></div>
<div class="cons"><h5>Cons</h5><ul>
<li>Maintenance burden — every new schema / error / type needs an explicit re-export</li>
<li>Easy to forget; downstream consumers can't reach a symbol that wasn't re-exported</li>
</ul></div>
</div>
</div>
</div>
</section>
<!-- ─── 06 ─ VERDICT ─────────────────────────────────────────────────── -->
<section id="verdict" style="border-bottom: 0;">
<div class="section-head">
<div class="section-num">§ 06</div>
<div> <div>
<h2 class="section-title">Do we <em>need</em> them?</h2> <h2 class="section-title">Do we <em>need</em> them?</h2>
</div> </div>