peopleanalyst

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).

18 patterns·source: people-analyst/performix/docs/REUSABLE_PATTERNS.md

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 canonical EXTRACTION-SPEC.md (which recommends ### PNN. Title with 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

#PatternWhen to reach for it
1Precompute-and-playback over recompute-at-renderAny analytics surface where the same value should be identical across users and across page reloads.
2Three-way adapter factory (mock / HTTP / MCP) with env switchA capability that needs to swap concrete back-ends without touching call sites. CROSS
3Vendored typed contracts for cross-repo service consumptionConsuming services owned by a different repo without importing its modules.
4MCP consumer with lazy session affinityLong-lived MCP transports inside a Next.js process; one warm session per process.
5Tier-gated read path with server-only boundaryMulti-tier storage where lower tiers must never leak to UI code. CROSS
6Privacy gate as a substrate primitiveTeam-level rollups of individually-attributable data.
7Capability architecture — contracts / core / adapters / ui / testsA repo that's going to grow more than five distinct functional capabilities.
8Zod-validated request/response envelopeAny HTTP or MCP boundary in a TypeScript project. CROSS
9Action-status transition gate (lightweight state machine)Multi-stage workflows that don't need an XState-class library. CROSS
10Binding-constraint diagnostic (lowest-of-N as the action signal)Multi-dimensional scoring where the action question is "what's stopping the system right now?"
11Substitution-boundary pipeline scaffold (NotImplementedError per stage)A multi-stage pipeline whose contracts are known but whose stage bodies will land over weeks.
12Sibling-repo build-time sync with bundled fallbackA 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.
13Watchlist-driven harvest with cursor write-backPersistent searches against an external API where each query has its own cadence and writes its own dated output. CROSS
14Three-tier promotion gate with sticky rejectionAn extraction/verification pipeline whose output must never auto-promote past a human reviewer. CROSS
15Cross-portfolio packages dir with vendor-pin conventionSharing UI primitives across sibling repos when npm publishing and monorepo migration are both premature.
16Theme-agnostic exportable component with render-prop chart slotA component you want consumers to vendor that should work in their design system without inheriting yours.
17Frontend-only sandbox mirror of a production diagnosticA public marketing-site "try it" surface that gives prospects the feel of a real diagnostic without the auth/DB/IRT stack.
18Delete-and-reinsert for composite-PK canonical stateIdempotent 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) and method (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 MetricEnvelope shape 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

ProCon
Render is always fast; users see the same valueStale records are possible — provenance + computedAt must be surfaced
Two observers see identical numbers under the same computedAtRecompute schedule has to be designed up front
Provenance is built into the schema, not bolted onEvery new metric requires an upstream computation path, not just a route handler
Player UI can never accidentally diverge from the canonical numberStorage 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

ProCon
Call sites have zero awareness of back-end choiceEach new back-end is a new adapter class + factory branch
Tests use the mock adapter without monkey-patchingThree implementations to keep schema-aligned
Local dev runs without infraEnv-switch logic is repeated per capability
Demo mode is the same code path as production, with mock dataAdapter 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 major bumps only. 0.4.20.4.5 is additive; consumer re-vendors on 0.5.x or 1.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 grep find . -name contract.ts and 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

ProCon
Cross-repo coupling is explicit and grep-ableManual re-vendor step on major bumps
Consumer build doesn't depend on the service's repo layoutDrift between vendored copy and upstream is possible
Schema-level validation catches transport errors at the boundaryLarger consumer codebase (one file per service)
Service can refactor internals without breaking consumersNew 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 Transport is 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

ProCon
First call pays the connect cost; subsequent calls are fastCold starts always pay the full connect on first call
One transport per process keeps resource use predictableNo pooling — single transport is the throughput ceiling
Promise-caching dedupes concurrent first-callersFailed initial connect requires manual retry or process restart
Per-call schema validation isolates transport bugs from contract bugsSchema 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-only is 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.ts is the tier-3 (suppressed, aggregated) reader. Tier-2 (segment-aggregated) and tier-1 (anonymized records) have separate modules with their own server-only guards.
  • Route handlers import only from the tier-matching module. The discipline is enforced by code review + the server-only build-time check, not by trust.
  • Return types are tier-specific. readInsight() returns Insight, not InsightRow. 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

ProCon
Build-time crash if a client component imports tier-3 readerOne module to maintain per tier
Type system + server-only together = belt + suspendersNew 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 checksModule 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-anonymizer ships from the People Analytics Toolbox, the consumer (Performix) vendors typed contracts.
  • Suppression is encoded in the Insight, not lost. The stored Insight has safetyDetails.gateVersion and safetyDetails.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

ProCon
Identity leaks are structurally hard, not policy-hardEvery aggregate carries the gate-call overhead
Suppressed-state is first-class; downstream consumers can't ignore itBelow-floor cells can't be "approximated" — they're gone
Privacy primitives live in the substrate, not in each queryRequires vendoring + contract discipline against data-anonymizer
Identity-risk scoring catches outlier-cohort leaks min-N missesTuning 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 skips ui/), but the shape never changes.
  • contracts/ is the only folder other capabilities may import from. Cross-capability imports go through the published Zod schemas. Internal core/ and adapters/ are private.
  • core/ is pure. No I/O, no React, no env reads, no Date.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

ProCon
New capability = new folder, predictable structureNew contributors must learn the five-folder convention
Refactoring within a capability doesn't fan out to othersCross-capability imports require a contracts/ round trip
core/ is trivially unit-testableMore folders to navigate for small capabilities
Extraction maturity is visible per capabilityCapabilities 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, not parse. Throwing on invalid input couples the validator to the response shape. safeParse returns 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>), not runDiagnostic(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.tsBodySchema 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

ProCon
Type system and runtime agree on input shapeEvery boundary needs a schema; not free
Malformed input fails at the boundary with a usable errorZod adds ~30KB to the bundle (server-side only here)
Internal callers trust their inputs; no defensive codeSchema + handler can drift if not co-located
Schema bounds encode product invariants in one placeComplex 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 a switch cascade.
  • Terminal states are explicit empty arrays, not omitted entries. Forces the author to declare "this is a sink." Easy to check transitions[s].length === 0 for 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.tsActionStatus discriminated union + ACTION_STATUS_TRANSITIONS table. src/capabilities/action-loop/core/plan-template.tscanTransitionActionStatus() 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

ProCon
Illegal transitions fail loudly at the boundaryNo support for guard conditions (e.g., "can only close if all subtasks complete")
Transition table reads like documentationMore states = combinatorial growth in the table
Validator is unit-testable in isolationNo automatic visualization (Graphviz would need a small script)
Lighter weight than an XState integrationCoordinated 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 null on 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.tsseverityFromScore() 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

ProCon
Output is one actionable signal, not N data pointsSome readers want the dashboard view; this pattern refuses it
Reliability gate filters noise out of action signalsReliability calibration is per-domain; not free
Severity buckets are stable across measurement noiseBucket 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 NotImplementedError until they ship. The pipeline runs end-to-end the day the scaffold lands — you can call runPipeline() against any input and inspect the RunRecord shape 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.ts without ever touching pipeline.ts.
  • StageError carries 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 adapters param so tests can pass mocks (see Pattern #2). Default adapters resolve from a factory; nothing in stage code reaches for process.env.
  • partial status is first-class. A pipeline that gets through stage 3 of 7 returns status: "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

ProCon
Pipeline contract ships before any stage worksReading scaffold stages can feel like vapor — every body says "NOT_IMPLEMENTED"
Future agents replace stages one at a time without coordinationStage signature changes are still a fan-out — pick the shapes carefully up front
Run record always has a typed shape, even for failed runsTempts you to ship the scaffold and forget to land the stages
StageError makes failure attribution mechanicalStages 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.
  • prebuild hook, not a manual step. npm runs prebuild automatically before build. 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 .mjs script, no deps. Pure node: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.jsonprebuild 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

ProCon
Single source of truth in dev; deployable in cloud buildsCommitted copy can lag; manual sync discipline matters
Zero monorepo coupling — the sibling can be moved/renamed without breaking cloud buildsBuild log noise (every build prints the sync line)
Pure Node, zero depsScript doesn't validate the artifact shape — bad JSON in the sibling gets copied without complaint
Hard-fails when both source and target are missingManual 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.json makes "what changed since last week?" trivially answerable — diff two files. No mutating-database semantics.
  • Cursor is written back into the source file. last_harvest_at lives in the watchlist file itself. The harvester is the only writer; operators read it to see staleness. No separate _cursor table.
  • 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 in buildUrl.

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

ProCon
New watchlist ships without code or DDLAPI quota management is per-script, not coordinated across watchlists
Per-watchlist output files diff naturallyStorage grows linearly with watchlist count × run count
Per-watchlist failure isolated; rest of run completesNo native cadence — all watchlists run on every invocation unless caller filters
Cursor in the source file = operator-visible stalenessConcurrent 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_verifiedneeds_review. Reviewer code can flip needs_reviewapproved or rejected. Nothing can flip agent_verifiedapproved directly — 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_queue table. 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.tsHumanReviewQueue port + createMockHumanReviewQueue() for dev. src/capabilities/research-to-model-engine/contracts/types.tsREVIEW_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

ProCon
Reviewer authority is structural, not policy — pipeline cannot betray itReviewer becomes the throughput bottleneck for canonical-state growth
Sticky rejection prevents repeated wasted extraction passesReviewers must annotate rejection reasons or context is lost
Four-status enum maps cleanly to inbox / queue / archive / liveSchema must thread review_status through every artifact type
Approval path is a separate function — auditable in one placeReviewers 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_VERSION is a one-line file. Not package.json — that pulls a runtime expectation. A bare text file is the version stamp; semver discipline is in README.md.
  • Pin file in the consumer. Every vendored copy carries PRODUCER_SHA + VENDORED_AT. Anyone asking "is this current?" runs git log producer-repo/packages/<name>/ and compares.
  • No CI/publishing. No GitHub Actions, no npm tokens, no release branches. The cost of npm publish workflow 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/ to packages/ 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

ProCon
Sharing across siblings without npm or monorepo overheadDrift is real — consumers re-vendor only when motivated
Producer can refactor internals freely; consumers carry an independent copyBug fixes in producer don't reach consumers automatically
LOCAL_MODS discipline surfaces consumer divergences for reviewMulti-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; no tsconfig paths, 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 via renderChart. The producer doesn't force recharts or nivo on 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 null for "use consumer's contextual theme". Some consumers (the producer's own routes) want the renderer to defer to a CSS-variable-driven theme. Explicit null opt-in makes that intent visible at the call site.
  • renderChart is 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 + react only." A failed re-vendor (consumer's npm install choking 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.tsChartPalette + 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

ProCon
Consumer vendors one file + sibling theme/types + has working component on day oneProducer must resist the urge to pull in helpers for ergonomics
Chart library is consumer's choice; no recharts-or-nothing lock-inProducer's own routes lose some convenience (re-importing the chart library they already use)
Default theme works without any consumer setupDefault hex values can look "off" against the consumer's design — re-vendor with their tokens recommended
Render-prop pattern is React-idiomatic and well-understoodThe 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

ProCon
Prospects experience the product instead of reading about itSandbox can drift from production semantics if not maintained
Zero runtime cost — pure static HTML + JSItem-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 UPDATE because 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 primaryListId doesn'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

ProCon
Cheapest "this is the canonical state" guaranteeWrong tool for tables with FKs that would cascade or many millions of rows
Idempotent regardless of starting stateBrief window where the list is empty mid-script (transaction closes this)
Avoids per-position ON CONFLICT gymnasticsRe-creates row IDs in some schemas (mitigated by composite PK; flag if you have surrogate keys)
Script is short + obvious to readDoesn'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"

  1. Capability folder (#7): src/capabilities/<name>/{contracts,core,adapters,ui,tests}.
  2. Contract first (#3, #8): Zod schemas for input + output go in contracts/. Other capabilities only import from here.
  3. 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.
  4. Pure compose (#1): core/compose.ts is a pure function; takes typed input, returns the Insight record. No I/O.
  5. Privacy gate (#6): if any aggregate is team-level, the gate-call goes inside core/compose.ts before the result is returned.
  6. Storage (#1): write the Insight to the precompute store; route handlers read from there, never re-call core/compose.ts at render time.
  7. Route handler (#5, #8): src/app/api/... validates with Zod, imports only from insight-store.ts, returns the precomputed result.

Recipe: "Consume a sibling service over MCP"

  1. Vendor the contract (#3): copy the upstream Zod schemas into src/lib/<service>/contract.ts. Header marks source + version.
  2. MCP adapter (#4): lazy session affinity; one transport per process. Bearer auth in connect headers.
  3. Adapter factory (#2): mock + HTTP + MCP, env switch. Default to mock so tests don't hit live infra.
  4. 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.