Skip to content

id-377 trial B — id-317 content_items residual-sweep re-verification

Trial B — content_items vocabulary/reference retirement (id-317 re-verification)

Section titled “Trial B — content_items vocabulary/reference retirement (id-317 re-verification)”

Date: 2026-07-29. Corpus: canonical @ ast-dataflow-efficacy-trials (clean, head 8b397be1). Protocol: for each question — tool answer, then independent rg/Read ground truth, then verdict. Tool run as a user (CLI cold path only; warm MCP server not registered — Trial A G7). No canonical edits were made; no trial-blocking tool bug was found.

Row ids are B* to avoid collision with Trial A’s R*; columns, gap_class vocabulary and gap numbering are shared. Trial A’s G1 / G2 / G3 / G6 / G7 are re-hit here and cross-referenced explicitly. G8 was filed mid-flight by the Trial A verifier (not by Trial A) and is independently corroborated here at B17/B18 on different columns; this trial’s own new gaps therefore start at G9 (O3+ for opportunities).

Note on inherited framing: Trial A’s scorecard claim that column-reads/column-writes are “the stars, zero over-reports” has been refuted for column-writes and is not carried over — see G8. Rows B6 and B8 were run before that hand-off arrived; both were re-audited against it and stand (B18).

#questioncommandwall clock (durationMs)answerground truthverdictgap_class
B1Pre-registered datapoint: reproduce string-literal-uses 'content_items' (recorded 27 hits / 11 files)bun run ast-dataflow string-literal-uses --value content_items12.1s (10237)7 hits / 7 files, ALL kind: argument, ALL under __tests__/rg tracked-file census: 345 files contain the string. The 4 script files in the recorded set are gone from the result: verify-intelligence-classification.ts, snapshot-content-state.ts, propagate-cert-metadata.ts, audit-opaque-json-rpcs.ts deleted; quality-gate.ts / eval-classification.ts / mcp-eval/protocol-compliance.ts re-pointed — bl-495 PR #124 (c6d28d2a, a0178af4). Datapoint has decayed because the debt shrank, not because the tool changedsufficient (as a literal probe)
B2Decompose the tool-vs-grep delta by causeclassifier over git grep -l content_items × corpus-membership probe (see table below)7 visible / 338 invisible, split 5 ways; every bucket reconciles to 345see 27-vs-186 decomposition tablegap ×3detector-missing + corpus-boundary
B3Which of the 3 exact-literal-in-code files does the tool miss, and why?diff tool result vs classifierMisses __tests__/lib/intelligence/pipeline.test.ts:375 (c.table === 'content_items', comparison) and __tests__/scripts/propagation/payload-contract.test.ts:83 (array element). Correctly ignores eslint-rules/tests/no-supabase-record-cast.test.ts ×10 (.from('content_items') nested inside RuleTester code: template literals — code-in-a-string, not code)rg + Read on all three. (Verification pass: these are the only two files carrying the bare 'content_items' literal, but 15 more in-corpus files carry the token inside a longer literal — see the corrected decomposition bucket 2, incl. three prod strings that ship at runtime.)gap (2) / sufficient precision (1)detector-missing
B4W0 / bl-495 — any script still carrying live .from('content_items')? Could the tool see them?corpus probe via createProject(tsconfig); rg "from\(\s*['\"]content_items['\"]"5.9s probeZero live .from('content_items') repo-wide. Surviving matches: 10 ESLint rule-test fixture strings, 3 migration comments, 2 test comments. Corpus probe: 51 of 82 scripts/*.ts ARE in corpus (transitively pulled in via __tests__/scripts/*, despite tsconfig.exclude:["scripts"]) — but scripts/quality-gate.ts, the hot file, is OUT today; scripts/eval-classification.ts and scripts/mcp-eval/protocol-compliance.ts are INgit log confirms PR #124 merged; 4 files deleted on disk. (Verification pass: quality-gate.ts was IN corpus before PR #124 and supplied 17 of the 27 original hits — PR #124’s rewrite is what dropped it out. It carried 18 occurrences then, 7 comment-only ones now, never 8.)W0 RESOLVED; the tool could have driven W0 at the time, and stopped being able to only after the fix landedcorpus-boundary (G12)
B5Cross-language recipe — what does schema-coverage report for a RETIRED table?bun run ast-dataflow-py schema-uses --exclude-tests > /tmp/py.json; bun run ast-dataflow schema-coverage --table content_items1.3s (1024) / 0.03s (30)Sidecar: 765 rows, 19 tables, 8 files, sqlglot:true; zero content_item* tables. schema-coverage: hard error.kind: "unknown_table", message “Table ‘content_items’ is not in Database[‘public’][‘Tables’] (70 tables in schema)”, hint “Dropped or misspelled tables report loudly instead of a silent 0/0.”Table dropped by id-131 — correctsufficient (best-in-trial) — output is unambiguous that the table is gone, not “clean”
B6Same probe via column-reads/column-writes — does the loud-error contract hold?column-reads --table content_items --column title; column-writes …5.2s (5188) / 2.5s (2531)Both return silent results: [], no error, for a table that does not exist at allgap — Trial A G3 CONFIRMED and widened: it is not just dropped columns; a dropped table is silent too, while schema-coverage (same binary, same generated types) reports it loudly. The fix already exists in-repoergonomics
B7DR-102 rule in real output — declared-only ⇒ undecidable, never wired?schema-coverage --evidence /tmp/py.json7.5s (3094)807 rows. Rows with verdict wired and all five evidence counters zero: 0. undecidable: 547, of which 2 zero-counter. Merged run rewrites caveats.scan to name the merged sidecar and removes "the Python pipeline (scripts/**/*.py)" from invisibleSurfacesmanual counter audit over all 807 rowssufficient — DR-102 v1 holds
B8Evidence-merge diff — does --evidence change verdicts?schema-coverage vs schema-coverage --evidence /tmp/py.json6.0s (4181) vs 7.5s (3094)807 rows both. Plain: unwired 85, undecidable 722, wired/read-only/write-only 0. Evidence: unwired 78, undecidable 547, read-only 67, write-only 58, wired 57. 183 / 807 verdicts change (22.7 %) — see transition table belowspot-checked pair_resolver.py:161/181 and flow.py:2295/2307sufficient (high value) — 6 columns flip out of unwired, i.e. the un-merged run reads them as dead schema
B9W2 identifiers — can references produce exact per-identifier rename scopes?references --symbol lib/ai/change-reports.ts:ContentItemRow; …template-coverage.ts:ContentItemForMatching; …lib/attention.ts:produceStaleContentItems; callers …produceStaleContentItems0.5s (544) / 1.4s (1366) / 0.9s (917) / —5 / 11 / 8 exact sites. ContentItemRow is declared 4 separate times repo-wide (change-reports.ts:52, app/api/cron/quality-score/route.ts:48, lib/content/content-suggestions.ts:74, plus PolledContentItemRow); the query returned only the 5 change-reports.ts sites — no bleedrg spot-check: ContentItemForMatching = 12 rg lines, tool returned 11 — the omitted template-coverage.test.ts:487 is a comment. produceStaleContentItems = 9 rg lines, tool returned 8 — the omitted attention.test.ts:111 is describe('produceStaleContentItems', …), a string labelsufficient (strong) for named symbols. Caveat: a rename still needs the describe label, which only a literal query would find
B10W2 paircontentItemId write-back + sweep, renamed as one unitreferences --symbol lib/edit-intent/write-back.ts:contentItemId; …sweep.ts:contentItemId; …write-back.ts:WriteBackParams0.5s / 0.7s / 0.9sBoth contentItemId probes: error.kind: out_of_corpus, “Symbol not found … Looked at functions, classes, methods, variables, interfaces, type aliases, enums, and named exports.” The containing interface resolves fine (2 sites)rg: contentItemId spans 24 files as a property key on ≥4 independent interfaces (write-back.ts:348, sweep.ts:71,94, arbitrate.ts:45, app/api/q-a-pairs/[id]/route.ts:117)gap — no member-level resolution; the pair-rename scope is not derivabledetector-missing (G9)
B11W3(a) — prove closure of the queryKeys.contentItems atomic setstring-literal-uses --value content-items; references --symbol lib/query/query-keys.ts:queryKeys6.0s (5950) / 0.9s (940)Kebab literal: 0 results (all 7 real sites are ['content-items'] as const array elements). references: 200 results, truncated: true, spanning ~80 files — it resolves the whole queryKeys object, not the contentItems memberrg queryKeys.contentItems5 code files: use-library-data.ts:135 ✓, use-batch-create.ts:160 ✓, use-library-bulk-actions.ts:**104** (plan says 88), publication-review-action-bar.tsx:**117** (plan says 114), components/review/PublicationReviewQueue.tsx:174 — not in the plan. Plus __tests__/lib/content-browsing/use-content-bulk-runner.test.ts:34,112 builds ['content-items'] raw, bypassing the key factorygap ×2 — closure not provabledetector-missing (G1 re-confirmed, array element) + G9/G10
B12W4 persisted values — does string-literal-uses find the 3 named entity_type='content_item' sites?string-literal-uses --value content_item5.5s (5549)6 hits: 5 test argument sites + lib/mcp/resources.ts:58. Zero of the three named sitesThe named sites moved and multiplied: review.ts:**403**, governance.ts:**1306**, content.ts:**1871** — and rg finds 24 property-value write sites across 11 files (app/api/cron/{freshness-transitions ×9, review-cadence ×4, quality-score ×2, classification-quality}, source-documents/[id]/send-to-review ×2, content-owners/bulk-assign, review/assignments, governance/review, + the 3 MCP tools). All are entityType: 'content_item',object-property valuesgap — 0/24 detectiondetector-missing (G1 re-confirmed, property value)
B13W4 RPC payload fieldscontent_item_count, dashboard content_items payloadstring-literal-uses --value content_item_count~5s0 resultsrg: 20+ real sites (lib/validation/schemas.ts:2716, lib/mcp/formatters/entities.ts:139,238,243, lib/mcp/tools/content.ts:1987,2040,2077, app/api/certifications/route.ts ×6, app/api/entities/[canonical_name]/route.ts:148, hooks/use-entity-detail.ts:37, components/source-document/source-document-history.tsx:33,244). These are identifiers / object keys, never quoted literals — so the zero is correct for this query and there is no query in the tool that finds themgap (coverage, not correctness)detector-missing (G11)
B14W5 MCP wire — tool names, arg, resource typestring-literal-uses --value {create,update,delete,supersede}_content_item; --value content_item_id~5–8s eachcreate_content_item 15, delete_content_item 13, update_content_item 6, supersede_content_item 2 = 36 sites, incl. every registration site (content.ts:408, content.ts:923, governance.ts:79, supersession.ts:62) and the eval consumers in scripts/mcp-eval/functional-correctness.ts. Resource type found at lib/mcp/resources.ts:58. content_item_id = 0rg: content_item_id at search.ts:647 is a Zod object key in the tool-arg shape (invisible, same class as B13); telemetry names are property values (pipelineName: 'mcp_create_content_item' ×6 in content.ts) → invisible (G1)sufficient for tool names (registerTool/callTool args) — the one wave the tool genuinely drives; gap for arg + telemetrymcp-surface + detector-missing
B15KEEP-FOREVER guards — excluded / correctly classified, not reported as debt?ast-dataflow-py schema-uses with and without --exclude-tests; rg for the 3 guards1.0s / 1.1sAll 3 present: scripts/tests/test_cocoindex_flow_write_path.py:2198 (test_content_items_is_structurally_absent), scripts/tests/test_producer_bi28_bidoutcome_proposal.py:285 (TestNoContentItemsWrite), scripts/tests/test_cocoindex_url_write_path.py:936 (ci-seed mirror-guard). Neither py run reports any content_item* table — the guards assert absence, so there is nothing to detect. --exclude-tests: 825→765 rows, 9→8 files (drops scripts/tests/)Correct: guards are not surfaced as debt in any querysufficient — but see B16
B16Are there absence guards the KEEP-FOREVER list misses?fallout of B3Yes — a 4th, TS-side: __tests__/scripts/propagation/payload-contract.test.ts:80-92 asserts content_items (and 5 siblings) are excluded from PAYLOAD_CONTRACT. Not on the KEEP-FOREVER list; invisible to the tool (array element, G1) — so a literal-driven sweep would neither protect it nor see itRead of the test bodygap (inventory)detector-missing
B17G8 corroboration (coordinator hand-off) — is column-writes over-reporting at table level? Independent measurement on this trial’s own columnscolumn-writes --table record_lifecycle --column lifecycle_type (anchor); --table notifications --column entity_type; --table notifications --column read_at2.3s (2274) / 1.9s (1911) / ~2sAnchor reproduced: 5 sites, all confidence: indirect. entity_type: 8 sites. read_at: 4 sitesEvery site opened. Anchor: governance.ts:606 (upsert literal — owner_kind/*_id/governance_review_status, no lifecycle_type), governance.ts:1257 and route.ts:231 (.update(updateData), variable) — and the string lifecycle_type does not occur anywhere in either file; content.ts:1226 + test-data-fixture.ts:305 are true. notifications.entity_type: 8/8 true (both indirect payloads — notifications.ts:96’s rows builder at :84-94 and test-data-fixture.ts’s array at :594-608 — do carry the column). read_at: notifications/read/route.ts:30 ✓, test-data-fixture.ts:611 ✓, data-factory.ts:204 defensible (...overrides spread), lib/notifications.ts:96 FALSE — its rows builder sets 7 columns, none of them read_atgap — G8 CONFIRMED independentlycorrectness
B18What is the actual mechanism, and is schema-coverage (and therefore B7/B8) exposed?audit of confidence field across B17 + exactWrites/indirectWrites over all 807 schema-coverage rowsAll 17 write sites across the three probes carry confidence: indirect; zero exact writes appeared in any probe. Repo-wide: only 115 of 807 columns have exactWrites>0 vs 616 with indirectWrites>0. Smoking gun: lib/notifications.ts:96 is returned for BOTH entity_type (true) and read_at (false) — the same site attributed to two different columns. But schema-coverage quarantines the class: of 585 (plain) / 522 (evidence) rows whose only write evidence is indirect, 0 are verdicted wired or write-only — all land in undecidable (or read-only, promoted by read evidence). And all 6 of B8’s unwired→live flips rest on confidence: exact sqlglot rows (pair_resolver.py:181 literal INSERT … (name_a, name_b, entity_type, decision, op_id), flow.py:2295/2307 declare_row)Read of pair_resolver.py:178-184, flow.py:2293-2297; counter audit of all 807 rowsG8 is a presentation-layer defect in column-writes, not an analysis-layer one — the same counter is handled conservatively one layer up. B7/B8 unaffectedoutput-shape

Reconciliation of the two headline numbers first, since both have moved:

measurerecorded in id-317today (2026-07-29)
ast-dataflow string-literal-uses 'content_items'27 hits / 11 files (7 tests + 4 scripts)7 hits / 7 files (7 tests, 0 scripts) — bl-495 PR #124 removed the script set
repo grep, non-test/e2e .ts/.tsx/.py/.sql186 files182 files
…minus supabase/ (historical DDL)147 (“research wave re-measure”)145 files
every tracked file containing the string345 files

Decomposition of the full 345-file grep set by why the tool can or cannot see it (classifier: comment-stripped per language, cross-checked against the tool’s own result set; corpus membership from a createProject(tsconfig.json) probe):

#causefileshits the tool returnswhy
1VISIBLE — exact 'content_items' literal in a CallExpression argument77the only shape string-literal-uses classifies here (kind: argument)
2in-corpus TS, string literal in a non-call-site position17 (3 non-test)02 are the bare 'content_items' literal — comparison (pipeline.test.ts:375) and array element (payload-contract.test.ts:83), both genuine misses (G1). The other 15 carry the token inside a longer literal: it()/describe() names, mock error messages, the ESLint RuleTester code: fixtures ×10 (a correct exclusion), and three prod strings that ship at runtime — lib/mcp/tools/content.ts:477,484,491 (Zod .describe() copy), lib/mcp/tools/review.ts:219 (MCP tool description, i.e. W5 wire copy), lib/content/content-suggestions.ts:202 (sb() telemetry label)
3in-corpus TS, identifier / property key, never a quoted literal24 (11 non-test)0content_items: API-response keys and type members. No identifier-text query exists (G11). (content_item_count / content_item_id are a different needle and are only in this 345-set when the file also carries content_items)
4in-corpus TS, comment-only221 (116 outside __tests__)0by design and correct — this is the tool’s precision advantage over rg
5TS out-of-corpus8 (all scripts/)0tsconfig excludes scripts, but membership is unpredictable — 10 of the 18 scripts/ files carrying the string are pulled in transitively anyway, as is supabase/types/database-overrides.ts (G12)
6non-TS68 (.sql 36, .py 23, .md 7, .json 1, .js 1)0.py partly reachable via ast-dataflow-py; .sql/.md never
total3457

(Buckets 2–5 were re-derived by the verification pass against a ts-morph AST classifier — the first-pass numbers were 3 / 41 / 207 / 19. Bucket 1, bucket 6 and the 345 total reproduced exactly; the correction moved 11 files out of “out-of-corpus” and surfaced 14 literal-bearing files that had been counted as prose. The conclusion is unchanged — see below.)

Headline reading. 221 of 345 files (64 %) are comment-only — the single largest bucket, and the one the tool is right to ignore. Of the 124 code-bearing files, only 7 sit in a shape string-literal-uses recognises. The boundary this draws: ast-dataflow is a dataflow tool, and content_items is 64 % prose + 20 % out-of-language + 7 % identifier. A vocabulary sweep is not a dataflow question, and no amount of detector work changes buckets 4 and 6.

Sidecar: ast-dataflow-py schema-uses --exclude-tests → 765 rows / 19 tables / 8 files / 1024 ms. Both schema-coverage runs return 807 column rows.

verdictplainwith --evidenceΔ
unwired8578−7
undecidable722547−175
read-only067+67
write-only058+58
wired057+57

183 of 807 verdicts change (22.7 %). Without a sidecar the TS-only run cannot emit wired/read-only/write-only at all — every column is either unwired or undecidable.

transitionnnotes
undecidableread-only67
undecidablewrite-only56
undecidablewired53
unwiredwired4entity_pair_resolutions.{decision,entity_type,name_a,name_b} — read pair_resolver.py:161, written :181
unwiredwrite-only2q_a_extractions.evaluation_criteria (flow.py:2295), .extraction_metadata (flow.py:2307)
unwiredundecidable1record_embeddings.updated_at (qa_dedup_proposer.py:200)

The 6 unwired → live flips are the load-bearing result: 7 % of the plain run’s unwired verdicts are wrong, and unwired is exactly the verdict a schema-cleanup wave would act on. The plain run does list "the Python pipeline (scripts/**/*.py)" under caveats.invisibleSurfaces — the caveat is present but unquantified (O4).

Refreshed W0–W5 premise-verdict table (id-317, re-verified 2026-07-29)

Section titled “Refreshed W0–W5 premise-verdict table (id-317, re-verified 2026-07-29)”
wavepremise as writtenverdictcurrent countswho can drive it
W0 functional — 4 scripts with live .from('content_items') + delete 3 dead one-offsRESOLVED0 live .from('content_items') repo-wide. verify-intelligence-classification.ts + all 3 one-offs deleted; quality-gate.ts, eval-classification.ts, mcp-eval/protocol-compliance.ts re-pointed (comments only). bl-495 PR #124, c6d28d2a + a0178af4rg only today. scripts/quality-gate.ts is out-of-corpus at HEAD, so the tool cannot verify W0’s hot file now (G12) — though it was in corpus pre-#124 and reported 17 of that file’s sites. Compensated with rg + a corpus-membership probe; ast-dataflow-py --scan-dir covers .py but the W0 files are .ts
W1 comment-only sweep (~99 files)HOLDS (slightly larger)116 comment-only files outside __tests__ (221 incl. tests), across app 33, lib 33, e2e 15, components 12, scripts 10, hooks 8, types 4, supabase 1rg / human only — by design. ast-dataflow’s value here is as the post-sweep verifier: its silence on all 221 is correct precision, so a non-zero result after the sweep = a real regression
W2 internal identifiersHOLDS, half-drivableContentItemRow 5 sites in lib/ai/change-reports.ts (+3 unrelated same-named local decls elsewhere — do not co-rename); ContentItemForMatching 11; produceStaleContentItems 8 (2 prod, 6 test)ast-dataflow drives the named-symbol halfreferences was exact on all three, file-scoped, comment-excluded, zero over-report. Cannot drive the contentItemId pair: no member-level resolution (G9). Rename also needs the describe('produceStaleContentItems') label, which references correctly excludes but a rename must patch
W3(a) queryKeys.contentItems + 'content-items' atomic setDECAYED — inventory grew5 code consumers, not 4: adds components/review/PublicationReviewQueue.tsx:174. Two plan line refs stale (use-library-bulk-actions 88→104, publication-review-action-bar 114→117). New hazard: __tests__/lib/content-browsing/use-content-bulk-runner.test.ts:34,112 builds ['content-items'] raw, bypassing the key factoryrg only. Kebab literal query = 0 (array element, G1); references --symbol queryKeys = 200 truncated and member-blind (G9/G10). Closure is not provable with the current tool
W3(b) API response keys content_items / content_item_countHOLDS24 files carry the content_items identifier form (11 non-test), plus 25 files on the separate content_item_count needle (82 lines) and 44 on content_item_id: certifications route + cert-summary-card + framework-summary-card, entities route + entity-detail-panel + use-entity-detail, guides route + guide-content + guide-section, formatters/dashboard.ts, validation/schemas.ts, mcp/tools/{content,review}.ts, content-suggestions.tsrg only — no identifier-text query exists (G11). Server+client lockstep risk unchanged
W4 persisted / RPCDECAYED — badly under-inventoriedPlan named 3 entity_type='content_item' sites; actual 24 property-value sites across 11 files (cron freshness-transitions ×9, review-cadence ×4, quality-score ×2, classification-quality, source-documents/send-to-review ×2, content-owners/bulk-assign, review/assignments, governance/review, MCP review.ts:403 / governance.ts:1306 / content.ts:1871). content_item_count 20+ sites. Backfill still not needed (S483 finding unchanged)rg only. string-literal-uses --value content_item found 0 of the 24 (property values, G1) and 0 content_item_count (G11). schema-coverage correctly reports the table as gone (B5) but says nothing about the persisted value
W5 MCP public wireHOLDS — ownership unchanged (id-71)Tool names 36 sites: create_content_item 15, delete_content_item 13, update_content_item 6, supersede_content_item 2; registrations at content.ts:408,923, governance.ts:79, supersession.ts:62. Resource type resources.ts:58. content_item_id at search.ts:647. Telemetry mcp_create_content_item ×6 in content.tsast-dataflow DRIVES this wave. Tool names + resource type are CallExpression arguments → fully visible with zero noise. Except: content_item_id is a Zod object key (G11, 0 hits) and telemetry names are property values (G1, 0 hits). Verdict process stays id-71’s
KEEP-FOREVER 3 absence guardsHOLDS + 1 additionAll 3 py guards present and correctly not reported as debt (no content_item* table in either py run — the guards assert absence, so there is nothing to detect). --exclude-tests drops scripts/tests/ cleanly (825→765 rows). New: a 4th, TS-side guard__tests__/scripts/propagation/payload-contract.test.ts:80-92 asserts content_items is excluded from PAYLOAD_CONTRACT; recommend adding it to the KEEP-FOREVER listtool-safe; the 4th guard is invisible to the tool (array element, G1) so a literal-driven sweep would neither see nor protect it

Net: one wave resolved (W0), two decayed toward more work (W3a, W4), four hold. ast-dataflow can drive exactly one wave end-to-end (W5 tool names) and half of one (W2 named symbols). W1, W3, W4 and the W2 property-pair remain rg + human.

idgap_classwhatreproproposed fix
G1detector-missingCONFIRMED, three more shapes. Trial A found comparison / property / array / type-position literals dropped. Trial B re-hits all of them in one task: comparison (pipeline.test.ts:375), array element (query-keys.ts:22-28 ×5, payload-contract.test.ts:83, use-content-bulk-runner.test.ts:34,112), property value (24 × entityType: 'content_item', 6 × pipelineName: 'mcp_create_content_item'). Detection on W4 is 0/24string-literal-uses --value content-items → 0 vs lib/query/query-keys.ts:22unchanged from Trial A: add comparison, caseClause, propertyValue, arrayElement, typeLiteral, initializer kinds. Trial B raises the priority — property-value is the single most common persisted-enum shape in this repo
G2output-shapeCONFIRMED with a counter-example. string-literal-uses zeros (B11 content-items, B13 content_item_count, B14 content_item_id) carry no caveat — yet schema-coverage in the same binary emits a full caveats block naming scanned surfaces, invisible surfaces and merged sidecars (B5/B7). The good implementation already existscontrast string-literal-uses --value content_item_count vs schema-coverage outputport schema-coverage’s caveats block to every query — no new analysis needed
G3ergonomicsCONFIRMED and widened. Trial A: dropped column → silent []. Trial B: a dropped table is also silent from column-reads/column-writes, while schema-coverage --table content_items reports unknown_table + a hint. Same binary, same generated types, opposite behaviourcolumn-reads --table content_items --column title[]; schema-coverage --table content_itemsunknown_tablereuse the existing unknown_table validator in column-reads/column-writes; add unknown_column alongside
G6/G12corpus-boundaryCONFIRMED and sharpened into a worse finding. The boundary is not just “scripts are excluded” — it is unpredictable: tsconfig.exclude lists scripts, yet 51 of 82 scripts/*.ts are in corpus via transitive import from __tests__/scripts/*. scripts/eval-classification.ts IN, scripts/quality-gate.ts OUT — with no way to tell from the CLI. A zero result is therefore uninterpretable — and membership is not even stable: quality-gate.ts was IN before PR #124 (17 hits) and is OUT after it, so the same query silently changed meaning across an unrelated refactorbun run ast-dataflow string-literal-uses --value content_items at 74a56c7d → 27 hits incl. 17 from scripts/quality-gate.ts; at 8b397be1 → 7 hits, that file no longer in corpusadd a CLI corpus-info (MCP-only today) and a per-response corpusSummary (file count, top-level dirs, “your --file/--symbol target is/ is not a member”)
G7performanceCONFIRMED. ~0.5–1.4 s for references, but 5–12 s per string-literal-uses (cold project load dominates); this trial spent ~2 min across 11 literal probesany two consecutive literal queriesunchanged: accept multiple --value args per invocation
G8correctnessFiled by the Trial A verifier, CONFIRMED here on different columns. column-writes returns confidence: indirect sites — writes whose payload it could not statically attribute — under an exact columnPath, so a site that writes some column of the table is presented as writing the named one. Independent measurement (B17), 17 sites, every one opened: record_lifecycle.lifecycle_type 3/5 false (60 %) — the string lifecycle_type does not occur anywhere in lib/mcp/tools/governance.ts or app/api/governance/review/route.ts; notifications.entity_type 0/8 false (0 %); notifications.read_at 1/4 clean false + 1 defensible (25 %). Pooled 4/17 clean false positives = 23.5 %. Smoking gun: lib/notifications.ts:96 is returned for BOTH entity_type (true) and read_at (false). The FP rate is not uniform — it tracks how many of a table’s columns a typical payload omits, so wide optional-column tables (facets) are worst and NOT-NULL-heavy tables look clean. Scope limit (B18): this is a presentation-layer defect. schema-coverage consumes the same counter and quarantines it — 0 of 585 (plain) / 522 (evidence) indirect-only-write rows are verdicted wired/write-only; all stay undecidable. column-reads is not implicated in these probescolumn-writes --table record_lifecycle --column lifecycle_type → 5 sites, 3 in files that never mention the column(a) suppress indirect writes from results by default behind --include-indirect; or (b) keep them but drop columnPath and set an explicit attributed: false + "writes an unattributed column of <table>" message. Either way the count must stop reading as a per-column answer. Trial A’s scorecard line “column-reads/column-writes … zero over-reports” is refuted and must not be inherited
G9detector-missingreferences has no member-level resolution. file.ts:propName for an interface/type property errors out_of_corpus — the wrong kind (same mis-kind Trial A flagged in G4: “unsupported granularity” reported as “not in corpus”). This blocks the W2 contentItemId write-back+sweep pair and W3’s queryKeys.contentItems closure — both explicitly atomic-set problems, the exact case where a rename must be provably completereferences --symbol lib/edit-intent/write-back.ts:contentItemIdout_of_corpussupport File.ts:Interface.member and File.ts:Object.member addressing; distinguish unsupported_granularity from out_of_corpus
G10output-shapereferences on a hub object (queryKeys) returns 200 rows with truncated: true, totalEstimated: 285, and no hint and no way to narrow. The count is honest; what is missing is the narrowing path — for closure proof you need the 85 unseen rows, and no argument shape can ask for themreferences --symbol lib/query/query-keys.ts:queryKeys → 200 of 285when truncated, add a hint pointing at member-scoped addressing (G9) and/or a --offset/--limit pair so the tail is reachable. (Verification pass: totalEstimated already ships — the original “indistinguishable from ≥200 and unbounded” reading was wrong and the fix narrows to the hint + pagination.)
G11detector-missingNo identifier-text query exists. The dominant residual shape in this task — content_items: response keys, content_item_count, content_item_id — is 24 files on the content_items needle alone, and 306 TS files across the three needles and is addressable by neither string-literal-uses (not a literal) nor references (no single declaration; the same key is an independent member of ~10 unrelated interfaces). This is a coverage hole, not a bugstring-literal-uses --value content_item_count → 0 vs 20+ real sitesadd identifier-uses --name <ident> over Identifier/PropertyAssignment/PropertySignature nodes — cheap (no type resolution), and it is what a vocabulary sweep actually needs
O3opportunityschema-coverage is the reference implementation for output ergonomics in this codebase: loud unknown_table + hint (B5), a caveats block that changes when a sidecar is merged (B7), a summary verdict histogram. Every other query has none of itB5/B7 vs B6/B11treat schema-coverage’s response envelope as the house style and retrofit; closes G2 and G3 with existing code
O4opportunityunwired is under-qualified. 6 of 85 plain-run unwired verdicts (7 %) are live Python-written columns. The caveat block does name the Python pipeline as invisible, but the verdict token itself reads as “dead”B8 transition tablerename to unwired-in-ts (or add externalSurfacesUnmeasured: true per row) whenever the run has no sidecar and scripts/**/*.py exists
O5opportunityThe absence-guard shape (expect(tables.has('content_items')).toBe(false)) is a first-class artefact of any retirement task and is currently invisible (array/comparison position, G1). B16 found an unlisted 4th guard by accidentB3/B16once G1 lands, a --kind comparison,arrayElement sweep becomes a mechanical way to find absence guards — worth a documented recipe in the skill

Tool scorecard (this task’s shape: vocabulary/reference retirement sweep)

Section titled “Tool scorecard (this task’s shape: vocabulary/reference retirement sweep)”
  • schema-coverage + --evidence: the star, and the best-engineered surface in either trial. Loud unknown_table on the retired table (no misleading “clean” 0/0), a caveats block that correctly drops the Python pipeline from invisibleSurfaces once a sidecar is merged, DR-102 v1 holding exactly (0 declared-only rows reported wired), and 183/807 verdict flips incl. 6 unwired→live that would otherwise read as dead schema. Run it with the sidecar or not at all. Also the only surface that handles the indirect-write class correctly (G8/B18) — it never promotes indirect-only evidence past undecidable.
  • column-writes: do not trust the counts. 23.5 % pooled false positives across 17 hand-checked sites (60 % on record_lifecycle.lifecycle_type, 0 % on notifications.entity_type, 25 % on notifications.read_at) — every returned site was confidence: indirect, i.e. an unattributed payload presented under an exact columnPath (G8). The FP rate scales with how many optional columns the table has, so it is worst exactly on the wide facet tables a retirement wave cares about. column-reads was not implicated in these probes and still looks clean, but it shares G3 (silent [] for an unknown table and an unknown column).
  • references: precise and correctly scoped where a named declaration exists — 5/5, 11/11, 8/8 with comments and string labels correctly excluded, and no bleed into three same-named local ContentItemRow declarations. Blind to members (G9), which is where the atomic-set work lives.
  • string-literal-uses: wrong shape for this task. 7 of 345 files, 0 of 24 W4 sites, 0 of the W3 kebab set. It is excellent at exactly one thing here — W5 MCP tool names (36 sites, zero noise), because registerTool/callTool put them in argument position. Everywhere else the literals live in property, array or comparison position (G1) or are not literals at all (G11).
  • ast-dataflow-py: quiet and correct. 765 rows in 1.0 s, --exclude-tests clean, no false-positive content_item* table from the synthetic-arg test fixtures, and the 3 KEEP-FOREVER absence guards correctly produce nothing.
  • Cross-trial corroboration: Trial A’s G1, G2, G3, G6, G7 all re-confirmed independently on a different task shape; G3 and G6 widened (dropped table also silent; corpus membership is unpredictable, not merely exclusive). G8 (filed by the Trial A verifier) reproduced on three columns Trial A never touched, with the FP rate quantified and bounded to the presentation layer. Trial A’s diagnosis that string-literal-uses “would BEAT rg if G1’s kinds existed” is supported: on the 221 comment-only files its silence is exactly right, and its 10 correct suppressions of ESLint code: fixture strings are noise rg cannot filter. Trial A’s “column-reads / column-writes are the stars, zero over-reports” is refuted for column-writes and not inherited.
  • Verdict pattern: 6 sufficient / 11 gap / 1 mixed across 18 rows.

Independent audit by a second agent. Read-only against canonical @ ast-dataflow-efficacy-trials, head 8b397be1, working tree clean, except for the in-place corrections marked (Verification pass: …) above. Every headline number re-derived from the tool and from source — not from this log’s own output.

Two defects were both numbered G8. Agreed final numbering, one id per defect across both logs:

idownerdefect
G1–G7Trial Aunchanged
G8Trial A’s verification pass (filed first)column-writes reports table-level writes under an exact columnPath
G9Trial Breferences has no member-level resolution (file.ts:propNameout_of_corpus)
G10Trial Breferences truncates a hub object at 200 with no hint and no pagination
G11Trial Bno identifier-text query exists
G12Trial Bcorpus membership is transitive, unpredictable and unstable across refactors (widening of G6)

Scheme note: the brief proposed G8→G10 / G9→G11, but Trial B already used G10 and G11, so a literal application would have double-assigned both. A monotonic +1 shift of Trial B’s four new ids (8→9, 9→10, 10→11, 11→12) preserves this log’s internal ordering, leaves Trial A untouched, and gives one id per defect. Applied throughout: the numbering preamble, rows B4/B10/B11/B13, the decomposition table, all seven wave rows, the gaps table, G10’s back-reference, and the scorecard.

2. Independent column-writes false-positive measurement — G8 CONFIRMED

Section titled “2. Independent column-writes false-positive measurement — G8 CONFIRMED”

Measured on three columns across three tables Trial A never touched, disjoint from B17’s set. Every returned site was opened and its payload read.

probesitesfalserate
source_documents.publication_status12325 %
q_a_pairs.answer_standard26311.5 %
form_instances.workflow_state10220 %
this pass48816.7 %
B17 (record_lifecycle.lifecycle_type, notifications.entity_type, notifications.read_at)17423.5 %
pooled, 6 columns / 5 tables651218.5 %

The eight: id138-erasure-cascade.integration.test.ts:172 (spread can only ever add created_at), test-data-fixture.ts:239 and :734 (mapped insert bodies with no such key), governance.ts:560, :696, :1019 (the q_a_pairs leg of three owner-kind splits — the payloads are publication_status + archive metadata, never answer_standard), test-data-fixture.ts:492 and data-factory.ts:65 (form_instances inserts that never set workflow_state; the latter’s ...overrides is never called with it).

Sharper repro than any in either log — the column argument carries no information at all. column-writes --table source_documents --column zzz_not_real returns 10 hits for a column that does not exist, each stamped columnPath: "zzz_not_real". Ten of the twelve real publication_status rows are in that same set, so only 2 of 12 are column-attributable. Noise floors: source_documents 10, q_a_pairs 7, form_instances 4, record_lifecycle 4, notifications 3 — 21 of this pass’s 48 rows (44 %) are rows the tool returns for any column name whatsoever.

Two mechanism findings to carry into the fix:

  • confidence does not discriminate. All 65 pooled rows are indirect and isTyped: false; the code path that would emit exact (inspectWriteArg, column-writes.ts:106-173) requires a typed client, and detectIsTyped returns false on every .from() chain these probes touched. So in this repo every column-writes row is indirect and the label separates nothing.
  • The one-hop chase in resolveOneHopObjectLiteral only follows const x = { … }. .insert(arr.map(…)), .insert(identifier-built-by-loop) and satisfies/as-wrapped literals all fall through to “cannot rule out”, which is why fixture builders dominate the false set.

B18’s scope limit independently verified and it holds. Plain run: 585 rows have indirect-only write evidence, 0 verdicted wired/write-only (all undecidable). Evidence run: 522 indirect-only rows, 0 verdicted wired/write-only (459 undecidable, 63 promoted to read-only by read evidence); 115 rows carry exactWrites>0, 616 carry indirectWrites>0. schema-coverage never promotes the suspect counter, so B7 and B8 are unaffected by G8 — the defect is confined to column-writes’ own presentation layer.

G3 widening re-confirmed. column-reads/column-writes --table zzz_not_a_table --column id both return silent []; schema-coverage --table zzz_not_a_table errors unknown_table with a hint. One refinement for the fix: for an unknown column on a real table the two queries diverge — column-reads returns 4 honestly-labelled columnPath: "*" / confidence: "wildcard" rows, while column-writes returns 10 rows falsely stamped with the bogus column name. “Silent []” understates column-writes: it fabricates.

The 27 → 7 drop is real decay, and now provable rather than inferred. A detached worktree at 74a56c7d (PR #124’s parent, 2026-07-17) running that commit’s own tool binary returns 27 hits / 11 files exactly: 7 test files × 1 hit, plus scripts/quality-gate.ts 17, verify-intelligence-classification.ts 1, eval-classification.ts 1, mcp-eval/protocol-compliance.ts 1 = 20 script hits. Today’s 7 files are precisely that measurement’s 7 test files. Not a methodology difference: string-literal-uses.ts has two commits in its history and the later one (aa494e49, 2026-07-27) only swaps in truncateSpatialclassifyLiteralKind and the five call-site kinds are untouched, and 7 ≪ the 200 limit either way. The one correction this forces is to the “quality-gate.ts was never in scope” reading — see §4.

Decomposition. Total 345 and the non-TS split (.sql 36, .py 23, .md 7, .json 1, .js 1 = 68) reproduce exactly, as do the 7 visible files. Buckets 2–5 did not; corrected in place from an independent ts-morph classifier (literal-node scan vs identifier scan vs comment-only, corpus membership from createProject(tsconfig.json)):

bucketloggedverified
literal in non-call-site position317
identifier / property key4124
comment-only207221
TS out-of-corpus198

The out-of-corpus figure was the log contradicting itself: B4 correctly reports 51 of 82 scripts/*.ts in corpus, yet the table assigned all 18 scripts/ files plus supabase/types/database-overrides.ts to “excluded”. Ten of those scripts files and the overrides file are members.

The comment-only correction matters more, and it is the failure mode the audit was looking for: three production files sat in the prose bucket while carrying live, runtime-shipped strings — lib/mcp/tools/review.ts:219 (an MCP tool description, i.e. W5 public wire copy), lib/mcp/tools/content.ts:477,484,491 (Zod .describe() argument help), and lib/content/content-suggestions.ts:202 (sb() telemetry label 'content_items.forSuggestions'). The other eleven are test names and mock error messages. By the log’s own W5 standard — which counts pipelineName: 'mcp_create_content_item' as a residual — these are residuals, and the review.ts one is arguably W5 scope. The conclusion is unchanged: prose still dominates (64 % rather than 60 %), the tool still sees 7 of 345, and W1/W3/W4 still need rg + human.

Two numbers left as logged, both non-load-bearing: the “182 / 145 files” row reproduces as 183 / 146 under git grep -l -- '*.ts' '*.tsx' '*.py' '*.sql' minus ^__tests__/|^e2e/ (a one-file filter-definition difference), and B7’s “547 undecidable, of which 2 zero-counter” holds only on the five core counters — both rows (reference_items.superseded_by, .thumbnail_url) carry unattributableTableSites: 1, so on all six counters the answer is 0.

Evidence merge — exact. Regenerated sidecar: 765 rows / 19 tables / 8 files / sqlglot: true. Both runs 807 rows. Plain unwired 85 / undecidable 722 / 0 elsewhere; evidence unwired 78 / undecidable 547 / read-only 67 / write-only 58 / wired 57. 183 of 807 changed = 22.7 %. Transition table matches row for row (67 / 56 / 53 / 4 / 2 / 1). DR-102 v1 holds: 0 wired rows with all counters zero. The caveats.invisibleSurfaces rewrite is confirmed — the plain run lists "the Python pipeline (scripts/**/*.py)", the merged run does not.

Both spot-checked unwired→live flips confirmed at source: entity_pair_resolutions.decisionpair_resolver.py:161 SELECT decision FROM public.entity_pair_resolutions … and :181 INSERT INTO public.entity_pair_resolutions (name_a, name_b, entity_type, decision, op_id); q_a_extractions.evaluation_criteria / .extraction_metadataflow.py:2295/:2307 in the declare_row payload and flow.py:3127 in a literal raw-SQL insert column list. Both rest on confidence: exact sidecar rows, so B18’s “the flips do not depend on the G8 counter” holds.

  • W0 RESOLVED — confirmed. git grep over all tracked files (corpus-independent, so the out-of-corpus scripts are covered by construction) finds zero live .from('content_items'). Survivors: 10 ESLint RuleTester fixture strings, 2 migration comments, 2 test comments, 2 lines in .claude/checks/multi-user-patterns.md. (Logged as “3 migration comments”; immaterial.)
  • W0’s tool-coverage claim REFUTED.scripts/quality-gate.ts (8 sites) was out-of-corpus; the tool never saw the hot file” is wrong twice: the file was in corpus before PR #124 and the tool reported 17 of its sites (§3), and it has never had 8 — 18 occurrences then, 7 comment-only now. It is out of corpus today because PR #124 rewrote it and its test. Corrected in place. This makes G12 worse, not better: membership is not just unpredictable, it is unstable — an unrelated refactor silently changed what a fixed query means.
  • W4 DECAYED — confirmed exactly. 24 entity_type/entityType: 'content_item' property-value sites across 11 files: cron/freshness-transitions ×9, cron/review-cadence ×4, cron/quality-score ×2, cron/classification-quality, source-documents/[id]/send-to-review ×2, content-owners/bulk-assign, review/assignments, governance/review, plus review.ts:403, governance.ts:1306, content.ts:1871. All three plan-named sites have moved to exactly those lines. string-literal-uses --value content_item returns 6 (5 test argument + resources.ts:58) — 0 of the 24.
  • W3(a) DECAYED — confirmed. queryKeys.contentItems has five code consumers; the fifth, components/review/PublicationReviewQueue.tsx:174, is genuinely absent from the plan. Both stale line refs confirmed (use-library-bulk-actions → 104, publication-review-action-bar → 117). Factory bypass confirmed: ['content-items'] is built raw at __tests__/lib/content-browsing/use-content-bulk-runner.test.ts:34 and :112, outside query-keys.ts:22.
  • 4th KEEP-FOREVER guard — confirmed. __tests__/scripts/propagation/payload-contract.test.ts:80-92, it('excludes client-provenance tables from the payload set'), asserts content_itemsPAYLOAD_CONTRACT. The 3 Python guards are present at the logged lines and correctly produce nothing in either py run.
  • Tool probes re-run, all exact: references 5 / 11 / 8; both contentItemId probes out_of_corpus; references queryKeys 200 truncated (totalEstimated: 285); content-items, content_item_count, content_item_id all 0; MCP tool names 15 + 13 + 6 + 2 = 36; schema-coverage --table content_itemsunknown_table with the logged message and hint verbatim.

git status --short in canonical: empty. No tools/ast-dataflow* edit exists, consistent with “no trial-blocking bug found”. The verification pass ran two throwaway ts-morph probes and one detached worktree at 74a56c7d, all under /tmp and all removed; git worktree prune clean. Docs-site attributable to Trial B: tasks/id-317.md (--numstat = 1 0, one appended Notes line, frontmatter and status untouched) plus the untracked trials/ dir. src/content/docs/ledgers/initiatives/12.md is NOT Trial B’s — its diff replaces “the pg_proc/migrations scan is STILL MANUAL” with the S511 scripts/census/pg_evidence.py write-up, i.e. pure id-399 content, and its mtime (19:25) sits with the id-399/id-396 lane, seven minutes before Trial B’s first write (19:32). The initiative-12 write-back remains unclaimed by this lane. tasks/id-370.md, id-396.md, id-399.md and the three parallel-lane spec dirs were not touched.

Safe for the synthesis wave to consume, as corrected. Nine of the twelve gaps carried by the two logs reproduce unchanged; G8 is independently confirmed on three further columns at a 16.7 % false-positive rate (18.5 % pooled over six) and is now the best-evidenced defect in the register. Three claims were refuted and fixed in place — quality-gate.ts’s corpus history, G10’s missing totalEstimated, and four decomposition bucket counts — and none of them changes a conclusion or invalidates a proposed tool-work item. The one finding a reader should not inherit uncorrected is the old bucket-4 reading: “comment-only, and the tool is right to ignore it” was true of 221 files but not of the three production files carrying live content_items strings, one of which is MCP wire copy.