Skip to content

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 on main TODAY — 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 in test_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 a Literal type alias). A repo-wide grep confirms RouteKind is referenced only in workspace_resolver.py and two test files (test_workspace_resolver.py, test_cocoindex_flow_fork_routing.py) — no app/ or lib/ consumer.
  • gitnexus_context({repo:'canonical', name:'buildManifest'}) → called by main (seed-synthetic-corpus.ts) only.
  • gitnexus_context({repo:'canonical', name:'coerce_extracted_form'})called by _ingest_form_branch only (confirmed by the GitNexus PreToolUse hook). The whole form_extractors/ package (extract_form_structure, coerce_extracted_form, FormExtractionError, the pdf/docx/xlsx readers) has flow.py as its only production consumer (grep: the sole production import site is flow.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 plus test_cocoindex_flow_write_path.py’s form-write classes all import form_extractors submodules 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:

  1. form_templates has TWO independent writers, and only one is being retired. The corpus walk writes form_templates/form_template_fields via the cocoindex ft_target/ftf_target mounts (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; the form_templates/form_template_fields tables persist unchanged for app-upload. No DDL, no data migration (§5).
  2. 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.json manifest file. There is no route enum, CHECK, or column in any migration (grep of supabase/migrations/*.sql for forms / route_kind / route CHECK returns nothing forms-related). So “retire the route” = narrow the Literal + remove the fork branch + purge every manifest tagged route:"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 loadWorkspaceManifest.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 the RouteKind narrowing must land in one commit — any half-state (Literal narrowed, a manifest still tagging route:"forms") aborts the next /walk at load.
  • Every test that stages a route:"forms" manifest breaks at model_validate time 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:lineCurrent state (verified)S437 drift
T1scripts/cocoindex_pipeline/workspace_resolver.py:65RouteKind = Literal["content", "forms", "qa_sidecar"]; forms also named in docstrings/comments :11, :53, :64, :130, :139one-pager said :65 “Literal + extra=forbid” — the extra="forbid" is on WorkspaceMapping/WorkspaceManifest, not the RouteKind line
T2scripts/cocoindex_pipeline/flow.py:1997–2011the if route == "forms": fork branch → _ingest_form_branch(...)one-pager :1997
T3scripts/cocoindex_pipeline/flow.py:733_item_failure_branch returns "forms" if route == "forms" else "content"one-pager mislabels this _route_to_branchno such symbol exists; the fn is _item_failure_branch (:712–732)
T4scripts/cocoindex_pipeline/flow.py:704BranchCounter._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:lineCurrent state (verified)note
T5scripts/cocoindex_pipeline/flow.py:3160–3398_ingest_form_branch (~239 LOC)one-pager said :3062DRIFT: actual :3160 (+98)
T6scripts/cocoindex_pipeline/flow.py:3399–3429_trim_stale_form_fields (only caller: _ingest_form_branch:3369)not enumerated
T7scripts/cocoindex_pipeline/flow.py:1495, :1525FORM_TEMPLATES_SCHEMA, FORM_TEMPLATE_FIELDS_SCHEMA (only consumers: the ft/ftf mounts)not enumerated
T8scripts/cocoindex_pipeline/flow.py:3593–3602ft_target/ftf_target cocoindex mounts (only consumer: _ingest_form_branch)not enumerated
T9scripts/cocoindex_pipeline/flow.py:156–160imports extract_form_structure, coerce_extracted_form, FormExtractionErrornot enumerated
T10scripts/cocoindex_pipeline/form_extractors/6 files, 2,235 LOC (__init__, orchestrator, pdf, docx, xlsx, shared) — imported ONLY by flow.pynot 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:lineCurrent state (verified)S437 drift
T11scripts/seed-synthetic-corpus.ts:331–341buildManifest emits a manifest whose ONLY mapping is {FORMS_PATH_PREFIX, route:'forms'} (:336/:338)one-pager :331/:338
T12scripts/seed-synthetic-corpus.ts:104, :101FORMS_PATH_PREFIX = 'forms/procurement/', FORMS_MANIFEST_WORKSPACE_NAME (:101)
T13scripts/seed-synthetic-corpus.ts:506–524, :729–745resolveFormsWorkspaceId + the --emit-manifest path that resolves the forms workspaceone-pager said :732 (the call site); def is :506
T14scripts/cocoindex_pipeline/fixtures/platform-corpus/forms/procurement/synthetic-sq-officesupplies.pdf + synthetic-itt-groundsmaint.docxthe 2 vendored forms binaries
T15scripts/cocoindex_pipeline/fixtures/platform-corpus/.kh-workspace-map.json.example:2–8manifest 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)”
#FileForms surface (verified)disposition
T16__tests__/integration/cocoindex/platform-corpus-shape.test.tsEXPECTED_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 :210edit (§4.1)
T17scripts/tests/test_platform_manifest_gate.pywhole class TestFormsPrefixConsumesWorkspaceId:471 + test_...:476; module docstring assertion (c) :31–34; comment :74; _CHARNWOOD_DOCX fixture usageretire the class; keep the file (other classes stay)
T18scripts/tests/test_workspace_resolver.pytest_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)
T19scripts/tests/test_cocoindex_flow_fork_routing.py12 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 staymixed (§4.3)
T20scripts/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)
T21scripts/tests/test_cocoindex_form_write_real_body.pyentire file — all 3 tests stage route="forms" (:292,:373,:469) AND exercise the REAL extract_form_structure + _trim_stale_form_fields; both dependencies removedretire the whole file (§4.5)
T22scripts/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 directlyretire iff T10 is included (§8)
T23scripts/tests/test_cocoindex_flow_write_path.pyMIXED 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:160TestHolderStampWiring:3411) staysretire 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.


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:65RouteKind = 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). RouteKind stays exported in __all__:297 (still a live type). WorkspaceMapping.route default stays "content"; existing content/qa manifests parse unchanged.
  • T2 flow.py:1997–2011 → delete the if route == "forms": branch entirely. The fork then routes qa_sidecar_ingest_qa_sidecar_branch, else _ingest_content_branch. UnmappedPath soft-warn → content and ResolutionFailure loud-abort are unchanged.
  • T3 flow.py:733 (_item_failure_branch) → collapse to return "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.)
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_target mounts (flow.py:3593–3602).
  • T9 delete the form_extractors imports (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 — the WorkspaceManifest docstring (workspace_resolver.py:147–149): “mappings … may be empty list — degenerate but legal; every path will then resolve to ResolutionFailure, and a non-matching path raises UnmappedPath (a ResolutionFailure subtype) which the fork catches as a benign soft-warn → content. The content/qa/edge corpus files are already unmapped today (only forms/ was mapped), so their routing is unchanged. The manifest FILE is still emitted — it remains the mandatory walk guard (absent/invalid → manifest_missing/manifest_invalid abort, per id-134 TECH §2.3 sense-1).
  • T12/T13 remove FORMS_PATH_PREFIX, FORMS_MANIFEST_WORKSPACE_NAME, resolveFormsWorkspaceId, and simplify the --emit-manifest path (:729–745) so it no longer resolves a forms workspace (it emits the empty-mappings manifest directly).
  • T14 git rm the 2 vendored forms binaries + the now-empty forms/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 (keep content/synthetic-capability-statement.pdf), DOCX_FILES (keep content/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/edgecontent/qa/edge.
  • Assert-the-retirement (new): it('has no forms/ tree and no forms route mapping (DR-014)')actualEntries contains no forms/ path AND manifest.mappings is 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’s route:"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 asserting route:"forms" is now a load-time ManifestLoadError (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), and test_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 its route="forms" tag now fails at manifest load; swap to route="content" (or qa_sidecar) so the “unmapped soft-warn → content” assertion still holds.
  • Keep TestContentRouteWritesContentTargetsOnly content 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 the forms/ mapping’s route:"forms" → a qa_sidecar (or second content) mapping so the two-route failure-containment coverage survives.
  • Re-point the BranchCounter tests (:1276 init-tally, :1278 test_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.


Verified: there is no DB-level forms-route artefact.

  • RouteKind is a pydantic Literal over the .kh-workspace-map.json file — no Postgres enum, CHECK, or route column. Grep of supabase/migrations/*.sql for forms / route_kind / route CHECK returns nothing forms-related.
  • form_templates / form_template_fields tables persist — the app-upload writer (ingest_source='app_upload') still uses them. Removing the cocoindex ft_target/ftf_target mounts (managed_by=ManagedBy.USER — cocoindex only UPSERTs, never DDLs) touches no schema. No data migration: existing form_templates rows (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.


  1. Missed route:"forms" manifest → load-time abort (highest-likelihood miss). Any surviving route:"forms" string (a test fixture, the .example, buildManifest) fails the next walk at model_validate. Mitigation: after the edit, grep -rn 'route.*forms\|"forms"\|route="forms"' scripts/ __tests__/ | grep -v form_template must return zero live (non-comment) hits; the full pytest + bun run test gate catches any residue (every forms test fixture fails loudly, never silently).
  2. Disturbing the content / qa_sidecar branches. T2/T3 edit the shared fork body. Mitigation: the retained TestContentRoute..., TestQaSidecarRoute..., TestAmbiguousResolution..., TestRouteLessManifestBackwardCompat cases prove the other two routes + soft-warn + loud-abort are intact; _ingest_content_branch/ _ingest_qa_sidecar_branch are not touched.
  3. 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.
  4. Tier B breadth (2,235 LOC). Deleting form_extractors/ is the largest edit and could mask an unnoticed reuse. Mitigation: grep + gitnexus both confirm flow.py is the sole production importer and _ingest_form_branch the sole caller of coerce_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 form TableSchemas, the ft/ftf mounts, and the entire form_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 on route:"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.)


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.