Files
agentic-dev/docs/architecture/data-flow-explainer.html
Danijel Martinek 449a4aedf5 docs(arch): rewrite §04 framing — drop 'mock is the surprising one'
User pushback: 'I already need mock data for dev and for testing at
runtime' — i.e., the dual role of the mock is obvious, not surprising.
Earlier framing was condescending.

Reframe around 'runtime reach':
- Section blurb leads with 'three artifacts at different distances
  from runtime'
- Three-roles card intro: same neighborhood, different reach. Mock is
  reached by runtime; contract + factory only by tests.
- 'The mock has two jobs' → 'The mock is reached from two directions
  — both legitimate, neither is the test version'.
- Contract framing now leads with its real purpose: it tests the mock
  alongside the real impl so you can trust the mock as a runtime
  artifact.
2026-05-06 18:46:23 +02:00

2471 lines
100 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1280">
<title>data-flow / template-vertical / explainer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT@0,9..144,300..900,0..100;1,9..144,300..900,0..100&family=JetBrains+Mono:ital,wght@0,400;0,500;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root {
--paper: #f4ecd8;
--paper-2: #ebe2cb;
--paper-3: #f9f3e3;
--paper-edge: #ddd0b1;
--ink: #1a1310;
--ink-2: #5b4a3c;
--ink-3: #8a7563;
--rule: rgba(26, 19, 16, 0.12);
--rule-strong: rgba(26, 19, 16, 0.28);
--accent: #7d2828;
--accent-soft: #b86a47;
--accent-2: #1f3d4a;
--accent-3: #c5912f;
--ok: #4a6a32;
--code-bg: #16110d;
--code-bg-2: #1d1814;
--code-paper: #ebe2cb;
--code-mute: #8a7563;
--code-accent: #d8a04a;
--measure: 66ch;
--col-narrow: 720px;
--col-wide: 1080px;
--col-flow: 1180px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--paper);
color: var(--ink);
font-family: "Fraunces", Georgia, serif;
font-feature-settings: "ss01", "ss02";
-webkit-font-smoothing: antialiased;
font-variation-settings: "opsz" 14, "SOFT" 50;
}
body {
position: relative;
background-image:
repeating-linear-gradient(0deg, transparent 0 31px, rgba(26,19,16,0.04) 31px 32px),
radial-gradient(circle at 22% 18%, rgba(125, 40, 40, 0.05), transparent 40%),
radial-gradient(circle at 88% 84%, rgba(31, 61, 74, 0.04), transparent 40%);
}
body::before {
content: "";
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><filter id='n'><feTurbulence baseFrequency='0.85' numOctaves='3'/><feColorMatrix values='0 0 0 0 0.10 0 0 0 0 0.07 0 0 0 0 0.05 0 0 0 0.10 0'/></filter><rect width='200' height='200' filter='url(%23n)'/></svg>");
pointer-events: none;
opacity: 0.45;
mix-blend-mode: multiply;
z-index: 1;
}
main, header, footer { position: relative; z-index: 2; }
a {
color: inherit;
text-decoration: underline;
text-decoration-thickness: 1px;
text-decoration-color: var(--rule-strong);
text-underline-offset: 3px;
transition: text-decoration-color 0.2s ease;
}
a:hover { text-decoration-color: var(--accent); }
/* ─── Layout ─────────────────────────────────────────────────────────── */
.frame {
max-width: 1280px;
margin: 0 auto;
padding: 0 56px;
}
.gutter-frame {
position: relative;
}
/* ─── Masthead ───────────────────────────────────────────────────────── */
header.masthead {
border-bottom: 1px solid var(--rule-strong);
padding-top: 48px;
padding-bottom: 12px;
}
.folio-row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-2);
border-bottom: 1px solid var(--rule);
padding-bottom: 12px;
margin-bottom: 64px;
}
.folio-row .folio-label {
display: inline-flex;
gap: 14px;
align-items: baseline;
}
.folio-row .folio-mark {
width: 9px; height: 9px;
background: var(--accent);
display: inline-block;
border-radius: 50%;
transform: translateY(1px);
}
.title-block {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 80px;
align-items: end;
padding-bottom: 56px;
border-bottom: 1px solid var(--rule);
}
h1.title {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 144, "SOFT" 0, "wght" 380;
font-size: clamp(64px, 7.5vw, 112px);
line-height: 0.92;
letter-spacing: -0.025em;
margin: 0 0 0 -6px;
color: var(--ink);
}
h1.title em {
font-style: italic;
font-variation-settings: "opsz" 144, "SOFT" 100, "wght" 380;
color: var(--accent);
}
h1.title .ampersand {
font-style: italic;
font-variation-settings: "opsz" 144, "SOFT" 100, "wght" 360;
color: var(--accent);
}
.lede {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 18, "SOFT" 30;
font-size: 17px;
line-height: 1.55;
color: var(--ink-2);
margin: 0;
max-width: 360px;
text-wrap: pretty;
}
.contents-row {
margin-top: 36px;
padding-top: 28px;
border-top: 1px dashed var(--rule);
display: grid;
grid-template-columns: 110px 1fr;
gap: 32px;
align-items: start;
}
.contents-label {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-3);
padding-top: 4px;
}
ol.contents {
list-style: none;
margin: 0; padding: 0;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 28px;
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.04em;
}
ol.contents li {
border-top: 1px solid var(--ink);
padding-top: 10px;
}
ol.contents a {
display: block;
text-decoration: none;
color: var(--ink);
}
ol.contents .num {
color: var(--accent);
display: block;
font-size: 11px;
margin-bottom: 6px;
letter-spacing: 0.18em;
}
ol.contents li:hover .num { color: var(--accent-2); }
/* ─── Section primitives ─────────────────────────────────────────────── */
main { padding-top: 56px; padding-bottom: 80px; }
section {
padding: 88px 0;
border-bottom: 1px solid var(--rule);
}
.section-head {
display: grid;
grid-template-columns: 110px 1fr;
gap: 32px;
align-items: baseline;
margin-bottom: 56px;
}
.section-num {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent);
padding-top: 12px;
}
.section-title {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 96, "SOFT" 0, "wght" 360;
font-size: clamp(40px, 5vw, 64px);
line-height: 0.96;
letter-spacing: -0.018em;
margin: 0 0 14px;
}
.section-title em {
font-style: italic;
font-variation-settings: "opsz" 96, "SOFT" 100, "wght" 360;
color: var(--accent);
}
.section-blurb {
max-width: 70ch;
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 40;
font-size: 16.5px;
line-height: 1.6;
color: var(--ink-2);
margin: 0;
text-wrap: pretty;
}
.section-blurb + .section-blurb { margin-top: 1em; }
p { line-height: 1.6; text-wrap: pretty; }
.col-row {
display: grid;
grid-template-columns: 110px 1fr;
gap: 32px;
align-items: start;
}
.col-row .col-label {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-3);
padding-top: 6px;
}
/* ─── Buttons / pills ────────────────────────────────────────────────── */
.pillbar {
display: inline-flex;
gap: 0;
border: 1px solid var(--ink);
background: var(--paper-3);
padding: 4px;
border-radius: 999px;
margin-bottom: 28px;
}
.pillbar button {
appearance: none;
border: 0;
background: transparent;
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
letter-spacing: 0.08em;
color: var(--ink-2);
padding: 7px 16px;
cursor: pointer;
border-radius: 999px;
transition: background 0.18s ease, color 0.18s ease, transform 0.18s ease;
text-transform: lowercase;
}
.pillbar button:hover { color: var(--ink); }
.pillbar button[aria-pressed="true"] {
background: var(--ink);
color: var(--paper);
}
.btn {
appearance: none;
border: 1px solid var(--ink);
background: transparent;
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
letter-spacing: 0.08em;
color: var(--ink);
padding: 8px 14px;
cursor: pointer;
text-transform: lowercase;
transition: background 0.18s ease, color 0.18s ease;
}
.btn:hover { background: var(--ink); color: var(--paper); }
.btn[disabled] { opacity: 0.35; cursor: not-allowed; }
.btn.primary {
background: var(--accent);
border-color: var(--accent);
color: var(--paper);
}
.btn.primary:hover { background: #5e1c1c; border-color: #5e1c1c; }
/* ─── Anatomy section ────────────────────────────────────────────────── */
.anatomy-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 64px;
margin-top: 16px;
align-items: start;
}
.tree {
font-family: "JetBrains Mono", monospace;
font-size: 13px;
line-height: 1.85;
color: var(--ink);
background: var(--paper-3);
border: 1px solid var(--rule-strong);
padding: 28px 32px;
border-radius: 4px;
position: relative;
}
.tree::before {
content: "packages/<feature>/";
position: absolute;
top: -10px; left: 24px;
background: var(--paper);
padding: 0 10px;
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-3);
}
.tree-row {
display: block;
cursor: pointer;
padding: 0 8px;
border-radius: 2px;
transition: background 0.12s ease;
white-space: pre;
}
.tree-row:hover { background: var(--paper-2); }
.tree-row.is-active {
background: var(--ink);
color: var(--paper);
}
.tree-row.is-active .dim { color: var(--paper-2); opacity: 0.7; }
.tree-row .dim { color: var(--ink-3); }
.tree-row .dot { color: var(--accent); }
.layer-card {
background: var(--paper-3);
border: 1px solid var(--rule-strong);
padding: 32px;
border-radius: 4px;
min-height: 360px;
}
.layer-card .layer-tag {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 8px;
}
.layer-card h3 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 36, "SOFT" 30, "wght" 420;
font-size: 30px;
line-height: 1.05;
margin: 0 0 14px;
letter-spacing: -0.01em;
}
.layer-card h3 em {
font-style: italic;
font-variation-settings: "opsz" 36, "SOFT" 100, "wght" 420;
color: var(--accent);
}
.layer-card p {
font-size: 15.5px;
line-height: 1.6;
color: var(--ink-2);
margin: 0 0 12px;
}
.layer-card .meta {
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--rule);
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--ink-3);
letter-spacing: 0.04em;
}
.layer-card .meta strong { color: var(--ink); font-weight: 500; }
.layer-card .examples {
margin-top: 12px;
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
color: var(--ink-2);
}
.layer-card .examples li { margin: 3px 0; padding-left: 14px; position: relative; list-style: none; }
.layer-card .examples li::before {
content: "";
position: absolute; left: 0;
color: var(--accent);
}
.layer-card .examples ul { list-style: none; padding: 0; margin: 6px 0 0; }
/* ─── Flow section ───────────────────────────────────────────────────── */
.feature-picker {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 56px;
flex-wrap: wrap;
}
.feature-picker .label {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-3);
margin-right: 12px;
}
.flow-grid {
display: grid;
grid-template-columns: 360px 1fr;
gap: 56px;
align-items: start;
}
.flow-pipeline {
position: relative;
padding: 0 8px;
}
.flow-stage {
display: grid;
grid-template-columns: 36px 1fr;
gap: 14px;
align-items: center;
padding: 14px 14px 14px 6px;
cursor: pointer;
border-radius: 4px;
transition: background 0.18s ease, transform 0.18s ease;
position: relative;
}
.flow-stage:hover { background: var(--paper-2); }
.flow-stage.is-active {
background: var(--ink);
color: var(--paper);
}
.flow-stage.is-active .stage-num { color: var(--accent-3); }
.flow-stage.is-active .stage-meta { color: var(--paper-2); opacity: 0.78; }
.flow-stage::after {
content: "";
position: absolute;
left: 23px;
bottom: -10px;
width: 1px;
height: 22px;
background: var(--rule-strong);
}
.flow-stage:last-of-type::after { display: none; }
.stage-num {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
font-weight: 500;
color: var(--accent);
text-align: right;
letter-spacing: 0.05em;
padding-right: 4px;
}
.stage-body {
display: flex;
flex-direction: column;
}
.stage-name {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 18, "SOFT" 30, "wght" 480;
font-size: 17px;
line-height: 1.2;
letter-spacing: -0.005em;
}
.stage-meta {
font-family: "JetBrains Mono", monospace;
font-size: 10.5px;
letter-spacing: 0.04em;
color: var(--ink-3);
margin-top: 2px;
}
.flow-stage.fork-success { color: var(--ok); }
.flow-stage.fork-error { color: var(--accent); }
.flow-stage.fork-success .stage-num,
.flow-stage.fork-error .stage-num { color: currentColor; }
.flow-stage.is-active.fork-success { background: #2c4220; color: var(--paper); }
.flow-stage.is-active.fork-success .stage-num { color: #c8d4a3; }
.flow-stage.is-active.fork-error { background: #5e1c1c; color: var(--paper); }
.flow-stage.is-active.fork-error .stage-num { color: #f0c4b3; }
.flow-fork-divider {
display: flex;
align-items: center;
gap: 10px;
margin: 16px 0 8px;
font-family: "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-3);
}
.flow-fork-divider::before,
.flow-fork-divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--rule-strong);
}
.flow-controls {
display: flex;
gap: 8px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--rule);
align-items: center;
}
.flow-controls .progress {
flex: 1;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--ink-3);
text-align: right;
letter-spacing: 0.06em;
}
.stage-detail {
border: 1px solid var(--rule-strong);
background: var(--paper-3);
padding: 36px 40px;
border-radius: 4px;
min-height: 580px;
position: relative;
}
.stage-detail .detail-tag {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 6px;
}
.stage-detail h3 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 60, "SOFT" 30, "wght" 380;
font-size: 38px;
line-height: 1.05;
letter-spacing: -0.015em;
margin: 0 0 8px;
}
.stage-detail h3 em {
font-style: italic;
font-variation-settings: "opsz" 60, "SOFT" 100, "wght" 380;
color: var(--accent);
}
.stage-detail .file-path {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
color: var(--ink-2);
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px dashed var(--rule);
}
.stage-detail p.detail-prose {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 17, "SOFT" 40;
font-size: 16.5px;
line-height: 1.6;
color: var(--ink);
margin: 0 0 24px;
max-width: 60ch;
text-wrap: pretty;
}
/* ─── Code blocks ────────────────────────────────────────────────────── */
pre.code {
background: var(--code-bg);
color: var(--code-paper);
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
line-height: 1.62;
padding: 22px 24px;
border-radius: 4px;
overflow-x: auto;
margin: 0;
border: 1px solid var(--code-bg-2);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
0 14px 40px -20px rgba(20, 14, 8, 0.5);
}
pre.code .c { color: var(--code-mute); font-style: italic; }
pre.code .k { color: var(--code-accent); }
pre.code .s { color: var(--code-paper); opacity: 0.92; }
pre.code .t { color: #b3c8a6; }
pre.code .n { color: #d2b896; }
pre.code .h { background: rgba(216, 160, 74, 0.16); padding: 0 4px; border-radius: 2px; }
pre.code::before {
content: attr(data-lang);
display: block;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #6b574a;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(216, 160, 74, 0.18);
}
/* ─── DI section ─────────────────────────────────────────────────────── */
.di-grid {
display: grid;
grid-template-columns: 1fr;
gap: 36px;
margin-top: 8px;
}
.di-board {
background: var(--paper-3);
border: 1px solid var(--rule-strong);
border-radius: 4px;
padding: 36px 40px;
position: relative;
overflow: hidden;
}
.di-board-head {
display: flex;
justify-content: space-between;
align-items: end;
margin-bottom: 32px;
padding-bottom: 18px;
border-bottom: 1px solid var(--rule);
gap: 20px;
}
.di-board-head h3 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 36, "SOFT" 30, "wght" 380;
font-size: 28px;
margin: 0;
letter-spacing: -0.01em;
}
.di-board-head h3 em {
font-style: italic;
font-variation-settings: "opsz" 36, "SOFT" 100, "wght" 380;
color: var(--accent);
}
.di-board-head .meta {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--ink-3);
text-align: right;
letter-spacing: 0.06em;
}
.di-canvas {
display: grid;
grid-template-columns: 280px 1fr 280px;
gap: 32px;
align-items: start;
position: relative;
}
.di-col h4 {
font-family: "JetBrains Mono", monospace;
font-size: 10.5px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-3);
margin: 0 0 16px;
}
.di-node {
display: block;
border: 1px solid var(--ink);
background: var(--paper);
padding: 12px 14px;
margin-bottom: 8px;
border-radius: 2px;
font-family: "JetBrains Mono", monospace;
font-size: 12px;
position: relative;
transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}
.di-node .node-kind {
font-size: 9.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-3);
margin-bottom: 4px;
}
.di-node .node-name { color: var(--ink); }
.di-node.is-mock { border-color: var(--ink); }
.di-node.is-real {
border-color: var(--accent);
background: #fff7e8;
}
.di-node.is-real .node-kind { color: var(--accent); }
.di-node.is-factory {
background: var(--paper-2);
border-style: dashed;
}
.di-node.is-factory .node-kind { color: var(--accent-2); }
.di-bridge {
position: relative;
padding-top: 36px;
text-align: center;
font-family: "JetBrains Mono", monospace;
font-size: 10.5px;
letter-spacing: 0.06em;
color: var(--ink-3);
}
.di-bridge .bridge-label {
background: var(--paper-3);
padding: 0 10px;
display: inline-block;
position: relative;
z-index: 2;
}
.di-explainer {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 28px;
margin-top: 28px;
}
.di-card {
background: var(--paper-3);
border: 1px solid var(--rule-strong);
border-radius: 4px;
padding: 28px 30px;
}
.di-card h4 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 24, "SOFT" 40, "wght" 460;
font-size: 22px;
margin: 0 0 12px;
letter-spacing: -0.005em;
}
.di-card h4 em {
font-style: italic;
font-variation-settings: "opsz" 24, "SOFT" 100, "wght" 460;
color: var(--accent);
}
.di-card p {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 40;
font-size: 15.5px;
color: var(--ink-2);
line-height: 1.6;
margin: 0 0 12px;
}
/* ─── Contracts / factories section ─────────────────────────────────── */
.cf-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
margin-top: 16px;
}
.cf-card {
border: 1px solid var(--rule-strong);
background: var(--paper-3);
padding: 32px 36px;
border-radius: 4px;
}
.cf-card .cf-tag {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 8px;
}
.cf-card h3 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 60, "SOFT" 0, "wght" 380;
font-size: 42px;
line-height: 1.0;
letter-spacing: -0.018em;
margin: 0 0 18px;
}
.cf-card h3 em {
font-style: italic;
font-variation-settings: "opsz" 60, "SOFT" 100, "wght" 380;
color: var(--accent);
}
.cf-card p {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 40;
font-size: 15.5px;
line-height: 1.6;
color: var(--ink-2);
margin: 0 0 14px;
}
.cf-card p strong { color: var(--ink); font-weight: 500; }
details.cf-detail {
border: 0;
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--rule);
}
details.cf-detail > summary {
list-style: none;
cursor: pointer;
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
letter-spacing: 0.06em;
color: var(--ink-2);
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
user-select: none;
}
details.cf-detail > summary::-webkit-details-marker { display: none; }
details.cf-detail > summary::before {
content: "+";
display: inline-flex;
width: 20px; height: 20px;
align-items: center; justify-content: center;
border: 1px solid var(--ink);
border-radius: 2px;
font-size: 14px;
background: var(--paper);
transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease;
}
details.cf-detail[open] > summary::before {
content: "";
background: var(--ink);
color: var(--paper);
}
details.cf-detail > summary:hover { color: var(--ink); }
details.cf-detail > .detail-body {
padding-top: 16px;
}
/* ─── Three-roles diagram (anatomy of mock/contract/factory) ─────────── */
.three-roles-card { margin-bottom: 40px; }
.three-roles-card h3 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 60, "SOFT" 0, "wght" 380;
font-size: 42px;
line-height: 1.0;
letter-spacing: -0.018em;
margin: 0 0 18px;
}
.three-roles-card h3 em {
font-style: italic;
font-variation-settings: "opsz" 60, "SOFT" 100, "wght" 380;
color: var(--accent);
}
.three-roles-card p {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 40;
font-size: 15.5px;
line-height: 1.6;
color: var(--ink-2);
margin: 0 0 12px;
}
.three-roles-card p strong { color: var(--ink); font-weight: 500; }
.three-roles-card code {
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
background: var(--paper-2);
padding: 1px 5px;
border-radius: 2px;
}
.three-roles-diagram {
display: flex;
flex-direction: column;
gap: 0;
margin: 32px auto;
max-width: 760px;
}
.role {
border: 1px solid var(--ink);
background: var(--paper);
padding: 16px 20px;
border-radius: 3px;
position: relative;
}
.role .role-where {
font-family: "JetBrains Mono", monospace;
font-size: 9.5px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-3);
margin-bottom: 6px;
}
.role > strong {
display: block;
font-family: "JetBrains Mono", monospace;
font-weight: 500;
color: var(--ink);
font-size: 14.5px;
margin-bottom: 6px;
letter-spacing: 0.01em;
}
.role .role-what {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 14, "SOFT" 40;
font-style: italic;
font-size: 13.5px;
line-height: 1.4;
color: var(--ink-2);
margin: 0;
}
.role .role-what em {
font-style: italic;
font-variation-settings: "opsz" 14, "SOFT" 100, "wght" 460;
color: var(--accent);
font-weight: 460;
}
.role .role-what code {
font-family: "JetBrains Mono", monospace;
font-style: normal;
font-size: 12px;
background: var(--paper-2);
padding: 1px 4px;
border-radius: 2px;
}
.role-interface {
border-width: 2px;
background: var(--paper-3);
}
.role-real {
background: #fbf1de;
border-color: var(--accent);
}
.role-real .role-where { color: var(--accent); }
.role-contract {
border-style: dashed;
border-color: var(--accent-2);
}
.role-contract .role-where { color: var(--accent-2); }
.role-factory {
border-color: var(--ok);
align-self: center;
width: min(78%, 540px);
}
.role-factory .role-where { color: var(--ok); }
.roles-pair {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.role-arrow {
align-self: center;
font-family: "JetBrains Mono", monospace;
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-3);
padding: 14px 0;
position: relative;
}
.role-arrow::before {
content: "";
position: absolute;
left: 50%;
top: 0;
width: 1px;
height: 14px;
background: var(--rule-strong);
transform: translateX(-0.5px);
}
.role-arrow::after {
content: "";
position: absolute;
left: 50%;
bottom: 0;
width: 1px;
height: 14px;
background: var(--rule-strong);
transform: translateX(-0.5px);
}
.role-jobs {
list-style: none;
padding: 0;
margin: 14px 0 0;
counter-reset: job;
}
ol.role-jobs { counter-reset: job; }
ol.role-jobs > li {
counter-increment: job;
position: relative;
padding: 14px 18px 14px 56px;
margin-bottom: 8px;
background: var(--paper-3);
border-left: 2px solid var(--accent);
border-radius: 0 3px 3px 0;
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 40;
font-size: 15.5px;
line-height: 1.55;
color: var(--ink);
}
ol.role-jobs > li::before {
content: counter(job, decimal-leading-zero);
position: absolute;
left: 18px;
top: 14px;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: 0.18em;
color: var(--accent);
}
ol.role-jobs > li strong {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 30, "wght" 520;
font-weight: 500;
color: var(--ink);
}
ol.role-jobs > li code {
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
background: var(--paper-2);
padding: 1px 5px;
border-radius: 2px;
}
ul.role-jobs {
list-style: none;
padding: 0;
margin: 14px 0 0;
}
ul.role-jobs > li {
padding: 12px 18px;
margin-bottom: 8px;
background: var(--paper-3);
border-left: 2px solid var(--accent-2);
border-radius: 0 3px 3px 0;
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 40;
font-size: 15.5px;
line-height: 1.55;
color: var(--ink);
}
ul.role-jobs > li strong {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 16, "SOFT" 30, "wght" 520;
font-weight: 500;
color: var(--ink);
}
ul.role-jobs > li code {
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
background: var(--paper-2);
padding: 1px 5px;
border-radius: 2px;
}
@media (max-width: 700px) {
.roles-pair { grid-template-columns: 1fr; }
.role-factory { width: 100%; }
}
/* ─── Verdict ────────────────────────────────────────────────────────── */
.verdict-block {
background: var(--ink);
color: var(--paper);
padding: 56px 64px;
border-radius: 4px;
position: relative;
overflow: hidden;
}
.verdict-block::before {
content: "";
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 90% -10%, rgba(125, 40, 40, 0.5), transparent 50%),
radial-gradient(circle at -10% 110%, rgba(31, 61, 74, 0.4), transparent 50%);
pointer-events: none;
}
.verdict-block h3 {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 96, "SOFT" 0, "wght" 360;
font-size: 56px;
line-height: 0.96;
letter-spacing: -0.02em;
margin: 0 0 24px;
position: relative;
max-width: 16ch;
}
.verdict-block h3 em {
font-style: italic;
font-variation-settings: "opsz" 96, "SOFT" 100, "wght" 360;
color: var(--accent-3);
}
.verdict-block p {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 18, "SOFT" 40;
font-size: 17.5px;
line-height: 1.6;
max-width: 60ch;
margin: 0 0 16px;
position: relative;
color: rgba(244, 236, 216, 0.86);
}
.verdict-block p strong { color: var(--paper); font-weight: 500; }
.verdict-block .badge-row {
display: flex;
gap: 24px;
margin-top: 32px;
position: relative;
flex-wrap: wrap;
}
.verdict-block .badge {
border: 1px solid rgba(244, 236, 216, 0.3);
padding: 14px 18px;
border-radius: 4px;
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
letter-spacing: 0.04em;
color: rgba(244, 236, 216, 0.78);
}
.verdict-block .badge strong {
display: block;
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 24, "SOFT" 30, "wght" 460;
font-style: italic;
font-size: 22px;
color: var(--accent-3);
margin-bottom: 4px;
letter-spacing: 0;
}
/* ─── Footer ─────────────────────────────────────────────────────────── */
footer {
border-top: 2px solid var(--ink);
padding: 36px 0 56px;
}
footer .foot-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--ink-2);
letter-spacing: 0.04em;
}
footer .foot-col h5 {
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
margin: 0 0 12px;
color: var(--ink-3);
font-weight: 500;
}
footer .foot-col a { display: block; margin: 4px 0; }
footer .colophon {
margin-top: 36px;
padding-top: 18px;
border-top: 1px dashed var(--rule);
font-size: 10.5px;
color: var(--ink-3);
letter-spacing: 0.04em;
}
/* ─── Errors-to-codes table ─────────────────────────────────────────── */
.codes-table {
margin-top: 32px;
border: 1px solid var(--rule-strong);
background: var(--paper-3);
border-radius: 4px;
overflow: hidden;
}
.codes-table-head {
display: grid;
grid-template-columns: 1fr 200px;
background: var(--ink);
color: var(--paper);
padding: 14px 22px;
font-family: "JetBrains Mono", monospace;
font-size: 10.5px;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.codes-row {
display: grid;
grid-template-columns: 1fr 200px;
padding: 14px 22px;
border-top: 1px solid var(--rule);
font-family: "JetBrains Mono", monospace;
font-size: 13px;
align-items: baseline;
}
.codes-row:first-child { border-top: 0; }
.codes-row .err { color: var(--ink); }
.codes-row .arrow { color: var(--ink-3); margin: 0 6px; }
.codes-row .code {
color: var(--accent);
font-weight: 500;
letter-spacing: 0.04em;
}
/* ─── Animations ─────────────────────────────────────────────────────── */
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.fade-in { animation: fadeIn 0.32s ease; }
/* ─── Responsive (basic) ─────────────────────────────────────────────── */
@media (max-width: 1100px) {
.frame { padding: 0 32px; }
.anatomy-grid, .flow-grid, .cf-grid, .di-explainer { grid-template-columns: 1fr; }
.title-block { grid-template-columns: 1fr; gap: 32px; }
ol.contents { grid-template-columns: repeat(2, 1fr); }
.di-canvas { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header class="masthead frame">
<div class="folio-row">
<span class="folio-label"><span class="folio-mark"></span> template-vertical / architecture / explainer</span>
<span>2026-05-06 · post-Plan-9</span>
</div>
<div class="title-block">
<h1 class="title">A guided tour<br>of one feature's<br><em>data flow</em>.</h1>
<p class="lede">An internal explainer for the post-Plan-9 architecture — written for the engineer who built it and just wants the mental model in one place. Click through the request flow, flip the DI binding mode, swap features. Real code from this repo, not a tutorial.</p>
</div>
<div class="contents-row">
<div class="contents-label">Contents</div>
<ol class="contents">
<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="#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="#verdict"><span class="num">05</span>The verdict</a></li>
</ol>
</div>
</header>
<main class="frame">
<!-- ─── 01 ─ ANATOMY ─────────────────────────────────────────────────── -->
<section id="anatomy">
<div class="section-head">
<div class="section-num">§ 01</div>
<div>
<h2 class="section-title">The shape of a <em>feature</em>.</h2>
<p class="section-blurb">Every feature package — <code>auth</code>, <code>blog</code>, <code>marketing-pages</code>, <code>navigation</code>, <code>media</code> — has the same internal layout. Click any layer to see what lives there and why.</p>
</div>
</div>
<div class="anatomy-grid">
<div class="tree" id="tree" aria-label="Feature package tree">
<span class="tree-row" data-layer="entities"><span class="dim">├─</span> entities/</span>
<span class="tree-row" data-layer="entities"> <span class="dim">├─</span> models/<span class="dim"> ← Zod schemas + types</span></span>
<span class="tree-row" data-layer="entities"> <span class="dim">└─</span> errors/<span class="dim"> ← domain errors (set this.name)</span></span>
<span class="tree-row" data-layer="application"><span class="dim">├─</span> application/</span>
<span class="tree-row" data-layer="application"> <span class="dim">├─</span> repositories/<span class="dim">&lt;x&gt;.repository.interface.ts</span></span>
<span class="tree-row" data-layer="application"> <span class="dim">├─</span> services/<span class="dim">&lt;x&gt;.service.interface.ts</span></span>
<span class="tree-row" data-layer="application"> <span class="dim">└─</span> use-cases/<span class="dim"> ← factory + xInputSchema + xOutputSchema</span></span>
<span class="tree-row" data-layer="infrastructure"><span class="dim">├─</span> infrastructure/</span>
<span class="tree-row" data-layer="infrastructure"> <span class="dim">├─</span> repositories/<span class="dim">&lt;x&gt;.repository.ts (real)</span></span>
<span class="tree-row" data-layer="infrastructure"> <span class="dim"></span><span class="dim">+ &lt;x&gt;.repository.mock.ts</span></span>
<span class="tree-row" data-layer="infrastructure"> <span class="dim">└─</span> services/<span class="dim">&lt;x&gt;.service.ts + .mock.ts</span></span>
<span class="tree-row" data-layer="adapters"><span class="dim">├─</span> interface-adapters/</span>
<span class="tree-row" data-layer="adapters"> <span class="dim">└─</span> controllers/<span class="dim"> ← factory + safeParse + presenter</span></span>
<span class="tree-row" data-layer="di"><span class="dim">├─</span> di/</span>
<span class="tree-row" data-layer="di"> <span class="dim">├─</span> symbols.ts<span class="dim"> ← inversify Symbol.for(...) keys</span></span>
<span class="tree-row" data-layer="di"> <span class="dim">├─</span> module.ts<span class="dim"> ← ContainerModule with .toDynamicValue</span></span>
<span class="tree-row" data-layer="di"> <span class="dim">├─</span> container.ts<span class="dim"> ← Container + .load(Module)</span></span>
<span class="tree-row" data-layer="di"> <span class="dim">└─</span> bind-production.ts<span class="dim"> ← swaps mocks → real impls at boot</span></span>
<span class="tree-row" data-layer="integrations"><span class="dim">├─</span> integrations/</span>
<span class="tree-row" data-layer="integrations"> <span class="dim">├─</span> api/</span>
<span class="tree-row" data-layer="integrations"> <span class="dim">│ ├─</span> procedures.ts<span class="dim"> ← xProcedure + defineErrorMiddleware</span></span>
<span class="tree-row" data-layer="integrations"> <span class="dim">│ └─</span> router.ts<span class="dim"> ← xProcedure.input(xInputSchema)</span></span>
<span class="tree-row" data-layer="integrations"> <span class="dim">└─</span> cms/<span class="dim"> ← Payload collections / globals</span></span>
<span class="tree-row" data-layer="ui"><span class="dim">├─</span> ui/</span>
<span class="tree-row" data-layer="ui"> <span class="dim">├─</span> index.ts<span class="dim"> ← public surface for queries / components</span></span>
<span class="tree-row" data-layer="ui"> <span class="dim">└─</span> query.ts<span class="dim"> ← React Query option builders</span></span>
<span class="tree-row" data-layer="testing"><span class="dim">├─</span> __factories__/<span class="dim"> ← defineFactory&lt;Entity&gt;((seq)=&gt;{...})</span></span>
<span class="tree-row" data-layer="testing"><span class="dim">├─</span> __contracts__/<span class="dim"> ← defineContractSuite&lt;IRepo&gt;(...)</span></span>
<span class="tree-row" data-layer="public"><span class="dim">└─</span> index.ts<span class="dim"> ← root: contracts only</span></span>
</div>
<div class="layer-card" id="layerCard">
<!-- populated by JS -->
</div>
</div>
</section>
<!-- ─── 02 ─ REQUEST FLOW ────────────────────────────────────────────── -->
<section id="flow">
<div class="section-head">
<div class="section-num">§ 02</div>
<div>
<h2 class="section-title">A request, <em>step by step</em>.</h2>
<p class="section-blurb">From a React Query call on the client to Payload's local API and back. Pick a feature, then click a stage — or hit play. The error path branches off at <em>Use case</em> or <em>Repository</em> when a domain error is thrown; the success path runs through the controller's presenter on the way out.</p>
</div>
</div>
<div class="feature-picker" role="tablist" aria-label="Feature">
<span class="label">feature</span>
<div class="pillbar" id="featurePicker">
<button data-feature="blog" aria-pressed="true">blog</button>
<button data-feature="auth" aria-pressed="false">auth</button>
<button data-feature="marketing-pages" aria-pressed="false">marketing-pages</button>
<button data-feature="navigation" aria-pressed="false">navigation</button>
<button data-feature="media" aria-pressed="false">media</button>
</div>
</div>
<div class="flow-grid">
<div class="flow-pipeline" id="flowPipeline">
<!-- populated by JS -->
<div class="flow-controls">
<button class="btn" id="prevStage">← prev</button>
<button class="btn primary" id="playStage">▶ play</button>
<button class="btn" id="nextStage">next →</button>
<span class="progress" id="stageProgress">1 / 11</span>
</div>
</div>
<div class="stage-detail" id="stageDetail">
<!-- populated by JS -->
</div>
</div>
<div class="codes-table" id="codesTable">
<!-- populated by JS — feature-scoped error map -->
</div>
</section>
<!-- ─── 03 ─ DEPENDENCY INJECTION ────────────────────────────────────── -->
<section id="di">
<div class="section-head">
<div class="section-num">§ 03</div>
<div>
<h2 class="section-title">Wiring the <em>container</em>.</h2>
<p class="section-blurb">Each feature owns one InversifyJS container. Symbols → factory bindings via <code>.toDynamicValue</code>. The <em>same symbol</em> resolves to a mock at dev time and to a real Payload-backed impl after <code>bindProduction*(config)</code> runs at app boot. Toggle below to see what swaps.</p>
</div>
</div>
<div class="di-board">
<div class="di-board-head">
<h3>Resolving <em>blogContainer.get(IGetArticlesController)</em></h3>
<div class="meta">mode →
<span class="pillbar" style="margin: 0 0 0 12px; vertical-align: middle;">
<button id="diMockBtn" aria-pressed="true">default · mock</button>
<button id="diProdBtn" aria-pressed="false">production · real</button>
</span>
</div>
</div>
<div class="di-canvas">
<div class="di-col">
<h4>Symbols</h4>
<div class="di-node" data-pair="repo">
<div class="node-kind">repo symbol</div>
<div class="node-name">BLOG_SYMBOLS.IArticlesRepository</div>
</div>
<div class="di-node is-factory" data-pair="usecase">
<div class="node-kind">use case symbol</div>
<div class="node-name">BLOG_SYMBOLS.IGetArticlesUseCase</div>
</div>
<div class="di-node is-factory" data-pair="controller">
<div class="node-kind">controller symbol</div>
<div class="node-name">BLOG_SYMBOLS.IGetArticlesController</div>
</div>
</div>
<div class="di-col" style="position: relative;">
<h4 style="text-align: center;">Binding</h4>
<div class="di-bridge"><span class="bridge-label" id="bridgeRepo">.to(MockArticlesRepository)</span></div>
<div class="di-bridge"><span class="bridge-label">.toDynamicValue((ctx) ⇒ getArticlesUseCase(ctx.container.get(...)))</span></div>
<div class="di-bridge"><span class="bridge-label">.toDynamicValue((ctx) ⇒ getArticlesController(ctx.container.get(...)))</span></div>
</div>
<div class="di-col">
<h4 style="text-align: right;">Resolves to</h4>
<div class="di-node is-mock" id="repoNode">
<div class="node-kind">class · mock</div>
<div class="node-name">MockArticlesRepository</div>
</div>
<div class="di-node is-factory">
<div class="node-kind">closure · factory</div>
<div class="node-name">getArticlesUseCase(repo)</div>
</div>
<div class="di-node is-factory">
<div class="node-kind">closure · factory</div>
<div class="node-name">getArticlesController(useCase)</div>
</div>
</div>
</div>
</div>
<div class="di-explainer">
<div class="di-card">
<h4>Why <em>.toDynamicValue</em>?</h4>
<p>A use case is a curried factory: <code>(deps) => async (input) => result</code>. It isn't a class, so <code>.to(SomeClass)</code> can't construct it. <code>.toDynamicValue((ctx) =>&nbsp;...)</code> runs at resolution time, lets the container fetch each dependency, and returns a <em>closure</em> that captures them.</p>
<p>Result: every <code>container.get(SYMBOL)</code> call hands you a fully-wired async function. Tests don't need any of this — they construct mocks and pass them in directly.</p>
</div>
<div class="di-card">
<h4>Two binding modes, one symbol.</h4>
<p>The <code>BlogModule</code> binds <code>IArticlesRepository</code> to <code>MockArticlesRepository</code> by default — useful at dev/test time. At app boot, <code>bindProductionBlog(config)</code> unbinds the symbol and rebinds it to <code>new ArticlesRepository(config)</code>. Use cases and controllers don't notice — they get whatever the symbol currently resolves to.</p>
<p>This is also why the boundary stays clean: features don't import <code>core-cms</code>; the app passes the Payload config in.</p>
</div>
</div>
<div class="cf-grid" style="margin-top: 36px;">
<div class="cf-card">
<div class="cf-tag">module · default bindings</div>
<h3>blog/di/<em>module.ts</em></h3>
<p>One module, one container. Loaded once at <code>blogContainer.load(BlogModule)</code>.</p>
<pre class="code" data-lang="typescript // packages/blog/src/di/module.ts"><span class="k">export const</span> <span class="n">BlogModule</span> = <span class="k">new</span> <span class="t">ContainerModule</span>((<span class="n">bind</span>) => {
<span class="n">bind</span>&lt;<span class="t">IArticlesRepository</span>&gt;(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>)
.<span class="n">to</span>(<span class="t">MockArticlesRepository</span>); <span class="c">// default</span>
<span class="n">bind</span>&lt;<span class="t">IGetArticlesUseCase</span>&gt;(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesUseCase</span>)
.<span class="n">toDynamicValue</span>((<span class="n">ctx</span>) =&gt;
<span class="n">getArticlesUseCase</span>(
<span class="n">ctx</span>.<span class="n">container</span>.<span class="n">get</span>&lt;<span class="t">IArticlesRepository</span>&gt;(
<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>,
),
),
);
<span class="n">bind</span>&lt;<span class="t">IGetArticlesController</span>&gt;(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesController</span>)
.<span class="n">toDynamicValue</span>((<span class="n">ctx</span>) =&gt;
<span class="n">getArticlesController</span>(
<span class="n">ctx</span>.<span class="n">container</span>.<span class="n">get</span>&lt;<span class="t">IGetArticlesUseCase</span>&gt;(
<span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesUseCase</span>,
),
),
);
});</pre>
</div>
<div class="cf-card">
<div class="cf-tag">app boot · production override</div>
<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 resolved Payload config.</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">config</span>: <span class="t">SanitizedConfig</span>): <span class="t">void</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>
}</pre>
</div>
</div>
</section>
<!-- ─── 04 ─ CONTRACTS & FACTORIES ───────────────────────────────────── -->
<section id="contracts">
<div class="section-head">
<div class="section-num">§ 04</div>
<div>
<h2 class="section-title">Mocks, contracts &amp; <em>factories</em>.</h2>
<p class="section-blurb">Three artifacts that sit near tests, at different distances from runtime. The <strong>mock repository</strong> is a real implementation of the interface — runtime code (DI, dev mode, storybook) reaches it. The <strong>contract</strong> is a test suite that runs against any implementation of the repo interface, mock or real. The <strong>factory</strong> builds valid entity values. The relationship between them is the interesting bit.</p>
</div>
</div>
<div class="cf-card three-roles-card">
<div class="cf-tag">where the boundaries are</div>
<h3>Same neighborhood, <em>different reach</em>.</h3>
<p>The mock <em>is</em> a test artifact, but it's also more than that — it's a real implementation of the repository interface, and runtime code reaches it directly. DI binds it as the default; dev mode runs against it when Payload isn't booted; storybook stories that need data resolve to it. The contract and factory are <em>only</em> reached from test files. That difference in reach is what determines where each lives.</p>
<p>So the mock sits in <code>infrastructure/repositories/</code> next to the real impl — they're sibling implementations of the same interface, both legitimate citizens of the runtime layer. The contract and factory live under <code>__</code>-prefixed directories that nothing outside <code>*.test.ts</code> ever imports from.</p>
<div class="three-roles-diagram">
<div class="role role-interface">
<div class="role-where">application/repositories/</div>
<strong>IArticlesRepository</strong>
<div class="role-what">interface — defines the shape every implementation must satisfy</div>
</div>
<div class="role-arrow">&nbsp; implemented by both &nbsp;</div>
<div class="roles-pair">
<div class="role role-mock">
<div class="role-where">infrastructure/repositories/</div>
<strong>MockArticlesRepository</strong>
<div class="role-what">in-memory · <em>default DI binding</em> · used by dev mode &amp; unit tests</div>
</div>
<div class="role role-real">
<div class="role-where">infrastructure/repositories/</div>
<strong>ArticlesRepository</strong>
<div class="role-what">Payload-backed · <em>production binding</em> · constructed at app boot</div>
</div>
</div>
<div class="role-arrow">&nbsp; both tested by &nbsp;</div>
<div class="role role-contract">
<div class="role-where">__contracts__/</div>
<strong>articlesRepositoryContract</strong>
<div class="role-what">portable test suite — same <code>it()</code> blocks, run twice (once per impl)</div>
</div>
<div class="role-arrow">&nbsp; consumes seed data from &nbsp;</div>
<div class="role role-factory">
<div class="role-where">__factories__/</div>
<strong>articleFactory</strong>
<div class="role-what">data builder — produces valid <code>Article</code> entities with overridable defaults</div>
</div>
</div>
<p style="margin-top: 32px;"><strong>The mock is reached from two directions.</strong> Both are legitimate, neither is "the test version":</p>
<ol class="role-jobs">
<li><strong>By the DI container at runtime.</strong> <code>BlogModule</code> binds <code>IArticlesRepository</code> to <code>MockArticlesRepository</code> at module-load time. Anything resolving that symbol — use cases, controllers, tRPC procedures, the dev server — gets the mock until <code>bindProductionBlog(config)</code> swaps it for the real Payload-backed one. See §03.</li>
<li><strong>By tests, via direct construction.</strong> Unit tests skip the container entirely. They construct the mock with <code>new MockArticlesRepository()</code> and pass it directly into the use-case factory function. Same class, different consumer — just a closure with a fake repo.</li>
</ol>
<p style="margin-top: 24px;"><strong>The contract and factory are reached from one direction only — tests.</strong> They never appear in runtime imports. Their roles:</p>
<ul class="role-jobs">
<li><strong>Contract</strong> = a single suite of <code>it()</code> blocks parameterized by <code>buildSubject</code>. Run it against the mock, run it against the real Payload-backed impl. If they diverge — your mock is lying about Payload's behavior, and you'd never catch it without the contract. This is its whole reason to exist: <em>it tests the mock so you can trust it</em>, alongside testing the real impl.</li>
<li><strong>Factory</strong> = a sequence-counter builder. <code>articleFactory.build({ slug: "x" })</code> hands you a valid <code>Article</code> with sensible defaults; you only override the fields the test cares about. Used by the contract <em>and</em> by every use-case / controller test that needs entity values without writing 8 lines of inline fixtures.</li>
</ul>
<details class="cf-detail">
<summary>show: the mock as DI binding (in module.ts)</summary>
<div class="detail-body">
<p>This is from <code>packages/blog/src/di/module.ts</code> — the very first binding in the module is the mock. Everything downstream (use cases, controllers) resolves through this default. <code>bindProductionBlog(config)</code> later replaces only this one line at app boot — use case + controller bindings stay put.</p>
<pre class="code" data-lang="typescript // packages/blog/src/di/module.ts"><span class="k">export const</span> <span class="n">BlogModule</span> = <span class="k">new</span> <span class="t">ContainerModule</span>((<span class="n">bind</span>) =&gt; {
<span class="c">// 1) Mock is the DEFAULT binding for the repo symbol.</span>
<span class="c">// Dev server, unit tests, storybook all resolve to this.</span>
<span class="n">bind</span>&lt;<span class="t">IArticlesRepository</span>&gt;(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>)
.<span class="n">to</span>(<span class="t">MockArticlesRepository</span>);
<span class="c">// 2) Use cases consume IArticlesRepository — they don't know or</span>
<span class="c">// care which impl they got. Same factory function in either mode.</span>
<span class="n">bind</span>&lt;<span class="t">IGetArticlesUseCase</span>&gt;(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IGetArticlesUseCase</span>)
.<span class="n">toDynamicValue</span>((<span class="n">ctx</span>) =&gt;
<span class="n">getArticlesUseCase</span>(
<span class="n">ctx</span>.<span class="n">container</span>.<span class="n">get</span>&lt;<span class="t">IArticlesRepository</span>&gt;(<span class="n">BLOG_SYMBOLS</span>.<span class="n">IArticlesRepository</span>),
),
);
<span class="c">// ... + 5 more bindings, all the same shape.</span>
});</pre>
</div>
</details>
<details class="cf-detail">
<summary>show: the mock as direct test fake (no container)</summary>
<div class="detail-body">
<p>Use-case + controller tests skip DI entirely. They construct the mock and pass it as the first argument to the use-case factory, then call the resulting closure with the input. Three lines of setup, then assertions.</p>
<pre class="code" data-lang="typescript // get-articles.use-case.test.ts"><span class="n">it</span>(<span class="s">"filters by status"</span>, <span class="k">async</span> () =&gt; {
<span class="c">// Construct the mock directly — no DI container, no rebinding.</span>
<span class="k">const</span> <span class="n">repo</span> = <span class="k">new</span> <span class="t">MockArticlesRepository</span>();
<span class="c">// Use the factory to seed valid entities (only override what we care about).</span>
<span class="n">articleFactory</span>.<span class="n">reset</span>();
<span class="k">await</span> <span class="n">repo</span>.<span class="n">createArticle</span>(<span class="n">articleFactory</span>.<span class="n">build</span>({ <span class="n">status</span>: <span class="s">"draft"</span> }));
<span class="k">await</span> <span class="n">repo</span>.<span class="n">createArticle</span>(<span class="n">articleFactory</span>.<span class="n">build</span>({ <span class="n">status</span>: <span class="s">"published"</span> }));
<span class="c">// Inject the mock into the use-case factory; call the resulting closure.</span>
<span class="k">const</span> <span class="n">useCase</span> = <span class="n">getArticlesUseCase</span>(<span class="n">repo</span>);
<span class="k">const</span> <span class="n">result</span> = <span class="k">await</span> <span class="n">useCase</span>({ <span class="n">status</span>: <span class="s">"published"</span> });
<span class="n">expect</span>(<span class="n">result</span>).<span class="n">toHaveLength</span>(<span class="n">1</span>);
});</pre>
</div>
</details>
<details class="cf-detail">
<summary>show: the contract testing both impls (the proof-of-parity bit)</summary>
<div class="detail-body">
<p>Two tiny test files, one shared suite. If the suite ever fails on the real impl but passes on the mock — your mock is lying about Payload's behavior and you'd ship a bug. The factory is doing real work here too: every <code>it()</code> in the suite uses <code>articleFactory.build(...)</code> for seed data, so the assertions stay readable.</p>
<pre class="code" data-lang="typescript // articles.repository.mock.test.ts"><span class="n">describe</span>(<span class="s">"MockArticlesRepository"</span>, () =&gt; {
<span class="n">articlesRepositoryContract</span>.<span class="n">run</span>(<span class="k">async</span> () =&gt; <span class="k">new</span> <span class="t">MockArticlesRepository</span>());
});
<span class="c">// articles.repository.test.ts (Payload-backed) — same suite, real impl</span>
<span class="n">vi</span>.<span class="n">mock</span>(<span class="s">"payload"</span>, () =&gt; ({ <span class="n">getPayload</span>: <span class="n">vi</span>.<span class="n">fn</span>() }));
<span class="n">describe</span>(<span class="s">"ArticlesRepository (Payload)"</span>, () =&gt; {
<span class="n">articlesRepositoryContract</span>.<span class="n">run</span>(<span class="k">async</span> () =&gt; {
<span class="k">const</span> <span class="n">stub</span> = <span class="n">buildPayloadStub</span>();
(<span class="n">getPayload</span> <span class="k">as</span> <span class="t">Mock</span>).<span class="n">mockResolvedValue</span>(<span class="n">stub</span>);
<span class="k">return new</span> <span class="t">ArticlesRepository</span>(<span class="n">stubPayloadConfig</span>);
});
});</pre>
<p style="margin-top: 16px;">When you run <code>pnpm test --filter @repo/blog</code>, the contract's twelve <code>it()</code> blocks run twice — once per implementation. Twenty-four assertions for the price of writing twelve.</p>
</div>
</details>
</div>
<div class="cf-grid">
<div class="cf-card">
<div class="cf-tag">__contracts__/</div>
<h3>The behavioral <em>contract</em>.</h3>
<p>A contract suite is a portable set of tests that asserts every implementation of a repository interface behaves the same way. You write it once, run it against the mock, run it again against the real Payload-backed impl. If they diverge — bug.</p>
<p>The suite takes a <code>buildSubject</code> callback so each implementation can supply its own setup (e.g., the Payload impl needs to mock <code>getPayload()</code> first; the in-memory mock just constructs).</p>
<details class="cf-detail">
<summary>show: defining the suite</summary>
<div class="detail-body">
<pre class="code" data-lang="typescript // packages/blog/src/__contracts__/articles-repository.contract.ts"><span class="k">export const</span> <span class="n">articlesRepositoryContract</span> =
<span class="n">defineContractSuite</span>&lt;<span class="t">IArticlesRepository</span>&gt;(
<span class="s">"IArticlesRepository"</span>,
({ <span class="n">buildSubject</span> }) =&gt; {
<span class="k">let</span> <span class="n">repo</span>: <span class="t">IArticlesRepository</span>;
<span class="n">beforeEach</span>(<span class="k">async</span> () =&gt; {
<span class="n">articleFactory</span>.<span class="n">reset</span>();
<span class="n">repo</span> = <span class="k">await</span> <span class="n">buildSubject</span>();
});
<span class="n">it</span>(<span class="s">"createArticle returns an article with the correct fields"</span>, <span class="k">async</span> () =&gt; {
<span class="k">const</span> <span class="n">seed</span> = <span class="n">articleFactory</span>.<span class="n">build</span>({ <span class="n">title</span>: <span class="s">"Hello World"</span> });
<span class="k">const</span> <span class="n">created</span> = <span class="k">await</span> <span class="n">repo</span>.<span class="n">createArticle</span>(<span class="n">seed</span>);
<span class="n">expect</span>(<span class="k">typeof</span> <span class="n">created</span>.<span class="n">id</span>).<span class="n">toBe</span>(<span class="s">"string"</span>);
<span class="n">expect</span>(<span class="n">created</span>.<span class="n">title</span>).<span class="n">toBe</span>(<span class="s">"Hello World"</span>);
});
<span class="n">it</span>(<span class="s">"getArticles filters by status"</span>, <span class="k">async</span> () =&gt; {
<span class="k">await</span> <span class="n">repo</span>.<span class="n">createArticle</span>(<span class="n">articleFactory</span>.<span class="n">build</span>({ <span class="n">status</span>: <span class="s">"draft"</span> }));
<span class="k">await</span> <span class="n">repo</span>.<span class="n">createArticle</span>(<span class="n">articleFactory</span>.<span class="n">build</span>({ <span class="n">status</span>: <span class="s">"published"</span> }));
<span class="k">const</span> <span class="n">drafts</span> = <span class="k">await</span> <span class="n">repo</span>.<span class="n">getArticles</span>({ <span class="n">status</span>: <span class="s">"draft"</span> });
<span class="n">expect</span>(<span class="n">drafts</span>).<span class="n">toHaveLength</span>(<span class="n">1</span>);
});
<span class="c">// ... ten more `it` cases covering every method on IArticlesRepository</span>
},
);</pre>
</div>
</details>
<details class="cf-detail">
<summary>show: running it against both impls</summary>
<div class="detail-body">
<pre class="code" data-lang="typescript // articles.repository.mock.test.ts"><span class="n">describe</span>(<span class="s">"MockArticlesRepository"</span>, () =&gt; {
<span class="n">articlesRepositoryContract</span>.<span class="n">run</span>(<span class="k">async</span> () =&gt; <span class="k">new</span> <span class="t">MockArticlesRepository</span>());
});
<span class="c">// articles.repository.test.ts (Payload-backed)</span>
<span class="n">vi</span>.<span class="n">mock</span>(<span class="s">"payload"</span>, () =&gt; ({ <span class="n">getPayload</span>: <span class="n">vi</span>.<span class="n">fn</span>() }));
<span class="n">describe</span>(<span class="s">"ArticlesRepository (Payload)"</span>, () =&gt; {
<span class="n">articlesRepositoryContract</span>.<span class="n">run</span>(<span class="k">async</span> () =&gt; {
<span class="k">const</span> <span class="n">stub</span> = <span class="n">buildPayloadStub</span>();
(<span class="n">getPayload</span> <span class="k">as</span> <span class="t">Mock</span>).<span class="n">mockResolvedValue</span>(<span class="n">stub</span>);
<span class="k">return new</span> <span class="t">ArticlesRepository</span>(<span class="n">stubPayloadConfig</span>);
});
});</pre>
</div>
</details>
</div>
<div class="cf-card">
<div class="cf-tag">__factories__/</div>
<h3>The data <em>factory</em>.</h3>
<p>A factory is a sequence-counter-driven builder for an entity. <code>articleFactory.build({ title: "X" })</code> hands you a complete, valid <code>Article</code> with sensible defaults — only the fields you specify get overridden. Call <code>.reset()</code> in <code>beforeEach</code> to keep ids deterministic.</p>
<p>The point: tests stop drowning in inline fixtures (<code>{ id: "abc", title: "...", slug: "...", content: null, status: "draft", authorId: "u1", createdAt: new Date(...), updatedAt: new Date(...) }</code>) and assert only the fields they care about.</p>
<details class="cf-detail">
<summary>show: defining a factory</summary>
<div class="detail-body">
<pre class="code" data-lang="typescript // packages/blog/src/__factories__/article.factory.ts"><span class="k">import</span> { <span class="n">defineFactory</span> } <span class="k">from</span> <span class="s">"@repo/core-testing/factory"</span>;
<span class="k">import type</span> { <span class="t">Article</span> } <span class="k">from</span> <span class="s">"../entities/models/article"</span>;
<span class="k">export const</span> <span class="n">articleFactory</span> = <span class="n">defineFactory</span>&lt;<span class="t">Article</span>&gt;(({ <span class="n">sequence</span> }) =&gt; ({
<span class="n">id</span>: <span class="s">`article-${sequence}`</span>,
<span class="n">title</span>: <span class="s">`Article ${sequence}`</span>,
<span class="n">slug</span>: <span class="s">`article-${sequence}`</span>,
<span class="n">content</span>: <span class="k">null</span>,
<span class="n">status</span>: <span class="s">"draft"</span>,
<span class="n">authorId</span>: <span class="s">"user-1"</span>,
<span class="n">createdAt</span>: <span class="k">new</span> <span class="t">Date</span>(<span class="s">"2026-01-01T00:00:00Z"</span>),
<span class="n">updatedAt</span>: <span class="k">new</span> <span class="t">Date</span>(<span class="s">"2026-01-01T00:00:00Z"</span>),
}));</pre>
</div>
</details>
<details class="cf-detail">
<summary>show: using it in a test</summary>
<div class="detail-body">
<pre class="code" data-lang="typescript // get-articles.use-case.test.ts"><span class="n">it</span>(<span class="s">"filters by status"</span>, <span class="k">async</span> () =&gt; {
<span class="k">const</span> <span class="n">repo</span> = <span class="k">new</span> <span class="t">MockArticlesRepository</span>();
<span class="n">articleFactory</span>.<span class="n">reset</span>();
<span class="k">await</span> <span class="n">repo</span>.<span class="n">createArticle</span>(<span class="n">articleFactory</span>.<span class="n">build</span>({ <span class="n">status</span>: <span class="s">"draft"</span> }));
<span class="k">await</span> <span class="n">repo</span>.<span class="n">createArticle</span>(<span class="n">articleFactory</span>.<span class="n">build</span>({ <span class="n">status</span>: <span class="s">"published"</span> }));
<span class="k">const</span> <span class="n">useCase</span> = <span class="n">getArticlesUseCase</span>(<span class="n">repo</span>);
<span class="k">const</span> <span class="n">result</span> = <span class="k">await</span> <span class="n">useCase</span>({ <span class="n">status</span>: <span class="s">"published"</span> });
<span class="n">expect</span>(<span class="n">result</span>).<span class="n">toHaveLength</span>(<span class="n">1</span>);
});</pre>
</div>
</details>
</div>
</div>
</section>
<!-- ─── 05 ─ VERDICT ─────────────────────────────────────────────────── -->
<section id="verdict" style="border-bottom: 0;">
<div class="section-head">
<div class="section-num">§ 05</div>
<div>
<h2 class="section-title">Do we <em>need</em> them?</h2>
</div>
</div>
<div class="verdict-block">
<h3>Short answer: <em>yes</em>, both.<br>Long answer below.</h3>
<p><strong>Contracts</strong> earn their keep the day a real implementation drifts from its mock — when a Payload field name changes, when a return shape mutates, when null vs undefined gets blurred. The contract suite catches the divergence at unit-test time instead of in production. Cost: ~50 lines per repo. Payoff: every behavioral guarantee gets tested twice (mock + real) for free.</p>
<p><strong>Factories</strong> earn theirs by the third test. Inline fixtures grow to a noisy 810 lines that obscure what the test is actually checking. <code>articleFactory.build({ slug: "x" })</code> says "I need a valid article and I only care about the slug." Nothing else.</p>
<p>Honest tradeoff: small upfront cost (one factory + one contract per feature). Large compounding payoff once you have ≥3 tests touching the entity, or any time you add a second impl behind the same interface. They are not optional ceremony — they are the thing that lets you trust your mocks.</p>
<div class="badge-row">
<div class="badge"><strong>360 tests</strong>across 15 suites · contracts run 2× per repo</div>
<div class="badge"><strong>R25 + R26</strong>output-validation + error-mapping (Plan 9)</div>
<div class="badge"><strong>defineFactory · defineContractSuite</strong>both live in @repo/core-testing</div>
</div>
</div>
</section>
</main>
<footer class="frame">
<div class="foot-grid">
<div class="foot-col">
<h5>Architecture</h5>
<a href="overview.md">overview.md</a>
<a href="dependency-flow.md">dependency-flow.md</a>
<a href="vertical-feature-spec.md">vertical-feature-spec.md</a>
</div>
<div class="foot-col">
<h5>Decisions</h5>
<a href="../decisions/adr-008-per-feature-di-containers.md">adr-008 · per-feature DI</a>
<a href="../decisions/adr-011-tdd-foundation.md">adr-011 · TDD foundation</a>
<a href="../decisions/adr-012-lazar-conformance.md">adr-012 · Lazar conformance</a>
<a href="../decisions/adr-013-input-output-unification.md">adr-013 · I/O unification</a>
</div>
<div class="foot-col">
<h5>Refactor logs</h5>
<a href="../superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md">2026-05-05 · Plan 8</a>
<a href="../superpowers/refactor-logs/2026-05-06-input-output-unification.md">2026-05-06 · Plan 9</a>
</div>
<div class="foot-col">
<h5>Guides</h5>
<a href="../guides/adding-a-feature.md">adding-a-feature.md</a>
<a href="../guides/tdd-workflow.md">tdd-workflow.md</a>
<a href="../guides/testing-strategy.md">testing-strategy.md</a>
</div>
</div>
<div class="colophon">
Set in Fraunces &amp; JetBrains Mono. Single-file HTML, no build step. Open it in any browser.
</div>
</footer>
<script>
// ─── Layer card data (anatomy) ───────────────────────────────────────
const LAYERS = {
entities: {
tag: "domain",
title: "Entities — the <em>nouns</em>",
body: "Pure domain shapes with zero outside knowledge. Every model is a Zod schema with a co-located <code>z.infer</code> type. Errors live one folder over and set <code>this.name = \"&lt;ClassName&gt;\"</code> in the constructor — that's R6 from Plan 9.",
meta: "Examples · <strong>Article, User, Cookie, Page, Header, Media · ArticleNotFoundError, AuthenticationError, InputParseError</strong>"
},
application: {
tag: "rules",
title: "Application — the <em>verbs</em>",
body: "Use cases live here, plus the interfaces they consume. After Plan 9 every use case is a factory: <code>(deps) ⇒ async (input) ⇒ result</code>. The file owns its <code>xInputSchema</code>, its <code>xOutputSchema</code>, and the runtime <code>.parse(...)</code> call before returning. Repository and service interfaces sit next door, blissfully unaware of who implements them.",
meta: "Examples · <strong>getArticlesUseCase, signInUseCase, deleteMediaUseCase</strong> · IArticlesRepository, IAuthenticationService"
},
infrastructure: {
tag: "implementations",
title: "Infrastructure — the <em>real world</em>",
body: "Two siblings per repository: <code>&lt;x&gt;.repository.ts</code> (real, Payload-backed, takes config in the constructor) and <code>&lt;x&gt;.repository.mock.ts</code> (in-memory, used by default in DI). Both are <code>@injectable</code>; both implement the same interface; both run through the same contract suite to prove they behave identically.",
meta: "Examples · <strong>ArticlesRepository · MockArticlesRepository · AuthenticationService · MockAuthenticationService</strong>"
},
adapters: {
tag: "transport boundary",
title: "Interface adapters — <em>controllers</em>",
body: "One controller per use case (Lazar's rule). It takes <code>unknown</code> input, runs <code>safeParse</code> against the use-case's schema, throws <code>InputParseError</code> on failure, calls the use case, then runs the result through a top-level <code>function presenter(value)</code> defined in the same file. Identity is fine — <code>return value;</code> — but the function form always exists so adding a transform later is one edit.",
meta: "Examples · <strong>getArticlesController · signInController · deleteMediaController</strong> (void return — no presenter)"
},
di: {
tag: "wiring",
title: "DI — <em>per-feature</em> container",
body: "Symbols are <code>Symbol.for(\"&lt;feature&gt;:I&lt;X&gt;\")</code> keys. The <code>ContainerModule</code> wires them: the repository to its mock by default; use cases and controllers via <code>.toDynamicValue</code> so their factory functions are constructed on every <code>container.get()</code>. <code>bind-production.ts</code> swaps the mock for the real impl at app boot using <code>.toConstantValue</code>.",
meta: "Files · symbols.ts · module.ts · container.ts · bind-production.ts"
},
integrations: {
tag: "outside world",
title: "Integrations — <em>tRPC + CMS</em>",
body: "Where the feature meets the framework. <code>integrations/api/procedures.ts</code> (Plan 9) builds an <code>xProcedure</code> with <code>defineErrorMiddleware</code> applied — it owns the feature's domain-error → <code>TRPCError</code> mapping. <code>router.ts</code> uses that procedure plus <code>.input(xInputSchema)</code> imported from the use case. <code>cms/</code> exports Payload collection / global definitions consumed by <code>core-cms</code>.",
meta: "api/procedures.ts · api/router.ts · cms/collections/&lt;x&gt;.ts · cms/globals/&lt;x&gt;.ts"
},
ui: {
tag: "frontend surface",
title: "UI — <em>queries &amp; components</em>",
body: "Plan 9 split the public surface: feature root (<code>@repo/blog</code>) exports <em>contracts only</em> — types, errors, schemas, IUseCase aliases, router type, constants. <code>./ui</code> exports React Query option builders and (eventually) components. Apps import schemas from the root for forms, queries from <code>./ui</code> for hooks. No mixing.",
meta: "ui/index.ts · ui/query.ts · @repo/&lt;feature&gt;/ui subpath in package.json"
},
testing: {
tag: "test ergonomics",
title: "<em>__factories__</em> &amp; <em>__contracts__</em>",
body: "Two test-only utilities. Factories build valid entities with one-line overrides (<code>articleFactory.build({ slug: \"x\" })</code>). Contracts are portable behavioral suites that run against any implementation of a repository interface — same suite passes against the mock and the real Payload impl, or one of them fails and you've found a bug. See §04.",
meta: "Powered by · @repo/core-testing/factory · @repo/core-testing/contract"
},
public: {
tag: "package.json exports",
title: "<em>index.ts</em> — the public surface",
body: "Five subpaths per feature: <code>.</code> (contracts: types, errors, schemas, IUseCase / IController aliases, router type, constants), <code>./ui</code> (queries + components), <code>./api</code> (the tRPC router — only <code>core-api</code> consumes it), <code>./cms</code> (Payload collections — only <code>core-cms</code> consumes), <code>./di/bind-production</code> (called by app boot). Anything else is private.",
meta: "Five subpaths · enforced by ESLint boundaries + Turborepo + the package.json exports map"
}
};
const tree = document.getElementById('tree');
const layerCard = document.getElementById('layerCard');
function renderLayer(layer) {
const data = LAYERS[layer] || LAYERS.entities;
layerCard.innerHTML = `
<div class="layer-tag">${data.tag}</div>
<h3>${data.title}</h3>
<p>${data.body}</p>
<div class="meta">${data.meta}</div>
`;
layerCard.classList.remove('fade-in');
void layerCard.offsetWidth;
layerCard.classList.add('fade-in');
}
tree.querySelectorAll('.tree-row').forEach(row => {
row.addEventListener('click', () => {
const layer = row.dataset.layer;
tree.querySelectorAll('.tree-row').forEach(r => r.classList.toggle('is-active', r.dataset.layer === layer));
renderLayer(layer);
});
});
// initialize
document.querySelectorAll('.tree-row[data-layer="application"]').forEach(r => r.classList.add('is-active'));
renderLayer('application');
// ─── Feature data ─────────────────────────────────────────────────────
const FEATURES = {
blog: {
name: "blog",
useCase: "getArticles",
procedure: "blogProcedure",
container: "blogContainer",
symbols: "BLOG_SYMBOLS",
controllerSymbol: "IGetArticlesController",
inputSchemaName: "getArticlesInputSchema",
outputSchemaName: "getArticlesOutputSchema",
repoInterface: "IArticlesRepository",
repoMock: "MockArticlesRepository",
repoReal: "ArticlesRepository",
notFound: "ArticleNotFoundError",
errorMap: [
["InputParseError", "BAD_REQUEST"],
["ArticleNotFoundError", "NOT_FOUND"],
],
presenterReturns: "the array unchanged (identity presenter)",
presenterCode: `function presenter(value: GetArticlesOutput) {\n return value;\n}`,
isVoidPresenter: false,
queryName: "listArticlesQuery",
queryArg: "{ status: \"published\" }",
},
auth: {
name: "auth",
useCase: "signIn",
procedure: "authProcedure",
container: "authContainer",
symbols: "AUTH_SYMBOLS",
controllerSymbol: "ISignInController",
inputSchemaName: "signInInputSchema",
outputSchemaName: "signInOutputSchema",
repoInterface: "IUsersRepository",
repoMock: "MockUsersRepository",
repoReal: "UsersRepository",
notFound: "AuthenticationError",
errorMap: [
["InputParseError", "BAD_REQUEST"],
["AuthenticationError", "UNAUTHORIZED"],
["UnauthenticatedError", "UNAUTHORIZED"],
["UnauthorizedError", "FORBIDDEN"],
],
presenterReturns: "value.cookie (drops session, returns just the cookie)",
presenterCode: `function presenter(value: SignInOutput) {\n return value.cookie;\n}`,
isVoidPresenter: false,
queryName: "(no query — sign-in is a mutation)",
queryArg: "{ username, password }",
},
"marketing-pages": {
name: "marketing-pages",
useCase: "getPageBySlug",
procedure: "marketingPagesProcedure",
container: "marketingPagesContainer",
symbols: "MARKETING_PAGES_SYMBOLS",
controllerSymbol: "IGetPageBySlugController",
inputSchemaName: "getPageBySlugInputSchema",
outputSchemaName: "getPageBySlugOutputSchema",
repoInterface: "IPagesRepository",
repoMock: "MockPagesRepository",
repoReal: "PagesRepository",
notFound: "PageNotFoundError",
errorMap: [
["InputParseError", "BAD_REQUEST"],
["PageNotFoundError", "NOT_FOUND"],
],
presenterReturns: "the page (identity); use case returns undefined for missing slug — semantics preserved",
presenterCode: `function presenter(value: GetPageBySlugOutput) {\n return value;\n}`,
isVoidPresenter: false,
queryName: "pageBySlugQuery",
queryArg: "{ slug: \"about\" }",
},
navigation: {
name: "navigation",
useCase: "getHeader",
procedure: "navigationProcedure",
container: "navigationContainer",
symbols: "NAVIGATION_SYMBOLS",
controllerSymbol: "IGetHeaderController",
inputSchemaName: "getHeaderInputSchema",
outputSchemaName: "getHeaderOutputSchema",
repoInterface: "IHeaderRepository",
repoMock: "MockHeaderRepository",
repoReal: "HeaderRepository",
notFound: "HeaderNotFoundError",
errorMap: [
["InputParseError", "BAD_REQUEST"],
["HeaderNotFoundError", "NOT_FOUND"],
],
presenterReturns: "the header (identity); use case throws HeaderNotFoundError on missing data",
presenterCode: `function presenter(value: GetHeaderOutput) {\n return value;\n}`,
isVoidPresenter: false,
queryName: "headerQuery",
queryArg: "{} (void input — z.object({}).strict())",
},
media: {
name: "media",
useCase: "deleteMedia",
procedure: "mediaProcedure",
container: "mediaContainer",
symbols: "MEDIA_SYMBOLS",
controllerSymbol: "IDeleteMediaController",
inputSchemaName: "deleteMediaInputSchema",
outputSchemaName: null,
repoInterface: "IMediaRepository",
repoMock: "MockMediaRepository",
repoReal: "MediaRepository",
notFound: "MediaNotFoundError",
errorMap: [
["InputParseError", "BAD_REQUEST"],
["MediaNotFoundError", "NOT_FOUND"],
],
presenterReturns: "nothing — controller returns Promise<void> with no presenter (R12)",
presenterCode: `// no presenter — void output\nasync (input: unknown): Promise<void> => {\n ...\n await deleteMediaUseCase(parsed.data);\n}`,
isVoidPresenter: true,
queryName: "(no query — deleteMedia is a mutation)",
queryArg: "{ id: \"media-123\" }",
},
};
// ─── Stages ───────────────────────────────────────────────────────────
function makeStages(F) {
const useCaseFile = `application/use-cases/${kebab(F.useCase)}.use-case.ts`;
const controllerFile = `interface-adapters/controllers/${kebab(F.useCase)}.controller.ts`;
const stages = [
{
id: "client",
title: "Frontend query",
meta: `@repo/${F.name}/ui`,
file: `packages/${F.name}/src/ui/query.ts`,
tag: "client",
prose: `<code>${F.queryName}</code> on the client builds a typed React Query option object. The args type is inferred all the way back from the use case's <code>${F.inputSchemaName}</code>, exported from the feature root.`,
code: F.queryName.startsWith("(") ?
`// auth procedures are mutations — invoked via useMutation, not useQuery
const trpc = useTRPC();
const signIn = useMutation(trpc.${F.name}.${F.useCase}.mutationOptions());
signIn.mutate(${F.queryArg});` :
`import { ${F.queryName} } from "@repo/${F.name}/ui";
const trpc = useTRPC();
const { data } = useQuery(${F.queryName}(trpc, ${F.queryArg}));`,
lang: "typescript // app code",
kind: "request",
},
{
id: "trpc",
title: "tRPC procedure",
meta: `${F.procedure}.input(${F.inputSchemaName})`,
file: `packages/${F.name}/src/integrations/api/router.ts`,
tag: "boundary",
prose: `tRPC parses the input through <code>${F.inputSchemaName}</code> — the <em>same schema</em> the use case will use. If parse fails here, tRPC throws <code>TRPCError(BAD_REQUEST)</code> natively. The mutation handler resolves the controller from the feature container and calls it.`,
code: `${F.useCase}: ${F.procedure}
.input(${F.inputSchemaName})
.${F.useCase.startsWith("get") || F.useCase === "list" || F.useCase === "getHeader" ? "query" : "mutation"}(({ input }) => {
const ctrl = ${F.container}.get<${F.controllerSymbol}>(
${F.symbols}.${F.controllerSymbol},
);
return ctrl(input);
})`,
lang: "typescript // integrations/api/router.ts",
kind: "request",
},
{
id: "middleware",
title: "defineErrorMiddleware",
meta: `${F.procedure} = t.procedure.use(...)`,
file: `packages/${F.name}/src/integrations/api/procedures.ts`,
tag: "wrap",
prose: `Every <code>${F.procedure}</code> call passes through this middleware, which wraps the handler in a try/catch (well — actually <code>result.ok</code> inspection because tRPC v11). On the way IN, it does nothing; on the way OUT (or on a throw), it inspects the cause against the feature's error tuples and remaps to <code>TRPCError</code>. <em>core-shared</em> never knows your error class names — you pass them in.`,
code: `import { defineErrorMiddleware } from "@repo/core-shared/trpc/define-error-middleware";
export const ${F.procedure} = t.procedure.use(
defineErrorMiddleware([
${F.errorMap.map(([err, code]) => ` [${err}, "${code}"],`).join("\n")}
]),
);`,
lang: "typescript // integrations/api/procedures.ts",
kind: "request",
},
{
id: "controller-parse",
title: "Controller · safeParse",
meta: `${kebab(F.useCase)}.controller.ts`,
file: `packages/${F.name}/src/${controllerFile}`,
tag: "validate",
prose: `The controller takes <code>input: unknown</code> and runs <code>${F.inputSchemaName}.safeParse(input)</code>. If it fails, throws <code>InputParseError</code> with the Zod issue as <code>cause</code> — the middleware will catch this and map it to <code>BAD_REQUEST</code>. Yes the schema runs twice on the tRPC path. That's fine — the cost is trivial and the controller stays callable from non-tRPC entry points (CLI, cron, server actions).`,
code: F.isVoidPresenter ?
`export const ${F.useCase}Controller =
(${F.useCase}UseCase: I${capitalize(F.useCase)}UseCase) =>
async (input: unknown): Promise<void> => {
const parsed = ${F.inputSchemaName}.safeParse(input);
if (!parsed.success) {
throw new InputParseError(
"Invalid ${F.useCase} input",
{ cause: parsed.error },
);
}
await ${F.useCase}UseCase(parsed.data);
};` :
`export const ${F.useCase}Controller =
(${F.useCase}UseCase: I${capitalize(F.useCase)}UseCase) =>
async (input: unknown): Promise<ReturnType<typeof presenter>> => {
const parsed = ${F.inputSchemaName}.safeParse(input);
if (!parsed.success) {
throw new InputParseError(
"Invalid ${F.useCase} input",
{ cause: parsed.error },
);
}
const result = await ${F.useCase}UseCase(parsed.data);
return presenter(result);
};`,
lang: "typescript // interface-adapters/controllers/...",
kind: "request",
},
{
id: "usecase",
title: "Use case",
meta: F.outputSchemaName ? `body ends with ${F.outputSchemaName}.parse(...)` : `void output — no output schema`,
file: `packages/${F.name}/src/${useCaseFile}`,
tag: "business logic",
prose: F.outputSchemaName ?
`The use case is a curried factory — <code>(deps) ⇒ async (input) ⇒ result</code>. It calls the repository, then runs <code>${F.outputSchemaName}.parse(result)</code> before returning. That last <code>.parse</code> is a Plan 9 contract: the use case <em>guarantees</em> what comes out, so a misbehaving repo fails loudly here, not silently downstream.` :
`Void-output use case: takes input, calls the repository, returns nothing. No <code>xOutputSchema</code> means no <code>parse</code> at the end — there's nothing to validate. The presenter rule (R11) carves out an exception for these: void in, void out, no presenter.`,
code: F.outputSchemaName ?
`export const ${F.useCase}UseCase =
(${F.repoInterface[1].toLowerCase() + F.repoInterface.slice(2)}: ${F.repoInterface}) =>
async (input: ${capitalize(F.useCase)}Input): Promise<${capitalize(F.useCase)}Output> => {
const result = await ${F.repoInterface[1].toLowerCase() + F.repoInterface.slice(2)}.${F.useCase}(input);
return ${F.outputSchemaName}.parse(result);
};` :
`export const ${F.useCase}UseCase =
(mediaRepository: ${F.repoInterface}) =>
async (input: ${capitalize(F.useCase)}Input): Promise<void> => {
const existing = await mediaRepository.getMedia(input.id);
if (!existing) {
throw new ${F.notFound}(\`Media with id "\${input.id}" not found\`);
}
await mediaRepository.deleteMedia(input.id);
};`,
lang: "typescript // application/use-cases/...",
kind: "request",
},
{
id: "repository",
title: "Repository",
meta: `infrastructure · ${F.repoMock} (default) / ${F.repoReal} (prod)`,
file: `packages/${F.name}/src/infrastructure/repositories/...`,
tag: "data",
prose: `In dev / test, the in-memory <code>${F.repoMock}</code> answers. In production, the container has been rebound to a fresh <code>${F.repoReal}</code> instance constructed with the Payload config — same interface, same contract suite, different storage. The use case can't tell which one it's talking to.`,
code: `// 1) default binding (mock — used for tests + dev)
bind<${F.repoInterface}>(${F.symbols}.${F.repoInterface}).to(${F.repoMock});
// 2) production override (called from app boot)
${F.container}
.bind(${F.symbols}.${F.repoInterface})
.toConstantValue(new ${F.repoReal}(config));`,
lang: "typescript // di/module.ts + di/bind-production.ts",
kind: "request",
},
{
id: "payload",
title: "Payload Local API",
meta: "third-party · CMS",
file: "(payload package)",
tag: "external",
prose: `The real repository calls <code>getPayload({ config })</code> and uses Payload's local API to read/write Postgres. The mock just operates on a private array. Both implement the same interface, both pass the same contract suite.`,
code: `async getMedia(id: string): Promise<Media | undefined> {
const payload = await getPayload({ config: this.config });
const result = await payload.findByID({
collection: "media",
id,
overrideAccess: true,
});
return result ? mediaSchema.parse(this.toDomain(result)) : undefined;
}`,
lang: "typescript // infrastructure/repositories/<x>.repository.ts",
kind: "request",
},
{
id: "fork",
title: "(success or error?)",
meta: "this is the fork",
file: "—",
tag: "branching",
prose: `From here the request walks back up the stack. If the use case returned cleanly, the result threads back through the controller's presenter. If anything along the way threw a domain error, tRPC's middleware catches the cause via <code>defineErrorMiddleware</code> and remaps it to a <code>TRPCError</code> with the right code.`,
code: `// success: data flows back through presenter -> controller return -> tRPC response
// error: throw -> tRPC catch -> middleware checks instanceof on cause -> TRPCError(code)
// Same procedure call, two paths — picked by what the use case did.`,
lang: "outcome",
kind: "fork",
},
{
id: "presenter",
title: "Presenter (success path)",
meta: F.isVoidPresenter ? "void return — no presenter" : "controller's top-level function presenter()",
file: `packages/${F.name}/src/${controllerFile}`,
tag: "shape",
prose: F.isVoidPresenter ?
`For void use cases, there's no presenter and the controller resolves to <code>Promise&lt;void&gt;</code>. The tRPC procedure simply returns nothing — the client treats it as an ack.` :
`Co-located with the controller in the same file. Returns <code>${F.presenterReturns}</code>. The controller's full return type is <code>Promise&lt;ReturnType&lt;typeof presenter&gt;&gt;</code> — a literal copy of whatever the presenter outputs. tRPC's response type follows.`,
code: F.presenterCode,
lang: "typescript // top of the controller file",
kind: "success",
},
{
id: "trpc-error",
title: "Middleware (error path)",
meta: "defineErrorMiddleware translates",
file: `packages/${F.name}/src/integrations/api/procedures.ts`,
tag: "translate",
prose: `When the use case or controller threw a domain error, the middleware sees it on <code>result.error.cause</code>. It scans the registered tuples by <code>instanceof</code>; the first match wins. Throws a fresh <code>TRPCError({ code, cause })</code> with the original error preserved on <code>cause</code> — the client gets a structured error with both the code and the message.`,
code: `// inside defineErrorMiddleware (core-shared)
if (e instanceof Error) {
for (const [Ctor, code] of map) {
if (e instanceof Ctor) {
throw new TRPCError({ code, message: e.message, cause: e });
}
}
}
throw e; // unmapped → tRPC default INTERNAL_SERVER_ERROR`,
lang: "typescript // core-shared/trpc/define-error-middleware.ts",
kind: "error",
},
{
id: "response",
title: "tRPC response",
meta: "wire · superjson serialization",
file: "—",
tag: "client",
prose: `tRPC serializes the response through superjson (Date and other rich types survive the round-trip). On the client, React Query unwraps it into your typed <code>data</code>. On the server, the cycle is done.`,
code: `// on the client:
const { data } = useQuery(${F.queryName.startsWith("(") ? "..." : F.queryName + "(trpc, ...)"});
// data: ${F.outputSchemaName ? capitalize(F.useCase) + "Output (after presenter shape)" : "void"}
// fully typed end to end through inferRouterOutputs<AppRouter>`,
lang: "typescript // app code",
kind: "success",
},
];
return stages;
}
function kebab(s) { return s.replace(/[A-Z]/g, c => "-" + c.toLowerCase()); }
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
// ─── Render flow ──────────────────────────────────────────────────────
const flowPipeline = document.getElementById('flowPipeline');
const stageDetail = document.getElementById('stageDetail');
const codesTable = document.getElementById('codesTable');
const stageProgress = document.getElementById('stageProgress');
const playBtn = document.getElementById('playStage');
const prevBtn = document.getElementById('prevStage');
const nextBtn = document.getElementById('nextStage');
let currentFeature = "blog";
let currentStages = makeStages(FEATURES[currentFeature]);
let currentStageIdx = 0;
let playing = false;
let playTimer = null;
function renderPipeline() {
// pop existing stage rows but keep controls
flowPipeline.querySelectorAll('.flow-stage, .flow-fork-divider').forEach(el => el.remove());
const ctrl = flowPipeline.querySelector('.flow-controls');
let insertedFork = false;
currentStages.forEach((stage, idx) => {
if (stage.kind === "fork" && !insertedFork) {
const div = document.createElement('div');
div.className = "flow-fork-divider";
div.textContent = "fork";
flowPipeline.insertBefore(div, ctrl);
insertedFork = true;
}
const row = document.createElement('div');
row.className = "flow-stage";
if (idx === currentStageIdx) row.classList.add('is-active');
if (stage.kind === "success") row.classList.add('fork-success');
if (stage.kind === "error") row.classList.add('fork-error');
row.dataset.idx = idx;
row.innerHTML = `
<div class="stage-num">${String(idx + 1).padStart(2, '0')}</div>
<div class="stage-body">
<div class="stage-name">${stage.title}</div>
<div class="stage-meta">${stage.meta}</div>
</div>
`;
row.addEventListener('click', () => {
currentStageIdx = idx;
stopPlay();
renderStage();
updatePipelineActive();
});
flowPipeline.insertBefore(row, ctrl);
});
updatePipelineActive();
}
function updatePipelineActive() {
flowPipeline.querySelectorAll('.flow-stage').forEach(row => {
row.classList.toggle('is-active', Number(row.dataset.idx) === currentStageIdx);
});
stageProgress.textContent = `${currentStageIdx + 1} / ${currentStages.length}`;
prevBtn.disabled = currentStageIdx === 0;
nextBtn.disabled = currentStageIdx === currentStages.length - 1;
}
function renderStage() {
const stage = currentStages[currentStageIdx];
const html = `
<div class="detail-tag">${stage.tag}</div>
<h3>${stage.title}</h3>
<div class="file-path">${stage.file}</div>
<p class="detail-prose">${stage.prose}</p>
<pre class="code" data-lang="${stage.lang}">${escapeHtml(stage.code)}</pre>
`;
stageDetail.innerHTML = html;
stageDetail.classList.remove('fade-in');
void stageDetail.offsetWidth;
stageDetail.classList.add('fade-in');
updatePipelineActive();
}
function escapeHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderCodesTable() {
const F = FEATURES[currentFeature];
const rows = F.errorMap.map(([err, code]) =>
`<div class="codes-row"><span class="err">${err}</span><span class="code"><span class="arrow">→</span> ${code}</span></div>`
).join('');
codesTable.innerHTML = `
<div class="codes-table-head">
<span>${F.procedure} · domain error → tRPC code</span>
<span style="text-align: right;">tuple count: ${F.errorMap.length}</span>
</div>
${rows}
`;
}
// ─── Feature picker ──────────────────────────────────────────────────
document.querySelectorAll('#featurePicker button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#featurePicker button').forEach(b => b.setAttribute('aria-pressed', 'false'));
btn.setAttribute('aria-pressed', 'true');
currentFeature = btn.dataset.feature;
currentStages = makeStages(FEATURES[currentFeature]);
currentStageIdx = 0;
stopPlay();
renderPipeline();
renderStage();
renderCodesTable();
});
});
// ─── Controls ────────────────────────────────────────────────────────
prevBtn.addEventListener('click', () => {
if (currentStageIdx > 0) {
currentStageIdx--;
stopPlay();
renderStage();
}
});
nextBtn.addEventListener('click', () => {
if (currentStageIdx < currentStages.length - 1) {
currentStageIdx++;
stopPlay();
renderStage();
}
});
function startPlay() {
playing = true;
playBtn.textContent = "❚❚ pause";
playBtn.classList.remove('primary');
if (currentStageIdx >= currentStages.length - 1) currentStageIdx = 0;
renderStage();
playTimer = setInterval(() => {
if (currentStageIdx >= currentStages.length - 1) {
stopPlay();
return;
}
currentStageIdx++;
renderStage();
}, 2400);
}
function stopPlay() {
playing = false;
playBtn.textContent = "▶ play";
playBtn.classList.add('primary');
if (playTimer) { clearInterval(playTimer); playTimer = null; }
}
playBtn.addEventListener('click', () => playing ? stopPlay() : startPlay());
// ─── DI mode toggle ──────────────────────────────────────────────────
const diMockBtn = document.getElementById('diMockBtn');
const diProdBtn = document.getElementById('diProdBtn');
const repoNode = document.getElementById('repoNode');
const bridgeRepo = document.getElementById('bridgeRepo');
function setDiMode(mode) {
if (mode === 'prod') {
diMockBtn.setAttribute('aria-pressed', 'false');
diProdBtn.setAttribute('aria-pressed', 'true');
repoNode.classList.remove('is-mock');
repoNode.classList.add('is-real');
repoNode.querySelector('.node-kind').textContent = 'class · real (Payload)';
repoNode.querySelector('.node-name').textContent = 'ArticlesRepository';
bridgeRepo.textContent = '.toConstantValue(new ArticlesRepository(config))';
} else {
diMockBtn.setAttribute('aria-pressed', 'true');
diProdBtn.setAttribute('aria-pressed', 'false');
repoNode.classList.remove('is-real');
repoNode.classList.add('is-mock');
repoNode.querySelector('.node-kind').textContent = 'class · mock';
repoNode.querySelector('.node-name').textContent = 'MockArticlesRepository';
bridgeRepo.textContent = '.to(MockArticlesRepository)';
}
}
diMockBtn.addEventListener('click', () => setDiMode('mock'));
diProdBtn.addEventListener('click', () => setDiMode('prod'));
// ─── Initialize ──────────────────────────────────────────────────────
renderPipeline();
renderStage();
renderCodesTable();
// Smooth-scroll anchors
document.querySelectorAll('a[href^="#"]').forEach(a => {
a.addEventListener('click', e => {
const target = document.querySelector(a.getAttribute('href'));
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
</script>
</body>
</html>