id-377 {377.6} — efficacy-trial gaps register (merged)
{377.6} efficacy trials — merged gaps register
Section titled “{377.6} efficacy trials — merged gaps register”Synthesis of two independent efficacy trials run against real parked backlog,
both independently verified by a second agent. Corpus: canonical @
ast-dataflow-efficacy-trials, head 8b397be1, working tree clean. Read-only —
no canonical edits in either trial or in this synthesis.
| trial | task | shape | log |
|---|---|---|---|
| A | id-292 | CV-literal rename-residual sweep (bid→procurement) | trial-a-id292-bid-literals.md |
| B | id-317 | vocabulary/reference retirement sweep (content_items) | trial-b-id317-content-items.md |
Naming correction (resolves an S510 retro item). The S510 continuation prompt’s
bl-292 / bl-317 are ordna tasks id-292 and id-317 — a loose legacy
bl- prefix, not a separate identifier space. The retro’s “no discoverable
definition” item is closed by this note. Same applies to bl-495 = the
live-breakage slice promoted out of id-317 (shipped as PR #124).
Verdict
Section titled “Verdict”Ready with caveats — as a precision instrument, not as an inventory instrument.
ast-dataflow is cleared for the codebase-cleanup effort where the question is “is this specific thing still there / still reachable?” and is not cleared where the question is “how many sites are there?”. Four caveats, each measured, each binding until the named gap lands:
- Never quote a
column-writescount. 18.5 % pooled false positives over 65 hand-checked sites; 44 % of returned rows come back for any column name. Open every site, or wait for G8. - Never read a
string-literal-useszero as absence. It means “no call-site-argument literal” — comparisons, property values and array elements are dropped silently. Measured 0/24 on id-317’s W4 (G1). - Never read any zero as corpus-covered. Membership is transitive, unpredictable and unstable — a fixed query silently changed meaning across an unrelated refactor (G12).
schema-coverage --evidenceis the one surface cleared for verdict-grade use — run it with the sidecar or not at all. It is also the only surface that handles the G8 counter correctly.
What the tool did drive end-to-end, unaided: id-317’s W5 MCP tool-name sweep
(36 sites, zero noise), the named-symbol half of W2 (references 5/5, 11/11, 8/8
with comments and string labels correctly excluded and no bleed into three
same-named local declarations), correct-zero confirmations of retirement (id-292
class 5 and class 8), the loud unknown_table on a retired table, and the
cross-language schema verdict (183/807 columns re-verdicted, 6 moved out of
unwired into live).
Part 1 — Tool gaps (owner: {377.2}/{377.4}/{377.5} tool-work)
Section titled “Part 1 — Tool gaps (owner: {377.2}/{377.4}/{377.5} tool-work)”Ranked by impact × effort. Impact asks one question: does the gap produce a WRONG verdict, or merely a slow/awkward one? Every wrong-verdict gap outranks every ergonomics gap. “Two-directional” marks a gap independently hit by both trials from different task shapes — the strongest signal in the register.
| rank | id | impact | effort | two-dir | one-line |
|---|---|---|---|---|---|
| 1 | G8 | wrong verdict — measured | low–med | ✅ (A-verifier + B, B-verifier) | column-writes fabricates column attribution |
| 2 | G1 | wrong verdict — silent zeros | med | ✅ | string-literal-uses drops the dominant literal shapes |
| 3 | G2+G3 | makes every other gap undetectable | low | ✅ | zeros and unknown tables/columns carry no caveat |
| 4 | G12 (⊃G6) | wrong verdict — retroactively | low (disclose) / high (policy) | ✅ | corpus membership is transitive, unpredictable, unstable |
| 5 | G11 | coverage hole — largest measured | low | — | no identifier-text query exists |
| 6 | G9 | blocks provable closure | med | — | references has no member-level resolution |
| 7 | G4 | wrong error kind on the house idiom | low | — | enum-uses blind to as-const arrays |
| 8 | G10 | ergonomics | low | — | references truncation has no narrowing path |
| 9 | G5 | ergonomics | low | — | fixture-uses is whole-token only |
| 10 | G7 | throughput | low | ✅ | ~5–12 s cold load per literal query |
Rank 1 — G8: column-writes fabricates column attribution
Section titled “Rank 1 — G8: column-writes fabricates column attribution”The register’s headline. Not an ergonomics wart: it is a correctness defect in the query that prior guidance told readers to trust, and it is what refuted Trial A’s original “column-reads/column-writes are the stars, zero over-reports” scorecard line.
Measured, twice, independently, on disjoint column sets:
| measurement | sites | false | rate |
|---|---|---|---|
Trial B (B17) — record_lifecycle.lifecycle_type, notifications.entity_type, notifications.read_at | 17 | 4 | 23.5 % |
Trial B verifier — source_documents.publication_status, q_a_pairs.answer_standard, form_instances.workflow_state | 48 | 8 | 16.7 % |
| pooled — 6 columns / 5 tables, every site opened | 65 | 12 | 18.5 % |
Sharpest repro — 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 48 rows
(44 %) come back for any column name whatsoever.
Two mechanism findings the fix must absorb:
confidencedoes not discriminate. All 65 pooled rows areindirectandisTyped: false; the path that would emitexact(inspectWriteArg,column-writes.ts:106-173) requires a typed client anddetectIsTypedreturns false on every.from()chain probed. In this repo every row isindirect, so the label separates nothing. Repo-wide only 115 of 807 columns haveexactWrites>0against 616 withindirectWrites>0.- The one-hop chase is too narrow.
resolveOneHopObjectLiteralfollows onlyconst x = { … }..insert(arr.map(…)), loop-built identifiers andsatisfies/as-wrapped literals all fall through to “cannot rule out”, which is why fixture builders dominate the false set. - Smoking gun:
lib/notifications.ts:96is returned for bothnotifications.entity_type(true) andnotifications.read_at(false) — the same site attributed to two different columns.
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. That is exactly backwards from what a retirement wave needs.
Proposed fix. Give writes the wildcard honesty reads already have: when the
queried column is not provably a key of the payload, emit columnPath: "*" +
confidence: "wildcard" (or attributed: false with an explicit “writes an
unattributed column of <table>” message), and stop the count reading as a
per-column answer. A --include-indirect flag can preserve today’s behaviour for
callers who want the superset. A reference implementation already exists in this
codebase on the Python side: an attributable declare_row payload key is
exact, an unattributable resolved site emits a table-scoped * smoke row plus a
loud caveat. The fix is DR-102’s declaration-vs-proof discipline applied to
column-writes’ presentation layer.
Contamination audit — does any already-shipped conclusion rest on this?
Section titled “Contamination audit — does any already-shipped conclusion rest on this?”Asked of every consumer of column-writes output found across the docs-site and
the skill surface.
| artefact | rests on column-writes? | verdict |
|---|---|---|
{377.4} evidence merge (schema-coverage --evidence, PR #150) | consumes the same counter, does not promote it | CLEAN — no re-derivation. Independently re-verified: 585 rows (plain) / 522 (evidence) have indirect-only write evidence and 0 are verdicted wired/write-only — all stay undecidable. All 6 unwired→live flips rest on confidence: exact sqlglot rows (pair_resolver.py:181, flow.py:2295/2307), confirmed at source. |
| DR-102 (declaration ≠ write proof) | no | HOLDS. Re-verified directly: 0 of 807 rows carry verdict wired with all five evidence counters zero. |
| initiative-12 census recipe (Discipline ¶) | no — the recipe is ast-dataflow-py schema-uses → schema-coverage --evidence | UNAFFECTED. |
| id-292 / id-317 task Notes | yes, quoted | ALREADY CORRECTED. Both Notes were written post-G8 and carry the caveat plus hand-verified site lists. |
id-69 RESEARCH.md §3.3 + PRODUCT.md BI-2/L206 | yes — and the reasoning is invalid | CONTAMINATED but MOOT — no re-derivation required. See below. |
canonical .claude/skills/ast-dataflow/SKILL.md:178-189 | yes — “every TS file that writes a Supabase column” | WRONG AS WRITTEN — live guidance. Needs a caveat now (proposal C1). |
canonical .claude/skills/write-tech-spec/SKILL.md:83-86 | yes — “the correct scope for migration-safety analysis” | WRONG AS WRITTEN — live guidance. Needs a caveat now (proposal C1). |
id-69, in detail. Two distinct problems, both real, neither actionable:
RESEARCH.md§3.3 concluded an “identical, exhaustive writer set (8 sites)” from runningcolumn-writes --table content_item_workspaceson--column workspace_idand--column content_item_idand getting the same answer. Under G8, identical-across-columns is the fingerprint of the defect, not corroboration of exhaustiveness — a table-level result is invariant in the column argument by construction. The conclusion probably survives on its merits (content_item_workspacesis a narrow junction table, and G8’s FP rate tracks omitted optional columns, so the floor there is near zero) — but the evidence offered for it proves nothing.PRODUCT.mdBI-2 (L200-206) makes the negative case an acceptance criterion: “No writer setssource_documents.workspace_idon the canonical path (verifiable by ast-dataflowcolumn-writes --table source_documents --column workspace_idshowing no canonical-path writer).” That check is not executable as written:source_documentshas a measured 10-row noise floor for any column name, so the query can never show zero. The criterion would have to be discharged by opening all ten.
Neither needs re-derivation: id-69 is archived (closed S371) and both subject
tables (content_items, content_item_workspaces) were eliminated by id-131. The
finding is recorded so that (a) nobody resurrects BI-2 as a runnable gate, and
(b) the “identical across two columns ⇒ exhaustive” inference is retired as a
reasoning pattern.
Rank 2 — G1: string-literal-uses drops the dominant literal shapes
Section titled “Rank 2 — G1: string-literal-uses drops the dominant literal shapes”Two-directional, and the only gap that produced silent zeros over load-bearing
production code in both trials. The query classifies five call-site contexts
(viMock / jsxProp / sqlTag / envKey / argument) and drops comparisons, object-property
values, array elements, type-position literals, return statements and const
initializers. Intentional by design (queries/string-literal-uses.ts:118-120,
“call-site context search, not raw text search”) — and exactly wrong for the CV /
rename / retirement sweeps that are the cleanup effort’s whole shape.
Measured: 6 of id-292’s 8 classes under-report, three to zero, including
lib/freshness.ts:103 (=== 'bid_discovered', load-bearing) and
lib/layer-inference.ts:94 (=== 'bid_library', the load-bearing inference rule).
id-317: 0 of 24 W4 entityType: 'content_item' property-value sites, 0 of 7
W3(a) ['content-items'] array elements, 0 of 6 pipelineName: 'mcp_create_content_item' telemetry values.
Fix: add comparison, caseClause, propertyValue, arrayElement,
typeLiteral, initializer kinds (or an --all-contexts flag defaulting on),
keeping today’s set as a filter. Trial B raises the priority: property-value is
the single most common persisted-enum shape in this repo. For comparison,
carry the LHS expression text (args.type, lifecycle_type) — that is what makes
load-bearing-vs-stale triage mechanical rather than manual (O2).
Both trials independently reached the same secondary conclusion, which is the
reason to fix rather than abandon this query: with G1’s kinds, it would beat
rg. Its silence on 221 comment-only files is exactly right, its 10 correct
suppressions of ESLint RuleTester code: fixture strings are noise rg cannot
filter, and its correct-zero on from_bid (survivors are comments) is the proof.
Rank 3 — G2 + G3: the silence problem
Section titled “Rank 3 — G2 + G3: the silence problem”Merged: one fix, one reference implementation, and together they are what make every other gap in this register undetectable by the user.
- G2 — zero results carry no caveat. Neither the kind filter (G1) nor corpus membership (G12) is stated in any response, so “no sites exist”, “no sites in the shapes I search” and “your target isn’t in my corpus” are structurally identical payloads.
- G3 —
column-reads/column-writeson an unknown column or an unknown table return silent[], whileschema-coverage— same binary, same generated types — reportsunknown_tableloudly with a hint. Trial A hit this live on the droppedworkspaces.type, where “0 readers” and “the column was deleted” were indistinguishable. Refinement the fix must handle: for an unknown column on a real table the two queries diverge —column-readsreturns honestly-labelledcolumnPath: "*"/confidence: "wildcard"rows, whilecolumn-writesreturns rows falsely stamped with the bogus column name. “Silent[]” understatescolumn-writes: it fabricates (this is the same defect as G8, reached from the other side).
Fix (O3): treat schema-coverage’s response envelope as the house style and
retrofit it to every query — caveats block (kinds searched, corpus summary,
excluded surfaces, merged sidecars), loud unknown_table/unknown_column, and a
summary histogram. No new analysis is required; the good implementation
already ships. Lowest effort, highest leverage in the register.
Rank 4 — G12 (widening of G6): corpus membership is unstable
Section titled “Rank 4 — G12 (widening of G6): corpus membership is unstable”The boundary is not “scripts are excluded”. tsconfig.exclude lists scripts, yet
51 of 82 scripts/*.ts are in corpus via transitive import from
__tests__/scripts/*; scripts/eval-classification.ts is IN, scripts/quality-gate.ts
is OUT, with no way to tell from the CLI.
Worse, and this is the finding that promotes G6 to G12: membership is not stable
across unrelated refactors. Proven by a detached worktree at 74a56c7d (PR #124’s
parent) running that commit’s own binary: string-literal-uses --value content_items
returned 27 hits / 11 files exactly, of which 17 came from
scripts/quality-gate.ts. Today the same query returns 7. PR #124 rewrote that
file and its test, dropping it out of the import graph — so a fixed query silently
changed meaning because of a refactor that had nothing to do with it. (Ruled out as
a methodology artefact: string-literal-uses.ts has two commits and the later one
only swaps in truncateSpatial; classifyLiteralKind and the five kinds are
untouched.)
Fix: short term, a CLI corpus-info (MCP-only today) plus a per-response
corpusSummary (file count, top-level dirs, and whether the --file/--symbol
target is a member) — fold into the G2/G3 envelope work. Long term, the
pin-or-declare-the-corpus decision, which is already open inside {377.4}; the
trials supply the evidence that it is a correctness question, not a tidiness one.
Rank 5 — G11: no identifier-text query exists
Section titled “Rank 5 — G11: no identifier-text query exists”The largest pure coverage hole measured. The dominant residual shape in a
vocabulary retirement — 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).
Fix: identifier-uses --name <ident> over Identifier / PropertyAssignment
/ PropertySignature nodes — cheap, no type resolution. Route through {377.2}
first (see the corpus-gate note below): rg already answers this, and an LSP
“find references” partly does, so it is precisely a build-vs-integrate question
rather than an obvious build.
Ranks 6–10 — the remainder
Section titled “Ranks 6–10 — the remainder”- G9 —
referenceshas no member-level resolution:file.ts:propNamefor an interface/type property errorsout_of_corpus, the wrong kind (same mis-kind as G4 — “unsupported granularity” reported as “not in corpus”). This is what blocked provable closure on both of id-317’s atomic-set problems (the W2contentItemIdwrite-back+sweep pair; W3(a)queryKeys.contentItems) — the exact case where a rename must be provably complete. Fix: supportFile.ts:Interface.memberaddressing; distinguishunsupported_granularityfromout_of_corpus. - G4 —
enum-usessupports enums and as-const objects but not as-const arrays, which is the actual KH CV convention. LiveFINAL_AWARD_FORM_TYPES(lib/validation/schemas.ts:1140) andVALID_PUBLICATION_STATUSES(lib/governance/publication-transitions.ts:31) both errorout_of_corpus. 3 of 3 probes failed in Trial A. Cheap. - G10 —
referenceson a hub object returns 200 rows withtruncated: trueandtotalEstimated: 285; the count is honest, but there is no--offset/--limitand no hint, so the 85 unseen rows are unreachable and closure is unprovable. Fix: pagination + a hint pointing at member-scoped addressing (G9). - G5 —
fixture-uses --needleis whole-token exact-match;bid_libraryinside"scripts/import_bid_library.py"is invisible with no flag to widen. Fix:--substring, or substring-by-default with amatchedfield showing the containing token. - G7 — ~0.5–1.4 s for
referencesbut 5–12 s perstring-literal-uses(cold project load dominates); Trial A spent ~2.5 min on redundant loads across 20 queries. Fix: accept multiple--valueargs per invocation to amortise one load across a literal set. Explicitly cheaper than registering the MCP server early, which DR-100’s rider defers to extraction.
Opportunities (not gaps — no wrong answer, but real leverage)
Section titled “Opportunities (not gaps — no wrong answer, but real leverage)”| id | what | trigger |
|---|---|---|
| O1 | No query connects a const to its literal value (LAYER_BID_DETAIL ↔ 'bid_detail'). references tracks the symbol, string-literal-uses the string, nothing joins them. Fix: carry literalValue on references rows for string-const declarations | Trial A R7 |
| O2 | Load-bearing-vs-stale classification stays human judgment. A comparison kind carrying the LHS expression text gets most of the way | Trial A R11 |
| O3 | schema-coverage is the reference implementation for output ergonomics. Retrofitting its envelope closes G2 and G3 with existing code | Trial B B5/B7 |
| O4 | unwired is under-qualified — 6 of 85 plain-run unwired verdicts (7 %) are live Python-written columns. The caveat names the Python pipeline but the verdict token reads as “dead”. Fix: unwired-in-ts, or a per-row externalSurfacesUnmeasured: true, whenever no sidecar was merged | Trial B B8 |
| O5 | The absence-guard shape (expect(tables.has('content_items')).toBe(false)) is a first-class artefact of any retirement task and is currently invisible. Once G1 lands, a --kind comparison,arrayElement sweep becomes a mechanical way to find absence guards — worth a documented recipe in the skill | Trial B B3/B16 |
Hard-case corpus check — and why it could not adjudicate 11 of 12 gaps
Section titled “Hard-case corpus check — and why it could not adjudicate 11 of 12 gaps”Every proposal above was checked against the 25-hard-case acceptance corpus
(research/declarative-surface-inventory.md). Result, stated plainly because it
is itself a finding:
| proposal | newly covers | does NOT cover |
|---|---|---|
| G8 (wildcard-honest write attribution) | the TS analogues of hard cases 7 (payload built in a loop into a dict), 8 (** spread inside a value), 15 (comprehension/.map() payload to .insert/.upsert), 24 (splatted parameter tuple built earlier) — all four are “the payload is not a resolvable literal”, which is precisely the class resolveOneHopObjectLiteral falls through on | 16 (on_conflict= column strings) and 17 (chain-modifier-only mentions) stay unaddressed — they are a detector question, not an attribution one |
| G1, G2, G3, G4, G5, G7, G9, G10, G11, G12 | none of the 25 | — |
The gate is mis-scoped, not the proposals. The 25 hard cases are a Python
declarative-write acceptance corpus, built for {377.4} against
scripts/cocoindex_pipeline/**; ten of the twelve gaps are TypeScript-surface
detector and output-contract gaps it was never built to adjudicate. G12 is a
particularly clear illustration: the corpus cannot speak to corpus instability at
all, because ast-dataflow-py scans a directory and never touches tsconfig
reachability, so the failure mode does not exist on that side.
Applying “reject proposals that cover nothing in the corpus” literally would reject eleven of twelve proposals, ten of which are backed by direct measurement on real backlog (18.5 % FP over 65 opened sites; 0/24 detection; a proven retroactive meaning-change). Nothing is rejected on corpus grounds. The trials’ own measurements are the stronger gate, and the honest conclusion is the missing artefact:
Proposal T1 — build the TS-side acceptance corpus. There is no TypeScript equivalent of
declarative-surface-inventory.md, which is why the TS queries shipped with these gaps undetected. The two trials already produced the seed: 65 hand-checkedcolumn-writessites with per-site verdicts, the 345-filecontent_itemsdecomposition (7 visible / 17 literal-wrong-position / 24 identifier / 221 comment-only / 8 out-of-corpus / 68 non-TS), and 8 id-292 literal classes with per-class ground truth. Signed off 2026-07-29 and folded into {377.7} as its first deliverable — not a separate record, because the corpus gap is what let the other six gaps ship undetected, so it gates the detector work rather than sitting beside it.
Where each gap folds
Section titled “Where each gap folds”| gap | folds into | why |
|---|---|---|
| G8 | {377.4} | It is the write-attribution layer DR-102 already governs; the Python side implements the correct discipline and the TS presentation layer does not |
| G12 (⊃G6) | {377.4} | Already open there as the corpus pin/declare decision — the trials add the instability proof that makes it a correctness item |
| G11 | {377.2} | rg and LSP find-references already answer identifier search; “do NOT replicate what exists” applies directly — integrate/wrap/build is the gate |
| G1 (comparison kind vs Postgres LS / category-5 tooling) | {377.2} then build | Worth one integrate-check before building six kinds; the rest of G1 has no external substitute |
| G2, G3, G10, G7 | {377.5} | Output-contract and CLI-ergonomics work that must land before extraction — you do not ship a package whose queries disagree on their error contracts. Pairs with the /mcp-builder audit already in {377.5} |
| G4, G5, G9, O1, O2, O5 | {377.7} | Minted 2026-07-29 on owner sign-off as the TS detector-coverage wave — no existing subtask covers TS detector granularity. T1 is folded in as its first deliverable, not a separate record: the missing TS acceptance corpus is why these six shipped undetected, so it gates the detector work |
| O4 | {377.4} | unwired qualification is an evidence-semantics question, same family as DR-102 |
Part 2 — Cleanup findings (owner: the codebase-cleanup effort / the named task)
Section titled “Part 2 — Cleanup findings (owner: the codebase-cleanup effort / the named task)”Deliberately separated from Part 1: different owners. Nothing here is a tool change; nothing in Part 1 is a codebase change. All of the following is already written into the relevant task file’s Notes except where marked NEW.
id-292 (bid→procurement literal residue) — stays parked, re-scoped
Section titled “id-292 (bid→procurement literal residue) — stays parked, re-scoped”- Class 2 grew a coupled surface.
lifecycle_typemoved to therecord_lifecyclefacet (id-131). The id-131 freshness RPC (20260702130000_id131_freshness_rpcs.sql:95) value-compares'bid_discovered'in SQL, so the “backfill UPDATE + TS literals atomically” plan must also patch thecompute_freshness-family RPC bodies. Real TS write surface is 2 sites (lib/mcp/tools/content.ts:1226,e2e/fixtures/test-data-fixture.ts:305), not the 5column-writesreported — the other three write governance columns only (G8). Pipeline reads it atl_records.py:1194. - Class 7 is cheaper than written, and narrower. The
workspaces.typeTEXT column is dropped (discriminator isapplication_type_id), so “the DB still emits the alias” no longer holds. The surviving alias is one MCP tool (lib/mcp/tools/workspaces.ts:36,76) — retirement is an MCP-deprecation decision, not a DB data migration. - Stale-comment cleanup candidate (NEW, standalone).
app/api/workspaces/route.ts:140carries a comment describing a legacy-alias mapping that does not exist in the code below it —WorkspaceCreateBodySchema.typeisz.enum(getValidTypeValues())andlib/workspace-types.ts:23-30returns six keys, none of them'bid', so the route rejects the alias at validation. The comment documents intent that was never implemented or was later removed. Delete it independently of any class-7 decision — it actively misleads. - KEEP-list entry is moot.
VALID_BID_STATUSES/TERMINAL_BID_STATUSEShave zero repo-wide hits andtypes/bid.tsno longer exists. The other two KEEP entries (bid_responsereorient tag;'bid'form_types CV) are live and stay protected. - Stale details-field refs (NEW):
app/api/items/route.tsno longer exists andlib/queue/handlers/batch-reclassify.tsno longer carries the literal — both are still named in id-292’s Details field. - Net: 5 live classes (1, 2, 3, 4, 6) + premise-changed 7; class 5 resolved, class 8 gone. Priority unchanged.
id-317 (content_items retirement) — stays parked, two waves decayed
Section titled “id-317 (content_items retirement) — stays parked, two waves decayed”- W0 RESOLVED — bl-495 shipped as PR #124 (
c6d28d2a+a0178af4). Zero live.from('content_items')repo-wide; survivors are 10 ESLintRuleTesterfixture strings plus comments. - W3(a) DECAYED — and carries a live invalidation hazard. Five code consumers,
not four: the plan misses
components/review/PublicationReviewQueue.tsx:174. Two stale line refs (use-library-bulk-actions88→104,publication-review-action-bar114→117). Hazard:['content-items']is built raw, outside the key factory, at__tests__/lib/content-browsing/use-content-bulk-runner.test.ts:34,112— an invalidation that silently misses is exactly the failure mode W3(a) exists to prevent. - W4 DECAYED — badly under-inventoried. The plan named 3
entity_type='content_item'sites; the actual count is 24 property-value sites across 11 files (cronfreshness-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, plusreview.ts:403,governance.ts:1306,content.ts:1871). All three plan-named sites have moved. Backfill still not needed (S483 finding unchanged). - Three production files were mis-filed as prose (NEW, carried from the Trial B
verification pass). They ship live
content_itemsstrings at runtime:lib/mcp/tools/review.ts:219(MCP tooldescription— arguably W5 public wire copy),lib/mcp/tools/content.ts:477,484,491(Zod.describe()argument help),lib/content/content-suggestions.ts:202(sb()telemetry label'content_items.forSuggestions'). By the plan’s own W5 standard — which countspipelineName: 'mcp_create_content_item'as a residual — these are residuals. - 4th KEEP-FOREVER guard, TS-side:
__tests__/scripts/propagation/payload-contract.test.ts:80-92assertscontent_itemsis excluded fromPAYLOAD_CONTRACT. Add it to the list — it is invisible to a literal-driven sweep (array element, G1), so such a sweep would neither see nor protect it. - W1 holds (221 comment-only files incl. tests, 116 outside
__tests__); W2, W3(b), W5 hold. W5 naming remains owned by id-71’s verdict process.
Tool-documentation cleanup (NEW — canonical repo, not docs-site)
Section titled “Tool-documentation cleanup (NEW — canonical repo, not docs-site)”Proposal C1 — approved 2026-07-29 and dispatched separately. Out of this task’s edit scope (docs-site only), so recorded here rather than applied; the caveats land in the canonical repo.
Two live guidance surfaces state the G8 defect as fact and should carry a caveat until G8 lands:
.claude/skills/ast-dataflow/SKILL.md:178-189— “column-writes — every TS file that writes a Supabase column”. It is not: it is every TS file that writes some column of that table, presented under the queried column’s name..claude/skills/write-tech-spec/SKILL.md:83-86— directs spec authors to runcolumn-writeson any column a spec touches, calling it “the correct scope for migration-safety analysis”. That is the exact use that produced id-69’s invalid §3.3 inference.
Minimal fix pending G8: one line in each — “counts are table-level; open every site before acting (id-377 {377.6} G8).”
Method note
Section titled “Method note”Both trials ran the same protocol — tool answer, then independent rg/Read
ground truth, then verdict — and both were audited by an independent second agent
who re-derived every headline number from source rather than from the log’s own
output. That audit refuted claims in both logs (Trial A’s “zero over-reports” on
column-writes, which produced G8; Trial B’s quality-gate.ts corpus history, its
totalEstimated claim, and four decomposition bucket counts). The corrected text
is what this register consumes. Gap numbering is settled: G1–G7 Trial A,
G8 Trial A’s verifier, G9–G12 Trial B.
Strengthened rather than doubted by the audits, and therefore safe to build on:
the 27→7 decay is proven (detached worktree at 74a56c7d reproduces 27 hits / 11
files exactly), the 183/807 evidence flips reproduce exactly, DR-102 holds, both
spot-checked unwired→live flips confirmed at source, and all four W-plan
re-scores confirmed.