Cocoindex Absorption Cleanup (ID-46 / T14) — TECH
Cocoindex Absorption Cleanup (ID-46 / T14) — TECH
Section titled “Cocoindex Absorption Cleanup (ID-46 / T14) — TECH”Context / Problem
Section titled “Context / Problem”ID-46 is a mechanical pure-deletion task: it retires a bounded, verified-dead residual
set of standalone scripts + dead lib/ exports left after (a) cocoindex
(scripts/cocoindex_pipeline/) became the sole canonical ingest/writer path, and (b) the
OLD Python pipeline core (scripts/kb_pipeline/ + its CLIs + backfills + ~32 tests + 3 TS
guards) was already deleted under ID-68.23 (50b3b2096, 08/06/2026). This TECH maps a
one-to-one Proposed-change against each of PRODUCT’s 13 invariants, fixes a safe deletion
ordering with per-slice green gates, and carries the implementation decomposition inline
(no separate {46.4} PLAN — the residual is a single bounded wave; RESEARCH §E).
No new code. No schema migration. No dependency removal. ID-46 touches neither
package.json nor requirements.txt: the HTML npm deps (@mozilla/readability, jsdom,
@mendable/firecrawl-js) are ID-42 {42.13}‘s to remove, and unpdf / jsdom /
pdfplumber / openpyxl / python-docx / faiss-cpu all stay (live consumers in
upload/images routes, ontology loader, cocoindex form_extractors, ID-52, ID-53). The
Executor must not edit either manifest.
Code-intelligence orientation (re-run on worktree subo-id-46 @ e8e981c0a, cited verbatim)
Section titled “Code-intelligence orientation (re-run on worktree subo-id-46 @ e8e981c0a, cited verbatim)”Per OQ-3 / Inv-2 discipline, the load-bearing pins from RESEARCH §G were re-confirmed at TECH-authoring time against the current worktree HEAD. Tool output verbatim:
extractQaPairs(lib/procurement-library-ingest/extract-qa-pairs.ts):gitnexus_impact({target:"extractQaPairs", direction:"upstream"})→"impactedCount": 0, "risk": "LOW", "summary": {"direct": 0, "processes_affected": 0, "modules_affected": 0}, "byDepth": {}. 0 callers. WithincludeTests:truethe count stays 0 (gitnexus excludes the in-file def; ast-dataflowreferencesin RESEARCH §G/§B separately recorded 17 refs = 1 def + 16__tests__/**).docx-to-markdown.ts:grep -rln "docx-to-markdown" lib app scripts --include='*.ts'→ single hitlib/procurement-library-ingest/extract-qa-pairs.ts. Sole in-repo importer is the Q&A parser — retires as a pair.normaliseTitleForDedup(lib/dedup-normalise.ts):gitnexus_impact({target:"normaliseTitleForDedup", direction:"upstream"})→"impactedCount": 0, "risk": "LOW", "summary": {"direct": 0, …}, "byDepth": {}. 0 callers (RESEARCH §G: ast-dataflowimporters --module @/lib/dedup-normalise= 1, the test).scripts/dedup.py+scripts/dedup_normalise.py:grep -rn -E "import +dedup…" scripts --include='*.py' | grep -v scripts/tests/→ single hitscripts/dedup.py:21: from dedup_normalise import normalise_title_for_dedup(a self-import).grep -rln "dedup_normalise" scripts --include='*.py'→scripts/dedup.py,scripts/tests/test_dedup_normalise.py. No live (non-test) Python importer of either.lib/extraction/extraction-result.ts(createPipelineExtractionResult,PipelineExtractionResult):gitnexus_context({name:"createPipelineExtractionResult"})→"incoming": {}(0 incoming refs);grep -rln "extraction-result|PipelineExtractionResult| createPipelineExtractionResult" lib app scripts components --include='*.ts*' | grep -v __tests__→ single hit = the file itself. 0 external consumers.lib/extraction/og-metadata.ts(extractOgMetadata):gitnexus_context({name:"extractOgMetadata"})→"incoming": {"calls": [{"name": "extractFromUrl", "filePath": "lib/extraction/url.ts"}]}. Sole consumer isextractFromUrlinlib/extraction/url.ts— the file ID-42{42.12}rewrites. Soog-metadata.tsis NOT yet dead; it is RETIRE-WHEN-DEAD, gated on ID-42.- batch-reclassify CLIs:
grep -n -E "from .@/lib/ai/classify|classifyContent|isExcludedEntity|validateDomain"→batch-reclassify.ts:29 import { isExcludedEntity, validateDomain } from '@/lib/ai/classify';batch-reclassify-keywords.ts:20 import { classifyContent } from '@/lib/ai/classify';reclassify-items.ts:24 import { classifyContent } from '@/lib/ai/classify'. All three import the live classify surface — deleting the scripts drops 3 callers of a module that retains 15+ others. - reader-html / firecrawl scripts:
grep -nE "<script names>" package.json→ exit 1 (no match);grep -rnE "<script names>" .github/→ exit 1 (no match). Zeropackage.json/ CI references.scripts/backfill-reader-html.ts:22-23importsJSDOM+Readabilityand is the sole importer ofextract-reader-html.ts.scripts/backfill-source-url-firecrawl.ts:196lazy-imports@mendable/firecrawl-js. reader_htmlcolumn readers (KEEP — column STAYS):grep -rln "reader_html" lib app components --include='*.ts*' | grep -v __tests__→ 6 hits, butapp/api/review/queue/route.ts:23matches only a comment (“REVIEW_COLUMNS … excludes embedding, summary_data, reader_html …”) and is NOT a reader. The true readers are:app/item/[id]/item-detail-client.tsx,lib/validation/schemas.ts,components/reader/source-metadata.tsx,components/item-detail/reader-view.tsx,components/item-detail/editor-view.tsx. The populator SCRIPTS retire; the column does not.scripts/extract_docx_tables.py(RETIRE — dead standalone CLI; reclassified KEEP→RETIRE during {46.3} on corrected grounding — see §F-2 correction below):grep -rn "extract_docx_tables" scripts --include='*.py'(non-self, non-test) → single hitscripts/extract_tender_questions.py:3, which is a docstring (“Adapted from extract_docx_tables.py — reads tables from .docx files…”), NOT an import. The file is a standalone CLI (if __name__ == "__main__"at:500, callingextract_qa_from_docxdefined:424). The cocoindexform_extractors/docx.pydoes NOT import it —docx.py:84,89,90importscripts.analyse_template,scripts.docx_utils(open_document_safe), andscripts.extract_tender_questions(_classify_header). The only.github/workflows/ci.ymlhit (:177) is a comment naming its TEST filetest_extract_docx_tables.pyas a quality-shard member, not a live invocation of the source. Conclusion:extract_docx_tables.pyis the dead OLD precursor to the LIVEextract_tender_questions.py, superseded by cocoindex form extraction — 0 live importers, test-only consumer → RETIRE (slice S-f /{46.10}). PRESERVE the LIVEscripts/extract_tender_questions.py+scripts/docx_utils.py(both imported by cocoindexform_extractors/docx.py) — they are NOT targets. (This file was NOT in the original briefed residual set; it surfaced during {46.3} and was reclassified KEEP→RETIRE on corrected grounding — a scope addition the parent should note.)- 3 ID-68.23-removed TS guards (confirm none remain to read a deleted source):
filesystem check →
__tests__/validation/pipeline-parity.test.ts,__tests__/validation/content-history-change-reason.test.ts,__tests__/lib/extraction-result-parity.test.tsall GONE. The only__tests__/*.tsstill referencing a to-be-deleted module name are the 3 dedicated tests that co-delete with their targets (parity.integration.test.ts,dedup-normalise.test.ts,extract-qa-pairs.test.ts). - ID-42 merge state (for the RETIRE-WHEN-DEAD gate):
bun scripts/ledger-cli.ts show task 42→"status": "in_progress"(subtasks {42.5-42.13} not done). ID-42 has NOT merged — at this authoring time the RETIRE-WHEN-DEAD slice stays blocked.
ccc search fallback was not required (gitnexus returned all symbol pins cleanly; this is a
mature, fully-indexed corpus, not greenfield).
What ID-46 deletes vs preserves vs carves out (the boundary)
Section titled “What ID-46 deletes vs preserves vs carves out (the boundary)”- RETIRE (slices S-a..S-d + S-f, 0-caller verified, deletable now once the T7/ID-28/ID-42 gate
opens): the Q&A parser pair, the dead dedup substrate (TS + Python), the 3 batch-reclassify
CLI scripts, the 3 reader-html/firecrawl backfill scripts, and the dead standalone
scripts/extract_docx_tables.py(S-f, reclassified KEEP→RETIRE — see §Context / §F-2) — plus their dedicated tests. - RETIRE-WHEN-DEAD (slice S-e, BLOCKED-ON ID-42 merge):
lib/extraction/og-metadata.ts(sole consumer =url.ts, rewritten by ID-42 {42.12}) +lib/extraction/extraction-result.ts(0 consumers; its parity guard already gone). Deletes only after ID-42 lands AND a fresh 0-caller check confirms them dead. - PRESERVE (never a target):
resolve-question.ts+extract-answer.ts(live callers),lib/ai/classify.ts(15+ live callers), thereader_htmlcolumn + its 5 readers, the remaininglib/extraction/*files (turndown, pdf, clean-mdx-tags, markdown-front-matter, markdown-title, diff-markers, content-type-detect, url-validation),scripts/extract_tender_questions.pyscripts/docx_utils.py(LIVE — imported by cocoindexform_extractors/docx.py),lib/dedup.ts+lib/entities/entity-dedup.ts, and every dependency.
- CARVE OUT (owned elsewhere):
lib/extraction/{html,url}.ts+ 3 HTML deps (ID-42); the app-side chunker +content_chunkswrites +markdown_batchUI (ID-56); thebatch_reclassifyqueue handler / job-type (backlog-188, UNDECIDED);analyse/route.ts+template_analyse(ID-52); Stage-5 entity resolution (ID-53); orphan + 6 telemetry columns +summary_data(separate schema-cleanup migration; backlog-189 reference is stale — OQ-46-8).
Proposed changes (one-to-one against PRODUCT invariants 1-13)
Section titled “Proposed changes (one-to-one against PRODUCT invariants 1-13)”| PRODUCT invariant | Proposed change (mechanical) | Slice | Proof command |
|---|---|---|---|
| 1 Q&A parser pair deleted | git rm lib/procurement-library-ingest/extract-qa-pairs.ts lib/procurement-library-ingest/docx-to-markdown.ts + the dedicated tests __tests__/lib/procurement-library-ingest/extract-qa-pairs.test.ts and the Q&A-parser cases in __tests__/integration/procurement-library-ingest/parity.integration.test.ts | S-a | bun run knip (no orphan), bun run test (green, no import of deleted module), bun build |
| 2 TS dedup-normalise helper deleted | git rm lib/dedup-normalise.ts __tests__/lib/dedup-normalise.test.ts | S-b | bun run test, bun run knip, bun build |
| 3 Standalone Python dedup scripts deleted | git rm scripts/dedup.py scripts/dedup_normalise.py scripts/tests/test_dedup_normalise.py | S-b | python3 -m pytest scripts/tests/ (green, no import error) |
| 4 Three batch-reclassify CLI scripts deleted | git rm scripts/batch-reclassify.ts scripts/batch-reclassify-keywords.ts scripts/reclassify-items.ts | S-c | bun run knip (verify lib/ai/classify exports NOT newly-orphaned — see Edge cases), bun build, bun run test |
| 5 Reader-html populator scripts deleted | git rm scripts/extract-reader-html.ts scripts/backfill-reader-html.ts (delete backfill-reader-html.ts first or together; it is the sole importer of extract-reader-html.ts) | S-d | bun run knip, bun build |
| 6 Firecrawl backfill script deleted (delete-if-present) | git rm scripts/backfill-source-url-firecrawl.ts iff present (ID-42 may have absorbed it via its own OQ — check merge state, tolerate already-gone) | S-d | bun run knip, bun build; test -f guard before git rm |
| 7 Zero dead-code residue | After S-a..S-d: grep sweep over .ts / .py / .sql / package.json / .github/ confirms no surviving import/re-export/reference of any deleted path; bun run knip clean-or-baseline | S-a..S-d (final gate) | grep sweeps (below) + bun run knip |
8 Live procurement-library-ingest/ members survive | No change. resolve-question.ts + extract-answer.ts untouched; the directory is NOT removed (only the 2 dead files within it) | (preserve) | bun run test (callers app/api/items/[id]/route.ts, app/api/items/batch/route.ts, lib/mcp/tools/content.ts compile/pass) |
9 lib/ai/classify.ts survives intact | No change to the module; only its 3 CLI-script callers go (inv 4). 15+ live callers unchanged | (preserve) | bun build + bun run test; knip must NOT report classifyContent/isExcludedEntity/validateDomain newly-dead (Edge cases) |
10 reader_html column + readers untouched | No change. No migration authored; the 5 readers unchanged; only the populator scripts go (inv 5) | (preserve) | grep reader_html over supabase/migrations/ shows no new migration; readers compile |
11 Remaining lib/extraction/* + deps untouched | No change. No lib/extraction/{turndown,pdf,clean-mdx-tags,markdown-front-matter,markdown-title,diff-markers,content-type-detect,url-validation}.ts deletion; no package.json/requirements.txt edit | (preserve) | git diff --name-only shows zero manifest changes; bun build |
| 12 Execution stays T7 + ID-28 + ID-42 gated | Process, not a file change. All execution subtasks are imp_deferred; per-file 0-caller re-verify at execution time; gate ungating is the orchestrator/Liam’s call (OQ-46-10), NOT decided here | (all slices) | n/a — gate is an execution precondition; restated in every subtask details |
13 RETIRE-WHEN-DEAD og-metadata.ts + extraction-result.ts (ID-42-gated) | git rm lib/extraction/og-metadata.ts lib/extraction/extraction-result.ts only after ID-42 merged AND fresh 0-caller check passes; else leave in place + record carry-over | S-e | ID-42 merge-state check + gitnexus_impact/grep 0-caller re-verify, then bun run knip + bun build |
7 (scope addition) Dead standalone extract_docx_tables.py deleted | git rm scripts/extract_docx_tables.py scripts/tests/test_extract_docx_tables.py + fix the now-stale ci.yml:177 comment naming the deleted test. NOT in the original briefed residual — surfaced during {46.3}, reclassified KEEP→RETIRE on corrected grounding (0 live importers; sole non-test ref is a docstring; cocoindex docx.py imports extract_tender_questions/docx_utils, NOT this) | S-f | python3 -m pytest scripts/tests/ (green, no import error), bun run lint (no stale ci.yml comment ref), grep extract_docx_tables → 0 hits |
Safe deletion ordering + per-step verification gate
Section titled “Safe deletion ordering + per-step verification gate”Slices are ordered to keep each intermediate state green and to isolate the one knip nuance
(the classify.ts caller-count drop). After EVERY slice the relevant subset of the gate
must stay green before the next slice begins. The gate commands and which invariant each
proves:
S-a Q&A parser pair + tests → bun run test ; bun run knip ; bun build (inv 1, 7)S-b dead dedup substrate (TS+py) → bun run test ; python3 -m pytest scripts/tests/ ; bun run knip ; bun build (inv 2, 3, 7)S-c 3 batch-reclassify CLI scripts → bun run knip ; bun build ; bun run test (inv 4, 9 — verify classify NOT newly-orphaned)S-d 3 reader-html/firecrawl bckfl → bun run knip ; bun build (inv 5, 6, 7)S-f extract_docx_tables.py + test → python3 -m pytest scripts/tests/ ; bun run lint (inv 7 scope-add — fix ci.yml:177 comment)--- whole-set final gate (after S-a..S-d, S-f) --- grep sweep (.ts/.py/.sql/package.json/.github) + bun run knip + bun run lint (inv 7, 11)--- S-e is a SEPARATE, LATER slice, gated on ID-42 merge ---S-e og-metadata.ts + extraction-result.ts → ID-42-merge check + 0-caller re-verify ; bun run knip ; bun build (inv 13)Command-to-invariant mapping (which command proves which invariant):
bun run testproves inv 1/2/8/9 (no test imports a deleted TS module; preserved callers still pass). Run asbun run test— neverbun test(KH gate).python3 -m pytest scripts/tests/proves inv 3 (no Python test imports a deleted dedup module).bun run knipproves inv 4/5/6/7/11 (no newly-orphaned export, no dead script entry; the surface only ever reduces).bun buildproves inv 7/8/9/10/11 (the whole TS graph still type-checks with the deletions and the preserved modules/readers intact).bun run lint(final) proves no dangling-import lint residue.
Ordering rationale: S-a/S-b are fully independent dead-code (no surviving caller, no
shared file) and may run in either order. S-c is placed third so the classify.ts
caller-count drop (inv 9 / Edge case) is isolated and its knip signal interpreted in a clean
state. S-d (scripts-only, no lib/ graph impact) and S-f (Python-only, scripts/ + the ci.yml
comment) are the remaining unconditional slices — all of S-a..S-d, S-f are mutually independent
(distinct file sets) and parallel-dispatchable. S-e is a distinct later slice — it must NOT be
folded into the unconditional wave because it is gated on a cross-Task merge (ID-42) that has
not happened.
Guard-fixture / test blast radius
Section titled “Guard-fixture / test blast radius”- Tests that delete WITH their targets (co-deletion, inv 1/2/3):
__tests__/lib/procurement-library-ingest/extract-qa-pairs.test.ts(S-a), the Q&A-parser cases in__tests__/integration/procurement-library-ingest/parity.integration.test.ts(S-a — remove only the Q&A-parser cases; if the file exercises only the deleted pair, remove the file; if it also covers preserved modules, excise only the dead cases),__tests__/lib/dedup-normalise.test.ts(S-b),scripts/tests/test_dedup_normalise.py(S-b). No test exercising a preserved module (inv 8-11) is removed. - No surviving guard reads a deleted source. The 3 TS guards that used to
(
pipeline-parity.test.ts,content-history-change-reason.test.ts,extraction-result-parity.test.ts) were already removed by ID-68.23 (50b3b2096) — filesystem-confirmed GONE ate8e981c0a. The Executor must re-confirm none has been re-introduced (test -fon the 3 paths) before the final gate. .knip-baseline.jsonre-baseline. Present (1287 bytes @e8e981c0a). The deletions should only reduce the knip surface. If a deletion surfaces a previously-suppressed dead export as newly-orphaned (or shrinks a baselined count), re-baseline in the same change sobun run knipstays green and the baseline reflects the post-deletion surface — never add a new unlisted entry. Document the re-baseline in the subtask journal.
Execution gate (per PRODUCT invariant 12)
Section titled “Execution gate (per PRODUCT invariant 12)”Execution stays gated on T7 (ID-45 full-corpus re-ingest complete + stable) + ID-28 stable
(cocoindex proven sole canonical writer) + ID-42 merged — as fixed by the Task ledger
(dependencies: [45, 28, 42]) and the sub-orchestrator brief (“execution stays T7-gated”;
“author now, execute post-cutover”). Therefore all execution subtasks below are status
imp_deferred.
The deletion-safety rationale (the residual sits on no canonical write path; each target has 0 production callers — RESEARCH §A/§B/§G) explains why the deletions are low-risk once the gate opens, but it is evidence, not grounds to ungate. Whether ID-46 is ungated from T7 given the core already landed under ID-68.23 is OQ-46-10 — a sequencing question the Planner does not resolve; it is the orchestrator’s / Liam’s call. This TECH records the briefed gate.
At execution time, per target, the Executor MUST re-run the §Context code-intel probes as a
fresh 0-production-caller re-verify (gitnexus_impact … upstream for the TS symbols; grep
for Python/CLI importers; test -f before each git rm). The gate having opened does not
excuse skipping the per-file reachability confirmation. Slice S-e additionally requires the
ID-42 merge-state check (bun scripts/ledger-cli.ts show task 42 → expect done/merged) and
a fresh 0-caller check on og-metadata.ts (its sole consumer url.ts must have had the
OG branch removed by {42.12}).
Risks / rollback
Section titled “Risks / rollback”- Pure deletion →
git revertis the rollback. Every slice is a single coherent commit; reverting the commit restores the deleted files exactly. No data migration, no schema change, nothing irreversible. - The per-slice green gate is the safety net. A red
bun run test/pytest/knip/bun buildafter a slice means a dependency was missed (an unexpected live consumer) — STOP, do not force the deletion, escalate to the orchestrator with the failing output. This is the fresh-0-caller re-verify catching a stale pin, exactly as intended. classify.tsfalse-positive risk (Edge case, inv 9). After S-c, knip may report aclassifyexport as “now unused”. This is a false positive to investigate, not a licence to delete —classifyContent/isExcludedEntity/validateDomainremain live via routes / MCP / the queue handler. Confirm withgitnexus_impactbefore touching anything inlib/ai/classify.ts; ID-46 does not modify that module.- ID-42 double-delete risk (inv 6, S-d).
backfill-source-url-firecrawl.tsmay already be gone if ID-42 absorbed it.git rmis guarded bytest -f(delete-if-present) so a prior ID-42 merge does not turn into an ID-46 failure. - Premature S-e (inv 13). Deleting
og-metadata.tsbefore ID-42 {42.12} removes theurl.tsOG branch would break the build (live consumer). S-e’s ID-42-merge gate + fresh 0-caller check prevent this.
Decomposition — {46.5}+ implementation subtasks (inline {46.4})
Section titled “Decomposition — {46.5}+ implementation subtasks (inline {46.4})”Six implementation subtasks {46.5}-{46.10}, grouped by coupling + the ID-42 boundary.
S-a..S-d + S-f ({46.5}-{46.8}, {46.10}) are the unconditional residual wave; they are
mutually independent (distinct file sets, no shared module) and carry no sibling
dependencies — the orchestrator may dispatch them in parallel once the T7/ID-28/ID-42 gate
opens. S-e ({46.9}) is the RETIRE-WHEN-DEAD slice: it carries a BLOCKED-ON ID-42 note in
details (a cross-Task gate, NOT a sibling dep — sibling-only constraint honoured) and no
sibling dependency on the others (it is independent of them). {46.10} (S-f) is the
KEEP→RETIRE scope addition surfaced during {46.3} (the F-2 grounding correction). All six are
status deferred (T7-gated, execute post-cutover). NB imp_deferred is a Task-level-only
status — invalid at subtask level (SubtaskSchema excludes it); subtasks use deferred. The records below are the canonical,
checked-in decomposition; the orchestrator adds them via
bun scripts/ledger-cli.ts add-subtasks 46 --file -.
Sibling-only dependency note: no subtask depends on a cross-Task subtask. The only
cross-Task coupling (S-e → ID-42 merge) is encoded as a BLOCKED-ON ID-42 execution-gate note
in details, not as a dependency edge — exactly as the brief directs and the sibling-only
constraint requires.
Budget: every description ≤250 chars and every testStrategy ≤300 chars (HARD
add-subtasks gate; measured with printf '%s' "<field>" | wc -c — see {46.3} report).
[ { "id": "5", "title": "Retire Q&A parser pair (extract-qa-pairs + docx-to-markdown)", "description": "Delete the prod-dead Q&A parser pair and its dedicated tests; preserve live siblings resolve-question.ts + extract-answer.ts.", "testStrategy": "bun run test + bun run knip + bun build all green; no test imports a deleted module; resolve-question/extract-answer callers still pass.", "details": "ID-46 slice S-a (PRODUCT inv 1, 7; TECH §Proposed changes row 1). git rm: lib/procurement-library-ingest/extract-qa-pairs.ts, lib/procurement-library-ingest/docx-to-markdown.ts, __tests__/lib/procurement-library-ingest/extract-qa-pairs.test.ts, and the Q&A-parser cases in __tests__/integration/procurement-library-ingest/parity.integration.test.ts (excise only the Q&A cases if it also covers preserved modules; else rm the file). PRESERVE resolve-question.ts + extract-answer.ts (live) — do NOT delete the directory. Code-intel discipline: pre-edit re-run gitnexus_impact({target:'extractQaPairs',direction:'upstream',repo:'<worktree>'}) (expect impactedCount 0); confirm docx-to-markdown sole importer is extract-qa-pairs.ts (grep). Pre-commit: gitnexus_detect_changes + git diff --name-only in worktree. Per-file 0-caller re-verify at execution. Green gate: bun run test ; bun run knip ; bun build. T7/ID-28/ID-42 gate: execute post-cutover only (status imp_deferred); see TECH §Execution gate + OQ-46-10.\n<info added on 2026-06-13>{46.3} TECH decomposition stub. Re-verified @ e8e981c0a: gitnexus_impact extractQaPairs = impactedCount 0/risk LOW; docx-to-markdown sole importer = extract-qa-pairs.ts. Executor: append execution journal here.</info>", "dependencies": [], "status": "deferred" }, { "id": "6", "title": "Retire dead dedup substrate (TS + Python)", "description": "Delete the prod-dead dedup-normalise TS helper and the standalone Python dedup CLIs + their tests; preserve live lib/dedup.ts + entity-dedup.ts.", "testStrategy": "bun run test + python3 -m pytest scripts/tests/ + bun run knip + bun build green; no import error for any deleted dedup module.", "details": "ID-46 slice S-b (PRODUCT inv 2, 3, 7; TECH rows 2-3). git rm: lib/dedup-normalise.ts, __tests__/lib/dedup-normalise.test.ts, scripts/dedup.py, scripts/dedup_normalise.py, scripts/tests/test_dedup_normalise.py. PRESERVE lib/dedup.ts + lib/entities/entity-dedup.ts (live — NOT targets). Code-intel discipline: pre-edit gitnexus_impact({target:'normaliseTitleForDedup',direction:'upstream'}) (expect impactedCount 0); grep -rn 'import dedup' scripts --include='*.py' | grep -v scripts/tests/ (expect only dedup.py:21 self-import). Pre-commit gitnexus_detect_changes + git diff --name-only. Green gate: bun run test ; python3 -m pytest scripts/tests/ ; bun run knip ; bun build. T7/ID-28/ID-42 gate: post-cutover only; see TECH §Execution gate.\n<info added on 2026-06-13>{46.3} TECH decomposition stub. Re-verified @ e8e981c0a: normaliseTitleForDedup impactedCount 0; dedup.py:21 self-imports dedup_normalise; no live (non-test) py importer. Executor: append execution journal here.</info>", "dependencies": [], "status": "deferred" }, { "id": "7", "title": "Retire 3 batch-reclassify CLI scripts (KEEP classify.ts)", "description": "Delete the 3 legacy batch-reclassify CLI scripts that call the live classify surface; lib/ai/classify.ts and its 15+ other callers stay.", "testStrategy": "bun run knip + bun build + bun run test green; classify exports NOT flagged newly-dead (false positive — 15+ live callers remain).", "details": "ID-46 slice S-c (PRODUCT inv 4, 9; TECH row 4). git rm: scripts/batch-reclassify.ts, scripts/batch-reclassify-keywords.ts, scripts/reclassify-items.ts. PRESERVE lib/ai/classify.ts (LIVE — classifyContent/isExcludedEntity/validateDomain have 15+ callers) AND the lib/queue/handlers/batch-reclassify.ts queue handler + batch_reclassify job-type (CARVE-OUT backlog-188, UNDECIDED — do NOT touch). Edge case: knip may report a classify export as now-unused after the CLI deletion — FALSE POSITIVE, investigate via gitnexus_impact, do NOT delete the symbol. Code-intel discipline: pre-edit confirm the 3 scripts import @/lib/ai/classify (grep); gitnexus_impact on any classify symbol knip flags before acting. Pre-commit gitnexus_detect_changes + git diff --name-only. Green gate: bun run knip ; bun build ; bun run test. T7/ID-28/ID-42 gate: post-cutover only.\n<info added on 2026-06-13>{46.3} TECH decomposition stub. Re-verified @ e8e981c0a: all 3 CLIs import @/lib/ai/classify; classify.ts has many live callers (KEEP). Executor: append execution journal here.</info>", "dependencies": [], "status": "deferred" }, { "id": "8", "title": "Retire 3 reader-html/firecrawl backfill scripts (delete-if-present, ID-42-aware)", "description": "Delete the 3 dead one-shot backfill scripts (reader-html pair + firecrawl); firecrawl delete-if-present in case ID-42 absorbed it. The reader_html column stays.", "testStrategy": "bun run knip + bun build green; reader_html column + its 5 readers unchanged; no package.json/CI ref to a deleted script.", "details": "ID-46 slice S-d (PRODUCT inv 5, 6, 7; TECH rows 5-6). git rm: scripts/backfill-reader-html.ts then scripts/extract-reader-html.ts (backfill is sole importer of extract); and `test -f scripts/backfill-source-url-firecrawl.ts && git rm scripts/backfill-source-url-firecrawl.ts` (DELETE-IF-PRESENT — ID-42 may have absorbed it; tolerate already-gone). PRESERVE the reader_html COLUMN + its 5 readers (item-detail-client.tsx, schemas.ts, source-metadata.tsx, reader-view.tsx, editor-view.tsx) — NB app/api/review/queue/route.ts is NOT a reader (comment-only). NO package.json/requirements.txt edit (HTML deps are ID-42 {42.13}). OQ-46-7: check ID-42 merge state (bun scripts/ledger-cli.ts show task 42) before firecrawl rm to avoid double-delete. Code-intel discipline: grep package.json + .github/ for the 3 script names (expect zero); pre-commit gitnexus_detect_changes + git diff --name-only (verify NO manifest change). Green gate: bun run knip ; bun build. T7/ID-28/ID-42 gate: post-cutover only.\n<info added on 2026-06-13>{46.3} TECH decomposition stub. Re-verified @ e8e981c0a: zero package.json/.github refs; backfill-reader-html imports extract-reader-html; firecrawl lazy-imports @mendable/firecrawl-js; ID-42 in_progress (not merged). Executor: append execution journal here.</info>", "dependencies": [], "status": "deferred" }, { "id": "9", "title": "RETIRE-WHEN-DEAD lib/extraction/{og-metadata,extraction-result}.ts (BLOCKED-ON ID-42)", "description": "Delete og-metadata.ts + extraction-result.ts ONLY after ID-42 has merged and a fresh 0-caller check confirms both dead; else leave in place and record carry-over.", "testStrategy": "ID-42 merged (ledger task 42 = done) AND fresh 0-caller check passes, then bun run knip + bun build green; else no-op + carry-over recorded.", "details": "ID-46 slice S-e (PRODUCT inv 13; TECH row 13). BLOCKED-ON ID-42 merge (cross-Task gate — NOT a sibling dep; sibling-only constraint honoured). Targets: lib/extraction/og-metadata.ts (sole consumer = extractFromUrl in lib/extraction/url.ts, which ID-42 {42.12} rewrites to drop the OG branch) + lib/extraction/extraction-result.ts (0 external consumers; parity guard already removed by ID-68.23). PRECONDITION (do not skip): (1) bun scripts/ledger-cli.ts show task 42 → expect status done/merged (currently in_progress — STAY BLOCKED if so); (2) fresh gitnexus_impact({target:'extractOgMetadata',direction:'upstream'}) AND grep og-metadata/extractOgMetadata over lib app components scripts --include='*.ts*' | grep -v __tests__ → expect 0 production consumers (url.ts must no longer call it). If both pass: git rm lib/extraction/og-metadata.ts lib/extraction/extraction-result.ts. If ID-42 NOT merged: NO-OP, leave both in place, record BLOCKED-ON ID-42 carry-over in journal (do NOT pre-delete a still-consumed module). Do NOT touch lib/extraction/{html,url}.ts (ID-42 carve-out). Pre-commit gitnexus_detect_changes + git diff --name-only. Green gate: bun run knip ; bun build. T7/ID-28/ID-42 gate: post-cutover only.\n<info added on 2026-06-13>{46.3} TECH decomposition stub. Re-verified @ e8e981c0a: gitnexus_context extractOgMetadata sole incoming = url.ts:extractFromUrl; extraction-result.ts incoming {} (0 consumers); ID-42 task status in_progress (NOT merged) → S-e currently blocked. Executor: append execution journal here.</info>", "dependencies": [], "status": "deferred" }, { "id": "10", "title": "Retire dead standalone extract_docx_tables.py (KEEP→RETIRE scope add)", "description": "Delete the dead standalone scripts/extract_docx_tables.py + its test, and fix the now-stale ci.yml comment. Reclassified KEEP→RETIRE during {46.3} on corrected grounding (0 live importers).", "testStrategy": "python3 -m pytest scripts/tests/ green (no import error) + bun run lint; grep extract_docx_tables across scripts/.github = 0 hits; extract_tender_questions.py + docx_utils.py untouched.", "details": "ID-46 slice S-f (PRODUCT inv 7 scope addition; TECH §Proposed changes scope-add row + §Context F-2 correction). NOT in the original briefed residual — surfaced during {46.3}, reclassified KEEP→RETIRE after the Checker F-2 grounding correction. git rm: scripts/extract_docx_tables.py, scripts/tests/test_extract_docx_tables.py; AND fix the now-stale comment at .github/workflows/ci.yml:177 that names test_extract_docx_tables.py as a quality-shard member (drop the test_extract_docx_tables.py reference from the comment; the remaining test_docx_cell_to_markdown.py reference stays). PRESERVE scripts/extract_tender_questions.py + scripts/docx_utils.py (LIVE — imported by cocoindex form_extractors/docx.py:90,89). Code-intel discipline: pre-edit re-verify grep -rn extract_docx_tables scripts --include='*.py' (non-self/non-test) → expect only the extract_tender_questions.py:3 docstring 'Adapted from' (NOT an import); confirm extract_docx_tables.py is __main__ CLI (:500). Pre-commit gitnexus_detect_changes + git diff --name-only. Green gate: python3 -m pytest scripts/tests/ ; bun run lint. T7/ID-28/ID-42 gate: post-cutover only.\n<info added on 2026-06-13>{46.3} TECH decomposition stub — RECLASSIFIED KEEP→RETIRE after Checker F-2 grounding correction. Re-verified @ e8e981c0a: sole non-self/non-test ref is extract_tender_questions.py:3 docstring (not import); cocoindex docx.py imports extract_tender_questions + docx_utils (NOT this); ci.yml:177 is a comment naming the test file. 0 live importers. Executor: append execution journal here.</info>", "dependencies": [], "status": "deferred" }]F-2 correction — extract_docx_tables.py reclassified KEEP→RETIRE (S350 fix-Planner pass)
Section titled “F-2 correction — extract_docx_tables.py reclassified KEEP→RETIRE (S350 fix-Planner pass)”The original {46.3} TECH §Context (and RESEARCH §G) wrongly claimed
scripts/extract_docx_tables.py was KEEP/LIVE because cocoindex form_extractors/docx.py
imported it. Re-verified @ e8e981c0a (Checker-supplied evidence, independently reproduced):
grep -rn "extract_docx_tables" scripts --include='*.py'(non-self, non-test) → single hitscripts/extract_tender_questions.py:3— a docstring (“Adapted from extract_docx_tables.py — reads tables from .docx files…”), NOT an import.scripts/extract_docx_tables.pyis a standalone CLI (if __name__ == "__main__"at:500, callingextract_qa_from_docx:424).- cocoindex
form_extractors/docx.pyimportsscripts.analyse_template(:84),scripts.docx_utils(:89),scripts.extract_tender_questions(:90) — NOTextract_docx_tables. - The only
.github/workflows/ci.yml:177hit is a comment naming the TEST filetest_extract_docx_tables.pyas a quality-shard member — not a live source invocation.
Conclusion: extract_docx_tables.py is the dead OLD precursor to the LIVE
extract_tender_questions.py, superseded by cocoindex form extraction — 0 live importers,
test-only consumer. Reclassified KEEP→RETIRE (slice S-f / {46.10}). extract_tender_questions.py
docx_utils.pyremain PRESERVE (LIVE). This file was not in the original briefed residual set; it surfaced during {46.3} as a scope addition.
Verification (S349 authoring + S350 F-2 fix — claims grounded against live code @ e8e981c0a)
Section titled “Verification (S349 authoring + S350 F-2 fix — claims grounded against live code @ e8e981c0a)”Date: 13/06/2026 (S349 authoring; S350 fix-Planner pass). Branch: worktree subo-id-46
@ e8e981c0a. Method: gitnexus_impact/gitnexus_context (type-checker-resolved) + grep
(python/SQL/string-literal, incl. package.json/.github/) + filesystem ls/test -f +
ledger-cli show task 42. No external-library API symbols newly cited that require
import-and-call verification (this TECH deletes KH-internal code only; it adds no new external
dependency call). All pins reproduce the PRODUCT §Problem / RESEARCH §G evidence, except the
extract_docx_tables.py KEEP claim which was a grounding error — corrected to RETIRE per §F-2
above. Two raw-grep corrections of note: (a) app/api/review/queue/route.ts is a reader_html
non-reader (excludes it by comment) — matches PRODUCT invariant 10’s NB; (b) the sole
extract_docx_tables cross-ref is a docstring, not an import (F-2).