ID-136 — Retire the corpus forms-route (forms = manual-upload) (TECH)
ID-136 — Retire the corpus forms-route — forms are manual-upload
Section titled “ID-136 — Retire the corpus forms-route — forms are manual-upload”Authored S438 (02/07/2026) against
reference/decision-register.md(DR-014 / DR-020) with an in-repo touch-point re-verification pass onmainTODAY — the S437 one-pager (specs/id-136-forms-route-retirement/notes/s437-bl392-recommendation.md) is treated as an evidence lead, not restated: several of its line anchors have drifted and its “~9 touch-points” is an undercount (see §2, §8).Subtask: {136.3} TECH. Tier: TECH+PLAN (no PRODUCT.md — the behaviour is settled by DR-014: forms enter via manual upload, the corpus forms-route is retired. This is a retirement/refactor, not a new user-facing behaviour). PLAN decomposition ({136.4}) is a separate later dispatch.
Product intent (settled, not re-litigated here): DR-014 — “Forms are manual-upload;
the corpus forms-route is retired (okf-v3 §8.2).” Forms enter the system app-side via
manual upload (§J Path-A/B/C → form_template_requirements), not the gated corpus
walk. This spec turns that ruling into the concrete current-reality retirement plan.
1. Context — how the forms-route works today, and what “retire” means
Section titled “1. Context — how the forms-route works today, and what “retire” means”1.1 The system as it stands on main (code-intel oriented)
Section titled “1.1 The system as it stands on main (code-intel oriented)”The corpus walk (app_main in scripts/cocoindex_pipeline/flow.py) resolves each walked
file to a route via a manifest, then forks to exactly one branch. There are three
routes today; forms is one of them.
Code-intelligence orientation (cited verbatim from the tool outputs, not paraphrased):
gitnexus_query({repo:'canonical', query:'forms route corpus manifest'})returned no process (the forms fork is not a named execution flow) and surfaced these definitions (file:line verbatim):Function:scripts/cocoindex_pipeline/flow.py:_ingest_form_branch(3159–3395),Function:scripts/seed-synthetic-corpus.ts:buildManifest(330–341),Function:scripts/seed-synthetic-corpus.ts:resolveFormsWorkspaceId(505–524),Function:scripts/cocoindex_pipeline/flow.py:_item_failure_branch(712–732), plus the forms test defs intest_workspace_resolver.py/test_cocoindex_flow_fork_routing.py/test_cocoindex_flow_failure_mode.py/test_platform_manifest_gate.py.gitnexus_context({repo:'canonical', name:'RouteKind'})→Variable:scripts/cocoindex_pipeline/workspace_resolver.py:RouteKind(line 64–65), no incoming/outgoing edges recorded (it is aLiteraltype alias). A repo-wide grep confirmsRouteKindis referenced only inworkspace_resolver.pyand two test files (test_workspace_resolver.py,test_cocoindex_flow_fork_routing.py) — noapp/orlib/consumer.gitnexus_context({repo:'canonical', name:'buildManifest'})→ called bymain(seed-synthetic-corpus.ts) only.gitnexus_context({repo:'canonical', name:'coerce_extracted_form'})→ called by_ingest_form_branchonly (confirmed by the GitNexus PreToolUse hook). The wholeform_extractors/package (extract_form_structure,coerce_extracted_form,FormExtractionError, the pdf/docx/xlsx readers) hasflow.pyas its only production consumer (grep: the sole production import site isflow.py:156–160; the forms fork is the only reach). Its test surface is broader — enumerated in T22/T23 (§2.4): four package-unit-test files plustest_cocoindex_flow_write_path.py’s form-write classes all importform_extractorssubmodules and break under T10.
Python write-site anchors (gitnexus is TS-only; grep-verified on main today): the fork
lives in _ingest_file_body, the forms branch at flow.py:1997–2011; _ingest_form_branch
flow.py:3160–3398; _trim_stale_form_fields flow.py:3399–3429; FORM_TEMPLATES_SCHEMA
flow.py:1495; FORM_TEMPLATE_FIELDS_SCHEMA flow.py:1525; the ft_target/ftf_target
mounts flow.py:3593–3602.
1.2 The forms-route is structurally self-contained
Section titled “1.2 The forms-route is structurally self-contained”Two facts make the retirement clean:
form_templateshas TWO independent writers, and only one is being retired. The corpus walk writesform_templates/form_template_fieldsvia the cocoindexft_target/ftf_targetmounts (Python,_ingest_form_branch). The surviving writer is the app-side manual upload —app/api/procurement/[id]/forms/route.ts:100(ingest_source:'app_upload', ID-130 {130.13}) — a pure-TypeScript path using its own Supabase client. Retiring the corpus route removes the cocoindex mounts only; theform_templates/form_template_fieldstables persist unchanged for app-upload. No DDL, no data migration (§5).- The route is a file-manifest
Literal, not a DB constraint.RouteKind = Literal["content", "forms", "qa_sidecar"]is a pydantic type over the.kh-workspace-map.jsonmanifest file. There is norouteenum, CHECK, or column in any migration (grep ofsupabase/migrations/*.sqlforforms/route_kind/ route CHECK returns nothing forms-related). So “retire the route” = narrow theLiteral+ remove the fork branch + purge every manifest taggedroute:"forms".
1.3 qa_sidecar reconciliation (owner S437 caveat — MUST NOT over-reach)
Section titled “1.3 qa_sidecar reconciliation (owner S437 caveat — MUST NOT over-reach)”The qa_sidecar route (__qa__/ reserved prefix → _ingest_qa_sidecar_branch,
flow.py:2502) is a third, separate route. Per the owner caveat, the __qa__
Q&A-sidecar round-trip is EARMARKED SUPERSEDED under the OKF record-model (id-131
{131.1} §2.2: a Q&A pair is a record; the S436 lineage audit’s live-vs-superseded split) —
superseded-pending, not yet retired.
Binding scope boundary for this task: ID-136 retires forms only. RouteKind
narrows to Literal["content", "qa_sidecar"] — qa_sidecar is RETAINED. Its
retirement is id-131’s call, not this task’s; folding a superseded-pending route retirement
into a forms retirement would over-reach and couple two unrelated changes. Where a
forms-generic test is re-pointed onto qa_sidecar (§4), a code comment flags that it may
move again when id-131 retires the sidecar — but that is a downstream id-131 concern.
1.4 The atomicity linchpin (why this is ONE PR)
Section titled “1.4 The atomicity linchpin (why this is ONE PR)”RouteKind is the linchpin. The moment the Literal drops "forms", every manifest
tagged route:"forms" fails at load — WorkspaceManifest.model_validate raises
ValidationError (extra="forbid" + narrowed Literal) → ManifestLoadError. At walk
start that is a terminal manifest_invalid abort of the whole run. Therefore:
- The vendored
.kh-workspace-map.json.example,seed-synthetic-corpus.ts:buildManifest, and theRouteKindnarrowing must land in one commit — any half-state (Literal narrowed, a manifest still taggingroute:"forms") aborts the next/walkat load. - Every test that stages a
route:"forms"manifest breaks atmodel_validatetime and must retire or re-point in the same PR (§4). This is not optional cleanup — it is forced by the linchpin change.
This is precisely the “internally-consistent corpus (all-forms OR no-forms)” constraint from the ratified description: land whole, then re-walk.
2. Touch-point inventory (verified in-repo on main, 02/07/2026)
Section titled “2. Touch-point inventory (verified in-repo on main, 02/07/2026)”Every anchor below was opened and confirmed today. Drift flags mark where the S437 one-pager is stale.
2.1 Routing core (the ratified “RouteKind + fork”)
Section titled “2.1 Routing core (the ratified “RouteKind + fork”)”| # | File:line | Current state (verified) | S437 drift |
|---|---|---|---|
| T1 | scripts/cocoindex_pipeline/workspace_resolver.py:65 | RouteKind = Literal["content", "forms", "qa_sidecar"]; forms also named in docstrings/comments :11, :53, :64, :130, :139 | one-pager said :65 “Literal + extra=forbid” — the extra="forbid" is on WorkspaceMapping/WorkspaceManifest, not the RouteKind line |
| T2 | scripts/cocoindex_pipeline/flow.py:1997–2011 | the if route == "forms": fork branch → _ingest_form_branch(...) | one-pager :1997 ✓ |
| T3 | scripts/cocoindex_pipeline/flow.py:733 | _item_failure_branch returns "forms" if route == "forms" else "content" | one-pager mislabels this _route_to_branch — no such symbol exists; the fn is _item_failure_branch (:712–732) |
| T4 | scripts/cocoindex_pipeline/flow.py:704 | BranchCounter._counts = {"forms": 0, "content": 0, "url": 0} | not enumerated in one-pager |
2.2 Forms-write machinery (orphaned once the fork is removed — see §8 scope flag)
Section titled “2.2 Forms-write machinery (orphaned once the fork is removed — see §8 scope flag)”| # | File:line | Current state (verified) | note |
|---|---|---|---|
| T5 | scripts/cocoindex_pipeline/flow.py:3160–3398 | _ingest_form_branch (~239 LOC) | one-pager said :3062 → DRIFT: actual :3160 (+98) |
| T6 | scripts/cocoindex_pipeline/flow.py:3399–3429 | _trim_stale_form_fields (only caller: _ingest_form_branch:3369) | not enumerated |
| T7 | scripts/cocoindex_pipeline/flow.py:1495, :1525 | FORM_TEMPLATES_SCHEMA, FORM_TEMPLATE_FIELDS_SCHEMA (only consumers: the ft/ftf mounts) | not enumerated |
| T8 | scripts/cocoindex_pipeline/flow.py:3593–3602 | ft_target/ftf_target cocoindex mounts (only consumer: _ingest_form_branch) | not enumerated |
| T9 | scripts/cocoindex_pipeline/flow.py:156–160 | imports extract_form_structure, coerce_extracted_form, FormExtractionError | not enumerated |
| T10 | scripts/cocoindex_pipeline/form_extractors/ | 6 files, 2,235 LOC (__init__, orchestrator, pdf, docx, xlsx, shared) — imported ONLY by flow.py | not in the one-pager’s 9 — the single largest expansion (§8) |
2.3 Seed + vendored corpus seam (the ratified “vendored seam” + “seed buildManifest”)
Section titled “2.3 Seed + vendored corpus seam (the ratified “vendored seam” + “seed buildManifest”)”| # | File:line | Current state (verified) | S437 drift |
|---|---|---|---|
| T11 | scripts/seed-synthetic-corpus.ts:331–341 | buildManifest emits a manifest whose ONLY mapping is {FORMS_PATH_PREFIX, route:'forms'} (:336/:338) | one-pager :331/:338 ✓ |
| T12 | scripts/seed-synthetic-corpus.ts:104, :101 | FORMS_PATH_PREFIX = 'forms/procurement/', FORMS_MANIFEST_WORKSPACE_NAME (:101) | ✓ |
| T13 | scripts/seed-synthetic-corpus.ts:506–524, :729–745 | resolveFormsWorkspaceId + the --emit-manifest path that resolves the forms workspace | one-pager said :732 (the call site); def is :506 |
| T14 | scripts/cocoindex_pipeline/fixtures/platform-corpus/forms/procurement/synthetic-sq-officesupplies.pdf + synthetic-itt-groundsmaint.docx | the 2 vendored forms binaries | ✓ |
| T15 | scripts/cocoindex_pipeline/fixtures/platform-corpus/.kh-workspace-map.json.example:2–8 | manifest template whose ONLY mapping is forms/procurement/ → route:"forms" | one-pager :7 ✓ |
2.4 Tests (retire / re-point / assert-the-retirement — forced by T1)
Section titled “2.4 Tests (retire / re-point / assert-the-retirement — forced by T1)”| # | File | Forms surface (verified) | disposition |
|---|---|---|---|
| T16 | __tests__/integration/cocoindex/platform-corpus-shape.test.ts | EXPECTED_ENTRIES (2 forms lines :48–49), PDF_FILES:58, DOCX_FILES:63, “exactly the 8 entries” :45,:89, forms manifest tests :147–168, “forms branch carries both” :174–181, “content/forms/qa/edge” comment :210 | edit (§4.1) |
| T17 | scripts/tests/test_platform_manifest_gate.py | whole class TestFormsPrefixConsumesWorkspaceId:471 + test_...:476; module docstring assertion (c) :31–34; comment :74; _CHARNWOOD_DOCX fixture usage | retire the class; keep the file (other classes stay) |
| T18 | scripts/tests/test_workspace_resolver.py | test_route_forms_prefix_resolves_forms:337 (retire); test_longest_prefix_winner_route_is_returned:381 (re-point forms→qa_sidecar); test_invalid_route_value_rejected_at_load_time:360 (extend: assert route:"forms" now rejected) | mixed (§4.2) |
| T19 | scripts/tests/test_cocoindex_flow_fork_routing.py | 12 tests, 4 forms-named: TestFormsRouteWritesFormTargetsOnly:229 (retire), TestFormsRouteSuffixGuard:372 (retire), test_form_branch_reingest...:566 (retire); TestContentRouteWritesContentTargetsOnly:293 uses route="forms" on an UNMAPPED-prefix contrast at :324 (re-point); TestQaSidecarRoute...:700, TestAmbiguousResolution...:427, TestRouteLessManifestBackwardCompat:627 stay | mixed (§4.3) |
| T20 | scripts/tests/test_cocoindex_flow_failure_mode.py | _two_route_manifest:1234 (forms+content pair → re-point forms→qa_sidecar); BranchCounter tests :1276, :1278 assert {"forms":0,...} / increment("forms") (re-point if T4 drops “forms”) | mixed (§4.4) |
| T21 | scripts/tests/test_cocoindex_form_write_real_body.py | entire file — all 3 tests stage route="forms" (:292,:373,:469) AND exercise the REAL extract_form_structure + _trim_stale_form_fields; both dependencies removed | retire the whole file (§4.5) |
| T22 | scripts/tests/test_form_extractors.py + test_form_extractor_orchestrator.py + test_docx_tracked_changes_regression.py (:49 docx, :52 shared) + test_xlsx_zero_archetype.py (:64 shared, :65 xlsx) + test_pdf_zero_archetype.py (:53 pdf, :54 shared, :55 xlsx) | 5 files that unit-test / import the form_extractors/ package submodules directly | retire iff T10 is included (§8) |
| T23 | scripts/tests/test_cocoindex_flow_write_path.py | MIXED file — 5 form-write classes stage route="forms" (:1784,:1866,:1956,:2042,:2100,:2151,:2177,:2422,:2454) AND import form_extractors.shared (:1705,:1871,:1911,:2048,:2105): TestFormWriteSuccessPath:1769, TestFormWriteMemoHitRoundTrip:1889, TestFormWriteGracefulEmptyProvenance:2022, TestFormWriteSkipAndFailurePaths:2139, TestFormWriteIdempotency:2409. T1 breaks the route="forms" construction; T10 breaks the import. Every other class in the file (~15 content/write-path classes, TestIngestFileWritePath:160 … TestHolderStampWiring:3411) stays | retire the 5 form-write classes + their forms-only helpers (_make_manifest forms usage, _ingest_form:1736); keep the rest (§4.7) |
True touch-point count: ~23 named touch-points (T1–T23) across ~17 files — source:
workspace_resolver.py + flow.py + seed-synthetic-corpus.ts + the form_extractors/
package (T10-conditional); ~12 test files (T16–T23; T22 alone spans 5); and the vendored
forms binaries + manifest template. Materially larger than the one-pager’s “~9” — the
“understated blast radius” it flagged as its top risk. T23 was absent from the first draft
(Checker-caught): the ratified enumeration and the S437 one-pager both missed
test_cocoindex_flow_write_path.py’s 5 form-write classes.
3. Proposed changes
Section titled “3. Proposed changes”Grouped into four tiers. Tier A + C + D are the mandatory retirement (make forms structurally impossible + keep the suite green). Tier B is the orphan-cleanup cascade — recommended for the same PR, flagged as a scope-delta in §8.
3.1 Tier A — routing core (mandatory; the linchpin)
Section titled “3.1 Tier A — routing core (mandatory; the linchpin)”- T1
workspace_resolver.py:65→RouteKind = Literal["content", "qa_sidecar"]. Update the forms-referencing docstrings/comments (:11, :53, :64, :130, :139) to drop forms as a valid route (keep the content-default and qa_sidecar prose).RouteKindstays exported in__all__:297(still a live type).WorkspaceMapping.routedefault stays"content"; existing content/qa manifests parse unchanged. - T2
flow.py:1997–2011→ delete theif route == "forms":branch entirely. The fork then routesqa_sidecar→_ingest_qa_sidecar_branch, else_ingest_content_branch.UnmappedPathsoft-warn → content andResolutionFailureloud-abort are unchanged. - T3
flow.py:733(_item_failure_branch) → collapse toreturn "content"(forms is unreachable; qa_sidecar/content failures already attribute to “content”). Update the docstring (:713–732) to drop the forms attribution language. - T4
flow.py:704(BranchCounter._counts) → drop the"forms"key →{"content": 0, "url": 0}. (Coupled to T20 counter tests.)
3.2 Tier B — orphaned forms-write machinery (recommended same-PR; §8 flag)
Section titled “3.2 Tier B — orphaned forms-write machinery (recommended same-PR; §8 flag)”Once T2 lands, all of the following are unreachable dead code:
- T5 delete
_ingest_form_branch(flow.py:3160–3398). - T6 delete
_trim_stale_form_fields(flow.py:3399–3429). - T7 delete
FORM_TEMPLATES_SCHEMA(:1495) +FORM_TEMPLATE_FIELDS_SCHEMA(:1525). - T8 delete the
ft_target/ftf_targetmounts (flow.py:3593–3602). - T9 delete the
form_extractorsimports (flow.py:156–160). - T10 delete the
scripts/cocoindex_pipeline/form_extractors/package (6 files).
Leaving Tier B produces an incoherent half-state: _ingest_form_branch referencing
ft_target/extract_form_structure while unreachable, and — critically — its only
real-body coverage (test_cocoindex_form_write_real_body.py, T21) must retire under
Tier C, so keeping the package means 2,235 LOC of unreachable and now-untested code that
a Knip/dead-export gate flags.
3.3 Tier C — tests (mandatory; forced by T1). See §4.
Section titled “3.3 Tier C — tests (mandatory; forced by T1). See §4.”3.4 Tier D — seed + vendored seam (mandatory; part of the linchpin commit)
Section titled “3.4 Tier D — seed + vendored seam (mandatory; part of the linchpin commit)”- T11
buildManifest→ return{ schema_version: 1, mappings: [] }(drop the workspaceId param). An empty-mappings manifest is explicitly legal — theWorkspaceManifestdocstring (workspace_resolver.py:147–149): “mappings … may be empty list — degenerate but legal; every path will then resolve toResolutionFailure”, and a non-matching path raisesUnmappedPath(aResolutionFailuresubtype) which the fork catches as a benign soft-warn → content. The content/qa/edge corpus files are already unmapped today (onlyforms/was mapped), so their routing is unchanged. The manifest FILE is still emitted — it remains the mandatory walk guard (absent/invalid →manifest_missing/manifest_invalidabort, per id-134 TECH §2.3 sense-1). - T12/T13 remove
FORMS_PATH_PREFIX,FORMS_MANIFEST_WORKSPACE_NAME,resolveFormsWorkspaceId, and simplify the--emit-manifestpath (:729–745) so it no longer resolves a forms workspace (it emits the empty-mappings manifest directly). - T14
git rmthe 2 vendored forms binaries + the now-emptyforms/procurement/dir. - T15 edit
.kh-workspace-map.json.example→{ "schema_version": 1, "mappings": [] }.
4. Test changes (which retire, which re-point, which assert the retirement)
Section titled “4. Test changes (which retire, which re-point, which assert the retirement)”4.1 T16 — platform-corpus-shape.test.ts (edit)
Section titled “4.1 T16 — platform-corpus-shape.test.ts (edit)”- Drop the 2 forms entries from
EXPECTED_ENTRIES(→ 6 entries),PDF_FILES(keepcontent/synthetic-capability-statement.pdf),DOCX_FILES(keepcontent/synthetic-sector-intel.docx). Update “8 expected entries” → 6 (:45,:89). - Retire the three forms-specific
it()blocks:maps forms/procurement/ → route "forms"(:147), the forms placeholder-workspace_id test (:158),forms branch carries both a PDF and a DOCX binary(:174). - Update the manifest-shape block to assert empty mappings (
mappings: []), and the no-IP comment (:210)content/forms/qa/edge→content/qa/edge. - Assert-the-retirement (new):
it('has no forms/ tree and no forms route mapping (DR-014)')—actualEntriescontains noforms/path ANDmanifest.mappingsis empty.
4.2 T18 — test_workspace_resolver.py (mixed)
Section titled “4.2 T18 — test_workspace_resolver.py (mixed)”- Retire
test_route_forms_prefix_resolves_forms(:337). - Re-point
test_longest_prefix_winner_route_is_returned(:381): swap the child prefix’sroute:"forms"→route:"qa_sidecar"(preserves the longest-prefix-winner route-differentiation coverage on the surviving non-content route; comment the id-131 superseded-pending caveat per §1.3). - Assert-the-retirement: extend/rename
test_invalid_route_value_rejected_at_load_time(:360, already uses a typo"froms") — add a case assertingroute:"forms"is now a load-timeManifestLoadError(the Literal no longer admits it).
4.3 T19 — test_cocoindex_flow_fork_routing.py (mixed)
Section titled “4.3 T19 — test_cocoindex_flow_fork_routing.py (mixed)”- Retire
TestFormsRouteWritesFormTargetsOnly(:229),TestFormsRouteSuffixGuard(:372), andtest_form_branch_reingest_same_bytes_mints_identical_pks(:566). - Re-point the contrast case in
TestContentRouteWritesContentTargetsOnly(:324) — the staged file is unmapped (the manifest maps a DIFFERENT prefix), so itsroute="forms"tag now fails at manifest load; swap toroute="content"(orqa_sidecar) so the “unmapped soft-warn → content” assertion still holds. - Keep
TestContentRouteWritesContentTargetsOnlycontent assertions,TestAmbiguousResolutionAtForkIsLoudZeroRows(:427),TestRouteLessManifestBackwardCompat(:627),TestQaSidecarRouteWritesSidecarTargetsOnly(:700).
4.4 T20 — test_cocoindex_flow_failure_mode.py (mixed)
Section titled “4.4 T20 — test_cocoindex_flow_failure_mode.py (mixed)”- Re-point
_two_route_manifest(:1234): change theforms/mapping’sroute:"forms"→ aqa_sidecar(or second content) mapping so the two-route failure-containment coverage survives. - Re-point the
BranchCountertests (:1276init-tally,:1278test_increment_forms_bumps_forms_only) to the post-T4 counter shape{"content": 0, "url": 0}(drop the forms-increment assertion).
4.5 T21 — test_cocoindex_form_write_real_body.py (retire whole file)
Section titled “4.5 T21 — test_cocoindex_form_write_real_body.py (retire whole file)”All 3 tests stage route="forms" and drive the real extract_form_structure /
_trim_stale_form_fields — both the route and the machinery are removed. The file has no
non-forms residue. git rm it. (This is the seam-patch real-body coverage for the
form_extractors package; its removal is the strongest signal that T10 should ride along.)
4.6 T22 — form_extractors package tests (retire iff T10 included)
Section titled “4.6 T22 — form_extractors package tests (retire iff T10 included)”Five files import form_extractors/ submodules directly and break the moment the package
(T10) is removed: test_form_extractors.py, test_form_extractor_orchestrator.py,
test_docx_tracked_changes_regression.py (:49 docx, :52 shared),
test_xlsx_zero_archetype.py (:64 shared, :65 xlsx), test_pdf_zero_archetype.py
(:53 pdf, :54 shared, :55 xlsx). git rm all five only if T10 is included. If the
Orchestrator defers T10, all five stay and the package remains covered (see §8). NOTE:
these five test only the extractor readers — they do not stage route="forms", so
(unlike T21/T23) they are not forced by T1; they are purely T10-conditional.
4.7 T23 — test_cocoindex_flow_write_path.py (MIXED — retire 5 form-write classes)
Section titled “4.7 T23 — test_cocoindex_flow_write_path.py (MIXED — retire 5 form-write classes)”Five classes exercise the form-write path and are forced out by both T1 (they stage
_make_manifest(..., route="forms")) and T10 (they import form_extractors.shared):
TestFormWriteSuccessPath, TestFormWriteMemoHitRoundTrip,
TestFormWriteGracefulEmptyProvenance, TestFormWriteSkipAndFailurePaths,
TestFormWriteIdempotency. Retire all five classes plus their forms-only helpers (the
_ingest_form helper :1736; drop the module-note about route="forms" at :1686). Every other class in the file stays (~15 content/write-path classes: TestIngestFileWritePath,
TestIngestFileStageCounters, TestMountEachArityContract, TestStablePrimaryKeysAcrossRuns,
TestIngestFileRelationshipWritePath, TestInv19QaDeclareSnapshot,
TestContentFingerprintAwaited, TestSourceDocumentProvenanceWritePath,
TestNoFictionalApiSurvives, TestLifespanProvidesDbCtx,
TestReingestUpsertPreservesAssociations, TestCanonicalRecordHasNoIntrinsicWorkspace,
TestStampExtractionBaseWiredIntoIngest:2847, TestWorkspacePathFixes:3113,
TestHolderStampWiring:3411) —
they route content and never touch the forms fork. If _make_manifest is shared with the
surviving classes, keep the helper and remove only the route="forms" call sites. This is
the same mixed treatment as T19/T20 — analogous, not a whole-file git rm (contrast T21).
Note: the five classes are forced by T1 even if T10 is deferred (the route="forms"
construction alone fails at model_validate), so — unlike T22 — T23’s retirement is
mandatory in-PR regardless of the DELTA-1 ruling.
Test-philosophy note (reference/test-philosophy.md): the surviving/added tests assert
observable behaviour — “a route:"forms" manifest is rejected at load”, “the corpus tree
has no forms files”, “content/qa routing is unchanged” — not the absence of internal
symbols. The shape guard remains a bun run test structural guard; the pipeline forks stay
behaviour-tested via the retained content/qa_sidecar/ambiguous cases.
5. Migration needs — NONE
Section titled “5. Migration needs — NONE”Verified: there is no DB-level forms-route artefact.
RouteKindis a pydanticLiteralover the.kh-workspace-map.jsonfile — no Postgres enum, CHECK, orroutecolumn. Grep ofsupabase/migrations/*.sqlforforms/route_kind/ route CHECK returns nothing forms-related.form_templates/form_template_fieldstables persist — the app-upload writer (ingest_source='app_upload') still uses them. Removing the cocoindexft_target/ftf_targetmounts (managed_by=ManagedBy.USER— cocoindex only UPSERTs, never DDLs) touches no schema. No data migration: existingform_templatesrows (app-upload or historical corpus) are untouched.
No migration file. No type regen (supabase/types/database.types.ts is unaffected —
the tables stay).
6. Atomicity, sequencing, and relation to {134.6} / {131.24}
Section titled “6. Atomicity, sequencing, and relation to {134.6} / {131.24}”One atomic PR (hard constraint). Per §1.4, T1 (RouteKind narrowing) + T11/T15 (manifest
emitters) + all Tier-C test edits must land together; a partial state aborts the next /walk
at manifest load. Recommended intra-PR order: (1) Tier D fixtures/seed + Tier A RouteKind
narrowing + Tier B source deletions in one coherent edit; (2) Tier C test edits; (3)
python3 -m pytest scripts/tests/ + bun run test green before commit. Run
gitnexus_impact({repo:'canonical', target:'RouteKind', direction:'upstream'}) and on the
fork before editing (blast radius is contained to the 3 files that reference RouteKind +
the forms tests — LOW/MEDIUM, no app//lib/ consumer).
Then re-walk. After the PR merges, a fresh corpus /walk sees an internally-consistent
no-forms corpus (empty-mappings manifest; content/qa/edge only). Do not re-walk
mid-retirement.
Relation to {134.6} (the promotion-confidence gate). The gate is forms-SILENT (id-134
TECH §4.2: no form_templates assertion, source_walk ≥ N_walk lower bound, source_documents
count is forms-independent since forms never wrote source_documents). Post-retirement the
walked-file count drops by 2 (the corpus loses 2 forms binaries) but every gate assertion
still holds — no {134.6} change is required. {134.6} may land before or after ID-136
with no contradiction; the only coupling is operational (don’t re-walk on a half-retired
corpus). The id-134 TECH already anticipates this (its §2.1 “Forms retirement” note, §9
risk #6).
Relation to {131.24} (document-admission gate, DR-020). Orthogonal. DR-020’s two-tier corpus admission and the app-side upload rework ({131.24}) govern how forms now enter (manual upload). ID-136 only removes the old corpus path; it neither implements nor depends on {131.24}. No cross-Task subtask dependency is introduced.
7. Risks and rollback
Section titled “7. Risks and rollback”- Missed
route:"forms"manifest → load-time abort (highest-likelihood miss). Any survivingroute:"forms"string (a test fixture, the.example,buildManifest) fails the next walk atmodel_validate. Mitigation: after the edit,grep -rn 'route.*forms\|"forms"\|route="forms"' scripts/ __tests__/ | grep -v form_templatemust return zero live (non-comment) hits; the fullpytest+bun run testgate catches any residue (every forms test fixture fails loudly, never silently). - Disturbing the content / qa_sidecar branches. T2/T3 edit the shared fork body.
Mitigation: the retained
TestContentRoute...,TestQaSidecarRoute...,TestAmbiguousResolution...,TestRouteLessManifestBackwardCompatcases prove the other two routes + soft-warn + loud-abort are intact;_ingest_content_branch/_ingest_qa_sidecar_branchare not touched. - Empty-mappings manifest surprises. If any deploy assumed a non-empty manifest, the empty-mappings emit could confuse an operator. Mitigation: documented as explicitly legal (§3.4, resolver docstring); the walk-guard still fires on absent/invalid.
- Tier B breadth (2,235 LOC). Deleting
form_extractors/is the largest edit and could mask an unnoticed reuse. Mitigation: grep + gitnexus both confirmflow.pyis the sole production importer and_ingest_form_branchthe sole caller ofcoerce_extracted_form(the other importers are tests — T22/T23); app-side forms are pure-TS. If the Orchestrator prefers caution, T10 (and the T22 package-tests) can defer (§8) without affecting the mandatory retirement — but T23’s 5 form-write classes retire regardless (T1 alone forces them).
Rollback. Pure git revert of the single PR restores the forms route wholesale (no DB
state to unwind, since no migration ran). If a re-walk already happened post-merge, reverting
restores the forms Literal + fork; a subsequent re-walk re-admits route:"forms". No
data-plane rollback needed — form_templates rows are writer-agnostic.
8. Scope deltas vs the ratified description (FLAG for the Orchestrator)
Section titled “8. Scope deltas vs the ratified description (FLAG for the Orchestrator)”The ratified description enumerates ~9 touch-points; the verified blast radius is ~23 touch-points (T1–T23) across ~17 files. Two deltas need an explicit ruling — I am flagging, not silently expanding:
-
DELTA-1 (recommend INCLUDE): the orphaned forms-write machinery (Tier B, T5–T10). Removing the fork (T2) orphans
_ingest_form_branch,_trim_stale_form_fields, the two formTableSchemas, the ft/ftf mounts, and the entireform_extractors/package (2,235 LOC). This exceeds the literal “flow.py forms fork” wording but is its direct cascade. Recommendation: include in the same atomic PR — its only real-body test (T21) is force-retired by Tier C, so deferral leaves unreachable and untested code that fails a dead-code gate. Alternative if the Orchestrator wants a tighter PR: keep T5–T10 (and the five T22 package-test files) and open a fast-follow “dead forms-machinery sweep” backlog item — but then T21’s whole-file removal and T23’s 5 form-write classes must still land now (they depend onroute:"forms", force-broken by T1), leaving the package temporarily untested. I judge same-PR removal cleaner. -
DELTA-2 (mechanical, already folded in): the forced test re-points/retirements (T16, T18–T23). The one-pager listed the existence of some extra test files but not that T1 forces most of them to change in-PR — and omitted
test_cocoindex_flow_write_path.py(T23) entirely (Checker-caught). This is not a scope choice — it is mandatory to keep the suite green — but it more than doubles the test-file edit count vs the one-pager’s framing (~12 test files touched; T22 alone spans 5).
Neither delta changes the behavioural outcome (DR-014); both are blast-radius accuracy. No new decision-register ruling is warranted — DR-014 already settles the behaviour, and no new binding invariant is discovered. (No DR-intent to return.)
9. Empirical-verification note
Section titled “9. Empirical-verification note”This TECH introduces no new external-library API surface — it is a net removal. The
retired form_extractors/ package consumes pypdf/python-docx/openpyxl, but no
new pinned third-party symbol is imported or called. Per shared-discipline.md §Empirical verification (scope: external-library symbols only), no import-and-call check is
required. The internal symbols touched (RouteKind, resolve_route, the flow fork,
buildManifest) are all Canonical Platform code, verified in-repo above.