|
| 1 | +# ADR-0038: Build Verification Loop — the agent builds, verifies, and corrects itself |
| 2 | + |
| 3 | +**Status**: Proposed (2026-06-11) |
| 4 | +**Deciders**: ObjectStack Protocol Architects |
| 5 | +**Builds on**: [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (drafts as the staging layer — this ADR **replaces its human-approval assumption for AI builds** with a machine gate; HITL stays for destructive actions), [ADR-0021](./0021-analytics-dataset-semantic-layer.md) (datasets — what most verification probes exercise), ADR-0037 / [framework#1694](https://github.com/objectstack-ai/framework/pull/1694) (Live Canvas — the *human-visibility* complement to this ADR's *machine-verification*) |
| 6 | +**Consumers**: `@objectstack/service-analytics` + `@objectstack/objectql` (verification probes), `../cloud/service-ai-studio` (graph lint, `verify_build` tool, self-correction protocol, eval intelligence), `../objectui` (build health card in chat), `ai_eval_cases`/`ai_eval_runs` (existing, currently-unused storage) |
| 7 | + |
| 8 | +**Premise**: pre-launch, no back-compat debt — specify the target end-state directly. |
| 9 | + |
| 10 | +**Design center**: **never make correctness depend on a human looking.** Humans are the laziest component in the system — they will not review, and the magic moment auto-publishes before they could. The agent that builds an app must be the same loop that verifies it and corrects it; a human is *informed* of the outcome, never *required* for it. The only reviewer that scales with AI-speed authorship is the machine. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## TL;DR |
| 15 | + |
| 16 | +**The problem, measured.** In one day of live verification (2026-06-10/11), six agent-authored defects shipped to a staging tenant — every one of them **passed schema validation** (`_diagnostics: valid`) and every one was found by a *human manually browsing*: |
| 17 | + |
| 18 | +| # | Defect | Why validation missed it | |
| 19 | +|---|---|---| |
| 20 | +| 1 | Dashboard widgets bound a `dataset` that didn't exist | reference *between* artifacts — single-artifact Zod can't see it | |
| 21 | +| 2 | Widget `values:["amount"]` matched no measure name in the dataset | cross-artifact name agreement | |
| 22 | +| 3 | Seed staged but rows never materialized on publish | runtime effect, invisible to schema | |
| 23 | +| 4 | Dataset queries returned 0/empty on populated objects (4 stacked infra bugs) | only a *real query* reveals it | |
| 24 | +| 5 | "Published!" while sample data silently failed to load | result-reporting gap | |
| 25 | +| 6 | View metadata `type:'list'` rendered as a red "Unknown component type" box | renderability is a *renderer* contract, not a schema | |
| 26 | + |
| 27 | +Schema-valid ≠ renders ≠ returns data ≠ matches intent. Each is a separate verification plane, and today only the first exists. |
| 28 | + |
| 29 | +**Decision.** Ship a five-layer **Build Verification Loop (BVL)** that runs *inside* the build turn and *after* publish, feeding every failure back to the agent for bounded self-correction: |
| 30 | + |
| 31 | +- **L1 Graph lint** (draft-time, deterministic): cross-artifact reference resolution over the staged set. |
| 32 | +- **L2 Renderability check** (pre-publish, deterministic): every artifact's renderer translation produces a *registered, typed* schema; every dataset compiles. |
| 33 | +- **L3 Runtime probes** (post-publish): row counts per seeded object; one real query per widget; generalizes the `seedApplied` pattern. |
| 34 | +- **L4 Self-correction protocol**: L1–L3 results are returned **to the agent** (`issues[]` in tool envelopes + a `verify_build` tool); the agent fixes and re-verifies, bounded retries; the chat shows a build-health card, not a plea for review. |
| 35 | +- **L5 Agent CI**: golden-prompt eval suite run headlessly per deploy/nightly against an ephemeral environment, persisted in the existing `ai_eval_cases`/`ai_eval_runs` objects. |
| 36 | + |
| 37 | +**Gate semantics change**: for AI whole-app builds, **the verification loop replaces the human approval gate**. A build auto-publishes only when L1+L2 pass; L3 failures trigger self-correction; an unrecoverable build *stays draft* and says so honestly. HITL approval remains **only** for destructive/irreversible actions (ADR-0033 pending-actions) — that is safety, not quality review. |
| 38 | + |
| 39 | +**Open-core boundary**: verification *mechanisms* (graph resolution, render contracts, query probes, eval storage) are open framework; *what to verify and how to judge intent* (lint rule packs, golden prompts, the LLM intent-review) is cloud intelligence. |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Context |
| 44 | + |
| 45 | +### Why the existing defenses don't compose into a loop |
| 46 | + |
| 47 | +| Defense (exists today) | Catches | Systemic gap | |
| 48 | +|---|---|---| |
| 49 | +| Per-type Zod at `stageDraft` (ADR-0033) | malformed single artifacts | all six defects were single-artifact-valid | |
| 50 | +| Draft gate + human Publish | nothing in practice | magic moment auto-publishes; humans don't review | |
| 51 | +| propose → confirm → apply | wrong *plan* | confirms intent, not product quality | |
| 52 | +| Deterministic normalization (dataset auto-create, widget-ref derivation, viewType→grid; shipped 2026-06-10/11) | makes *known* mistakes impossible | reactive: one fix per discovered failure mode | |
| 53 | +| `seedApplied` reporting | seed materialization failures | one probe, one artifact type — the pattern, not the system | |
| 54 | +| `ai_eval_cases` / `ai_eval_runs` objects | — | empty skeleton, wired to nothing | |
| 55 | + |
| 56 | +The strongest defense — deterministic normalization — should remain the first resort ("make the mistake impossible"). The BVL is for the unbounded remainder: an agent is a generator of *novel* mistakes, so the system needs a *general* verifier, not an ever-growing list of special cases. |
| 57 | + |
| 58 | +### The correction loop already works — it's just manual |
| 59 | + |
| 60 | +The live incident that motivates L4: a human told the agent *"the Spending Dashboard shows Dataset 'expense' not found — fix it"*, and the agent diagnosed, created the missing dataset with exactly-referenced measure names, and offered Publish — correctly, in one turn. The agent's repair capability is not the gap. **The gap is that a human had to be the error transport.** The BVL is, at its core, replacing that human with the build pipeline. |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +## Decision |
| 65 | + |
| 66 | +### The verification contract |
| 67 | + |
| 68 | +Every layer emits the same shape, so the agent, the chat UI, and the eval harness consume one stream: |
| 69 | + |
| 70 | +```ts |
| 71 | +interface BuildIssue { |
| 72 | + layer: 'graph' | 'render' | 'runtime' | 'intent'; |
| 73 | + severity: 'error' | 'warning'; |
| 74 | + artifact: { type: string; name: string }; // what is broken |
| 75 | + ref?: { type: string; name: string; member?: string }; // what it points at |
| 76 | + code: string; // e.g. 'dangling_dataset', 'unknown_measure', 'typeless_schema', |
| 77 | + // 'empty_query', 'seed_not_applied', 'intent_mismatch' |
| 78 | + message: string; // agent-actionable, names the exact artifact + member |
| 79 | + fix?: string; // machine hint, e.g. 'create dataset "expense" with measure "amount"' |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +`issues[]` is carried (a) in every authoring tool's result envelope, (b) in the `verify_build` tool result, (c) on the build-health card in chat, (d) in `ai_eval_runs` rows. |
| 84 | + |
| 85 | +### L1 — Graph lint at draft time (cloud `service-ai-studio`, deterministic) |
| 86 | + |
| 87 | +After `apply_blueprint` / `create_metadata` stage their drafts and **before** the envelope returns, resolve every cross-artifact reference over the *draft-overlaid* registry (`previewDrafts` reads): |
| 88 | + |
| 89 | +- `widget.dataset` exists; every `values[]` name ∈ dataset measures; every `dimensions[]` name ∈ dataset dimensions; |
| 90 | +- `view.objectName`/`object` exists; `fields[]` ⊆ object fields; kanban `groupField` exists and is a select; |
| 91 | +- `app` navigation targets (object/dashboard/view) all exist; |
| 92 | +- `seed.object` exists; record keys ⊆ object fields; lookup/external-id references resolvable within the staged set; |
| 93 | +- `dataset.object` exists; measure/dimension `field`s exist on it. |
| 94 | + |
| 95 | +Violations return as `issues[]` **in the same tool result**, so the agent sees them in the turn that caused them and fixes before the user ever could. Incidents #1 and #2 die here. |
| 96 | + |
| 97 | +### L2 — Renderability check, pre-publish (framework + objectui contract) |
| 98 | + |
| 99 | +"Will this artifact mount, or red-box?" is decidable without a browser because both halves are deterministic: |
| 100 | + |
| 101 | +- the **view → component schema** translation and the **component registry** (the `'list'`→typeless-schema bug was exactly this contract breaking) — export the translation as a pure function and ship the registry's type list as data, so the check runs server-side; |
| 102 | +- **dataset compilation** (`compileDataset`) — compile every drafted dataset; compile errors are issues; |
| 103 | +- dashboards: every widget translates to a registered type with a satisfiable query shape. |
| 104 | + |
| 105 | +Incident #6 dies here. (The renderer keeps its own ErrorBoundary fallbacks — defense in depth, not the primary net.) |
| 106 | + |
| 107 | +### L3 — Runtime probes, post-publish (framework mechanisms, generalizing `seedApplied`) |
| 108 | + |
| 109 | +Immediately after an auto-publish, the build pipeline (not the user) exercises the published app: |
| 110 | + |
| 111 | +- **per seeded object**: row count > 0 (else issue `seed_not_applied`, carrying the existing `seedApplied` error detail); |
| 112 | +- **per dashboard widget**: execute its real dataset selection once (the same `/analytics/dataset/query` path users hit); empty-on-populated-object or error → issue `empty_query` with the compiled SQL/strategy detail; |
| 113 | +- **per view**: a `limit 1` list read through the same governed path. |
| 114 | + |
| 115 | +Incidents #3, #4, #5 die here — they were all invisible until something *actually queried*. Probe results attach to the publish response (like `seedApplied` today) and flow into the same `issues[]` stream. |
| 116 | + |
| 117 | +### L4 — Self-correction protocol (cloud, the loop itself) |
| 118 | + |
| 119 | +- A `verify_build` tool (cloud service-ai-studio) runs L1+L2 on demand and L3 when the build is published; the system prompt instructs the agent: **after building, verify; if issues, fix and verify again** — at most N rounds (default 2), then stop and report honestly. |
| 120 | +- The **auto-publish gate becomes machine-conditional**: publish fires only when L1+L2 are clean. A failing build stays draft, the agent attempts repair; if still failing, the chat shows the health card with remaining issues and a Review affordance. *No silent broken publishes, no waiting on a human either.* |
| 121 | +- An **intent review** (cloud intelligence, cheap model): after mechanical verification passes, one LLM pass judges the built app against the user's original goal — nav coherent, labels sensible, fields match the domain, dashboards answer the asked questions. Output: `intent` issues (warnings, non-blocking by default). This is the "AI reviews itself" half that no deterministic check covers. |
| 122 | +- The chat renders one **build-health card**: ✓ structure, ✓ renders, ✓ 12 rows, ✓ 3/3 widgets return data, ⚠ warnings — replacing both the silent success and the human-review plea. |
| 123 | + |
| 124 | +### L5 — Agent CI: the golden-prompt eval suite (cloud, uses the existing skeleton) |
| 125 | + |
| 126 | +- `ai_eval_cases`: golden prompts ("build an expense tracker with a spending dashboard", "build a recruiting app", …) each with machine-checkable assertions: app exists; objects/views/dashboards present; row counts > 0; every widget query returns rows; zero render-error boxes (headless DOM sweep — the same browser automation + `mint-session.mjs` used to find this week's bugs). |
| 127 | +- Runs headlessly against an **ephemeral environment** per cloud deploy (and nightly), recording to `ai_eval_runs`. A red run blocks nothing initially (report-only), then graduates to a deploy gate once stable. |
| 128 | +- Every production incident becomes a new eval case — the suite is the immune system's memory: none of this week's six defects can recur silently once encoded. |
| 129 | + |
| 130 | +### Sequencing |
| 131 | + |
| 132 | +| Phase | Scope | Effort | |
| 133 | +|---|---|---| |
| 134 | +| 1 | L1 graph lint + envelope `issues[]` + L4 wiring (verify-fix-reverify, gate-on-clean) | ~1 week, cloud | |
| 135 | +| 2 | L3 runtime probes + build-health card in chat | ~3–4 days, framework + objectui | |
| 136 | +| 3 | L2 renderability contract | ~3–5 days, framework + objectui | |
| 137 | +| 4 | L5 eval suite on `ai_eval_*` + intent review | ~1 week, cloud | |
| 138 | + |
| 139 | +Each phase is independently shippable; Phase 1 alone converts this week's discovery latency from *human-hours* to *same-turn*. |
| 140 | + |
| 141 | +## Non-goals |
| 142 | + |
| 143 | +- **Not** removing HITL for destructive/irreversible actions — ADR-0033's pending-action approvals remain; that is safety, not quality review. |
| 144 | +- **Not** a general-purpose test runner for user code — the BVL verifies *agent-authored metadata and its runtime behavior*, nothing else. |
| 145 | +- **Not** a replacement for deterministic normalization — "make the mistake impossible" stays the first resort; the BVL catches what normalization hasn't met yet. |
| 146 | + |
| 147 | +## Risks |
| 148 | + |
| 149 | +| Risk | Mitigation | |
| 150 | +|---|---| |
| 151 | +| Verification latency inflates the magic moment | L1/L2 are in-memory and sub-second; L3 runs post-publish in parallel with the user exploring; probes are `limit 1`/single-aggregate | |
| 152 | +| Self-correction loops forever | bounded rounds (2), then honest surface | |
| 153 | +| Probes mutate state | all probes are reads; seeds are upserts keyed on externalId (idempotent) | |
| 154 | +| Eval env drift vs prod | eval runs on the same image staging runs; ephemeral env per run | |
| 155 | +| LLM intent-review cost | cheap model, single pass, warnings-only | |
0 commit comments