Skip to content

Heading-derived contentchunks columns — disposition RESEARCH

Heading-derived content_chunks columns — disposition RESEARCH

Section titled “Heading-derived content_chunks columns — disposition RESEARCH”

Status: RESEARCH artefact for ratification. This document does NOT decide; it gives Liam a ratifiable disposition for the four heading-derived columns on content_chunks.

Context (what [GAP-CMI-004] is, why now). Task ID-56 ratified the cocoindex chunking stage at OQ-CMI-56-3 → cocoindex.ops.text.RecursiveSplitter, a budget-driven (chunk_size + chunk_overlap), heading-unaware splitter (docs/specs/id-56-content-model-invariants/RESEARCH.md:487-495; docs/specs/id-56-content-model-invariants/TECH.md:§2.X). Because budget-split does not preserve heading boundaries, the four heading-derived columns on content_chunksheading_text, heading_level, heading_path, parent_chunk_id — are NULL (or the '{}' array default) on every cocoindex-emitted chunk. The open question of what to do with these columns is [GAP-CMI-004] / OQ-CMI-56-4 (docs/specs/id-56-content-model-invariants/PRODUCT.md:183-200; docs/specs/id-56-content-model-invariants/RESEARCH.md:409-459, which recommends keeping them nullable). The PRODUCT/TECH default is disposition (a); this document audits all three options against the live code/schema so Liam can confirm or override with full evidence.


The four columns, sourced directly from the canonical content_chunks DDL in supabase/migrations/20260416102457_pre_squash_reconciliation.sql:3450-3466:

ColumnSQL type (DDL)Nullability (DDL)DB defaultFK / index
heading_texttextNULLABLE ("heading_text" "text", no NOT NULL) — supabase/migrations/20260416102457_pre_squash_reconciliation.sql:3454nonepartial B-tree idx_content_chunks_heading WHERE heading_text IS NOT NULLsupabase/migrations/20260416102457_pre_squash_reconciliation.sql:4623
heading_levelsmallintNULLABLE:3455nonenone
heading_pathtext[]NOT NULL with default ("heading_path" "text"[] DEFAULT '{}'::"text"[] NOT NULL) — :3456'{}' (empty array)none
parent_chunk_iduuidNULLABLE:3459nonepartial B-tree idx_content_chunks_parent WHERE parent_chunk_id IS NOT NULLsupabase/migrations/20260416102457_pre_squash_reconciliation.sql:4631; self-ref FK content_chunks_parent_chunk_id_fkeycontent_chunks(id) ON DELETE CASCADEsupabase/migrations/20260416102457_pre_squash_reconciliation.sql:5331

Precision point (load-bearing for option (a)): three of the four columns are truly nullable, but heading_path is NOT NULL DEFAULT '{}' — it never holds SQL NULL; on a cocoindex-emitted chunk it falls to the empty-array default, not NULL. The {56.8} smoke-test encodes exactly this distinction: heading keys are OMITTED from the declared row dict so the three nullable cols become NULL and heading_path becomes '{}' (scripts/tests/test_cocoindex_chunking.py:16-19,302-311).

Current population state. Under the ratified cocoindex chunking stage, all four are unpopulated on every emitted chunk: the {56.8} stage OMITS the heading keys from the declare_row dict, mirroring the content_text_hash GENERATED-ALWAYS omission pattern (scripts/tests/test_cocoindex_chunking.py:9-20,299-311). Empirically (see §6), zero of 408 live staging content_chunks rows carry any heading value.

Generated-types view (cited per spec; types file is permission-restricted to this agent — re-confirmed via the underlying SQL DDL and the spec line refs). The generated supabase/types/database.types.ts content_chunks Row presents the columns at supabase/types/database.types.ts:521 (parent_chunk_id: string | null, confirmed by the spec’s V-6 line range :509-571; docs/specs/id-56-content-model-invariants/RESEARCH.md:447) and docs/specs/id-56-content-model-invariants/TECH.md:244-245. The two FKs on the Row are content_chunks_content_item_id_fkey (→ content_items) and content_chunks_parent_chunk_id_fkey (self-ref) — docs/specs/id-56-content-model-invariants/TECH.md:244.

Note on verification scope: supabase/types/database.types.ts is denied to this agent’s Read/Bash grep tools (directory permission). The schema facts above are taken from the live SQL migration DDL (which IS readable) and cross-checked against the spec line refs that previous Planners verified against the generated file. The generated-type nullability assertions (RPC Returns lines 4404-4406) are carried verbatim from docs/specs/id-56-content-model-invariants/TECH.md:162,246 and PRODUCT C-13 (docs/specs/id-56-content-model-invariants/PRODUCT.md:69) — marked below where they rest on the spec citation rather than a fresh file read.


Every read/write site of the four columns across SQL, TypeScript, and Python, with each site’s NULL-tolerance status. Queries used to assemble this map:

  • grep -rn 'heading_text|heading_level|heading_path|parent_chunk_id' lib app components hooks contexts types (TS/TSX consumers).
  • grep -rn 'parent_chunk_id' lib app components hooks contexts types scripts supabase (whole-corpus, all extensions — for the zero-TS-consumer claim).
  • grep -rn '<cols>' supabase/migrations + grep -rn '<cols>' scripts (SQL + Python, outside ast-dataflow’s TypeScript-only reach).
  • gitnexus_query({query: 'search_content_chunks heading columns chunk retrieval', repo: '…/id56-heading'}) — returned zero execution flows (empty processes / process_symbols), only standalone symbol definitions; the heading columns participate in no graph-indexed execution flow. Definitions surfaced: lib/content/chunk-store.ts:storeChunks (write), lib/content/chunking.ts:chunkByHeadings, lib/mcp/formatters/search.ts:formatChunkSearchResults (:195-241) + ChunkSearchResult (:142-158), lib/mcp/formatters/content.ts:formatContentItemChunks (:106-122) + ContentItemChunk (:96-104), scripts/kb_pipeline/chunk.py:chunk_by_headings (:55-205) + store_chunks (:214-284). (Verbatim tool output recorded in the dispatch journal.)
  • gitnexus_context({name: 'search_content_chunks', repo: '…/id56-heading'}) — returned "Symbol 'search_content_chunks' not found": the RPC is a Postgres function, not a TypeScript symbol, so gitnexus does not index it as a callable node. Verdict level / caller count / affected-flow data is therefore not available from gitnexus for this symbol; the blast-radius assessment in §4 rests on the grep/ast-level consumer enumeration below instead. (ast-dataflow column-reads content_chunks.heading_text is also unavailable — ast-dataflow indexes object property access on typed TS symbols, not Supabase string-keyed .select() projections; the grep sweep is the authoritative TS consumer enumeration here.)
SiteWhat it doesNULL-tolerance
search_content_chunks RPC, latest def supabase/migrations/20260430192325_widen_search_rpcs_visibility_filter.sql:333-358RETURNS TABLE(... heading_text text, heading_level smallint, heading_path text[], ...) selecting cc.heading_text, cc.heading_level, cc.heading_path (:356-358)SQL-side tolerant. The RETURNS TABLE columns carry NO NOT NULL — SQL TABLE columns are nullable by default, and the SELECT passes the underlying column values through unchanged. The function returns NULL/'{}' for cocoindex chunks without error. The intolerance is purely at the generated-TypeScript layer (§2.2).
Prior RPC defs …20260428212936_extend_search_content_chunks_review_filters.sql:58-60,80-82 and …20260416102457_pre_squash_reconciliation.sql:3029,3043-3045earlier search_content_chunks revisions, same Returns shapesame — superseded by the 20260430 def above
SiteRead / writeCurrent NULL-tolerance
lib/mcp/formatters/search.ts:151-153 (ChunkSearchResult interface)TS type for RPC resultAlready nullableheading_text: string | null, heading_level: number | null, heading_path: string[] | null
lib/mcp/formatters/search.ts:173-175 (ChunkSearchResultSchema Zod)runtime validationAlready .nullable()z.string().nullable(), z.number().nullable(), z.array(z.string()).nullable()
lib/mcp/formatters/search.ts:214-217 (formatChunkSearchResults)renderTolerantr.heading_text || '(preamble)'; r.heading_path?.length ? r.heading_path.join(' > ') : '(document root)'. NULL renders as (preamble) / (document root)
lib/mcp/tools/search.ts:577-698 (MCP search_content_chunks tool)calls RPC, casts result as ChunkSearchResult[] at :677, passes to formatter at :679Tolerant via cast. The tool does a blanket cast (no per-field mapping), so it inherits the formatter’s NULL handling. No per-column access in the tool body itself
lib/mcp/tools/content.ts:184 (raw .select('id, heading_text, heading_level, heading_path, position, char_count, word_count')) + map :194-196direct table read in get_content_item chunk fetchTolerant — typed row.heading_text as string | null (:194), row.heading_level as number | null (:195), (row.heading_path as string[] | null) ?? [] (:196, coalesces to empty array)
lib/mcp/formatters/content.ts:97-105 (ContentItemChunk interface)TS type for the aboveAlready nullableheading_text: string | null, heading_level: number | null, heading_path: string[]
lib/mcp/formatters/content.ts:112-116 (formatContentItemChunks)renderTolerantchunk.heading_level ? ' '.repeat(...) : ''; chunk.heading_text || '(preamble)'
lib/content/chunk-store.ts:34-35,64,95-100,126,136 (legacy app-side writer)WRITE — populates all four including parent_chunk_id second-pass update (:126,136)RETIRED in v1 per docs/specs/id-56-content-model-invariants/TECH.md:23,16; the eight regenerateChunks call sites are rewired to a folder-drop wrapper (docs/specs/id-56-content-model-invariants/TECH.md:16)
lib/content/chunking.ts:24-26,79-81,131-133,182-187 (legacy chunker, ContentChunk shape + heading derivation)derive heading values pre-writeRETIRED in v1 (same retirement as chunk-store.ts)

Key finding — parent_chunk_id has ZERO read-consumers anywhere in the TypeScript app corpus. The whole-corpus sweep (grep -rn 'parent_chunk_id' lib app components hooks contexts types scripts supabase) returns matches in only: (1) the retired legacy WRITER lib/content/chunk-store.ts:75-100,126,136; (2) the retired Python WRITER scripts/kb_pipeline/chunk.py:252,277; (3) the cocoindex smoke-test asserting it is OMITTED scripts/tests/test_cocoindex_chunking.py:17,306; (4) the schema DDL itself supabase/migrations/20260416102457_pre_squash_reconciliation.sql:3459,4631,5331; (5) the generated types supabase/types/database.types.ts:521,537,553,567-568 (Row + FK metadata). No application READ site, no RPC Returns column, no UI, no hook, no formatter reads parent_chunk_id. It is a schema-only self-ref FK that no live read path consumes. This materially lowers the consumer-retirement cost of dropping parent_chunk_id under option (b) — for that one column the cost is schema-DDL-only.

§2.3 Python consumers (legacy writers + new smoke-test)

Section titled “§2.3 Python consumers (legacy writers + new smoke-test)”
SiteRead / writeStatus
scripts/kb_pipeline/chunk.py:32-34,55-205,247-252,277 (chunk_by_headings, Chunk dataclass, store_chunks)WRITE — Python pipeline regex heading-split writer populating all four columns + parent_chunk_id second-pass (:277)Legacy heading-split writer (chunk.py:1-7 docstring: “Splits markdown content at H2 (or H1 fallback) heading boundaries… Mirrors the TypeScript implementation in lib/content/chunking.ts”); superseded by the cocoindex stage under the canonical-pipeline cutover
scripts/tests/test_chunk.py:34-36,58-71,100-102tests of the legacy heading-split writerretire with chunk.py
scripts/tests/test_cocoindex_chunking.py:16-19,299-311asserts the cocoindex stage OMITS all four (NULL / '{}') per disposition (a)the live encoding of the GAP-CMI-004 default

(The scripts/analyse_template.py, scripts/extract_docx_tables.py, scripts/extract_tender_questions.py, scripts/cocoindex_pipeline/form_extractors/docx.py matches on the bare token heading_text are local variables in unrelated DOCX-extraction code — not consumers of the content_chunks columns. Confirmed by inspection of the grep context: they assign a Python local heading_text, not a content_chunks column.)


The columns stay in the schema; cocoindex chunks carry NULL / '{}'; consumers tolerate it. This is the PRODUCT/TECH default (docs/specs/id-56-content-model-invariants/PRODUCT.md:188; docs/specs/id-56-content-model-invariants/TECH.md:57).

(i) Downstream impact. Effectively none beyond what the specs already mandate. Every TS read consumer is already NULL-tolerant: the ChunkSearchResult interface/Zod (lib/mcp/formatters/search.ts:151-153,173-175), the formatter (lib/mcp/formatters/search.ts:214-217), the content.ts direct read (lib/mcp/tools/content.ts:194-196), and the ContentItemChunk formatter (lib/mcp/formatters/content.ts:112-116) all coalesce NULL to (preamble) / (document root) / []. No consumer code change is needed for the application to keep functioning with NULL heading values.

(ii) Schema-migration cost. ONE migration, and it is NOT unique to option (a) — it is the search_content_chunks RPC Returns nullability update (TECH “Migration 2”, docs/specs/id-56-content-model-invariants/TECH.md:162-181). The current generated database.types.ts declares the RPC’s heading_text / heading_level / heading_path Returns columns as non-nullable (string / number / string[] at supabase/types/database.types.ts:4404-4406, cited per docs/specs/id-56-content-model-invariants/TECH.md:162,246 and docs/specs/id-56-content-model-invariants/PRODUCT.md:69 — UNVERIFIED by direct file read, types file is permission-restricted; the underlying SQL RETURNS TABLE at supabase/migrations/20260430192325_widen_search_rpcs_visibility_filter.sql:333-336 carries no NOT NULL, so the non-nullability is a Supabase generator artefact, not a SQL constraint). The migration redeclares the function so the generator emits the three columns as nullable, followed by database.types.ts regen. No DDL on the content_chunks table itself. No Zod change (already .nullable()). No consumer-retirement work.

Critical distinction (brief-mandated): TECH Migration 2 (RPC Returns nullability) is required under option (a) just as much as under option (b). It is the cost of letting cocoindex NULLs flow safely to typed consumers — NOT a cost unique to dropping the columns. Dropping (option b) does NOT avoid this migration; it changes its shape (remove the columns from the Returns rather than nullable-ise them).

(iii) Historical-data implication. Columns retained ⇒ any historically-populated values survive untouched. Per §6, the live staging corpus has zero populated heading values across 408 chunks, so there is nothing to preserve on staging; prod is UNVERIFIED. The “soft-archived q_a_pair rows MAY retain populated heading values” hypothesis (docs/specs/id-56-content-model-invariants/PRODUCT.md:188) is not borne out on staging (zero q_a_pair-parent chunks; zero archived-parent chunks — §6). Option (a) is the zero-loss option regardless: if any populated values exist anywhere (prod), they are kept.

(iv) Ratifiable recommendation for (a). RECOMMEND as the default. It is the lowest-impact, lowest-risk path: zero consumer churn, one already-required migration, zero data loss. The only cost is mild schema clutter (four permanently-NULL columns) and a slightly misleading idx_content_chunks_heading / idx_content_chunks_parent partial index that will index nothing going forward (a negligible, near-empty index).


Remove heading_text, heading_level, heading_path, parent_chunk_id from content_chunks. Cleaner schema; coordinated migration + RPC + consumer retirement.

(i) Downstream impact — discrete work items, each with file:line. Because gitnexus does not index the RPC (search_content_chunks “not found” — §2), the blast radius is the grep-enumerated consumer set, not a graph verdict. Work items:

  1. content_chunks column-drop DDL. ALTER TABLE public.content_chunks DROP COLUMN heading_text, DROP COLUMN heading_level, DROP COLUMN heading_path, DROP COLUMN parent_chunk_id; plus dropping the two partial indexes idx_content_chunks_heading (supabase/migrations/20260416102457_pre_squash_reconciliation.sql:4623) and idx_content_chunks_parent (:4631) and the self-ref FK content_chunks_parent_chunk_id_fkey (:5331). DROP COLUMN parent_chunk_id cascades the FK automatically. Via supabase migration new + supabase db push only (CLAUDE.md DDL discipline).
  2. search_content_chunks RPC redeclare. Remove heading_text, heading_level, heading_path from the RETURNS TABLE(...) clause (supabase/migrations/20260430192325_widen_search_rpcs_visibility_filter.sql:333-358) and from the SELECT body (:356-358). This is the same migration touch-point as option (a)‘s Migration 2 — but column-removal rather than nullable-isation.
  3. Generated-types regendatabase.types.ts regen; the content_chunks Row and the RPC Returns both lose the three/four columns.
  4. lib/mcp/formatters/search.ts — remove heading_text/heading_level/heading_path from ChunkSearchResult (:151-153), from ChunkSearchResultSchema (:173-175), and rework formatChunkSearchResults (:214,216-217) which currently renders sectionTitle from heading_text and the Path: line from heading_path.join(' > '). The ## N. {sectionTitle} heading and the **Path:** line lose their data source — a UX decision (drop the lines, or substitute position-based labelling).
  5. lib/mcp/tools/content.ts:184,194-196 — remove the three columns from the raw .select(...) projection and the row map.
  6. lib/mcp/formatters/content.ts:97-105,112-116 — remove from ContentItemChunk and rework formatContentItemChunks (currently indents by heading_level and titles by heading_text). “Document Sections” rendering loses its structure source.
  7. MCP App contract checkmcp-app-contracts.test.ts (per lib/mcp/CLAUDE.md) asserts app types.ts match formatter interfaces; if any MCP App surfaces chunk sections, its types and the contract test must be updated.
  8. Tool description copy — the search_content_chunks tool description (lib/mcp/tools/search.ts:584) advertises “Each chunk includes its heading path (breadcrumb) showing where it sits in the document structure” — this promise becomes false and the description must be rewritten.

parent_chunk_id (work item 1 only) has zero application consumers (§2.2 key finding), so its retirement is DDL-only. The cost of (b) is concentrated entirely in heading_text / heading_level / heading_path consumer churn (items 4-8).

(ii) Schema-migration cost. Higher than (a): a content_chunks DROP COLUMN migration (item 1) PLUS the RPC redeclare (item 2, same touch-point as (a)‘s Migration 2 but a different edit) PLUS types regen PLUS Zod edits (NOT just nullable — full removal) PLUS the consumer-retirement set (items 4-8). bun run build will surface every type-strict consumer after the regen.

(iii) Historical-data implication. Dropping the columns destroys any historically-populated heading values irreversibly. Per §6, staging has none (so no loss on staging), but prod is UNVERIFIED — if prod chunks carry populated heading_path breadcrumbs (plausible: the legacy chunk-store.ts / chunk.py writers populated them by design), (b) discards that document-structure context permanently. This is the strongest argument for not choosing (b) before a prod-side population check. NB the TECH single-path ratification states the full client corpus is re-ingested under the canonical pipeline (docs/specs/id-56-content-model-invariants/TECH.md:59), which would null out any prod heading values anyway on re-ingest — but the re-ingest is an implementation event, not a reason to pre-emptively drop the columns.

(iv) Ratifiable recommendation for (b). Do NOT recommend for v1. It trades a meaningful consumer-churn + irreversible-data-loss cost for a cosmetic schema-cleanliness gain, on a schema that is already NULL-tolerant end-to-end. If schema hygiene becomes a priority later, (b) is a clean v1.1 follow-up — and at that point parent_chunk_id can be dropped cheaply (DDL-only) even if the other three are kept, since it has zero consumers.


§5. Option (c) — re-purpose for AST-aware boundaries (tree-sitter) — OUT OF v1 SCOPE

Section titled “§5. Option (c) — re-purpose for AST-aware boundaries (tree-sitter) — OUT OF v1 SCOPE”

RecursiveSplitter.split() accepts an optional language parameter enabling tree-sitter syntax-aware splitting (docs/specs/id-56-content-model-invariants/TECH.md:128; docs/specs/id-56-content-model-invariants/RESEARCH.md:440-444). Under (c), when language is supplied for markdown/code paths, the tree-sitter node type (e.g. section name, function name) could re-populate heading_text / heading_level / heading_path semantically.

(i) Downstream impact. Net-positive for consumers IF implemented: the existing NULL-tolerant renderers (formatChunkSearchResults :214-217, formatContentItemChunks :113-116) would simply start receiving populated values again — no consumer code change needed, the columns light back up. But it requires new WRITE-side work: the chunking-stage @coco.fn must supply language and map tree-sitter node metadata into the four columns — work that lives in docs/specs/cocoindex-flow-scaffolding/TECH.md (the WRITE side), not in ID-56’s read contract.

(ii) Schema-migration cost. Zero column DDL (the columns already exist with the right types). The RPC Returns nullability update (option a’s Migration 2) is still wanted as an interim, because until (c) ships the columns are NULL. The cost is entirely WRITE-side engineering + a tree-sitter grammar dependency, not migration.

(iii) Historical-data implication. Neutral-to-positive: keeping the columns (as in (a)) preserves the option to back-fill them via (c) later. Choosing (b) would FORECLOSE (c) without re-adding the columns first.

(iv) Ratifiable recommendation for (c). OUT OF v1 SCOPE — defer to a v1.1 Task. Recommend recording it as a v1.1 candidate (it is already flagged as such in docs/specs/id-56-content-model-invariants/RESEARCH.md:444,459 and the C-14 RETIRED note docs/specs/id-56-content-model-invariants/TECH.md:53,136). Do not implement now. Crucially, (c) is only reachable if the columns are retained — i.e. choosing (a) now keeps the (c) door open at zero cost; choosing (b) now closes it.


§6. Empirical check result (historical-data dimension)

Section titled “§6. Empirical check result (historical-data dimension)”

Status: VERIFIED against live staging data (turayklvaunphgbgscat, the .env.local target per CLAUDE.md), via read-only SELECT through the Supabase MCP tool. Prod NOT checked (UNVERIFIED for prod).

Query 1 — population counts across all content_chunks:

total_chunks : 408
heading_text_populated : 0
heading_level_populated : 0
heading_path_populated : 0 (non-NULL AND <> '{}')
parent_chunk_id_populated: 0

Query 2 — breakdown by parent content_type + archived state:

content_type | chunk_count | heading_text_pop | heading_path_pop | archived_parent_chunks
article | 408 | 0 | 0 | 0

Findings:

  • On staging, 100% of the 408 content_chunks rows already carry zero heading values — even before the cocoindex cutover. The columns are de-facto unused on staging.
  • There are no q_a_pair-parented chunks and no archived-parent chunks on staging — all 408 belong to article-type parents. The PRODUCT hypothesis that soft-archived q_a_pair rows MAY retain populated heading values (docs/specs/id-56-content-model-invariants/PRODUCT.md:188) is NOT borne out on staging. (It remains UNVERIFIED for prod — staging is a refreshed mirror, not a 1:1 prod copy, so prod may differ. The claim rests on schema/spec reasoning for prod.)
  • Implication: the historical-data risk of option (b) is nil on staging and UNVERIFIED on prod. A prod-side population check (the same two SELECTs against the prod project) would fully de-risk (b) if Liam wants to pursue it later.

Section titled “§7. Overall recommendation + recommended default”

Overall recommendation: (a) — keep the four columns as nullable legacy. Recommended default if Liam does not intervene.

Rationale (ranked):

  1. Already NULL-tolerant end-to-end. Every TS read consumer (lib/mcp/formatters/search.ts:151-153,173-175,214-217; lib/mcp/tools/content.ts:194-196; lib/mcp/formatters/content.ts:112-116) already coalesces NULL/'{}' gracefully. Option (a) needs zero consumer changes.
  2. The one required migration is NOT unique to dropping. The search_content_chunks RPC Returns nullability update (docs/specs/id-56-content-model-invariants/TECH.md:162-181) is required under (a) AND (b). Dropping does not save it; it merely changes its shape. So (a) costs strictly less migration work than (b).
  3. Zero data loss, and keeps (c) reachable. Retaining the columns preserves any prod-side historical values (UNVERIFIED but possible) and is the prerequisite for a future v1.1 tree-sitter re-population (option c). Dropping forecloses both.
  4. Empirically low stakes. Staging shows zero populated heading values across 408 chunks (§6) — the columns are already inert. The only downside of keeping them is cosmetic schema clutter and two near-empty partial indexes.
  5. parent_chunk_id has zero consumers — but this argues for cheap LATER removal under a v1.1 hygiene pass, not for a v1 drop that would also churn the three heading-rendering consumers.

The exact ratifiable choice statement for Liam:

OQ-CMI-56-4 / [GAP-CMI-004] disposition — ratify one:

  • (a) Keep nullable legacy [RECOMMENDED DEFAULT]. Retain heading_text, heading_level, heading_path, parent_chunk_id on content_chunks. Land only the search_content_chunks RPC Returns nullability migration (TECH Migration 2 — required regardless). No consumer churn, no data loss, keeps option (c) open. {56.4} PLAN carries NO column-drop Subtask.
  • (b) Drop the columns. Add a content_chunks DROP COLUMN migration + RPC column-removal + Zod/formatter/tool consumer retirement (8 work items, §4) + regen. Cleaner schema; irreversibly loses any prod-side historical heading values (UNVERIFIED; zero on staging); forecloses option (c). {56.4} PLAN gains a dedicated column-drop + consumer-retirement Subtask. parent_chunk_id alone could be dropped DDL-only (zero consumers) if a partial drop is preferred.
  • (c) Re-purpose for AST-aware boundaries — OUT OF v1 SCOPE. Record as a v1.1 Task only; do not implement in v1. Requires option (a) (columns retained) as a precondition.

  • Live schema DDL: supabase/migrations/20260416102457_pre_squash_reconciliation.sql:3450-3466,4623,4631,5331.
  • Latest search_content_chunks RPC: supabase/migrations/20260430192325_widen_search_rpcs_visibility_filter.sql:333-358; prior …20260428212936_extend_search_content_chunks_review_filters.sql:58-60,80-82; original …20260416102457_pre_squash_reconciliation.sql:3029,3043-3045.
  • TS consumers: lib/mcp/formatters/search.ts:142-158,165-181,195-241; lib/mcp/tools/search.ts:577-698; lib/mcp/tools/content.ts:178-207; lib/mcp/formatters/content.ts:94-123.
  • Retired legacy writers: lib/content/chunk-store.ts:27-152; lib/content/chunking.ts:22-218; scripts/kb_pipeline/chunk.py:1-205,214-284.
  • Cocoindex stage behaviour: scripts/tests/test_cocoindex_chunking.py:1-33,290-326.
  • Generated types (cited per spec, file permission-restricted to this agent): supabase/types/database.types.ts:521 (Row), :4404-4406 (RPC Returns) via docs/specs/id-56-content-model-invariants/TECH.md:162,244-246 + docs/specs/id-56-content-model-invariants/PRODUCT.md:69.
  • Anchor specs: docs/specs/id-56-content-model-invariants/TECH.md:52,57,128,136,162-181,244-246; docs/specs/id-56-content-model-invariants/PRODUCT.md:69,183-200; docs/specs/id-56-content-model-invariants/RESEARCH.md:409-459,487-495.
  • Code-intelligence: gitnexus_query (zero execution flows, definitions only); gitnexus_context({name: 'search_content_chunks'}) (symbol not found — Postgres function, not TS-indexed).
  • Empirical: read-only SELECT ×2 against staging turayklvaunphgbgscat (§6).

cocoindex pin context: cocoindex[postgres]==1.0.3 (requirements.txt:44). No NEW external-library symbol is introduced by this disposition doc (the RecursiveSplitter contract is already verified in docs/specs/id-56-content-model-invariants/TECH.md:§6), so no fresh OQ-3 import-and-call check is required here.