parts / performix
Performix — reusable patterns
Protected-feedback performance intelligence with a precompute-and-playback architecture. Eighteen patterns total: the load-bearing shapes (Insight player paradigm, the privacy gate, the three-way adapter factory, MCP consumer discipline, the binding-constraint diagnostic), the cross-portfolio sharing shapes from the Wave 1–10 marketing-site build-out (packages-dir with vendor-pin, theme-agnostic exportable component with render-prop chart slot, sibling-repo build-time sync), and the substrate-pipeline scaffolding (NotImplementedError per stage, three-tier promotion gate with sticky rejection, watchlist-driven harvest, frontend-only sandbox of the production diagnostic).
Performix — Reusable Engineering Patterns
Production-validated patterns from the Performix codebase, stripped of business context, written to be dropped into any new system.
Each pattern has the same structure: Problem → The Pattern (TS sketch) → Key Design Decisions → This Codebase (real file paths) → Tradeoffs. Pick by the index. The patterns marked CROSS appear in 2+ portfolio repos and reflect architectural convictions that hardened across products.
Companion to the per-product REUSABLE_PATTERNS.md files in vela, devplane, baby-namer, principia, and people-analytics-toolbox.
Note on numbering convention. Performix and Namesake use
## N. Title(h2) section headers with sub-sections### Problem,### The Pattern,### Key Design Decisions,### This Codebase,### Tradeoffs. This is a deviation from the canonicalEXTRACTION-SPEC.md(which recommends### PNN. Titlewith bolded sub-section labels). The DP-173 library indexer's per-product parser handles this older convention correctly. New entries here continue the older convention to preserve catalog consistency.
Pattern Index
| # | Pattern | When to reach for it |
|---|---|---|
| 1 | Precompute-and-playback over recompute-at-render | Any analytics surface where the same value should be identical across users and across page reloads. |
| 2 | Three-way adapter factory (mock / HTTP / MCP) with env switch | A capability that needs to swap concrete back-ends without touching call sites. CROSS |
| 3 | Vendored typed contracts for cross-repo service consumption | Consuming services owned by a different repo without importing its modules. |
| 4 | MCP consumer with lazy session affinity | Long-lived MCP transports inside a Next.js process; one warm session per process. |
| 5 | Tier-gated read path with server-only boundary | Multi-tier storage where lower tiers must never leak to UI code. CROSS |
| 6 | Privacy gate as a substrate primitive | Team-level rollups of individually-attributable data. |
| 7 | Capability architecture — contracts / core / adapters / ui / tests | A repo that's going to grow more than five distinct functional capabilities. |
| 8 | Zod-validated request/response envelope | Any HTTP or MCP boundary in a TypeScript project. CROSS |
| 9 | Action-status transition gate (lightweight state machine) | Multi-stage workflows that don't need an XState-class library. CROSS |
| 10 | Binding-constraint diagnostic (lowest-of-N as the action signal) | Multi-dimensional scoring where the action question is "what's stopping the system right now?" |
| 11 | Substitution-boundary pipeline scaffold (NotImplementedError per stage) | A multi-stage pipeline whose contracts are known but whose stage bodies will land over weeks. |
| 12 | Sibling-repo build-time sync with bundled fallback | A site that consumes a JSON artifact from a sibling repo locally but must also build on a cloud runner where the sibling isn't checked out. |
| 13 | Watchlist-driven harvest with cursor write-back | Persistent searches against an external API where each query has its own cadence and writes its own dated output. CROSS |
| 14 | Three-tier promotion gate with sticky rejection | An extraction/verification pipeline whose output must never auto-promote past a human reviewer. CROSS |
| 15 | Cross-portfolio packages dir with vendor-pin convention | Sharing UI primitives across sibling repos when npm publishing and monorepo migration are both premature. |
| 16 | Theme-agnostic exportable component with render-prop chart slot | A component you want consumers to vendor that should work in their design system without inheriting yours. |
| 17 | Frontend-only sandbox mirror of a production diagnostic | A public marketing-site "try it" surface that gives prospects the feel of a real diagnostic without the auth/DB/IRT stack. |
| 18 | Delete-and-reinsert for composite-PK canonical state | Idempotent seed scripts where a row set keyed by composite PK must match a declared order. |
1. Precompute-and-playback over recompute-at-render
Problem
Analytics surfaces tempted to recompute on read are slow under load, drift across observers (two users hit the same dashboard and see different numbers because rounding changed in flight), and tightly couple UI components to raw upstream data. The framing matters — analytics surfaces are not query forms; they are music players. The user does not press play to ask the player to assemble the song from raw audio.
The Pattern
// 1. Compute upstream, on a schedule or on a write trigger.
// Output: a first-class typed record with provenance.
type Insight = {
insightId: string;
computedAt: string; // ISO timestamp
segment: SegmentKey;
metric: MetricKey;
period: PeriodKey;
value: number;
sampleSize: number;
provenance: { upstream: string; method: string };
};
// 2. Store. Insights are records, not derived views.
await db.insert(insightsTable).values(insight);
// 3. Read at render. Never recompute here.
// (Marked `server-only` so client bundles can't accidentally import.)
import "server-only";
export async function getInsight(id: string): Promise<Insight | null> {
const row = await db.query.insightsTable.findFirst({ where: eq(insightId, id) });
return row ? rowToInsight(row) : null;
}
// 4. Route handlers retrieve, validate, return. Zero computation.
export async function GET(req: Request, ctx: { params: { id: string } }) {
const insight = await getInsight(ctx.params.id);
if (!insight) return Response.json({ error: "not_found" }, { status: 404 });
return Response.json(insight);
}
Key Design Decisions
- Insights are records, not views. A view recomputes; a record is a fact. If the upstream method changes, you write a new record with the new method — old records keep their old values, and provenance tells you which is which.
- Computation is upstream, not on-render. A scheduled job, an on-write trigger, or an admin-invoked recompute. The render path is read-only by construction.
- Provenance is mandatory.
upstream(where the input came from) andmethod(which calculator produced the number) belong on every record. A surface that displays the number without provenance can't explain why two observers see different values across a deployment. - The
MetricEnvelopeshape is the cross-spoke composition unit.metric × segment × period × value × sampleSize × provenance × enrichment. Every read returns one of these. - No "auto" recompute on stale. If a record is stale, an upstream pipeline produced a new one; staleness in the player is visible (
computedAt) but never triggers in-render compute. This is the discipline that separates a player from a query form.
This Codebase
docs/ARCHITECTURE.md §"compute, store, retrieve, play" and §"the pipeline" — the canonical framing.
src/lib/insight-store.ts — "server-only" read module that route handlers and player UI both pull from.
src/app/api/teams/[teamId]/diagnosis/route.ts — route handler that retrieves a precomputed diagnosis Insight; no compute logic in the handler.
src/capabilities/insight-composition/core/compose.ts — the upstream composer that produces Insight records; pure function, no I/O.
Known gaps. No deterministic recompute scheduler yet (the pipeline runs on admin trigger + capability-level writes). When a third upstream source lands, a coordinator that fans recompute requests across segments will be needed.
Tradeoffs
| Pro | Con |
|---|---|
| Render is always fast; users see the same value | Stale records are possible — provenance + computedAt must be surfaced |
Two observers see identical numbers under the same computedAt | Recompute schedule has to be designed up front |
| Provenance is built into the schema, not bolted on | Every new metric requires an upstream computation path, not just a route handler |
| Player UI can never accidentally diverge from the canonical number | Storage cost grows with segment × metric × period |
2. Three-way adapter factory (mock / HTTP / MCP) with env switch
CROSS — generalizes from DevPlane P09 (Runtime Provider Registry).
Problem
A capability needs a back-end implementation that changes by environment: tests want a deterministic mock; local dev wants a local HTTP service; production wants the live MCP gateway. Hard-coding any one of these into the capability bleeds environment concerns into core logic. Threading a single configurable client through everything makes the call sites carry parameters they don't care about.
The Pattern
// 1. Define the port — what callers see, regardless of back-end.
export interface ReincarnationPort {
estimateAbility(req: EstimateRequest): Promise<EstimateResult>;
}
// 2. Three adapters implementing the same port.
class MockReincarnationAdapter implements ReincarnationPort { /* deterministic */ }
class HttpReincarnationAdapter implements ReincarnationPort { /* fetch */ }
class McpReincarnationAdapter implements ReincarnationPort { /* mcp client */ }
// 3. Factory picks one per process boot from env, caches the choice.
let cached: ReincarnationPort | null = null;
export function reincarnationClient(): ReincarnationPort {
if (cached) return cached;
const mode = process.env.REINCARNATION_MODE ?? "mock";
switch (mode) {
case "mock": cached = new MockReincarnationAdapter(); break;
case "http": cached = new HttpReincarnationAdapter(env.HTTP_URL!); break;
case "mcp": cached = new McpReincarnationAdapter(env.MCP_URL!, env.MCP_KEY!); break;
default: throw new Error(`unknown REINCARNATION_MODE: ${mode}`);
}
return cached;
}
// 4. Call sites are oblivious.
const result = await reincarnationClient().estimateAbility(req);
Key Design Decisions
- Port first, adapter second. The interface is the contract. Adapters are implementation details that can be swapped per env without changing the caller.
- One factory per capability, not one global registry. Each capability owns its own port + adapters + factory. Avoids the "central provider registry" anti-pattern where one config file knows about everything.
- Env switch picked at process boot, not per call. Cached. No runtime mode flipping; if you want a different back-end mid-process, restart.
- Mock adapter is a first-class citizen, not a test fixture. Lives in the same
adapters/folder. Tests, Storybook, demo modes, and local dev without infra all use it. - HTTP and MCP adapters share schemas but not transport code. Each one validates its own response with the same Zod contract; the validation, not the transport, is what guarantees the port contract holds.
This Codebase
src/capabilities/cams-diagnostic/adapters/client.ts — the reincarnationClient() factory; env switch across mock / http / mcp.
src/capabilities/protected-feedback/adapters/client.ts — same shape for the data-anonymizer capability.
src/capabilities/cams-diagnostic/adapters/mcp.ts — the MCP adapter implementation.
Known gaps. No standardized port-discovery: each capability re-implements the factory locally. If this grows to 5+ capabilities, a thin shared helper (makeEnvSwitchedFactory<T>(...)) would dedupe ~20 lines per capability without becoming a "central registry."
Tradeoffs
| Pro | Con |
|---|---|
| Call sites have zero awareness of back-end choice | Each new back-end is a new adapter class + factory branch |
| Tests use the mock adapter without monkey-patching | Three implementations to keep schema-aligned |
| Local dev runs without infra | Env-switch logic is repeated per capability |
| Demo mode is the same code path as production, with mock data | Adapter classes are slightly heavier than free functions |
3. Vendored typed contracts for cross-repo service consumption
Problem
A consumer app calls a service that lives in a different repo. Direct module import couples the consumer's build to the service's internals (and breaks when the service renames a file). Publishing the service as an npm package adds release process to every contract change. Re-deriving types in the consumer drifts silently when the service evolves.
The Pattern
// 1. In the consumer, copy the contract schemas into a vendored module.
// File: src/lib/reincarnation/contract.ts
/**
* VENDORED FROM: people-analyst/people-analytics-toolbox @ v0.4.2
* Re-vendor when CONTRACT_VERSION bumps major.
* Do not edit — re-copy from upstream.
*/
import { z } from "zod";
export const EstimateRequestSchema = z.object({
itemId: z.string(),
responseLog: z.array(z.object({ /* ... */ })),
});
export type EstimateRequest = z.infer<typeof EstimateRequestSchema>;
export const EstimateResultSchema = z.object({
ability: z.number(),
standardError: z.number(),
});
export type EstimateResult = z.infer<typeof EstimateResultSchema>;
export const CONTRACT_VERSION = "0.4.2";
// 2. Adapters validate responses with the vendored schemas.
// The contract — not the transport — is what guarantees the port holds.
const raw = await fetch(`${url}/reincarnation/estimate`, { /* ... */ });
return EstimateResultSchema.parse(await raw.json());
Key Design Decisions
- Vendor the schema, not the implementation. The consumer copies what the contract is, not how the service computes the answer. Internal logic stays inside the service.
- Header comment is mandatory. Source repo, version, re-vendor trigger — every vendored file says where it came from and when to refresh.
- Vendor on
majorbumps only.0.4.2→0.4.5is additive; consumer re-vendors on0.5.xor1.x.x. Avoids re-touching every consumer for every minor change. - Vendored modules live under
src/lib/<service>/contract.ts. One folder per consumed service. Easy to grepfind . -name contract.tsand see every cross-repo dependency. - Don't generate from OpenAPI. Hand-vendoring forces the consumer to read the contract once and notice when something interesting changed.
This Codebase
src/lib/reincarnation/contract.ts — vendored Zod schemas for the toolbox's reincarnation spoke; header marks source + version.
src/lib/data-anonymizer/contract.ts — vendored from people-analytics-toolbox/data-anonymizer; same shape.
src/capabilities/cams-diagnostic/adapters/mcp.ts — adapter that imports the vendored schemas and validates MCP responses against them.
Known gaps. No automated drift check between vendored and upstream — relies on the upstream service bumping CONTRACT_VERSION on breaking changes and the consumer noticing. A scripts/check-vendored-contracts.ts that diffs against the upstream repo would catch silent drift.
Tradeoffs
| Pro | Con |
|---|---|
| Cross-repo coupling is explicit and grep-able | Manual re-vendor step on major bumps |
| Consumer build doesn't depend on the service's repo layout | Drift between vendored copy and upstream is possible |
| Schema-level validation catches transport errors at the boundary | Larger consumer codebase (one file per service) |
| Service can refactor internals without breaking consumers | New consumers must know to vendor, not import |
4. MCP consumer with lazy session affinity
Problem
MCP (Model Context Protocol) connections are expensive to open — TLS handshake, capability negotiation, auth exchange. Opening a fresh session per call adds 100ms+ to every request. But Next.js processes are not long-lived in serverless deployments, and connections that linger across cold starts leak resources.
The Pattern
// One transport per process, lazily opened on first call,
// reused for subsequent calls within the same process lifetime.
class McpClient {
private connection: Promise<Transport> | null = null;
private async ensureConnected(): Promise<Transport> {
if (this.connection) return this.connection;
// Cache the *promise*, not the resolved value — so concurrent
// first-callers all await the same in-flight connect.
this.connection = this.openTransport();
return this.connection;
}
private async openTransport(): Promise<Transport> {
const t = await connectMcp({
url: this.url,
headers: { Authorization: `Bearer ${this.apiKey}` },
});
return t;
}
async call<T>(toolName: string, input: unknown, schema: z.ZodType<T>): Promise<T> {
const transport = await this.ensureConnected();
const raw = await transport.invoke(toolName, input);
return schema.parse(raw);
}
}
Key Design Decisions
- Cache the promise, not the resolved value. Two concurrent first-callers must await the same in-flight connect, not race two parallel opens. Caching the resolved
Transportis a footgun: it doesn't dedupe the initial race. - One transport per process. No connection pooling — MCP is not a database driver. If you need parallelism, parallelize the calls, not the transports.
- Bearer auth in the connect headers, not per-call. Auth is established at session-open; per-call auth headers are an MCP misconfiguration.
- Schema validation at every call site. Even though the transport is shared, every response is parsed against the vendored Zod contract. Trust the contract, not the transport.
- No reconnect logic on transient failures. If a call fails because the session dropped, let the caller retry — they have business-level context about whether retry is safe.
This Codebase
src/capabilities/cams-diagnostic/adapters/mcp.ts — the MCP adapter for the reincarnation spoke; implements ensureConnected() + call().
Known gaps. No telemetry on session lifetime (how long does a session typically live before the process recycles?). Future work: emit a metric on session open + close.
Tradeoffs
| Pro | Con |
|---|---|
| First call pays the connect cost; subsequent calls are fast | Cold starts always pay the full connect on first call |
| One transport per process keeps resource use predictable | No pooling — single transport is the throughput ceiling |
| Promise-caching dedupes concurrent first-callers | Failed initial connect requires manual retry or process restart |
| Per-call schema validation isolates transport bugs from contract bugs | Schema parse on every call adds ~1-2ms per request |
5. Tier-gated read path with server-only boundary
CROSS — DevPlane P13 (Server-Rendered Page with Injected Initial State); Vela lib/platform/ boundary.
Problem
Multi-tier storage means lower tiers (raw survey responses) carry individually-attributable data that must never reach the client bundle. Type-system enforcement isn't enough — a careless import in a 'use client' file can drag raw rows into a JS bundle the browser downloads. The risk isn't malice; it's a misclick on autocomplete.
The Pattern
// One read module. Marked `server-only` so the bundler crashes a client
// import at build time, not at runtime.
// File: src/lib/insight-store.ts
import "server-only";
import { db } from "@/db/server";
// Functions return only tier-3 types — Insights with provenance,
// suppression-checked, never raw rows.
export async function readInsight(id: string): Promise<Insight | null> { /* ... */ }
export async function listInsightsForTeam(teamId: string): Promise<Insight[]> { /* ... */ }
// Raw read functions live elsewhere in the codebase, accessible only to
// upstream pipeline code (composer, anonymizer). Never imported by
// route handlers or UI.
Key Design Decisions
server-onlyis the dependency. The npm package crashes the build if a client component imports the module. Type-system enforcement is bypassable; this is bypass-resistant.- One read module per tier.
insight-store.tsis the tier-3 (suppressed, aggregated) reader. Tier-2 (segment-aggregated) and tier-1 (anonymized records) have separate modules with their ownserver-onlyguards. - Route handlers import only from the tier-matching module. The discipline is enforced by code review + the
server-onlybuild-time check, not by trust. - Return types are tier-specific.
readInsight()returnsInsight, notInsightRow. The shape itself encodes the privacy claim. server-only≠ "secure." It prevents client-bundle leakage. It doesn't replace RLS, auth, or rate limiting.
This Codebase
src/lib/insight-store.ts lines 1-51 — "server-only" directive + tier-3 read functions returning Insight types.
src/app/api/teams/[teamId]/diagnosis/route.ts — route handler imports only from insight-store; never touches lower tiers directly.
docs/ARCHITECTURE.md §"Where this gets enforced" — the policy this pattern implements.
Known gaps. No automated audit that verifies every tier-3 surface goes through insight-store.ts. A lint rule could enforce "API routes import from insight-store, not db directly."
Tradeoffs
| Pro | Con |
|---|---|
| Build-time crash if a client component imports tier-3 reader | One module to maintain per tier |
Type system + server-only together = belt + suspenders | New contributors must know which reader to import |
| Centralized policy enforcement (every tier-3 query goes through here) | Refactoring across tiers is more friction |
| Surface for adding observability, caching, suppression checks | Module can grow large; needs internal organization at >20 functions |
6. Privacy gate as a substrate primitive
Problem
Team-level rollups of individually-attributable data — survey responses, performance feedback, behavioral signals — leak identity when: (a) the rollup cell is small enough that one person dominates, (b) free-text comments carry names, dates, or quoted phrases, (c) two non-leaky cohorts intersected produce a leaky sub-cohort, (d) identity-risk markers (a single highly-paid, single-tenure, single-location employee) survive aggregation. Ad-hoc gates ("just don't show if N < 5") miss cases (b) through (d). The gate has to be a substrate primitive every read passes through, not a per-query convention.
The Pattern
// The gate is a single function. Every rollup goes through it.
import { dataAnonymizerClient } from "@/lib/data-anonymizer/client";
export async function gateAggregate(input: {
rows: AnonymizedRow[];
groupBy: SegmentKey[];
comments?: CommentField[];
}): Promise<GateResult> {
const result = await dataAnonymizerClient().enforceGate({
rows: input.rows,
minN: 5, // min-N suppression
kCellMin: 3, // k-anonymity on intersected segments
redactComments: input.comments ?? [], // free-text redaction
identityRiskScore: true, // outlier-risk detection
});
// Below the floor, returns { status: "suppressed" } — never partial data.
return result;
}
// Insight composer calls the gate before writing.
const gateResult = await gateAggregate({ rows, groupBy, comments });
if (gateResult.status === "suppressed") {
return { insightId, status: "suppressed", reason: gateResult.reason };
}
return { insightId, status: "ok", value: gateResult.value };
Key Design Decisions
- One gate, not many. Every aggregate function that surfaces team-level data routes through the same call. Adding the gate per-query is how leaks happen.
- Multi-layer enforcement in one call. Min-N, k-cell, redaction, identity-risk are not optional flags — they all run, every time. The caller cannot disable individual layers.
- Below the floor, status = "suppressed". Never partial data, never an "approximated" value, never a "trust me" silent rollup. The downstream consumer must handle the suppressed shape explicitly.
- The gate is vendored from a separate service. Suppression logic is portfolio-level infrastructure —
data-anonymizerships from the People Analytics Toolbox, the consumer (Performix) vendors typed contracts. - Suppression is encoded in the Insight, not lost. The stored Insight has
safetyDetails.gateVersionandsafetyDetails.suppressionReason, so even later observers can see why a number is absent.
This Codebase
src/lib/data-anonymizer/contract.ts — the vendored Zod contract for the gate.
src/capabilities/protected-feedback/adapters/client.ts — the protected-feedback capability owns the gate-call path.
src/capabilities/insight-composition/core/compose.ts lines 68-74 — the composer embeds safetyDetails from the gate result into the Insight before storage.
Known gaps. Comment-redaction works in English; non-English text passes through with weaker redaction. Future: redaction-language-pack ingestion.
Tradeoffs
| Pro | Con |
|---|---|
| Identity leaks are structurally hard, not policy-hard | Every aggregate carries the gate-call overhead |
| Suppressed-state is first-class; downstream consumers can't ignore it | Below-floor cells can't be "approximated" — they're gone |
| Privacy primitives live in the substrate, not in each query | Requires vendoring + contract discipline against data-anonymizer |
| Identity-risk scoring catches outlier-cohort leaks min-N misses | Tuning the risk threshold is per-domain |
7. Capability architecture — contracts / core / adapters / ui / tests
Problem
A repo with five-plus distinct functional capabilities accumulates cross-capability dependencies that turn refactors into fan-out work. A capability that owns CapabilityFoo's types, CapabilityFoo's pure logic, CapabilityFoo's back-end calls, CapabilityFoo's React surfaces, and CapabilityFoo's tests can be evolved without touching the other four. A repo that mixes types under /types, logic under /lib, calls under /services, UI under /components, and tests under /__tests__ cannot.
The Pattern
src/capabilities/<capability-name>/
├── contracts/ # Zod schemas + TS types — the public surface
│ ├── input.ts
│ └── result.ts
├── core/ # Pure functions. No I/O, no env, no React.
│ └── compose.ts
├── adapters/ # Back-end clients (port + mock/http/mcp).
│ ├── client.ts # The factory.
│ ├── mock.ts
│ └── mcp.ts
├── ui/ # React components scoped to this capability.
│ └── DiagnosticPanel.tsx
└── tests/ # Unit tests; integration tests live one level up.
└── compose.test.ts
Key Design Decisions
- Capability boundaries are functional, not technical. A "capability" is a thing the product can do, not a layer of the stack. CAMS diagnostic is a capability; data-anonymizer is a capability; insight composition is a capability. "Auth" or "UI" are not capabilities — they're cross-cutting.
- The five folders are fixed.
contracts,core,adapters,ui,tests. Not all of them are always populated (a capability without a UI surface skipsui/), but the shape never changes. contracts/is the only folder other capabilities may import from. Cross-capability imports go through the published Zod schemas. Internalcore/andadapters/are private.core/is pure. No I/O, no React, no env reads, noDate.now(). Pure functions are testable, reusable, and easy to reason about; if they need impurity, it's an adapter call passed in.- Extraction maturity is an explicit dimension. Level 0: app-bound capability with no extraction. Level 1: contracts defined, internal use only. Level 2: adapters port-based, swappable. Level 3: capability could ship as an npm package. Capabilities advance levels deliberately.
This Codebase
Every directory under src/capabilities/ exemplifies the shape:
src/capabilities/cams-diagnostic/ — extraction level 2 (port-based, swappable adapter set).
src/capabilities/protected-feedback/ — extraction level 2.
src/capabilities/insight-composition/ — extraction level 1 (contracts defined, app-bound logic).
src/capabilities/action-loop/ — extraction level 1.
docs/ARCHITECTURE.md §"Implications for the eight capabilities" — the policy doc.
Known gaps. No CI rule that prevents cross-capability imports outside contracts/. The discipline relies on code review.
Tradeoffs
| Pro | Con |
|---|---|
| New capability = new folder, predictable structure | New contributors must learn the five-folder convention |
| Refactoring within a capability doesn't fan out to others | Cross-capability imports require a contracts/ round trip |
core/ is trivially unit-testable | More folders to navigate for small capabilities |
| Extraction maturity is visible per capability | Capabilities under 100 lines feel over-structured |
8. Zod-validated request/response envelope
CROSS — DevPlane P05 (Validator-at-the-Boundary for Untrusted Input).
Problem
Untrusted input — HTTP request bodies, MCP tool inputs, third-party webhook payloads — looks like a typed object to TypeScript but isn't. The compiler trusts the type assertion; the runtime hands you null, a missing field, or a number where a string was expected. Without a validation boundary, every internal function has to defensively re-check every field, or you accept silent crashes when the input is malformed.
The Pattern
// Every boundary validates with Zod. Internal code trusts the parsed type.
import { z } from "zod";
const BodySchema = z.object({
teamId: z.string().uuid(),
surveyResponses: z.array(z.object({
itemId: z.string(),
value: z.number().min(1).max(7),
})).min(1).max(50),
metadata: z.record(z.string()).optional(),
});
export async function POST(req: Request, ctx: { params: { teamId: string } }) {
const raw = await req.json().catch(() => null);
const parsed = BodySchema.safeParse(raw);
if (!parsed.success) {
return Response.json(
{ error: "invalid_request", details: parsed.error.flatten() },
{ status: 400 },
);
}
// From here on, `parsed.data` is `BodySchema._type` — typed, validated.
const result = await runDiagnostic(parsed.data);
return Response.json(result, { status: 200 });
}
Key Design Decisions
- One Zod schema per boundary. Not "one validator function per field." A single schema is documentation + validation + type derivation in one place.
safeParse, notparse. Throwing on invalid input couples the validator to the response shape.safeParsereturns a discriminated result; the handler decides what error envelope to return.- Internal functions take the parsed type, not the raw input.
runDiagnostic(body: z.infer<typeof BodySchema>), notrunDiagnostic(body: unknown). The boundary's job is to make internal callers' lives easier, not pass through opacity. - Schema bounds carry product invariants.
.min(1).max(50)on response array prevents both DoS payloads and meaningless empty submissions in one declaration. The bound is the constraint; no separate "if (responses.length > 50)" check needed. - Response shape is also schema-validated at the test boundary, even if not at runtime. Catches drift between what the handler returns and what consumers expect.
This Codebase
src/app/api/teams/[teamId]/diagnosis/route.ts — BodySchema validates the diagnosis POST body before any compute.
src/capabilities/cams-diagnostic/adapters/mcp.ts — every MCP tool input + output validated against contracts/ schemas.
Every src/capabilities/*/contracts/ directory carries the schemas the boundaries use.
Known gaps. No standardized error-response envelope across all routes — some return { error, details }, some return { message }. Worth normalizing once the route catalog crosses 20 endpoints.
Tradeoffs
| Pro | Con |
|---|---|
| Type system and runtime agree on input shape | Every boundary needs a schema; not free |
| Malformed input fails at the boundary with a usable error | Zod adds ~30KB to the bundle (server-side only here) |
| Internal callers trust their inputs; no defensive code | Schema + handler can drift if not co-located |
| Schema bounds encode product invariants in one place | Complex unions can produce confusing error messages |
9. Action-status transition gate (lightweight state machine)
CROSS — DevPlane P04 (Discriminated-Union State Machine with Two-Phase Commit); Vela #8 (Editorial Lifecycle State Machine); Namesake #13 (State-machine via DB columns + action endpoint).
Problem
Workflow records (action plans, reviews, content lifecycle) move through stages — draft → proposed → accepted → in_progress → closed — and most transitions are illegal (you can't jump from closed back to proposed). Without enforcement, a sloppy admin click or a misaligned API call breaks the lifecycle invariant. A full state-machine library (XState et al) is overkill for the common case of "fewer than 8 states, fewer than 12 transitions."
The Pattern
// Discriminated-union state + a static transition table.
export type ActionStatus =
| "draft" | "proposed" | "accepted" | "in_progress" | "closed" | "reviewed";
export const ACTION_STATUS_TRANSITIONS: Record<ActionStatus, ActionStatus[]> = {
draft: ["proposed"],
proposed: ["accepted", "draft"],
accepted: ["in_progress"],
in_progress: ["closed"],
closed: ["reviewed"],
reviewed: [], // terminal
};
// Pure validator — testable in isolation.
export function canTransitionActionStatus(
from: ActionStatus,
to: ActionStatus,
): boolean {
return ACTION_STATUS_TRANSITIONS[from].includes(to);
}
// Route handler enforces.
const current = await readAction(actionId);
if (!canTransitionActionStatus(current.status, target)) {
return Response.json(
{ error: "invalid_transition", from: current.status, to: target },
{ status: 409 },
);
}
await updateActionStatus(actionId, target);
Key Design Decisions
- Transitions are data, not control flow. A
Record<from, to[]>lookup table is easier to read, audit, and visualize than aswitchcascade. - Terminal states are explicit empty arrays, not omitted entries. Forces the author to declare "this is a sink." Easy to check
transitions[s].length === 0for terminal detection. - The validator is a pure function.
canTransitionActionStatus(from, to): boolean. No I/O. Trivially unit-testable; trivially composable. - Enforce at the boundary. Route handler reads current state, validates target, writes if allowed. Don't trust internal callers to remember.
- HTTP 409 (Conflict), not 400. The request is well-formed; the system state makes it invalid. Conflict is the semantically correct status code.
This Codebase
src/capabilities/action-loop/contracts/types.ts — ActionStatus discriminated union + ACTION_STATUS_TRANSITIONS table.
src/capabilities/action-loop/core/plan-template.ts — canTransitionActionStatus() validator.
Known gaps. No transition history table — when an action moves through states, only the current state is stored. Adding a transition_log table would enable audit + rollback.
Tradeoffs
| Pro | Con |
|---|---|
| Illegal transitions fail loudly at the boundary | No support for guard conditions (e.g., "can only close if all subtasks complete") |
| Transition table reads like documentation | More states = combinatorial growth in the table |
| Validator is unit-testable in isolation | No automatic visualization (Graphviz would need a small script) |
| Lighter weight than an XState integration | Coordinated multi-record transactions need a separate pattern |
10. Binding-constraint diagnostic (lowest-of-N as the action signal)
Problem
A surface that scores N dimensions (capability, alignment, motivation, support; or strength, speed, agility, recovery; or pick-your-domain) tempts the dashboard pattern: show all N as bars, let the reader interpret. But the action question — what is stopping this system right now? — isn't a dashboard question. It's a "which dimension is the binding constraint?" question. Showing all four with equal weight buries the answer.
The Pattern
type DimensionScore = {
dimension: "capability" | "alignment" | "motivation" | "support";
score: number; // 0-100
reliability: number; // 0-1; gate on this before trusting the score
};
type BindingConstraint = {
dimension: DimensionScore["dimension"];
severity: "mild" | "moderate" | "severe";
rationale: string;
};
export function bindingConstraint(
scores: DimensionScore[],
reliabilityFloor = 0.6,
): BindingConstraint | null {
// 1. Drop dimensions that don't meet the reliability gate.
const eligible = scores.filter((s) => s.reliability >= reliabilityFloor);
if (eligible.length === 0) return null;
// 2. Lowest-scoring eligible dimension wins.
const lowest = eligible.reduce((a, b) => (a.score < b.score ? a : b));
// 3. Severity is bucketed, not raw — so the explanation is stable.
const severity =
lowest.score < 40 ? "severe" : lowest.score < 60 ? "moderate" : "mild";
return {
dimension: lowest.dimension,
severity,
rationale: `${lowest.dimension} scored ${lowest.score} — lowest of the four; reliability ${lowest.reliability.toFixed(2)}`,
};
}
Key Design Decisions
- One card per system, not one card per dimension. The output of the diagnostic is a single answer, not a dashboard. Discipline says: don't display all four; pick the one to act on.
- Reliability gates before scoring matters. A dimension with low sample size or wide CI doesn't get to be "the constraint" — the gate prevents acting on noise.
- Severity is bucketed. "Severe / moderate / mild" reads as actionable; "your score is 47" reads as an artifact. Buckets are stable across small score shifts; raw scores aren't.
- Rationale is part of the output, not a hover-state. The diagnostic that says "alignment is starving" has to say why in the same payload — otherwise the reader has to dig for context.
- Returns
nullon no-eligible. Not a fake answer, not the highest-reliability-low-score, not "best guess." The system says "I can't tell you" when it can't.
This Codebase
src/capabilities/cams-diagnostic/contracts/cams.ts — the DimensionScore + BindingConstraint types.
docs/CAMS.md — canonical specification of the diagnostic model.
src/capabilities/insight-composition/core/compose.ts — severityFromScore() ties dimension score to severity bucket.
Known gaps. When two dimensions tie at the lowest score, the current implementation picks deterministically by enum order. Domain reality (CAMS) says ties should surface both with a "co-constraint" rationale — partial-ship.
Tradeoffs
| Pro | Con |
|---|---|
| Output is one actionable signal, not N data points | Some readers want the dashboard view; this pattern refuses it |
| Reliability gate filters noise out of action signals | Reliability calibration is per-domain; not free |
| Severity buckets are stable across measurement noise | Bucket thresholds are arbitrary; defend them or they look arbitrary |
null return is honest; never a "best guess" | Downstream consumers must handle the null case |
11. Substitution-boundary pipeline scaffold (NotImplementedError per stage)
Problem
You're building a multi-stage pipeline whose contracts are clear but whose stage bodies will land over weeks (different agents, different external deps, some blocked on spec sessions). You want the pipeline's shape — input/output types per stage, orchestration order, error attribution — to land first, so future agents can replace one stage at a time without touching the orchestration or the neighbouring stages. You also want the unimplemented pipeline to be runnable end-to-end so the run-record shape can be inspected before any stage works.
The Pattern
// Stage names are a closed enum — every stage failure is attributed to one.
export type StageName =
| "fetchSource"
| "extractClaims"
| "tagDimensions"
| "deriveItems"
| "persistOutputs";
/** Throw from a stage to attribute a failure to the right stage name. */
export class StageError extends Error {
readonly stage: StageName;
readonly cause: unknown;
constructor(stage: StageName, message: string, cause?: unknown) {
super(`[${stage}] ${message}`);
this.stage = stage;
this.cause = cause;
}
}
/** All scaffold stages throw this; the runner can still observe shape. */
export class NotImplementedError extends StageError {
constructor(stage: StageName, hint: string) {
super(stage, `NOT_IMPLEMENTED — ${hint}`);
}
}
// Each stage is a separately-exported function with a locked signature.
// Future agents replace the body; the signature is the contract.
export async function fetchSource(
req: IngestRequest,
): Promise<{ source_id: string; meta: SourceMeta }> {
void req;
throw new NotImplementedError(
"fetchSource",
"wire URL/DOI/PDF fetchers + canonical slug rule",
);
}
export async function extractClaims(
source_id: string,
meta: SourceMeta,
extractor: ClaimExtractor, // port, see Pattern #2 — injectable for tests
): Promise<ExtractedClaim[]> {
void source_id; void meta; void extractor;
throw new NotImplementedError(
"extractClaims",
"call ClaimExtractor.extract + apply abstract-only downgrade",
);
}
// ... one function per stage, all throwing NotImplementedError ...
// The orchestrator catches StageError, records which stage failed,
// and returns a partial RunRecord with the right `status` + `errors`.
export async function runPipeline(
request: IngestRequest,
): Promise<RunRecord> {
const run_id = `run.${randomUUID()}`;
const started_at = new Date().toISOString();
let claims_extracted = 0;
let items_proposed = 0;
try {
const { source_id, meta } = await fetchSource(request);
const claims = await extractClaims(source_id, meta, getExtractor());
claims_extracted = claims.length;
const tagged = await tagDimensions(claims);
const items = await deriveItems(tagged);
items_proposed = items.length;
const persisted = await persistOutputs({ source_id, claims, items });
return {
run_id, started_at, finished_at: persisted.finished_at,
status: "succeeded", claims_extracted, items_proposed, errors: [],
};
} catch (err) {
const error =
err instanceof StageError
? { stage: err.stage, message: err.message, cause: err.cause }
: { stage: "fetchSource" as const, message: String(err) };
return {
run_id, started_at, finished_at: new Date().toISOString(),
status: claims_extracted === 0 ? "failed" : "partial",
claims_extracted, items_proposed, errors: [error],
};
}
}
Key Design Decisions
- Stage signatures are the contract. Bodies throw
NotImplementedErroruntil they ship. The pipeline runs end-to-end the day the scaffold lands — you can callrunPipeline()against any input and inspect theRunRecordshape before a single stage works. - One file per stage signature, one orchestrator file. The orchestrator never changes when a stage gets implemented; the stage file is the unit of change. New agents pick up
fetchSource.tswithout ever touchingpipeline.ts. StageErrorcarries the stage name. When the catch block at the orchestrator level fires, the run record names the exact stage that failed. No grep-the-stack-trace.- Stages are pure-of-orchestration. A stage may do I/O (fetch from an external API, write to a DB), but it never calls another stage. The orchestrator is the only place stage order is encoded.
- Adapters injected at the boundary. The orchestrator accepts an optional
adaptersparam so tests can pass mocks (see Pattern #2). Default adapters resolve from a factory; nothing in stage code reaches forprocess.env. partialstatus is first-class. A pipeline that gets through stage 3 of 7 returnsstatus: "partial"with the prior outputs counted. Better than "failed" + nothing — operators see how far the run got.
This Codebase
src/capabilities/research-to-model-engine/core/pipeline.ts — orchestrator with seven stages, try/catch + partial-status logic.
src/capabilities/research-to-model-engine/core/stages.ts — seven stage functions, every body throws NotImplementedError with a TODO hint pointing at the spec doc.
src/capabilities/research-to-model-engine/contracts/types.ts — all stage input/output types + the run-record shape; this file is the contract substrate the scaffold protects.
scripts/research/run-pipeline.ts — the CLI runner; works on day one against the stub stages.
Known gaps. No per-stage retry policy yet — a transient failure in fetchSource doesn't currently get re-tried by the orchestrator. When a stage acquires retry semantics it's an orchestrator change, not a stage change.
Tradeoffs
| Pro | Con |
|---|---|
| Pipeline contract ships before any stage works | Reading scaffold stages can feel like vapor — every body says "NOT_IMPLEMENTED" |
| Future agents replace stages one at a time without coordination | Stage signature changes are still a fan-out — pick the shapes carefully up front |
| Run record always has a typed shape, even for failed runs | Tempts you to ship the scaffold and forget to land the stages |
StageError makes failure attribution mechanical | Stages that need to share state (e.g., a transaction) have to thread it via inputs |
12. Sibling-repo build-time sync with bundled fallback
Problem
A site (or service) needs to consume a JSON artifact authored in a sibling repo. Locally, you want the consumer to read directly from the sibling — single source of truth, no drift. On a cloud build runner (Vercel, GitHub Actions), the sibling repo isn't checked out — the build needs to use a committed-in-repo copy of the artifact. You don't want two code paths; you want one prebuild script that does the right thing in both environments.
The Pattern
// scripts/sync-artifact.mjs — runs as a prebuild step.
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const siteRoot = join(__dirname, "..");
const source = join(
siteRoot, "..", "sibling-repo", "data", "artifact.json",
);
const target = join(siteRoot, "content", "artifact.json");
if (!existsSync(source)) {
if (existsSync(target)) {
console.log(
`[sync] Source not found (${source}); using committed target ` +
`${target} as-is. This is expected on cloud builds where the ` +
`sibling repo isn't checked out.`,
);
process.exit(0);
}
console.error(
`[sync] Source not found AND no committed target exists.\n` +
`Either check out the sibling repo or commit ${target}.`,
);
process.exit(1);
}
mkdirSync(dirname(target), { recursive: true });
copyFileSync(source, target);
console.log(`[sync] ${source} → ${target}`);
// package.json
{
"scripts": {
"sync:artifact": "node scripts/sync-artifact.mjs",
"prebuild": "npm run sync:artifact",
"build": "next build"
}
}
Key Design Decisions
- One script, two environments. Local dev with the sibling checked out → fresh copy on every build. Cloud build without the sibling → silently use the committed copy. No branching on
process.env.CI; the script just checks if the source path exists. - Committed target is the safety net, not the primary path. The committed copy can lag; that's fine for a cloud build because the dev who pushes is responsible for running the sync first. The script's job is to make "I forgot to sync" a clean fallback, not to mask staleness.
prebuildhook, not a manual step. npm runsprebuildautomatically beforebuild. Forgetting the sync is impossible locally; the cloud build inherits the same flow.- Hard fail when neither exists. The script exits 1 if both the source AND the committed target are missing — the build should never produce HTML referencing a file that isn't there.
- Plain
.mjsscript, no deps. Purenode:fs+node:path. Works on any Node runtime; doesn't pull a build toolchain. - Explicit log lines in both branches. The dev who watches the build log can answer "is this build using the sibling-repo source, or the committed fallback?" without reading the script.
This Codebase
performix-site/scripts/sync-science-library.mjs — full implementation; reads ../performix/data/research/science-library-seed.json locally, falls back to content/learn/science-library-seed.json on Vercel.
performix-site/package.json — prebuild hook wires the sync into the build.
Known gaps. No drift detection between the committed copy and the sibling source — if the sibling has moved on but the committed copy is stale, the cloud build silently uses the stale one. A weekly CI job that diffs the two and opens an issue would close the gap.
Tradeoffs
| Pro | Con |
|---|---|
| Single source of truth in dev; deployable in cloud builds | Committed copy can lag; manual sync discipline matters |
| Zero monorepo coupling — the sibling can be moved/renamed without breaking cloud builds | Build log noise (every build prints the sync line) |
| Pure Node, zero deps | Script doesn't validate the artifact shape — bad JSON in the sibling gets copied without complaint |
| Hard-fails when both source and target are missing | Manual coordination needed when sibling's schema evolves |
See also: Pattern #3 (Vendored typed contracts for cross-repo service consumption) — same problem space (cross-repo consumption without monorepo dependency) for type contracts; this pattern is the variant for data artifacts. PA Toolbox P19 (Idempotent Bootstrap Migration from Bundled JSON) — same "ship the seed with the build artifact" instinct, applied at runtime instead of build time.
13. Watchlist-driven harvest with cursor write-back
CROSS — generalizes from Principia P09 (Cron-Driven Watchlist Scheduler with Cadence-Based Next-Run).
Problem
You want persistent searches against an external API — academic papers, RSS feeds, GitHub repos, public datasets — that run on a cadence and persist their results as dated harvest files. Each search has its own query, its own cadence, its own output destination. You don't want every search to be a separate cron script, and you don't want one giant "harvest everything" job that fails the whole thing when one source flakes.
The Pattern
// One watchlist per JSON file. Self-contained, declarative.
// watchlists/<id>.json
{
"id": "topic-x",
"label": "Topic X — recent works",
"query": {
"search": "topic x AND (predictor OR meta-analysis)",
"filter": "from_publication_date:1995-01-01,cited_by_count:>30",
"sort": "cited_by_count:desc",
"per_page": 50
},
"tags": ["beachhead-a"],
"last_harvest_at": null
}
// One harvester CLI iterates over watchlists.
const WatchlistSchema = z.object({
id: z.string().min(1),
label: z.string().min(1),
query: z.object({
search: z.string(),
filter: z.string(),
sort: z.string().default("cited_by_count:desc"),
per_page: z.number().int().min(1).max(200).default(50),
}),
tags: z.array(z.string()).optional(),
last_harvest_at: z.string().nullable().optional(),
});
type Watchlist = z.infer<typeof WatchlistSchema>;
async function harvestOne(watchlist: Watchlist, opts: { outDir: string }) {
const url = buildUrl(watchlist.query);
const res = await fetch(url, { headers: { Accept: "application/json" } });
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
const works = (await res.json()).results.map(normalize);
const harvestedAt = new Date().toISOString();
const stamp = harvestedAt.slice(0, 10);
const outPath = join(opts.outDir, `${watchlist.id}-${stamp}.json`);
writeFileSync(outPath, JSON.stringify({
version: "1.0.0",
watchlist_id: watchlist.id,
harvested_at: harvestedAt,
query: watchlist.query,
returned: works.length,
works,
}, null, 2) + "\n");
// Write the cursor BACK into the watchlist file so the next run
// (or an operator inspecting the file) sees when it last ran.
watchlist.last_harvest_at = harvestedAt;
writeFileSync(
join("watchlists", `${watchlist.id}.json`),
JSON.stringify(watchlist, null, 2) + "\n",
);
return { id: watchlist.id, returned: works.length };
}
async function harvestAll(opts: { outDir: string }) {
const files = readdirSync("watchlists").filter((f) => f.endsWith(".json"));
const summaries = [];
for (const file of files) {
// One bad watchlist must NOT kill the rest.
try {
const wl = WatchlistSchema.parse(JSON.parse(
readFileSync(join("watchlists", file), "utf8"),
));
summaries.push(await harvestOne(wl, opts));
} catch (err) {
console.error(`[${file}] ${(err as Error).message}`);
summaries.push({ id: file, returned: 0, error: true });
}
}
// Surface low-yield watchlists so operators can tighten queries.
const low = summaries.filter((s) => !s.error && s.returned < 10);
if (low.length > 0) {
console.warn(`${low.length} watchlist(s) returned <10 results.`);
}
}
Key Design Decisions
- Watchlists are data files, not code. A new query is a new JSON file in
watchlists/. No deploy, no schema migration. Operators can edit, pause (by removing the file from the directory), or fork them. - Output files are dated and per-watchlist.
<id>-YYYY-MM-DD.jsonmakes "what changed since last week?" trivially answerable — diff two files. No mutating-database semantics. - Cursor is written back into the source file.
last_harvest_atlives in the watchlist file itself. The harvester is the only writer; operators read it to see staleness. No separate_cursortable. - One watchlist's failure doesn't kill the run. The outer loop catches per-watchlist exceptions and continues. The summary at the end names the failures.
- Low-yield warnings. When a watchlist returns fewer than N results, the harvester logs a warning. Either the query is too narrow (operator tightens) or the source is sparse (different signal).
- API politeness baked in.
mailto=<owner>in the URL (OpenAlex's polite-pool convention) so the source doesn't rate-limit you. Add per-vendor politeness rules inbuildUrl.
This Codebase
scripts/research/openalex-watchlist-harvest.ts — full harvester; iterates data/research/watchlists/*.json, writes to data/research/harvests/<id>-<date>.json, writes back last_harvest_at.
data/research/watchlists/cams-*.json + data/research/watchlists/beachhead-*.json — 7 live watchlists.
data/research/harvests/*.json — dated per-watchlist output files; each one is the canonical "what did this watchlist return on date X".
Known gaps. No cadence enforcement — the harvester runs every watchlist on every invocation. A "skip if last_harvest_at is within N days" check would let one cron line drive heterogeneous cadences (see Principia P09 for that variant).
Tradeoffs
| Pro | Con |
|---|---|
| New watchlist ships without code or DDL | API quota management is per-script, not coordinated across watchlists |
| Per-watchlist output files diff naturally | Storage grows linearly with watchlist count × run count |
| Per-watchlist failure isolated; rest of run completes | No native cadence — all watchlists run on every invocation unless caller filters |
| Cursor in the source file = operator-visible staleness | Concurrent writes to the same watchlist file could race (single-runner assumption) |
See also: Principia P09 (Cron-Driven Watchlist Scheduler with Cadence-Based Next-Run) — same shape with per-watchlist cadence + scheduler. Principia P07 (Idempotent Background Job Queue with Exponential-Backoff Retry) — the per-fetch retry shape that pairs naturally with the harvester when one watchlist's source flakes.
14. Three-tier promotion gate with sticky rejection
CROSS — generalizes from Principia P13 (Curator-Mediated Promotion).
Problem
You have a pipeline that extracts structured artifacts from messy sources — LLM extractions, automated validators, scrapers, third-party APIs. The output is best-effort: frequently wrong, occasionally hallucinated, sometimes excellent. You want the pipeline to surface its output to a human reviewer, but you absolutely must not let any extractor write to the canonical store unsupervised. The promotion path from "candidate" to "live" must be an explicit human action that cannot be triggered by another pipeline stage.
The Pattern
// Four statuses, three jurisdictions:
// - "agent_verified" — pipeline emitted it; review pending
// - "needs_review" — promoted to reviewer's queue
// - "approved" — reviewer accepted; safe to promote to live
// - "rejected" — reviewer said no; never re-enqueue
export type ReviewStatus =
| "agent_verified"
| "needs_review"
| "approved"
| "rejected";
interface RunEntry {
run_id: string;
artifacts: Artifact[];
review_status: ReviewStatus;
}
export interface ReviewQueue {
enqueue(entry: RunEntry): Promise<void>;
listPending(): Promise<RunEntry[]>;
get(runId: string): Promise<RunEntry | null>;
/** Reviewer-only path. Flips run + all child artifacts together. */
setStatus(runId: string, status: ReviewStatus): Promise<void>;
}
// Pipeline handler. Writes artifacts as `agent_verified` — NEVER higher.
async function ingest(source: Source, queue: ReviewQueue): Promise<RunEntry> {
const artifacts = await runExtractionPipeline(source);
const entry: RunEntry = {
run_id: `run.${randomUUID()}`,
artifacts,
review_status: "agent_verified", // <-- start state, always
};
await queue.enqueue(entry);
return entry;
}
// Verification job — may re-run extraction or validation against a row.
// Hard guard: rejected rows never re-enter the pipeline.
async function verify(runId: string, queue: ReviewQueue): Promise<void> {
const entry = await queue.get(runId);
if (!entry) throw new Error("not found");
if (entry.review_status === "rejected") {
console.log(`skipped: previously reviewer-rejected (${runId})`);
return; // sticky rejection — never re-process
}
const validated = await crossValidate(entry.artifacts);
// Verification can promote agent_verified → needs_review (queue for human),
// but NEVER → approved. Promotion to approved is a separate code path.
if (validated.allOk) {
await queue.setStatus(runId, "needs_review");
}
}
// Reviewer-side code path. Lives in admin UI / CLI, NOT in the pipeline.
export async function reviewerApprove(
runId: string,
queue: ReviewQueue,
liveStore: LiveStore,
): Promise<void> {
const entry = await queue.get(runId);
if (!entry) throw new Error("not found");
if (entry.review_status !== "needs_review") {
throw new Error("only needs_review entries can be approved");
}
// Promote to the live store — this is the only code path that writes there.
for (const a of entry.artifacts) await liveStore.upsert(a);
await queue.setStatus(runId, "approved");
}
export async function reviewerReject(
runId: string,
queue: ReviewQueue,
): Promise<void> {
const entry = await queue.get(runId);
if (!entry) throw new Error("not found");
// Rejection is recorded; the row stays for forensics + dedup.
// Sticky — handlers above will skip it on future passes.
await queue.setStatus(runId, "rejected");
}
Key Design Decisions
- Three jurisdictions encoded in one status enum. Pipeline handlers can flip
agent_verified→needs_review. Reviewer code can flipneeds_review→approvedorrejected. Nothing can flipagent_verified→approveddirectly — that path doesn't exist. - Rejection is sticky. Once a reviewer says "no," every handler in the pipeline short-circuits when it encounters the row. Even if a downstream dispatcher tries to re-enqueue, the defensive check at the handler boundary catches it.
- Promotion-to-live is a separate code path.
reviewerApprove()lives in admin tooling, not in the pipeline. The pipeline's reachable surface area can't accidentally write to the live store. - Status changes are atomic across run + artifacts. When the reviewer accepts/rejects a run, all child artifacts flip together. No half-approved runs.
- The queue interface is a port. In-memory mock for tests + dev; real impl writes to a
review_queuetable. Pipeline code never knows which is in use. - No silent fail-to-approve. When zero validators are configured, the run stays
agent_verified— never fakes an approval just because no one objected.
This Codebase
src/capabilities/research-to-model-engine/adapters/human-review-queue.ts — HumanReviewQueue port + createMockHumanReviewQueue() for dev.
src/capabilities/research-to-model-engine/contracts/types.ts — REVIEW_STATUSES enum + ReviewStatusSchema.
src/capabilities/research-to-model-engine/core/pipeline.ts — every emitted artifact gets review_status: "agent_verified" as default.
Known gaps. No reviewer-UI yet (PFX-23 follow-on). The promotion path is currently CLI-only. Real productionization needs a /admin/research-review console with the four-status filter.
Tradeoffs
| Pro | Con |
|---|---|
| Reviewer authority is structural, not policy — pipeline cannot betray it | Reviewer becomes the throughput bottleneck for canonical-state growth |
| Sticky rejection prevents repeated wasted extraction passes | Reviewers must annotate rejection reasons or context is lost |
| Four-status enum maps cleanly to inbox / queue / archive / live | Schema must thread review_status through every artifact type |
| Approval path is a separate function — auditable in one place | Reviewers without tooling are stuck approving via CLI/SQL |
See also: Principia P13 (Curator-Mediated Promotion — Read-Only Detection, Approval-Only Mutation) — same shape with a richer multi-vendor verification adjudication layer. Performix #9 (Action-status transition gate) — the underlying state-machine primitive that the status enum sits on top of. PA Toolbox P18 (Discriminated-Union Response with Block-vs-OK Status) — same "refusal is a first-class status, not an exception" discipline.
15. Cross-portfolio packages dir with vendor-pin convention
Problem
You have multiple sibling repos in a portfolio that want to share React components, helpers, or other code primitives. npm publishing is overkill at the current consumer volume (one or two repos vendoring, low cadence). Monorepo consolidation is the right long-term move but is a multi-week project that doesn't pay back until cross-team velocity actually demands it. You need a today-shape that lets sibling repos share code without either of those costs.
The Pattern
producer-repo/
├── packages/
│ ├── README.md # the discipline doc
│ ├── widget-renderer/
│ │ ├── CONTRACT_VERSION # one-line file: "1.0.0"
│ │ ├── README.md # what this exports + how to vendor
│ │ └── src/
│ │ ├── index.ts
│ │ └── WidgetRenderer.tsx
│ └── input-instrument/
│ ├── CONTRACT_VERSION
│ └── ...
// consumer-repo/lib/widget-renderer/WidgetRenderer.tsx
//
// VENDORED FROM: producer-repo/packages/widget-renderer
// CONTRACT_VERSION: 1.0.0
// PRODUCER_SHA: a8c4f9e2
// VENDORED_AT: 2026-05-22
// LOCAL_MODS: replaced @/lib/utils import with inline `cn` helper.
//
// Re-vendor when CONTRACT_VERSION bumps; preserve LOCAL_MODS list.
import type { ReactNode } from "react";
// ... vendored body ...
<!-- producer-repo/packages/README.md -->
# Cross-portfolio packages
**Pattern:** vendor-and-drift-watch. Producer is the canonical author.
Consumers copy a snapshot + write a pin file recording producer SHA +
CONTRACT_VERSION. Producer doesn't track consumer state; consumers self-bump.
## Packages
| Package | CONTRACT_VERSION | First external consumer |
|---|---|---|
| `widget-renderer` | 1.0.0 | consumer-repo/lib/widget-renderer (sha …) |
## Vendoring discipline
1. Copy `packages/<name>/src/` into consumer's tree.
2. Write a pin file: `PACKAGE`, `CONTRACT_VERSION`, `PRODUCER_SHA`,
`VENDORED_AT`.
3. Document LOCAL_MODS in the vendored dir (so re-vendoring catches them).
4. Re-vendor on each CONTRACT_VERSION bump.
## Semver
- Major: breaking change to public prop / signature; consumers MUST update.
- Minor: additive (new optional prop); consumers can update at leisure.
- Patch: bug fix or doc change; consumers should update without urgency.
Key Design Decisions
- Producer is canonical; consumers copy. No bi-directional sync, no symlinks, no
file:deps. The consumer's copy is independent — they can hotfix locally, and the LOCAL_MODS comment surfaces the divergence for the next vendor pass. CONTRACT_VERSIONis a one-line file. Notpackage.json— that pulls a runtime expectation. A bare text file is the version stamp; semver discipline is inREADME.md.- Pin file in the consumer. Every vendored copy carries
PRODUCER_SHA+VENDORED_AT. Anyone asking "is this current?" runsgit log producer-repo/packages/<name>/and compares. - No CI/publishing. No GitHub Actions, no npm tokens, no release branches. The cost of
npm publishworkflow is the cost you're explicitly deferring. - LOCAL_MODS section in the vendored file. When a consumer modifies the vendored copy (different bundler can't resolve a producer-internal path; design-system swap), they document the change. Re-vendor passes see the LOCAL_MODS and re-apply them deliberately.
- Inverts the consumer pattern. The same producer repo vendors typed contracts from sibling services (see #3).
packages/is the same vendor-pin discipline going the other direction. One muscle, two flows. - Promotion criteria are explicit. A module promotes from
src/topackages/only when (a) a second portfolio consumer asks for it, (b) a directive names it cross-portfolio, or (c) the API surface has stabilized.
This Codebase
packages/README.md — full discipline doc, semver rules, promotion criteria.
packages/feedback-instrument/CONTRACT_VERSION — one-line 1.0.0.
packages/feedback-instrument/src/FeedbackInstrument.tsx — vendored from src/components/feedback/ with a one-comment provenance header at the top.
packages/insight-card-renderer/README.md — pointer to the actual canonical location (src/capabilities/insight-player/ui/exportable/) for the case where the canonical source predates this directory.
docs/DECISIONS/2026-05-22-cross-repo-ui-sharing-via-packages-dir.md — the decision record.
Known gaps. No automated drift check. A weekly job that compares each packages/<name>/CONTRACT_VERSION against vendored-pin files in known consumers (PA-site, etc.) and reports laggards would catch silent staleness.
Tradeoffs
| Pro | Con |
|---|---|
| Sharing across siblings without npm or monorepo overhead | Drift is real — consumers re-vendor only when motivated |
| Producer can refactor internals freely; consumers carry an independent copy | Bug fixes in producer don't reach consumers automatically |
| LOCAL_MODS discipline surfaces consumer divergences for review | Multi-consumer breaking change requires manual fan-out announcement |
| Easy to promote later to npm or workspace package (same shape) | At >3-4 consumers, the manual fan-out cost crosses publishing cost |
See also: Pattern #3 (Vendored typed contracts for cross-repo service consumption) — same vendor-pin discipline applied to typed contracts; this pattern is the variant for runtime components. PA Toolbox P03 (Vendored Typed Contract Pattern for Cross-Repo Service Consumption) — the producer-side discipline that makes vendoring sustainable. Meta Factory's packages/<name> cross-repo consumer-contract pattern uses the same semver-ratification rules.
16. Theme-agnostic exportable component with render-prop chart slot
Problem
You've built a React component (a card renderer, a chart, an instrument wrapper) that one or more sibling repos want to vendor. The component lives in a brand context — your tokens, your design language, your chart library. The vendoring consumer has a different brand context. If the component bakes in your CSS variables, your cn helper, or your chart library, the consumer either inherits your design system or has to fight it. You want the exported surface to work in their context without inheriting yours.
The Pattern
// Self-contained file. NO imports from `@/lib/utils` or product-specific
// design tokens. Sibling files (`./types`, `./themes`) + `react` only.
"use client";
import type { ReactNode } from "react";
import { DEFAULT_THEME, type Palette, type ThemeMarker } from "./themes";
import type { RendererProps } from "./types";
/**
* Inlined `cn` helper. Three-line clsx wrapper so the exportable file has no
* dependency on the producer's shadcn-ui / `@/lib/utils` tree. Consumers
* vendoring this file get a working `cn` without any other imports.
*/
function cn(...inputs: Array<string | undefined | null | false>): string {
return inputs.filter(Boolean).join(" ");
}
export function Renderer(props: RendererProps) {
const {
title, body,
palette,
chart, // structured chart spec (consumer-defined)
renderChart, // OPT-IN render-prop; omitted = caption-only fallback
chartCaption,
className,
} = props;
// Default palette = the cross-portfolio default. Explicit null = consumer's
// own theme behavior. A concrete Palette object overrides everything.
const effectivePalette: Palette | ThemeMarker =
palette === undefined ? DEFAULT_THEME : palette;
return (
<article className={cn("rounded-lg border p-6 flex flex-col gap-5", className)}>
<h3>{title}</h3>
<p>{body}</p>
{/* Chart slot: consumer's renderer (any chart library) OR fallback */}
{chart && renderChart ? (
renderChart({ chart, palette: effectivePalette })
) : chart && chartCaption ? (
<figcaption className="text-sm text-neutral-600">{chartCaption}</figcaption>
) : null}
</article>
);
}
// themes.ts — the palette contract is intentionally tiny.
export interface Palette {
primary: string; // main series
secondary: string; // second series in comparison charts
accent: string; // highlight tone
neutral: string; // supporting / overflow
}
// Sentinel: pass null to opt INTO the consumer's contextual theme behavior.
export const PRODUCER_THEME = null;
export type ThemeMarker = typeof PRODUCER_THEME;
// Concrete default: cross-portfolio paper-and-ink, literal fallback values.
// Consumers re-vendor this constant pointing at their own var(--color-*) tokens.
export const DEFAULT_THEME: Palette = {
primary: "#1a1a1a",
secondary: "#5a5a5a",
accent: "#a85a1f",
neutral: "#c8c2b5",
};
Key Design Decisions
- Inline every helper. No
import { cn } from "@/lib/utils"— the function is three lines, copied into the file. The vendored copy works the moment it's on disk; notsconfigpaths, no helper module to also vendor. - Chart slot is a render-prop, not a built-in chart library. Producer ships the structured chart spec (
chart) and the styling concerns (palette); consumer plugs in their chart library viarenderChart. The producer doesn't forcerechartsornivoon the consumer. - Default palette is concrete hex. Not
var(--color-primary)— that breaks the moment the consumer doesn't have that token. Concrete fallback values make the renderer work in any design system on day one; consumers can swap the values for their own tokens during the vendor pass. - Sentinel
nullfor "use consumer's contextual theme". Some consumers (the producer's own routes) want the renderer to defer to a CSS-variable-driven theme. Explicitnullopt-in makes that intent visible at the call site. renderChartis opt-in. Consumers who don't pass it get a graceful text-only fallback (the chart caption). They don't have to plug in a chart library to use the renderer at all.- Self-containment is a maintained invariant. A header comment names the rule: "this file imports from sibling files +
reactonly." A failed re-vendor (consumer'snpm installchoking on@/lib/utils) is the symptom that catches violations.
This Codebase
src/capabilities/insight-player/ui/exportable/InsightCardRenderer.tsx — full self-contained renderer; inlines cn, defers structured charts via renderChart, accepts a palette with concrete-hex defaults.
src/capabilities/insight-player/ui/exportable/types.ts — the prop interface consumers vendor.
src/capabilities/insight-player/ui/exportable/themes.ts — ChartPalette + PERFORMIX_CAMS_THEME sentinel + PAPER_INK_THEME (PA-site default) literal hex values.
peopleanalyst-site/lib/insight-card-renderer/ — vendored copy in PA-site; identical to source modulo the design-system pin.
Known gaps. No machine-checked self-containment lint rule. A simple grep "from \"@/" InsightCardRenderer.tsx in CI would catch accidental product-internal imports.
Tradeoffs
| Pro | Con |
|---|---|
| Consumer vendors one file + sibling theme/types + has working component on day one | Producer must resist the urge to pull in helpers for ergonomics |
| Chart library is consumer's choice; no recharts-or-nothing lock-in | Producer's own routes lose some convenience (re-importing the chart library they already use) |
| Default theme works without any consumer setup | Default hex values can look "off" against the consumer's design — re-vendor with their tokens recommended |
| Render-prop pattern is React-idiomatic and well-understood | The render-prop signature must accept the structured chart spec, which couples the contract |
See also: Pattern #15 (Cross-portfolio packages dir with vendor-pin convention) — the distribution shape this pattern lives inside. Pattern #3 (Vendored typed contracts for cross-repo service consumption) — same vendor-and-pin discipline for types instead of components. PA Toolbox P13 (Cross-Cutting Domain Envelope) — the structured spec shape the chart slot consumes; pairing the two gives a producer + consumer matched envelope + renderer.
17. Frontend-only sandbox mirror of a production diagnostic
Problem
You have a marketing-site visitor who wants to feel what your product actually does before signing up. A video is the easy answer and the wrong one — videos show; sandboxes let the prospect experience. But the real diagnostic lives behind auth, a database, an IRT engine, an item bank with reliability gates. You can't expose that to anonymous traffic. You want a frontend-only mirror that gives the prospect the feel of the real diagnostic — the same questions, the same refusal-when-signal-is-weak discipline, the same single-finding output — without any of the production stack.
The Pattern
// lib/sandbox/items.ts — handful of items copied from the production seed.
// Sync by hand; pool changes deliberate + infrequent.
export const DIMENSIONS = ["dim_a", "dim_b", "dim_c", "dim_d"] as const;
export type Dimension = (typeof DIMENSIONS)[number];
export type Item = {
id: string;
dimension: Dimension;
prompt: string;
info: { title: string; body: string };
};
export const ITEMS: Item[] = [
{ id: "a-1", dimension: "dim_a", prompt: "...", info: { /* ... */ } },
// ... 11 more
];
// lib/sandbox/settle.ts — simplified settle that preserves the production
// discipline (refuse-when-no-signal) without the real math.
const SPREAD_FLOOR = 0.5;
export function settle(items: Item[], answers: Answer[]) {
// 1. Per-dimension mean of 1-5 answers.
const sumByDim = Object.fromEntries(
DIMENSIONS.map((d) => [d, { sum: 0, count: 0 }]),
);
for (const a of answers) {
const item = items.find((i) => i.id === a.itemId);
if (!item) continue;
sumByDim[item.dimension].sum += a.value;
sumByDim[item.dimension].count += 1;
}
const scored = DIMENSIONS
.map((d) => ({ dimension: d, mean: sumByDim[d].count > 0
? sumByDim[d].sum / sumByDim[d].count : null }))
.filter((d): d is { dimension: Dimension; mean: number } => d.mean !== null);
if (scored.length < 2) {
return { binding: null, rationale:
"Not enough answered items to pick a binding constraint yet." };
}
const sorted = [...scored].sort((a, b) => a.mean - b.mean);
const [lowest, second] = sorted;
const spread = second.mean - lowest.mean;
// 2. Refuse to commit if the signal is too weak. Real diagnostic uses
// Cronbach-α reliability floor; sandbox uses simple spread floor.
if (spread < SPREAD_FLOOR) {
return { binding: null, rationale:
`The four dimensions are scoring within ${SPREAD_FLOOR} of each ` +
`other (spread: ${spread.toFixed(2)}). No single dimension is ` +
`starving enough to call binding — the real diagnostic would ask ` +
`more items here.` };
}
// 3. One winner. Single rationale string. No dashboard.
return {
binding: lowest.dimension,
rationale: `${lowest.dimension} is scoring lowest (${lowest.mean.toFixed(2)}) ` +
`by ${spread.toFixed(2)} below the next dimension. That gap is the call.`,
};
}
// app/try/page.tsx — public route, no auth, no API calls.
import { DiagnosticWalkthrough } from "@/components/try/walkthrough";
export default function TryPage() {
return (
<main className="max-w-2xl mx-auto px-6 py-12">
<h1>Try the diagnostic</h1>
<DiagnosticWalkthrough />
</main>
);
}
Key Design Decisions
- Sandbox is honest about being a sandbox. The refusal-rationale explicitly names what the real diagnostic does differently ("the real diagnostic would ask more items here"). Prospects who get a null result aren't confused — they're told the real product handles this case more deeply.
- Same discipline, simpler math. Production uses IRT-weighted scores + Cronbach-α reliability floor. Sandbox uses arithmetic mean + spread-floor. Different math, same kind of decision: refuse-when-signal-is-weak.
- Items are hand-synced, not fetched. The sandbox doesn't hit any API. Item changes in production go into the sandbox via a manual sync (no auto-sync because the pool changes are deliberate enough that an automated drift would be noise).
- One binding constraint, not a dashboard. Same shape as the production output (#10 binding-constraint diagnostic). The sandbox doesn't add features the real product doesn't have.
- Pure-frontend, deploys statically. No serverless function. No DB connection. No auth wrapper. The page is a
'use client'component fed by a static items array. The bandwidth + carbon + maintenance cost is ~zero. - Single rationale string, not a structured breakdown. Production might surface CI bounds and dimension scores; the sandbox just says "X is lowest by Y, that's the call." Forces the prospect's brain to engage with the concept, not the chart.
- Reset is one-click. Prospects who want to play through multiple times shouldn't have to reload. The component owns its state; reset clears it.
This Codebase
performix-site/app/try/page.tsx — the public route (200 on https://performix.app/try).
performix-site/components/try/diagnostic-walkthrough.tsx — client component with progress strip, items grouped by dimension, settled-card terminal state.
performix-site/lib/try/items.ts — 12 items (3 per dimension), hand-synced from performix/src/lib/canned-cams-items.ts.
performix-site/lib/try/settle.ts — frontend settle; explicit docstring naming what the production settle does differently.
Known gaps. No telemetry on which binding constraint prospects most commonly land on — would be useful marketing data ("X% of prospects who try the sandbox land at dimension Y"). Privacy-first opt-in only when it ships.
Tradeoffs
| Pro | Con |
|---|---|
| Prospects experience the product instead of reading about it | Sandbox can drift from production semantics if not maintained |
| Zero runtime cost — pure static HTML + JS | Item-pool sync is manual; production updates don't auto-flow |
| Refusal discipline matches production (no false signal) | Prospects who hit the refusal path may misread "I broke it" — copy must be careful |
| Mirrors production output shape (single binding constraint, not dashboard) | Doesn't show CI / reliability — some prospects want the rigor visible |
See also: Pattern #10 (Binding-constraint diagnostic — lowest-of-N as the action signal) — the production shape the sandbox mirrors. Pattern #12 (Sibling-repo build-time sync with bundled fallback) — the build-sync discipline that could automate the item-pool drift if the cadence ever justifies it.
18. Delete-and-reinsert for composite-PK canonical state
Problem
You have an idempotent seed script that writes a row set to a table keyed by a composite primary key (e.g., (parent_id, child_id) for an ordered list-of-items relationship). When the canonical state — the order of items in a list, the membership of a set — changes between seed runs, naive INSERT … ON CONFLICT DO UPDATE is awkward: the conflict target doesn't cover the "position" column, so re-ordering creates orphans, and a deleted-from-source item that's still in the DB persists silently. You want one cheap operation that guarantees the table matches the seed file exactly.
The Pattern
import { db } from "../src/db/client";
import { eq } from "drizzle-orm";
import { listItems, lists, userPrefs } from "../src/db/schema";
const LIST_ID = "list-x";
const USER_ID = "user-1";
async function seedList() {
// 1. Upsert the parent row (no order coupling — safe to ON CONFLICT UPDATE).
await db
.insert(lists)
.values({ id: LIST_ID, name: "Canonical List X", refreshedAt: new Date() })
.onConflictDoUpdate({
target: lists.id,
set: { name: "Canonical List X", refreshedAt: new Date() },
});
// 2. For the child set: DELETE-then-INSERT.
// Cheapest "this is the canonical state" guarantee. No need to diff,
// no orphan-handling code, no per-position ON CONFLICT gymnastics.
await db.delete(listItems).where(eq(listItems.listId, LIST_ID));
const itemRows = CANONICAL_ITEM_ORDER.map((itemId, position) => ({
listId: LIST_ID,
itemId,
position,
}));
if (itemRows.length > 0) {
await db.insert(listItems).values(itemRows);
}
// 3. Upsert user-preference pointer (single-row, simple upsert OK).
await db
.insert(userPrefs)
.values({ userId: USER_ID, primaryListId: LIST_ID })
.onConflictDoUpdate({
target: userPrefs.userId,
set: { primaryListId: LIST_ID, updatedAt: new Date() },
});
}
const CANONICAL_ITEM_ORDER = [
"item-3", // position 0 — moved up from position 2
"item-1", // position 1
"item-5", // position 2 — new this revision
"item-2", // position 3
// item-4 absent — deleted from the canonical set
];
Key Design Decisions
- Delete-then-insert is one transaction's worth of "match this exactly". Diffing the existing rows against the seed to produce a minimal set of inserts/updates/deletes is more code, more conflict-cases, more bugs. The replace-all op is bounded and obvious.
- Use it for small sets of cheap-to-rebuild rows. List-membership, ordered-position lookups, configuration tables. NOT for tables with downstream FKs that would cascade-delete, NOT for tables with millions of rows.
- Pair with a single-row upsert on the parent. The parent (the list, the user pref) gets
ON CONFLICT DO UPDATEbecause it's not order-coupled. The child set gets delete-then-insert because the position columns and the membership are both load-bearing. - Run inside a transaction in production. Sketch above is two statements for clarity; production should wrap them so a crash between delete and insert doesn't leave the list empty.
- Verify the seed module matches the DB shape at script start. Throw if the seed module is missing the canonical id, or the pointer's
primaryListIddoesn't match — catches "I forgot to update the seed module" before the script writes garbage. - Idempotent — safe to re-run. Each run produces exactly the same final state regardless of starting state. Re-running after a partial failure is safe; re-running on a perfectly-seeded DB is a no-op in effect (the row content is identical).
This Codebase
scripts/seed-leader-performance-brief.ts — full script with parent-upsert + child-replace-all + pointer-upsert + seed-module validation.
src/db/insights.ts — schema with composite PK on smart_list_items.(smartListId, insightId) that motivates the pattern.
src/db/seeds/insights.ts — the seed module the script reads; the validate-then-write step pulls the canonical state from here.
Known gaps. Not wrapped in a transaction yet — a crash between the delete and the re-insert would leave the list empty. Low risk in practice (the script is short, the DB is fast), but the right shape is db.transaction(async (tx) => { ... }).
Tradeoffs
| Pro | Con |
|---|---|
| Cheapest "this is the canonical state" guarantee | Wrong tool for tables with FKs that would cascade or many millions of rows |
| Idempotent regardless of starting state | Brief window where the list is empty mid-script (transaction closes this) |
| Avoids per-position ON CONFLICT gymnastics | Re-creates row IDs in some schemas (mitigated by composite PK; flag if you have surrogate keys) |
| Script is short + obvious to read | Doesn't preserve created_at semantics on rows that "stayed" |
See also: Pattern #14 (Three-tier promotion gate with sticky rejection) — when the canonical-state set should be reviewer-approved rather than re-seeded from code. Principia P02 (Provenance-Merging Upsert Wrapper) — the opposite pattern when row history matters and replace-all would lose audit trail.
Recipes — compose patterns into systems
These are the standing recipes that combine patterns above. Add new ones here as they crystallize.
Recipe: "Add a new analytical capability"
- Capability folder (#7):
src/capabilities/<name>/{contracts,core,adapters,ui,tests}. - Contract first (#3, #8): Zod schemas for input + output go in
contracts/. Other capabilities only import from here. - Adapter port (#2): pick mock / HTTP / MCP based on whether the back-end is local-deterministic, a sibling service, or a remote MCP gateway. Three adapters share the contract.
- Pure compose (#1):
core/compose.tsis a pure function; takes typed input, returns the Insight record. No I/O. - Privacy gate (#6): if any aggregate is team-level, the gate-call goes inside
core/compose.tsbefore the result is returned. - Storage (#1): write the Insight to the precompute store; route handlers read from there, never re-call
core/compose.tsat render time. - Route handler (#5, #8):
src/app/api/...validates with Zod, imports only frominsight-store.ts, returns the precomputed result.
Recipe: "Consume a sibling service over MCP"
- Vendor the contract (#3): copy the upstream Zod schemas into
src/lib/<service>/contract.ts. Header marks source + version. - MCP adapter (#4): lazy session affinity; one transport per process. Bearer auth in connect headers.
- Adapter factory (#2): mock + HTTP + MCP, env switch. Default to mock so tests don't hit live infra.
- Schema validate at every call (#3, #8): even with the warm transport, every response is
safeParsed.
This file is the source-graded record of the engineering patterns that have hardened in production code. New patterns join the list by crystallizing in actual code first, then getting written up here. Speculative patterns belong in docs/build-out/, not in this file.