feat(cache): add a pinned-version vocabulary rule to the pool allowlist - #159
Conversation
Issue #126: on the only real compiled bundle in the repo, the pool captures 1 of 12 rows. Measured properly (docs/gate/pool-vocabulary.md) instead of retyping that number: the authoritative write path (src/cache/write.ts, not the compiler's stricter pre-check) already puts 7 of 12 in the pool today, before this change. The remaining five are blocked by the literal_in_assertion rule on url-matches assertions, and the recorder blanket-tags every role_name/label/text candidate tenant_scoped:true independent of content — both out of this PR's scope and already flagged elsewhere in the repo. So the vocabulary rule this PR adds (src/cache/vocabulary.ts, a committed snapshot of strings independently verified against the public grafana/grafana repo at tag v9.5.21, wired additively into taint.ts) measures zero row-level yield change on that bundle. It has real, demonstrated effect on repair-proposed locators (ADR-0009), which carry no blanket tenant_scoped tag, and on the bundle's one non-tagged locator (a testid). ADR-0017 records the decision to ship it anyway, as a genuine but currently narrow mechanism, rather than either overclaiming a yield win or concluding pooling doesn't work. Collision safety (a tenant string byte-identical to a vocabulary entry must still be refused) uses the existing tenant_scoped signal and caller_marked_tenant rule, unconditionally ahead of any vocabulary match — proven with a mutation test in tests/canary/vocabulary.test.ts, merge-blocking. Multi-version yield (ADR-0003's 8 pinned versions) is no_data: this environment has no running Docker daemon to record the other seven. docs/pitch/ claims resting on pooling as a realized network effect are annotated with the measured number, not deleted — the mechanism holds, the yield claim does not yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review of PR #157, both fixes propagating ADR-0004 / C5 DECISION.md, no new claims: - The Ask still asked for support to "finish Track 2 adjudication" in three places (deck slide 21, one-pager §Ask, proof-points F11). C5 adjudicated on 2026-07-25 and ADR-0004 records the vertical lock as closed, so the ask requested work that is already done — and one-pager §Kill criteria ("Track 2: already FAIL") contradicted its own §Ask two sections later. Narrowed the ask to the Track-1 gate number and cited the closure. - Deck slide 18 / proof-points E1 still pitched A7 seller-side portal fill as "a conditionally credible wedge" with no C4/C5 citation — which is INTEGRITY-AUDIT D-01 verbatim ("residual deck slides may still lag", Critical if used without a C5 cite), the row this PR's proof-points edit claims to close. narrative.md and objections §8 already say C4 ALREADY_SOLVED (QAuto) + C5 FAIL close that wedge; slide 18 and E1 now say the same, cited. Left alone: F4/F6 and the moat prose owned by #126/#159; docs/pitch/ README.md's "Update narrative after C5" (author's documented follow-up). lint:docs clean (57 docs); secret-scan clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(pitch): reconcile pitch pack with Track-2 FAIL verdict narrative.md, deck-outline.md, and objections.md still opened with "search in progress" / "TBD" / "pending C5" — stale relative to the already-accepted verdict (both docs' own status tables already said FAIL). proof-points.md had four register rows citing "C5 pending" instead of the settled FAIL. This reconciles all of it against ADR-0004 and DECISION.md: no invented numbers, just propagating a decision that had already landed elsewhere in the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(pitch): clear three residual C5-pending lines the reconciliation missed Review follow-ups on top of the #34 pass, all in the five named pitch files: - narrative.md status table row 3 still said "finalize after C5"; C5 adjudicated 2026-07-25 and there is no lock to finalize. - proof-points.md D7 still warned "D1 objections text may still deny this", which contradicts the same PR's G5 row and objections §7. - deck-outline.md open question cited §Surface scorecard for the "because A7 said so" quote, which lives in §Specific next action → Explicit non-goals. lint:docs clean (57 docs); secret-scan clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(pitch): close the two stale-status residues the #34 pass missed Review of PR #157, both fixes propagating ADR-0004 / C5 DECISION.md, no new claims: - The Ask still asked for support to "finish Track 2 adjudication" in three places (deck slide 21, one-pager §Ask, proof-points F11). C5 adjudicated on 2026-07-25 and ADR-0004 records the vertical lock as closed, so the ask requested work that is already done — and one-pager §Kill criteria ("Track 2: already FAIL") contradicted its own §Ask two sections later. Narrowed the ask to the Track-1 gate number and cited the closure. - Deck slide 18 / proof-points E1 still pitched A7 seller-side portal fill as "a conditionally credible wedge" with no C4/C5 citation — which is INTEGRITY-AUDIT D-01 verbatim ("residual deck slides may still lag", Critical if used without a C5 cite), the row this PR's proof-points edit claims to close. narrative.md and objections §8 already say C4 ALREADY_SOLVED (QAuto) + C5 FAIL close that wedge; slide 18 and E1 now say the same, cited. Left alone: F4/F6 and the moat prose owned by #126/#159; docs/pitch/ README.md's "Update narrative after C5" (author's documented follow-up). lint:docs clean (57 docs); secret-scan clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Review verification against https://raw.githubusercontent.com/grafana/grafana/v9.5.21/packages/grafana-e2e-selectors/src/selectors/components.ts (accessed 2026-08-12) puts `applyButton: 'data-testid Apply changes and go back to dashboard'` on line 138, not 137. Line 137 is the closing brace of the preceding `DataPane` block; the neighbouring `toggleVizPicker` citation at line 139 was already correct. Citation-only fix: the primary source (PanelEditor.tsx:362) and the snapshot string itself are unchanged, and all four other entries verified exact. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review — independently verified, not taken on faithReviewed at 1. The 1/12 vs 7/12 claim — verified, the PR is right and the issue is wrongI wrote my own measurement script (deliberately not reusing the Line B is the load-bearing one: re-running These are genuinely different code paths, and I confirmed why they disagree rather than just observing that they do:
So on the six rows (steps 2, 3, 4, 5, 8, 9) whose 2. "The vocabulary rule changes that by exactly zero" — verified, both reasons check outSame script at the PR head: still Both stated causes are real, and I checked them at the source rather than accepting the writeup: (a) (b) Recorder blanket-tagging — the PR says this was confirmed in the source trajectory; I confirmed it in the recorder source, which is stronger. The narrower claims hold too:
I'll say plainly that I went in expecting this section to be a rationalization for shipping something that doesn't work. It isn't. The measurement is honest, the culprits named are the actual culprits, and the reasoning for shipping anyway — that killing the mechanism over a measurement indicting two other subsystems would be the worse error — is sound. Reporting a zero delta on your own PR is the right behaviour and I'd rather see more of it. 3. Sourcing — 4 of 5 exact; 1 off-by-one found and fixedI fetched all cited files from
One nuance worth recording: the PR excludes 4. Collision guarantee — genuine, and it survives harder cases than the canary testsI read I then probed cases the canary does not cover. All pass:
5. Fail-closed posture — intact, additive only
6. Checks I ran myselfBoth match the PR's stated numbers exactly. Re-ran both after my citation fix; still green. ⛔ Blocks merge: conflict with
|
…lary # Conflicts: # docs/README.md
Resolves the docs/README.md Decisions-table conflict again: #159 landed the ADR-0017 row on main where this branch adds ADR-0016. Both rows kept, in numeric order. No other file conflicted.
Closes #126. ADR-0017. Related: #118 (ADR-0014, read order — already landed).
The pool captures 1 of 12 rows on the only real compiled bundle in the repo. The issue's diagnosis:
isChromeName()is an exact match against ~50 generic words, and locator names likeAdd new panel/toggle-viz-picker/Plugin visualization item Statare not tenant data — they're Grafana's own UI vocabulary at a pinned open-source version, reproducible by anyone who runsdocker run grafana/grafana-oss:9.5.21.The issue asked for three things: measure yield properly, decide whether product vocabulary is pool-safe, and record it. Measuring it properly changed the question.
The measurement changed the question
The bundle's stamped
1/12is the compiler's own pre-check (src/compiler/pool.ts), whichdocs/gate/compiler.mdalready documents as deliberately stricter than the authority. Routed through the actual authoritative path instead (buildPoolRow,src/cache/write.ts) — verified withgit stashagainst the pre-PR code, so this is the number before this PR changes anything:7 of 12, not 1 of 12, before this PR does anything. Six of those seven pool through the existing structural-locator allowance (boundary-spec.md rule 2) — no vocabulary rule needed. The gap between "1" and "7" is the compiler's pre-check disagreeing with its own authority in the conservative direction (safe, per its own doc — "a pre-check may be stricter than the authority; it may never be looser" — but currently misleading anyone who reads the artifact's stamped field as the real number).
This PR's vocabulary rule changes that 7/12 by exactly zero, for two reasons, neither of which is
isChromeName's narrow list:literal_in_assertion, not locator taint — aurl-matchesassertion whose template residue is a URL path refuses the row unconditionally, before any locator is considered.docs/gate/compiler.mdalready named this "the single biggest reason" and deferred it to B5 as a separate decision. Still deferred here — reopening it means bundling two decisions into one PR.role_name/label/textcandidatetenant_scoped: true, independent of content — confirmed directly in the source trajectory JSON.caller_marked_tenantcorrectly honors that upstream claim ahead of any vocabulary match (that's the fail-closed behavior the boundary spec requires, not a bug), but it means a content-based rule is structurally inert on every locator this recorder has ever produced.src/recorder/is out of this PR's scope.Full writeup, including the per-locator (not row-level) effect the rule does have on this bundle's one testid, in
docs/gate/pool-vocabulary.md.What shipped anyway, and why
src/cache/vocabulary.ts— a committed snapshot of five strings (the four the issue named, plus one testid found while verifying them), each independently checked against the publicgrafana/grafanaGitHub repo at tagv9.5.21today, with file + line + access date."Apply"(the fifth locator on the live bundle) is not in the snapshot: not independently verified in the time available, and it wouldn't have mattered anyway — that exact locator is alreadytenant_scoped: truein the source trajectory.No live fetch in the shipped code — the fail-closed guarantee can't depend on GitHub being reachable at write time or in CI. Network access was used once, by hand, to build the citations.
Wired additively:
allowlist.tsgainedisPoolSafeAccessibleName/isPoolSafeTestId(existingisChromeName/isAllowedTestIduntouched, still used unqualified inassertionHasTenantLiteral).taint.ts'saria_label_or_name_tenant,role_text_tenant, andnon_vocab_testidrules call the composed functions;caller_marked_tenantand every other rule are unchanged.Why ship a rule that measures zero yield improvement: the evidence indicts the compiler pre-check and the recorder's tagging policy, not the principle that pinned-version product vocabulary is poolable. Concluding "pooling doesn't work" from a measurement that names the wrong culprit would be a worse error than shipping a currently-quiet, architecturally real mechanism. It also isn't inert everywhere: repair-proposed locators (ADR-0009 / #64) carry no blanket
tenant_scopedtag — nothing inupdate.ts/confidence.tssets one — so a repair proposingtoggle-viz-pickerwas refused before this PR and pools after it, purely on content. That path has no model behind it yet (#27), so no yield number is claimed for it either — only that the mechanism is real and tested.The collision guarantee
The issue asked how the rule tells "Grafana's own accessible name" apart from "a tenant string that happens to collide with one." It uses a signal the boundary already had:
tenant_scoped, honored unconditionally bycaller_marked_tenant, which runs independently of the vocabulary check and is never overridden by it.tests/canary/vocabulary.test.ts(merge-blocking) proves both directions:toggle-viz-picker(not inUI_CHROME_NAMES) pools, shaped the way a repair proposal is shapedAlias, taggedtenant_scoped: true, is still refused — and a mutation test (removingcaller_marked_tenant) shows the guarantee actually depends on that rule, not on rule ordering by accidentMulti-version yield:
no_dataThe issue's checklist wants yield per version across ADR-0003's 8 pins. This environment has no running Docker daemon (verified:
docker psfails with no daemon socket present) and noANTHROPIC_API_KEY, so recording fresh trajectories against the other seven versions isn't possible here.docs/gate/pool-vocabulary.mdstates this as a gap, not a zero, and names exactly what a follow-up run needs.Docs touched
docs/gate/pool-vocabulary.md(measurement),docs/decisions/ADR-0017-pool-vocabulary-rule.md(decision)docs/gate/cache.mdanddocs/gate/compiler.md: the stale "1 of 12" references annotated against the 7/12 authoritative recount, not deleteddocs/decisions/ADR-0014-cache-read-path.md: open questions updated with what the pool actually contains, per Cache read path: resolve a program from the cache and record hit/miss — the amortization claim has no mechanism without it #118docs/privacy/boundary-spec.md: the "Open, and shared with Pool captures 1 of 12 rows on the live bundle: the allowlist cannot tell product vocabulary from tenant data #126" note now points at the answerdocs/pitch/proof-points.md(F4, F6),narrative.md,objections.md: footnoted with the measured number and a link to ADR-0017 — touched only those specific lines, nothing else, since a parallel agent is reconciling Track-2 status in the same five files right nowScope, explicitly
Not touched:
src/intent/,src/recorder/cli.ts,src/session/— out of scope per the task split, and two of the three findings above (literal_in_assertion, recorder blanket-tagging) live partly in that territory anyway; naming them here is not fixing them here.Before you open the PR checklist, honestly:
docs/gate/with version and date — both numbers, and why they disagreedocs/pitch/claims reconciled with the measured numberno_data, no Docker testbed here🤖 Generated with Claude Code