ID-104 {104.3} TECH — bottom-up agent-eval engine
ID-104 {104.3} TECH — bottom-up agent-eval engine
Section titled “ID-104 {104.3} TECH — bottom-up agent-eval engine”Status:
{104.3}TECH spec. Authored by a FRESH Planner instance (Q-PLANNER-2 / B4 — NOT the{104.1}RESEARCH /{104.2}PRODUCT Planner) against the ratified predecessors:{104.1}RESEARCH.md and{104.2}PRODUCT.md (B-INV-1..25, groups A–I; RESEARCH + PRODUCT both ratified by Liam, S356). Every Proposed change below maps 1:1 to a numbered B-INV via the orphan-check table in §M — the format ID-71 TECH §M established and the Checker + the neweval-runnerself-eval verify. British English throughout. DD/MM/YYYY dates.This spec fixes the mechanics; it does not re-litigate behaviour. Settled context (do NOT re-open, per S356): bottom-up rebuild (not lift-and-extend); canonical
AgentEvalContractinlib/eval/contract.tswith seven mandatory fields + optionalgraduation_metric; 4-tierblock|warn|info|infraseverity; single centraleval-runner(exit 0/1/2); DB-backed baseline registry portingcheckRegressionsemantics;metrics.tsreused as-is;recordAiCall()single capture point with outcome-signal enumwin|fail|loop|refusal; in-house graduation metric (never Raindrop cloud); three layered surfaces (Workshop-local + create-skill viewer + KH/admin/refinementstub); L1/L3/L4 reused as suites; scope-phased HITL (substrate IN, organs DEFERRED with the present-but-emptypatterns/proposalsstubs).This TECH is the HARD-UPSTREAM gate that unblocks ID-71 Wave 2/3. The canonical
AgentEvalContractfinalised in §Contract is the type ID-71’s M38 guard imports (ID-71 TECH §OQ-2). ID-104 is sequenced FIRST.
Context
Section titled “Context”ID-104 builds the bottom-up agent-eval substrate platform-direction.md’s two anchor
principles stand on: eval-everything (every AI touchpoint born-evaluable) and
progressive-trust graduation (auto-apply earned per-workflow on an in-house metric). It
owns the canonical AgentEvalContract, the touchpoint registry, the 4-tier severity +
variance_band model, recordAiCall() cost/signal capture, the central eval-runner, the
DB-backed baseline lifecycle, the in-house graduation metric, the nightly lane, and the
KH-owned /admin/refinement stub-spine. Behaviour is fixed in {104.2} PRODUCT; this spec
fixes how each invariant lands. The full reuse-vs-rebuild verdicts are settled in RESEARCH §3
and restated here as concrete architecture, not re-litigated.
Code-intelligence orientation (cited verbatim, re-verified 15/06/2026 — not paraphrased)
Section titled “Code-intelligence orientation (cited verbatim, re-verified 15/06/2026 — not paraphrased)”Per the binding orientation rule, the symbols this spec mandates be touched were queried
before drafting. The knowledge-hub GitNexus index is partially stale for this surface
(several saveBaseline incoming-call UIDs returned byte-garble in RESEARCH/PRODUCT, and the
query below returned mostly unrelated Start → cost/quality processes), so per RESEARCH
§1.5 the conclusive evidence is the clean symbol records + direct source reads + grep/SQL
sweeps. Which tool gave which finding is stated explicitly:
gitnexus_query({query:'eval runner baseline severity touchpoint cost tracking recordAiCall registry', repo:'knowledge-hub'})returned mostly noise (cost/qualityStart →processes) but itsdefinitionsblock cleanly surfaced the real eval surface:Function:lib/eval/baseline.ts:saveBaseline(:35-53) +:evalPassed(:115-125);Function:components/provenance/cost-tab-stub.tsx:CostTabStub(:21-128) +:fetchCostAggregate(:27-72); andFunction:scripts/quality-gate.ts:severityFor(:323-333) — the content quality-gate severity mapper, a DISTINCT surface, NOT the AI-eval layer (do NOT conflate — RESEARCH §3 flagged this).gitnexus_context({name:'CostTabStub', repo:'knowledge-hub'})returnedambiguous(3 candidates:components/provenance/cost-tab-stub.tsx:21,app/provenance/provenance-content.tsx:82×2). The component is the canonical interim slot.- Direct read of
lib/eval/baseline.tsconfirmssaveBaselinewrites JSON to__tests__/fixtures/eval-baselines/${suiteName}.baseline.json(:16,:52);checkRegression(:65-105) implementsmin(currentValue < min→ fail) +max_drop((baseline - current) > max_drop→ fail) — both one-sided (the improvement-asymmetry to port intact);evalPassed(:116-126) returnstrueon no-baseline (first run). - Direct read of
lib/eval/types.tsconfirmsEvalBaseline.thresholdsisRecord<string, { min?: number; max_drop?: number }>only (:42) — no severity, novariance_band, no touchpoint/contract/kind/grounding_shape fields.EvalResult(:28-35) carriessuite_name/metrics/passed/failures. There is noAgentEvalContract. - Direct read of
lib/eval/metrics.tsconfirms it is pure + stateless (precision/recall/ f1Score/accuracy/rougeL/rouge1/mrr/ndcgAtK/precisionAtK — no I/O). Reused as-is (RESEARCH §3). - Direct read of
components/provenance/cost-tab-stub.tsxconfirmsCostTabStub(labelled “Interim — Wave B”) readspipeline_runs.costover 30 days viatryQuery(:34-41) — it reads the pipeline cost aggregate; there is no per-touchpointrecordAiCall()persistence behind it. It is the natural host slot for the rollup (B-INV-17). UsescreateClient+tryQuery+logBestEffortWarn— the KH safe-access pattern. scripts/eval-search.ts(grep of themain()pattern) confirms the per-suite entry: importssaveBaseline/checkRegression(:29-30),async function main()(:255),checkRegression(baseline, metrics)(:419),saveBaseline(SUITE_NAME, …)(:432), andprocess.exit(1)at :119/:284/:290/:297/:308/:491/:497. It usesexit(1)for BOTH quality-fail AND infra-error — there is no 0/1/2 split today; the deterministic exit disposition (B-INV-10) is genuinely new. Seven siblingscripts/eval-*.tsexist (classification, entity-classification, holder-rule-ts, procurement-drafting, search, summarisation, tag-morphology-adoption) — each its own entry point; no central dispatcher.package.json(grep) confirmseval:*per-suite scripts (:51-56) and thetest:mcp-eval/:rq/:fc+seed:mcp-evalscripts (:36-42);eval:allchains the per-suite scripts sequentially..github/workflows/ci.yml(grep) confirms themcp-evalmatrix job (l1→test:mcp-eval,l3→:rq,l4→:fc, :888-890), gated onmcp-eval-seed(:649,:709) +detect-changes(:745), push-events-only / paths-filtered (:777, real-API gate ID-91/bl-246). There is NO scheduled/nightly lane today — the nightly full-set lane (B-INV-13) is greenfield; onlystaging-reference-refresh.yml+supabase-advisors.ymlare non-CI scheduled lanes.- SQL sweep (
grep create-table over supabase/migrations/*.sql): ZEROai_call/cost_track/cost_event/eval_result/eval_run/eval_baseline/touchpoint/agent_evaltables. Python + TS + SQL sweep forrecordAiCall|record_ai_call: ZERO hits. (gitnexus + ast-dataflow do not cover SQL/Python — grep is the conclusive tool here.) Theccc searchfallback errored in RESEARCH/PRODUCT (local cocoindex daemon — environment issue, not an absence signal); grep + GitNexus definitions are conclusive on their own. - Filesystem checks:
app/admin/refinementdoes NOT exist (onlyapp/admin/content-dedupis present) — the stub route is greenfield..claude/skills/create-skill/eval-viewer/viewer.htmlis PRESENT (the skills-surface viewer, B-INV-20-ii, reused as-is).
Orientation verdict (matches RESEARCH §1.5 / PRODUCT): the touchpoint registry, the
canonical AgentEvalContract, the central eval-runner, the 4-tier severity model, the
DB-backed baseline store, recordAiCall() + its cost/signal tables, the nightly lane, the
graduation metric, and the /admin/refinement route are GREENFIELD. The adjacent
existing artefacts are: the file-JSON baseline lifecycle (baseline.ts — port the
regression semantics, rebuild the persistence), the pure metrics.ts (reuse as-is), the
labelled-interim cost-tab-stub.tsx (fold the rollup behind it), and the CI-wired L1/L3/L4
mcp-eval suite (reuse as the suite layer). Tool/query catalogues: .gitnexus/CLAUDE.md,
.ast-dataflow/CLAUDE.md.
Line anchors are indicative, not exact. Cited line numbers locate the symbol at authoring time and may drift; the symbol name + file path is the durable anchor. The Executor resolves by symbol, not line.
Empirical verification (Q-EX2 — external-library symbols only)
Section titled “Empirical verification (Q-EX2 — external-library symbols only)”ID-104 introduces no new external-library API surface: the engine is built on the
already-pinned @anthropic-ai/sdk (token-usage capture path, already in use by
lib/anthropic.ts:estimateCost), the already-pinned @supabase/supabase-js (DB I/O via
sb()/tryQuery()), and stdlib/framework built-ins (Next.js App Router, fs/path).
Raindrop Workshop is a local OTel viewer process (localhost:5899), not an imported KH
dependency — KH POSTs nothing to it on the client-data path (B-INV-21). No
import-and-call verification block is required (the Q-EX2 forcing function scopes to
external-library symbols a spec mandates calling — none are introduced here). The
token-usage fields recordAiCall() reads off the Anthropic response (usage.input_tokens /
usage.output_tokens / usage.cache_*) are an existing, exercised surface
(lib/anthropic.ts), not a new claim.
Proposed changes
Section titled “Proposed changes”Each change is keyed T<N> to the B-INV it satisfies (full table in §M). Changes are
grouped by area (A–I, mirroring PRODUCT’s invariant groups); the §M table is the
orphan-check of record.
Area A — AgentEvalContract + touchpoint registry (B-INV-1..5)
Section titled “Area A — AgentEvalContract + touchpoint registry (B-INV-1..5)”- T1 canonical contract module. Declare
AgentEvalContract(the type in §Contract) in a NEW ID-104-owned modulelib/eval/contract.ts— the single source of truth. NOT cribbed intolib/eval/types.ts(that file keeps the legacyEvalBaseline/EvalResult/gold-item shapes; the contract is its own module). ID-71’s M38 guard importsAgentEvalContractfrom@/lib/eval/contract(direct file import — no barrel re-export). Co-locate the four field unions (TouchpointKind,GroundingShape,SeverityTier, plus theOutcomeSignalenum used by T14) so ID-71 and the registry import one module. (B-INV-1.) - T2 seven mandatory fields + optional graduation_metric.
lib/eval/contract.tsexports the exact seven-field shape (touchpoint_id,kind,owner,suite_name,grounding_shape,severity_on_fail,variance_band) + optionalgraduation_metric?: string, with the ratified union types. A Zod schemaagentEvalContractSchema(z.infer≡ the interface) is co-exported so registry writes validate at the boundary. ID-104 MAY add further bottom-up fields (e.g.file_sha256?for git-backed touchpoints — RESEARCH §2b) but the seven are REQUIRED and unrenamed. (B-INV-2.) - T3 registry-of-record table. NEW migration table
eval_touchpoints(§Migration M1):touchpoint_idPK (unique),kind,owner,suite_name,grounding_shape,severity_on_fail,variance_band,graduation_metric(nullable),contract_version int,registry_version int,file_sha256(nullable), timestamps. A registry helperlib/eval/registry.ts(registerTouchpoint/getTouchpoint/listTouchpoints) writes viasb()/tryQuery(); a duplicatetouchpoint_idinsert is REJECTED (PK conflict surfaced as a clear error, not swallowed). (B-INV-3.) - T4 registration-as-gate. The
eval-runner(T9) resolves each touchpoint fromeval_touchpointsBEFORE dispatch; an eval-relevant touchpoint with no registry row is REJECTED with an explicitnot registered: <id>signal (exit 2 disposition, T10). This is the substrate ID-71 B-INV-13’s born-evaluable guard enforces against. (B-INV-4.) - T5 version advance on contract change.
registerTouchpointbumps the per-touchpointcontract_versionwhen any contract field changes (computed by comparing the incoming contract hash to the stored row) and advances the table-levelregistry_version; the/admin/refinementlisting + theversion-historyendpoint (T22) read it. (B-INV-5.)
Area B — severity + variance model (B-INV-6..8)
Section titled “Area B — severity + variance model (B-INV-6..8)”- T6 canonical 4-tier severity.
SeverityTier = 'block'|'warn'|'info'|'infra'lives inlib/eval/contract.ts; every contract carries exactly one.infrais reserved for transient-provider failures (Anthropic 529 / timeout / 503 — phase-3 §6.6 porting source) and is NEVER counted as a quality regression. Supersedes the historic phase-3 3-tier draft (block|warn|info, whichlib/eval/types.tsnever shipped). (B-INV-6.) - T7 severity → exit disposition. A NEW
lib/eval/severity.tsdisposition(results)maps the worst severity across a run to an exit class:block-fail → gate fail;warn/info→ recorded-but-pass;infra→ recorded as infrastructure noise, does not fail the gate. Theeval-runner(T9/T10) consumes it. (B-INV-7.) - T8
variance_bandregression tolerance. PortcheckRegression(baseline.ts:65-105) semantics intolib/eval/regression.tskeyed by touchpoint + contractvariance_band(default0.02): a drop within±variance_bandis NOT a regression; a drop beyond it (in the worsening direction) IS; an improvement of any size passes (improvement-asymmetry, F17 — implemented one-sided per the portedmin/max_dropmath, NEVERMath.abs(...)). (B-INV-8.)
Area C — central eval-runner, baseline lifecycle, nightly lane (B-INV-9..13)
Section titled “Area C — central eval-runner, baseline lifecycle, nightly lane (B-INV-9..13)”- T9 single central
eval-runner. NEWscripts/eval-runner.ts: readseval_touchpoints(T3), dispatches each touchpoint’ssuite_name(the L1/L3/L4 suites + the legacy per-suite callees, registered as touchpoints — T23), applies the severity model (T7) + regression math (T8), and writes results UNIFORMLY toeval_runs(§Migration M2). Replaces the per-suitescripts/eval-*.tsmain()→saveBaselinepattern (no central dispatcher exists today — orientation-confirmed). The seven legacyeval-*.tsscripts become thin callees the runner invokes (their check logic unchanged); a--touchpoint <id>/--allflag selects scope. (B-INV-9.) - T10 deterministic exit 0/1/2.
scripts/eval-runner.tsexits0(all gating checks pass),1(≥1block-severity regression — quality gate fail),2(runner/infra error — could not complete, including an unregistered touchpoint per T4, an Anthropic 529 surfaced asinfrawith no other failures still exits 0, but a runner crash / DB-unreachable exits 2). Distinct from the legacy uniformexit(1); a CI job branches on the three codes. (B-INV-10.) - T11 DB-backed baseline store + ported regression math. NEW migration table
eval_baselines(§Migration M3): per-touchpoint(touchpoint_id, metrics jsonb, thresholds jsonb, registry_version, promoted_by, promoted_at), replacing the flat__tests__/fixtures/eval-baselines/*.json.lib/eval/baseline-store.tsexposesloadBaseline(touchpointId)/promoteBaseline(...)reading from the DB viasb(). The PORTEDcheckRegressionmath (T8) runs against the store; the file-JSONsaveBaseline/loadBaselineinlib/eval/baseline.tsare retained only until the seven legacy suites are re-pointed (Executor migrates fixtures into rows; the file path is then dead). (B-INV-11.) - T12 promote / history / compare + audit.
lib/eval/baseline-store.tsexposespromoteBaseline(touchpointId, result, actor)(writes a neweval_baselinesrow + aneval_baseline_auditrow recording who/when/whichregistry_version— §Migration M3),baselineHistory(touchpointId)(ordered prior baselines),compareBaselines(a, b)(metric deltas). Names mirror the historic phase-4promoteBaseline/baselineHistory/compareBaselineshooks (porting the lifecycle shape, rebuilding the storage). (B-INV-12.) - T13 nightly lane. NEW workflow
.github/workflows/eval-nightly.yml(schedule: cron, daily) runsbun run scripts/eval-runner.ts --allagainst the FULL registered touchpoint set, records every result + severity disposition intoeval_runs, and is DISTINCT from the push-gatedmcp-evalmatrix (which stays a subset, paths-filtered). It targets the staging Supabase branch (real-API gate posture mirrorsmcp-eval-seed); failures surface on the/admin/refinementsurface, not as a hard PR block. (B-INV-13.)
Area D — recordAiCall() cost + signal capture + rollup (B-INV-14..17)
Section titled “Area D — recordAiCall() cost + signal capture + rollup (B-INV-14..17)”- T14 single capture point
recordAiCall(). NEWlib/eval/record-ai-call.tsexportingrecordAiCall(args)— the ONE capture point. Writes (viasb()/tryQuery()):touchpoint_id,model+ tier,input_tokens/output_tokens/cache_*(off the Anthropicusageobject — existing surface,lib/anthropic.ts),cost_usd(computed via the existingestimateCostpath), andoutcome_signal= the RATIFIED enumwin | fail | loop | refusal(sized for exactly this set, extensible via the enum). Greenfield (no persistence today). (B-INV-14.) - T15 persisted substrate, zero egress. NEW migration table
ai_call_events(§Migration M4): per-call rows keyed bytouchpoint_id, with theoutcome_signalenum column, RLS so rows are tenant-safe + admin-readable, written on-platform only (no off-platform egress of captured data — the cost/signal data never leaves KH/Supabase). A network assertion in the test suite confirms zero off-platform egress. (B-INV-15.) - T16 guardable presence.
recordAiCall(is a greppable literal; a guard test (__tests__/eval/record-ai-call-guard.test.ts) detects an instrumented touchpoint that omits the call (ID-71’s M38 extends this pattern — the same forcing-function shape asmcp-fixture-sync.test.ts). (B-INV-16.) - T17 rollup into
cost-tab-stub.tsx. Re-pointcomponents/provenance/cost-tab-stub.tsxfetchCostAggregatefrom the interimpipeline_runs.costread to a real aggregate overai_call_events(T15) keyed bytouchpoint_id(TanStack Query fetcher inlib/query/per the components/CLAUDE.md rule; semantic tokens only; keep the “Interim — Wave B” banner until the aggregate is wired, then update the copy). NO fourth surface is spawned — the persistence folds behind the existing slot. (B-INV-17.)
Area E — in-house graduation metric (B-INV-18..19)
Section titled “Area E — in-house graduation metric (B-INV-18..19)”- T18 in-house graduation metric. NEW
lib/eval/graduation.tscomputes the per-workflow quality metric (WS-5 progressive-trust) fromai_call_events(T15) +eval_runs(T9) on KH’s own infrastructure — NO client-data trajectory POSTed to Raindrop cloud (a network assertion confirms zero Raindrop-cloud egress). Directly satisfies ID-71 B-INV-15. (B-INV-18.) - T19 contract-addressable metric. A contract MAY declare
graduation_metric?: string(T2); where present, the runner +/admin/refinementsurface report the touchpoint’s current value against it (lib/eval/graduation.ts:metricFor(touchpointId)). This PRODUCT computes the metric; auto-apply on it is the DEFERRED follow-up (T24). (B-INV-19.)
Area F — three layered observability surfaces (B-INV-20..22)
Section titled “Area F — three layered observability surfaces (B-INV-20..22)”- T20 three layered surfaces, composed by kind. (i) Raindrop Workshop local viewer
(
localhost:5899, OTel-fed) for agents + goose recipes — runtime traces, LOCAL only; (ii)create-skill’s.claude/skills/create-skill/eval-viewer/viewer.htmlfor skills (present, reused as-is); (iii) the KH-owned/admin/refinementstub (T22) as the single registry-of-record spine across ALL kinds, deep-linking to (i)/(ii). Layered by kind, not redundant; the KH surface is the ONLY registry-of-record. (B-INV-20.) - T21 Raindrop Workshop-local only; hosted excluded. Raindrop runs locally with empty
writeKey(⇒ cloud POST is a no-op, OTel-fed); hostedraindrop.aiis PERMANENTLY excluded from the client-data path (HR-5 / WS-14 blocker). A network assertion confirms Workshop with emptywriteKeyperforms zero trace egress; no code path POSTs client-derived data toraindrop.ai. (B-INV-21.) - T22 KH
/admin/refinementstub-spine. NEW admin-only routeapp/admin/refinement/page.tsx(Warm Meridian semantic tokens only; UK English; WCAG 2.1 AA — colour never the sole signal), gated viagetAuthorisedClient()→auth.success→authFailureResponse(auth). Lists registered touchpoints +registry_version+ per-touchpoint unprocessed-signal count, PLUS FOUR per-touchpoint API endpoints an operator cancurl(phase-4 AC#12 REVISED F-10 shape):GET /api/refinement/touchpoints/[id]/signals,/patterns,/proposals,/version-history— each admin-gated viaauthFailureResponse(auth).patterns+proposalsship PRESENT-BUT-EMPTY (they back the DEFERRED organs, T24 — empty 200 with a stable shape, NOT 404/absent), so the follow-up only fills them. New API routes are NOT added toproxy.tspublicRoutes(admin-only, must redirect to/loginfor non-admins). The full cross-touchpoint dashboard is a NAMED follow-up, not launch. (B-INV-22.)
Area G — L1/L3/L4 reuse as the suite layer (B-INV-23)
Section titled “Area G — L1/L3/L4 reuse as the suite layer (B-INV-23)”- T23 register the 96 CI-wired checks as touchpoints. The shipped
scripts/mcp-eval/suite — L1 protocol-compliance (42 checks), L3 response-quality (17), L4 functional-correctness (37) = 96 CI-wired checks — is REGISTERED as touchpoints ineval_touchpoints(T3) with a boundAgentEvalContract(suite_name: 'l1'|'l3'|'l4',severity_on_fail+variance_bandattached). Their pass/fail routes througheval-runner(T9) + the contract, NOT the file-JSONsaveBaselinepath. The check logic inprotocol-compliance.ts/response-quality.ts/functional-correctness.tsis UNCHANGED — only the orchestration around it (registry + contract + runner) is rebuilt. A seed migration /registerTouchpointbootstrap script registers the three suites + the seven legacyeval-*.tssuites at first run. (B-INV-23.)
Area H — scope-phased HITL boundary (B-INV-24)
Section titled “Area H — scope-phased HITL boundary (B-INV-24)”- T24 substrate IN; organs DEFERRED-with-anchor. IN scope NOW (T1–T19): registry, contract,
severity|variance,
recordAiCall()capture, baseline lifecycle, graduation metric. DEFERRED to a NAMED follow-up Task gated on signal volume: the cross-touchpoint pattern detector, the parallel A/B runner against gold standards, the auto-rollback registry. Recorded in ID-104’s “deferred, gated on signal volume” register with a back-pointer to B-INV-24 (the Orchestrator opens the follow-up Task; this is a ledger coordination note, §X). Thepatterns/proposalsendpoints (T22) ship present-but-empty as the deferral anchor — NOT a silent orphan. (B-INV-24.)
Area I — ID-71 born-evaluable consumption satisfaction (B-INV-25)
Section titled “Area I — ID-71 born-evaluable consumption satisfaction (B-INV-25)”- T25 no-code-change — satisfied by T1/T2/T3/T4/T6/T18/T19/T16. ID-71’s M38/M13/M14/M15/M40
guards read: the canonical contract (T1/T2), the registry + registration gate (T3/T4), the
severity model (T6), the grounding-shape field (T2), the graduation metric (T18/T19), and the
recordAiCall()guard surface (T16). ID-71 importsAgentEvalContractfrom@/lib/eval/contractand reads all consumption fields with NO adaptation; no eval-engine internal is duplicated in ID-71. This is a satisfied-by-X record (the satisfaction is the composition of T1..T19), not a new code change. (B-INV-25.)
The canonical AgentEvalContract definition (HARD-UPSTREAM gate for ID-71)
Section titled “The canonical AgentEvalContract definition (HARD-UPSTREAM gate for ID-71)”This is the finalised, ID-104-owned canonical definition that lands in lib/eval/contract.ts
(NOT lib/eval/types.ts). ID-71’s M38 guard imports it directly. The seven mandatory fields +
optional graduation_metric are frozen by S356; OutcomeSignal (used by recordAiCall(), T14)
is co-located in the same module.
// lib/eval/contract.ts — OWNED + canonically defined by ID-104 (bottom-up).// Single source of truth; ID-71 imports `AgentEvalContract` from here (no barrel re-export).
export type TouchpointKind = | 'tool' | 'prompt' | 'skill' | 'inline' | 'agent_recipe';
export type GroundingShape = | 'structured_output' | 'forced_tool_strict' | 'citations' | 'n/a'; // B-INV-2 / ID-71 B-INV-35
export type SeverityTier = | 'block' // fails the gate (non-zero exit) | 'warn' // recorded + surfaced, does not fail the gate | 'info' // recorded only | 'infra'; // transient-provider failure (Anthropic 529 / timeout) — NOT a quality regression
/** Outcome signal captured by recordAiCall() — RATIFIED enum (sized for exactly this set, * extensible by adding members). */export type OutcomeSignal = 'win' | 'fail' | 'loop' | 'refusal';
export interface AgentEvalContract { touchpoint_id: string; // stable id: tool name | prompt name | skill | recipe slug kind: TouchpointKind; owner: string; // touchpoint registry: owner of record suite_name: string; // the eval suite this touchpoint runs under (e.g. 'l1' | 'l3' | 'l4') grounding_shape: GroundingShape; severity_on_fail: SeverityTier; variance_band: number; // per-touchpoint regression tolerance (default 0.02) graduation_metric?: string; // B-INV-19: in-house WS-5 auto-apply metric (optional, contract-addressable)}A co-exported Zod schema agentEvalContractSchema (with z.infer<typeof agentEvalContractSchema>
structurally equal to AgentEvalContract) validates registry writes at the boundary (T2).
Migration plan (staging-first; supabase/CLAUDE.md DDL discipline)
Section titled “Migration plan (staging-first; supabase/CLAUDE.md DDL discipline)”All DDL via supabase migration new + foreground db push (NEVER MCP execute_sql/
apply_migration for DDL; db push prompts interactively — run foreground). A worktree
Executor’s FIRST action is supabase link --project-ref turayklvaunphgbgscat (staging — worktrees
inherit no link state). Apply to staging first; the prod push is a GATED operator step.
After the schema change, regen types: supabase gen types typescript --project-id rovrymhhffssilaftdwd --schema public > supabase/types/database.types.ts (NEVER hand-edit).
RLS is role-based via get_user_role(). No public.*() SECURITY DEFINER functions are
introduced (the engine reads/writes via per-user sb() clients), so no REVOKE EXECUTE … FROM anon grants are required — but if a registry/rollup RPC is added during implementation,
it MUST carry SET search_path = public, extensions + an explicit REVOKE … FROM anon.
Four new tables (one migration may carry several; group by concern):
| # | Table | Purpose | Key columns | RLS |
|---|---|---|---|---|
| M1 | eval_touchpoints | registry-of-record (T3/T5/T23) | touchpoint_id PK, kind, owner, suite_name, grounding_shape, severity_on_fail, variance_band, graduation_metric, contract_version, registry_version, file_sha256 | admin read/write; get_user_role() |
| M2 | eval_runs | uniform run results (T9/T10/T13) | id, touchpoint_id FK, metrics jsonb, passed, severity_disposition, exit_class, run_at, source (nightly|ci|manual) | admin read; writer = service/runner |
| M3 | eval_baselines + eval_baseline_audit | DB-backed baseline + lifecycle (T11/T12) | baselines: (touchpoint_id, metrics jsonb, thresholds jsonb, registry_version, promoted_by, promoted_at); audit: (touchpoint_id, action, actor, registry_version, at) | admin read; promote = admin write |
| M4 | ai_call_events | recordAiCall() cost + signal (T14/T15/T17) | id, touchpoint_id, model, tier, input_tokens, output_tokens, cache_*, cost_usd, outcome_signal (enum win|fail|loop|refusal), created_at | tenant-safe + admin read; never egresses off-platform |
outcome_signal is a Postgres enum sized for exactly the ratified four
(win|fail|loop|refusal), extensible via ALTER TYPE … ADD VALUE in a future migration.
The cost-tab-stub.tsx rollup (T17) reads an aggregate over ai_call_events.
Testing and validation
Section titled “Testing and validation”Each B-INV maps to a concrete check. bun run test is the regression gate (NEVER bun test);
the MCP eval L4 layer (scripts/mcp-eval/functional-correctness.ts) remains the primary
MCP-tool verifier; behaviour-first per reference/test-philosophy.md. The eval-runner itself
is verified by a registered self-eval touchpoint.
- B-INV-1/2/5 (contract + version): unit test imports
AgentEvalContractfrom@/lib/eval/contract, asserts the seven fields +graduation_metricoptional with the ratified union types;agentEvalContractSchemarejects a missing/renamed field; a contract change advancescontract_version+registry_version. - B-INV-3/4 (registry + gate): a duplicate
touchpoint_idinsert is rejected; the runner rejects an unregistered touchpoint withnot registered: <id>and exit class 2. - B-INV-6/7/8 (severity + variance): a
blockfail → gate fail;warn/info/infra→ pass-with-recorded; a within-band drop passes, a beyond-band drop fails, an improvement of any size passes (improvement-asymmetry); an Anthropic 529 classifiedinfrais NOT a regression. - B-INV-9/10 (runner + exit): every touchpoint dispatches through
eval-runner; the three conditions map deterministically to exit0/1/2; a CI job branches on the code. - B-INV-11/12 (baseline store + lifecycle): a baseline resolves from
eval_baselines(not flat JSON); ported regression math matches the legacycheckRegressionoutputs on the same inputs;promoteBaselinewrites an audit row;baselineHistory/compareBaselinesreturn prior baselines + deltas. - B-INV-13 (nightly lane):
eval-nightly.ymlruns--allon schedule against the full registered set; results land ineval_runs+ surface on/admin/refinement. - B-INV-14/15/16/17 (cost + signal): an instrumented call writes an
ai_call_eventsrow keyed bytouchpoint_idwith one of the fouroutcome_signalvalues; rows survive restart; a network assertion confirms zero off-platform egress; the guard test fails an instrumented touchpoint omittingrecordAiCall(;cost-tab-stub.tsxreads a realai_call_eventsaggregate (no fourth surface). - B-INV-18/19 (graduation): the metric is produced by
lib/eval/graduation.tsfrom on-platform data; a network assertion confirms zero Raindrop-cloud egress; a touchpoint declaringgraduation_metricreports its current value via runner + surface. - B-INV-20/21/22 (surfaces): each surface is present + scoped to its kind;
/admin/refinementis the single registry-of-record; a non-admin request is rejected viaauthFailureResponse(auth); an admin sees the listing + version + signal counts and cancurlthe four endpoints;patterns/proposalsreturn empty-200 (present-but-empty); a network assertion confirms Raindrop Workshop (emptywriteKey) zero egress + noraindrop.aiPOST path; semantic tokens only, colour never the sole signal (WCAG 2.1 AA). - B-INV-23 (L1/L3/L4 reuse): each suite is a registered touchpoint with a bound contract
routing through the runner; a diff confirms the check logic in the three
mcp-evalfiles is unchanged from the shipped suite. - B-INV-24 (scope-phased HITL): substrate invariants (T1–T19) ship; the three deferred
organs are recorded as a named follow-up with a signal-volume gating condition + back-pointer;
patterns/proposalsendpoints present-but-empty (not absent, not built). - B-INV-25 (ID-71 consumption): ID-71’s guard imports the ID-104 contract and reads all consumption fields; no eval-engine internal is duplicated in ID-71.
Deploy + nightly-CI-lane mechanics
Section titled “Deploy + nightly-CI-lane mechanics”- Nightly lane (
.github/workflows/eval-nightly.yml, B-INV-13/T13).on: schedule: - cron(daily, off-peak UTC) +workflow_dispatch(manual). One job:supabase linkstaging →bun install→bun run scripts/eval-runner.ts --all. The runner exits 0/1/2 (T10); the job records the exit + uploads theeval_runssummary as an artefact. Mirrors themcp-eval-seedreal-API gate posture (push/schedule only; staging Anthropic + Supabase secrets; concurrency group to avoid overlap). It is NOT PR-blocking (the push-gatedmcp-evalmatrix stays the PR gate); a nightlyblockregression surfaces on/admin/refinementfor operator triage. - PR-gate wiring (
ci.yml). No new PR-blocking job at launch — the existingmcp-eval(l1/l3/l4) matrix continues to gate MCP-tool quality, now routing througheval-runner+ the contract (T23) rather than the legacysaveBaselinepath. Theeval-runner --touchpointscope flag lets a future paths-filtered PR subset run only affected touchpoints. - Type regen + schema parity. After M1–M4 land on staging + (gated) prod, regen
database.types.ts; theschema-parityside-workflow asserts prod↔staging parity; thetask-view-vendor-driftreminder does not apply (no ledger-schema change).
Risks and mitigations
Section titled “Risks and mitigations”- GitNexus index partial-staleness on the eval surface. Several
saveBaselineincoming-call UIDs returned byte-garble (RESEARCH §1.5). Mitigation: this spec’s orientation is grounded in direct source reads + grep/SQL sweeps (conclusive), not the stale graph edges; the Executor runsbun run gitnexus:analyzebefore symbol-modifying work and resolves by symbol, not line. - DB-backed baseline cutover (file-JSON → rows). Re-pointing the seven legacy
eval-*.tssuites + the L1/L3/L4 suites from__tests__/fixtures/eval-baselines/*.jsontoeval_baselinesrows risks a window where both paths exist. Mitigation: a one-shot fixture-import bootstrap (T11/T23) seeds rows from the existing JSON; the file path is removed only after every suite is re-pointed and the nightly lane is green; the portedcheckRegressionmath is unit-tested against the legacy outputs on identical inputs (parity proof). - Zero-egress assertions (Raindrop + graduation metric). A regression could silently POST
client-derived data off-platform. Mitigation: B-INV-15/18/21 network assertions in the test
suite (Workshop empty
writeKey; noraindrop.aiPOST path; graduation metric computed on-platform) — these are gating tests, not advisory. recordAiCall()instrumentation drift. An instrumented touchpoint could omit the capture call. Mitigation: the T16 grep-guard (themcp-fixture-sync.test.tsforcing-function shape) fails CI on an un-instrumented touchpoint; ID-71’s M38 extends it./admin/refinementproxy allowlist. A new admin API route accidentally added toproxy.tspublicRouteswould expose registry data. Mitigation: the four endpoints are admin-only viaauthFailureResponse(auth)and are deliberately NOT inpublicRoutes(non-admins redirect to/login); a test asserts a non-admin request is rejected.- ID-71 hard-upstream coupling. ID-71 Wave 2/3 blocks until
lib/eval/contract.tsexists. Mitigation: T1/T2 (the contract module) is the first slice; ID-71’slib/eval/types.tsplaceholder is a last-resort schedule fallback only (NOT intent — ID-71 TECH §OQ-2). - Nightly real-API cost.
--allagainst the full registered set burns real Anthropic on the L3/L4 suites nightly. Mitigation: off-peak schedule; staging-targeted; the runner’s--touchpointscope flag + the existing--skip-ai/--skip-searchskip-flag policy bound cost; watch spend during the soak.
Cross-Task coordination intents {#X}
Section titled “Cross-Task coordination intents {#X}”Orchestrator-applied ledger/coordination notes (sibling-only Subtask dep constraint honoured — these are cross-Task, so they stay at Task level, NOT bent into Subtask deps):
- ID-71 (AI tooling,
spec_needed/in-flight) ← ID-104 HARD-UPSTREAM.lib/eval/contract.ts(T1/T2) is the type ID-71’s M38 guard imports; ID-104 is sequenced BEFORE ID-71 Wave 2/3 (ID-71 TECH §OQ-2 / §X#2). ID-71 CONSUMES the contract only and MUST NOT implementeval-runner/ severity /recordAiCall()/ the contract. ID-104 also answers ID-71’s three §X#2a-c deliverable requirements: (2a) the three named surfaces (T20/T21/T22); (2b) inherit-or-scope phase-4 HITL = SCOPED-phased (T24); (2c) L1/L3/L4 = REUSED as suites (T23). - Deferred HITL organs follow-up (T24 / B-INV-24) — NAMED, gated on signal volume. The
Orchestrator opens a follow-up Task for the cross-touchpoint pattern detector + parallel A/B
runner + auto-rollback registry, gated on
ai_call_eventsaccumulating signal; thepatterns/proposalsendpoints (T22) are its present-but-empty anchor. Recorded in the “deferred, gated on signal volume” register with a back-pointer to B-INV-24. cost-tab-stub.tsxrollup (T17). Re-pointing the interimpipeline_runs.costread toai_call_eventstouches aprovenancecomponent; coordinate with any in-flight provenance work so the slot is not double-owned.
B-INV → Proposed-change mapping (orphan-check) {#M}
Section titled “B-INV → Proposed-change mapping (orphan-check) {#M}”1:1 mapping; every B-INV has a change (or an explicit “no code change — satisfied by X”). No B-INV without a change; no change without a B-INV.
| B-INV | Proposed change | Notes |
|---|---|---|
| B-INV-1 | T1 | canonical AgentEvalContract in lib/eval/contract.ts (single source of truth) |
| B-INV-2 | T2 | seven mandatory fields + optional graduation_metric; Zod schema; ratified unions |
| B-INV-3 | T3 | eval_touchpoints registry-of-record (M1); duplicate touchpoint_id rejected |
| B-INV-4 | T4 | registration-as-gate; unregistered touchpoint rejected (exit 2) |
| B-INV-5 | T5 | contract_version/registry_version advance on contract change |
| B-INV-6 | T6 | canonical 4-tier block|warn|info|infra; infra ≠ regression |
| B-INV-7 | T7 | lib/eval/severity.ts maps severity → exit disposition |
| B-INV-8 | T8 | ported checkRegression semantics; variance_band default 0.02; improvement-asymmetry |
| B-INV-9 | T9 | single central scripts/eval-runner.ts; replaces per-suite main()+saveBaseline |
| B-INV-10 | T10 | deterministic exit 0/1/2 (quality-fail vs infra-error distinct) |
| B-INV-11 | T11 | DB-backed eval_baselines (M3); ported regression math; flat JSON retired |
| B-INV-12 | T12 | promoteBaseline/baselineHistory/compareBaselines + eval_baseline_audit |
| B-INV-13 | T13 | eval-nightly.yml cron lane runs --all registered set; distinct from PR gate |
| B-INV-14 | T14 | recordAiCall() single capture; outcome_signal enum win|fail|loop|refusal |
| B-INV-15 | T15 | ai_call_events (M4) persisted, tenant-safe, zero off-platform egress |
| B-INV-16 | T16 | greppable recordAiCall( guard test (ID-71 M38 extends) |
| B-INV-17 | T17 | rollup folds into cost-tab-stub.tsx; no fourth surface |
| B-INV-18 | T18 | in-house lib/eval/graduation.ts; zero Raindrop-cloud egress assertion |
| B-INV-19 | T19 | contract-addressable graduation_metric; runner/surface report value |
| B-INV-20 | T20 | three surfaces layered by kind; KH /admin/refinement = single registry-of-record |
| B-INV-21 | T21 | Raindrop Workshop-local (empty writeKey, zero egress); hosted raindrop.ai excluded |
| B-INV-22 | T22 | /admin/refinement stub-spine; four curl-able endpoints; patterns/proposals present-but-empty |
| B-INV-23 | T23 | 96 L1/L3/L4 checks REUSED as touchpoints; check logic unchanged |
| B-INV-24 | T24 | substrate IN; pattern-detector/A-B-runner/auto-rollback DEFERRED-with-anchor (§X#2) |
| B-INV-25 | T25 | no code change — satisfied by T1/T2/T3/T4/T6/T18/T19/T16 (ID-71 imports contract, no duplication) |
Coverage: all 25 invariants mapped; no orphan invariants, no orphan changes. ONE
“no code change — satisfied by X” entry (B-INV-25, composed of T1..T19). Net-new tables: M1
eval_touchpoints, M2 eval_runs, M3 eval_baselines+eval_baseline_audit, M4
ai_call_events. The 1:1 B-INV↔change coverage is preserved across the ratified PRODUCT.
Open questions for Liam ratification
Section titled “Open questions for Liam ratification”The big decisions are SETTLED (S356); only genuinely new TECH-level questions are raised. One, and it is non-blocking (a sensible default is stated and used):
- (T2 /
file_sha256bottom-up field) — RESEARCH §2b notes git-backed touchpoints (skills/ prompts) benefit from afile_sha256on the registry row to detect drift (the deferred pattern-detector’s eventual input). This spec includesfile_sha256as an OPTIONAL, nullable registry column (M1) — present-but-unenforced at launch (no producer populates it until the deferred organs land). Confirm this is the right launch posture (column present so the follow-up needs no migration), versus omitting it until the follow-up. Default taken: include it nullable — adding a column later is a migration; a present-nullable column is free and avoids a future schema change on the registry-of-record table. No other OQs.
End of {104.3} TECH. The canonical AgentEvalContract (§Contract, lib/eval/contract.ts) is
the HARD-UPSTREAM gate that unblocks ID-71 Wave 2/3 (ID-71 TECH §OQ-2). 25 B-INV → 25 T-changes,
1:1 in §M (one “no code change — satisfied by X”: B-INV-25). Four new tables (M1 eval_touchpoints,
M2 eval_runs, M3 eval_baselines+audit, M4 ai_call_events with the ratified win|fail|loop|refusal
enum), staging-first per supabase/CLAUDE.md. Nightly lane eval-nightly.yml runs the full
registered set; the PR gate stays the mcp-eval matrix (now routed through eval-runner+contract).
One non-blocking OQ (file_sha256 nullable column — default taken). Feeds {104.4} PLAN decomposition.