Skip to content

Cocoindex Absorption Cleanup (ID-46 / T14) — TECH

Cocoindex Absorption Cleanup (ID-46 / T14) — TECH

Section titled “Cocoindex Absorption Cleanup (ID-46 / T14) — TECH”

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. With includeTests:true the count stays 0 (gitnexus excludes the in-file def; ast-dataflow references in 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 hit lib/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-dataflow importers --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 hit scripts/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 is extractFromUrl in lib/extraction/url.ts — the file ID-42 {42.12} rewrites. So og-metadata.ts is 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). Zero package.json / CI references. scripts/backfill-reader-html.ts:22-23 imports JSDOM + Readability and is the sole importer of extract-reader-html.ts. scripts/backfill-source-url-firecrawl.ts:196 lazy-imports @mendable/firecrawl-js.
  • reader_html column readers (KEEP — column STAYS): grep -rln "reader_html" lib app components --include='*.ts*' | grep -v __tests__ → 6 hits, but app/api/review/queue/route.ts:23 matches 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 hit scripts/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, calling extract_qa_from_docx defined :424). The cocoindex form_extractors/docx.py does NOT import it — docx.py:84,89,90 import scripts.analyse_template, scripts.docx_utils (open_document_safe), and scripts.extract_tender_questions (_classify_header). The only .github/workflows/ci.yml hit (:177) is a comment naming its TEST file test_extract_docx_tables.py as a quality-shard member, not a live invocation of the source. 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 → RETIRE (slice S-f / {46.10}). PRESERVE the LIVE scripts/extract_tender_questions.py + scripts/docx_utils.py (both imported by cocoindex form_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.ts all GONE. The only __tests__/*.ts still 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), the reader_html column + its 5 readers, the remaining lib/extraction/* files (turndown, pdf, clean-mdx-tags, markdown-front-matter, markdown-title, diff-markers, content-type-detect, url-validation), scripts/extract_tender_questions.py
    • scripts/docx_utils.py (LIVE — imported by cocoindex form_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_chunks writes + markdown_batch UI (ID-56); the batch_reclassify queue 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 invariantProposed change (mechanical)SliceProof command
1 Q&A parser pair deletedgit 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.tsS-abun run knip (no orphan), bun run test (green, no import of deleted module), bun build
2 TS dedup-normalise helper deletedgit rm lib/dedup-normalise.ts __tests__/lib/dedup-normalise.test.tsS-bbun run test, bun run knip, bun build
3 Standalone Python dedup scripts deletedgit rm scripts/dedup.py scripts/dedup_normalise.py scripts/tests/test_dedup_normalise.pyS-bpython3 -m pytest scripts/tests/ (green, no import error)
4 Three batch-reclassify CLI scripts deletedgit rm scripts/batch-reclassify.ts scripts/batch-reclassify-keywords.ts scripts/reclassify-items.tsS-cbun run knip (verify lib/ai/classify exports NOT newly-orphaned — see Edge cases), bun build, bun run test
5 Reader-html populator scripts deletedgit 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-dbun 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-dbun run knip, bun build; test -f guard before git rm
7 Zero dead-code residueAfter 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-baselineS-a..S-d (final gate)grep sweeps (below) + bun run knip
8 Live procurement-library-ingest/ members surviveNo 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 intactNo 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 untouchedNo 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 untouchedNo 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 gatedProcess, 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-overS-eID-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 deletedgit 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-fpython3 -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 test proves inv 1/2/8/9 (no test imports a deleted TS module; preserved callers still pass). Run as bun run testnever bun test (KH gate).
  • python3 -m pytest scripts/tests/ proves inv 3 (no Python test imports a deleted dedup module).
  • bun run knip proves inv 4/5/6/7/11 (no newly-orphaned export, no dead script entry; the surface only ever reduces).
  • bun build proves 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.

  • 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 at e8e981c0a. The Executor must re-confirm none has been re-introduced (test -f on the 3 paths) before the final gate.
  • .knip-baseline.json re-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 so bun run knip stays green and the baseline reflects the post-deletion surface — never add a new unlisted entry. Document the re-baseline in the subtask journal.

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}).

  • Pure deletion → git revert is 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 build after 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.ts false-positive risk (Edge case, inv 9). After S-c, knip may report a classify export as “now unused”. This is a false positive to investigate, not a licence to deleteclassifyContent / isExcludedEntity / validateDomain remain live via routes / MCP / the queue handler. Confirm with gitnexus_impact before touching anything in lib/ai/classify.ts; ID-46 does not modify that module.
  • ID-42 double-delete risk (inv 6, S-d). backfill-source-url-firecrawl.ts may already be gone if ID-42 absorbed it. git rm is guarded by test -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.ts before ID-42 {42.12} removes the url.ts OG 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 hit scripts/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.py is a standalone CLI (if __name__ == "__main__" at :500, calling extract_qa_from_docx :424).
  • cocoindex form_extractors/docx.py imports scripts.analyse_template (:84), scripts.docx_utils (:89), scripts.extract_tender_questions (:90) — NOT extract_docx_tables.
  • The only .github/workflows/ci.yml:177 hit is a comment naming the TEST file test_extract_docx_tables.py as 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.py remain 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).