feat(runner): decide the repair context budget before #27 spends tokens on it - #150
Conversation
…ns on it Closes #125. ADR-0012. The repair model was given a URL, a page title, and a list of ARIA landmark role names -- then asked to produce a corrected locator, which is a role plus an accessible name. The context did not contain the shape of the answer. That is not a hard task, it is an underdetermined one, and it mattered before #27 rather than after: on a cache miss the system pays replay + repair + the fresh run it still owes, and PRD section 9's kill line is a RATIO. A repair that burns tokens at a structurally near-zero rate pushes the project toward FAIL for a reason that is a design parameter rather than a property of the thesis. An unforced FAIL is as misleading as an unearned PASS. Three levels: landmarks (the old floor, kept for baselining), interactive (chosen), tree (available). A fourth -- a DOM excerpt -- is REJECTED in the ADR rather than left unimplemented-but-blessed, because it is the first level whose output cannot be reviewed by reading a fixed field list. Measured on live Grafana 9.5.21 rather than argued. On /dashboard/new the identifying string of three of the live bundle's targets is present at interactive and absent at landmarks; tree found no target interactive did not, at 19-63% more payload. So landmarks cannot succeed for reasons unrelated to the model, and tree buys nothing here. What is NOT measured, and the ADR says so plainly, is whether a model succeeds at either -- that needs #27, and the ADR does not claim it. No level reads an input's value. Pinned behaviourally (a filled password field, a filled text input and a placeholder, one canary across all three) and structurally (the source string handed to the browser has no route to a value). serializeRepairContext() is the only authorized egress, built by naming fields rather than deleting them from a copy, so a field added to RepairContext next year is invisible until someone adds a line a reviewer sees. params is excluded -- that is the bag secrets live in -- along with assertion.expected, param_refs, and error_message, which reads like diagnostics but can quote page content. tests/canary/repair-egress.test.ts is merge-blocking and covers both directions, including that no client serializes the raw context around the boundary. repair_context_level goes on the run row: a self-heal rate is not reproducible without knowing what the model could see. #74's guard caught a real mistake mid-change. My first cut copied the visibility predicate into the new capture site -- exactly the bug that test exists to prevent. The predicate is now extracted as VISIBILITY_PREDICATE_JS and shared, so there is still one carrier under src/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review — approving and mergingFull CI green, targets Merging. Four findings below, none blocking; the second is the one I'd actually fix. 1.
|
myselfsiddharth
left a comment
There was a problem hiding this comment.
Approving — full CI green, ADR separates measured from argued, and the counter-check tests make the canaries non-vacuous. Four non-blocking findings in the review comment above; the run-row/payload context_level disagreement is the one worth a follow-up.
…ting Closes #27 except its live exit criterion (see below). Stacked on #150. StubRepairModelClient proposes null and reports zero tokens, so self-heal rate is structurally 0 and cost_repair structurally zero -- blocking two PRD section 9 metrics outright, one of which is a ratio with a kill line at 70%. AnthropicRepairModelClient sees only serializeRepairContext()'s output (ADR-0012, #150). It never touches RepairContext, which carries params -- the runtime bindings with secrets in them. A client trusted to pick the safe fields itself is a convention, not a boundary. Opt-in throughout: the stub stays the default, so npm run ci, dry runs and every existing path make no network call and spend nothing. Enabling it is `gate:matrix --repair-model <m>`, and the client throws at construction when ANTHROPIC_API_KEY is unset rather than degrading -- a run that silently used the stub would report a self-heal rate of 0 that looks measured. Prompt caching is deliberately OFF. cache_read_input_tokens and cache_creation_input_tokens bill differently from plain input, and a repair cost that quietly excluded cache writes would understate against the 70% line. All four fields are summed anyway, so enabling caching later cannot silently change what the number means. Failure paths report the tokens they burned. A refusal or a network error returns corrected_action: null WITH the consumed tokens, never zero: a failure path reporting free repair understates against the same kill line. Never retried silently -- a hidden retry hides cost. stop_reason is checked before reading content, because a decline is HTTP 200 with possibly empty content. A proposal carrying an assertion is dropped whole, not merged. The output schema offers no assertion field at all, so the ask is never made; assertAssertionUnchanged remains the runtime guard. 21 unit tests, SDK injected, no network. Guard-proven: dropping cache tokens from the billed input fails 1, merging a tampering proposal fails 4, reporting zero tokens on a refusal fails 1. NOT DONE, and #27's stated exit criterion: no live repair has been observed. That needs a real key and spends money. Self-heal rate stays structurally 0 until someone runs it, and this repo does not fabricate a metric row. Adds @anthropic-ai/sdk as the first runtime dependency besides playwright; npm audit --omit=dev reports 0 vulnerabilities. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ting (#151) * feat(runner): real Anthropic repair client, opt-in, with token accounting Closes #27 except its live exit criterion (see below). Stacked on #150. StubRepairModelClient proposes null and reports zero tokens, so self-heal rate is structurally 0 and cost_repair structurally zero -- blocking two PRD section 9 metrics outright, one of which is a ratio with a kill line at 70%. AnthropicRepairModelClient sees only serializeRepairContext()'s output (ADR-0012, #150). It never touches RepairContext, which carries params -- the runtime bindings with secrets in them. A client trusted to pick the safe fields itself is a convention, not a boundary. Opt-in throughout: the stub stays the default, so npm run ci, dry runs and every existing path make no network call and spend nothing. Enabling it is `gate:matrix --repair-model <m>`, and the client throws at construction when ANTHROPIC_API_KEY is unset rather than degrading -- a run that silently used the stub would report a self-heal rate of 0 that looks measured. Prompt caching is deliberately OFF. cache_read_input_tokens and cache_creation_input_tokens bill differently from plain input, and a repair cost that quietly excluded cache writes would understate against the 70% line. All four fields are summed anyway, so enabling caching later cannot silently change what the number means. Failure paths report the tokens they burned. A refusal or a network error returns corrected_action: null WITH the consumed tokens, never zero: a failure path reporting free repair understates against the same kill line. Never retried silently -- a hidden retry hides cost. stop_reason is checked before reading content, because a decline is HTTP 200 with possibly empty content. A proposal carrying an assertion is dropped whole, not merged. The output schema offers no assertion field at all, so the ask is never made; assertAssertionUnchanged remains the runtime guard. 21 unit tests, SDK injected, no network. Guard-proven: dropping cache tokens from the billed input fails 1, merging a tampering proposal fails 4, reporting zero tokens on a refusal fails 1. NOT DONE, and #27's stated exit criterion: no live repair has been observed. That needs a real key and spends money. Self-heal rate stays structurally 0 until someone runs it, and this repo does not fabricate a metric row. Adds @anthropic-ai/sdk as the first runtime dependency besides playwright; npm audit --omit=dev reports 0 vulnerabilities. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(runner): type the repair request instead of casting it away Review follow-ups on #151. `as never` was applied to the whole `messages.create` request to work around one narrow mismatch — `effort` typed as `string` against the SDK's union — and took `output_config`, `messages`, `system`, and `max_tokens` out of the checker with it. Since no live call has been observed, the compiler is currently the only thing between a malformed request and the first run that spends money. Narrowing `effort` to the SDK's own `OutputConfig["effort"]` lets the cast go entirely; `"maximum"` now fails at build rather than at the API (verified by sabotage: TS2322). Also drops `minimum: 0` from `REPAIR_OUTPUT_SCHEMA`. Numerical constraints are not supported by structured outputs, and the schema is compiled server-side on first use — a rejection would land on exactly the paid call this is saving up for. `wait_ms` is optional and unvalidated by `sanitizeProposedAction` anyway. Documents two decisions that were made but not written down: `max_tokens` caps adaptive thinking and response text together, and server-side `fallbacks` are omitted on purpose so `model_id` keeps naming the model that was actually billed. package-lock.json reconciles with main, which promoted ajv/ajv-formats to runtime deps after this branch was cut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: myselfsiddharth <siddharthmehta0906@gmail.com>
Closes #125. ADR-0012.
The repair model was given a URL, a page title, and
["main", "navigation"]— then asked to produce a corrected locator, which is a role plus an accessible name. The context did not contain the shape of the answer.That is not a hard task, it is an underdetermined one, and #125's argument for why it had to be settled before #27 is correct: on a cache miss the system pays replay plus repair plus the fresh run it still owes, and PRD §9's kill line is a ratio. A repair that burns tokens at a structurally near-zero rate pushes the project toward FAIL for a reason that is a design parameter rather than a property of the thesis. An unforced FAIL is as misleading as an unearned PASS.
Measured, not argued
Live Grafana 9.5.21, capturing at each level and checking whether the identifying string of the live bundle's step targets is present:
//dashboard/newAdd new panel,Save dashboard,DashboardsTwo things follow, and the ADR claims only these two: at
landmarksthe answer is absent, so repair cannot succeed there for reasons unrelated to the model; andtreefound nothinginteractivedid not, at 19–63% more payload.What is not measured — and the ADR says so in the decision, not a footnote — is whether a model actually succeeds at either level. That needs #27. #125's protocol (break a locator, n≥3 per level, report the spread) is the right shape and is recorded as the open question.
I also corrected two claims I'd written into the code docstring before measuring: an unsupported "0 of 11" and an invented "~4x payload". The
landmarksresult is now stated as true by construction —elementsis empty — which is a property of the design, not a measurement, and dressing it up as data would have been the exact failure the audit's category B exists for.Levels, and the one that was rejected
landmarks(the old floor, kept for baselining) ·interactive(chosen) ·tree(available). A fourth — a DOM excerpt with allowlisted attributes — is rejected in the ADR rather than left unimplemented-but-blessed: it is the first level whose output cannot be reviewed by reading a fixed field list. The others emit a closed set of fields; a DOM excerpt emits whatever the page contains, filtered, and correctness then rests on the filter being exhaustive against markup nobody has seen.No level reads an input's value
Pinned two ways, because one would not be enough:
Guard-proven: making the capture emit
el.valuefails 3 cases.The egress boundary
serializeRepairContext()is the only authorized shape, built by naming fields rather than deleting them from a copy — so a field added toRepairContextnext year is invisible to the model until someone adds a line, and that line is a diff a reviewer sees.Excluded:
params(the bag secrets live in),assertion.expected,param_refs, anderror_message— that last is the one worth naming, because it reads like diagnostics and is what a well-meaning change adds first, but a Playwright locator error can quote the resolved selector and surrounding text.tests/canary/repair-egress.test.tsis merge-blocking and covers both directions: nothing unauthorized in the payload, and no client in the tree serializing the raw context around the boundary. Guard-proven — leakingparamsfails 3, passing the raw text locator fails 2.repair_context_levelgoes on the run row, written only when a repair actually ran. A self-heal rate is not reproducible without knowing what the model could see.#74's guard caught me mid-change
My first cut copied the visibility predicate into the new capture site — exactly the bug
tests/unit/landmarks.test.tsexists to prevent, and it failed immediately. The predicate is now extracted asVISIBILITY_PREDICATE_JSand interpolated into both sites, so there is still one carrier undersrc/.It then caught the fix's own docstring for spelling the DOM API name in prose. I reworded the comment rather than loosening the scan — narrow, but the narrowness is what makes it hold.
Third time this session a repo guard has caught something a passing suite hid, and the third time
secret-scan'senv-assignmentpattern has been tripped by a test constant named the obvious thing. That one is noted in the test file for the next person.Scope
The repair proposal is unchanged —
corrected_actiononly,assertAssertionUnchangeduntouched. Richer input is not a licence to touch the assertion.boundary-spec.mdgains a section distinguishing the two exits: it governs what enters the pool, ADR-0012 governs what is sent to a model. Neither supersedes the other. Open and shared with #126: an accessible name is page-authored — vendor vocabulary on pinned open-source software, possibly tenant strings on a closed portal.🤖 Generated with Claude Code