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_chunks —
heading_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.
§1. Column inventory
Section titled “§1. Column inventory”The four columns, sourced directly from the canonical content_chunks DDL in
supabase/migrations/20260416102457_pre_squash_reconciliation.sql:3450-3466:
| Column | SQL type (DDL) | Nullability (DDL) | DB default | FK / index |
|---|---|---|---|---|
heading_text | text | NULLABLE ("heading_text" "text", no NOT NULL) — supabase/migrations/20260416102457_pre_squash_reconciliation.sql:3454 | none | partial B-tree idx_content_chunks_heading WHERE heading_text IS NOT NULL — supabase/migrations/20260416102457_pre_squash_reconciliation.sql:4623 |
heading_level | smallint | NULLABLE — :3455 | none | none |
heading_path | text[] | NOT NULL with default ("heading_path" "text"[] DEFAULT '{}'::"text"[] NOT NULL) — :3456 | '{}' (empty array) | none |
parent_chunk_id | uuid | NULLABLE — :3459 | none | partial B-tree idx_content_chunks_parent WHERE parent_chunk_id IS NOT NULL — supabase/migrations/20260416102457_pre_squash_reconciliation.sql:4631; self-ref FK content_chunks_parent_chunk_id_fkey → content_chunks(id) ON DELETE CASCADE — supabase/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.tsis 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 fromdocs/specs/id-56-content-model-invariants/TECH.md:162,246and 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.
§2. Consumer map
Section titled “§2. Consumer map”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 (emptyprocesses/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-dataflowcolumn-reads content_chunks.heading_textis also unavailable — ast-dataflow indexes object property access on typed TS symbols, not Supabase string-keyed.select()projections; thegrepsweep is the authoritative TS consumer enumeration here.)
§2.1 SQL consumers (read)
Section titled “§2.1 SQL consumers (read)”| Site | What it does | NULL-tolerance |
|---|---|---|
search_content_chunks RPC, latest def supabase/migrations/20260430192325_widen_search_rpcs_visibility_filter.sql:333-358 | RETURNS 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-3045 | earlier search_content_chunks revisions, same Returns shape | same — superseded by the 20260430 def above |
§2.2 TypeScript consumers
Section titled “§2.2 TypeScript consumers”| Site | Read / write | Current NULL-tolerance |
|---|---|---|
lib/mcp/formatters/search.ts:151-153 (ChunkSearchResult interface) | TS type for RPC result | Already nullable — heading_text: string | null, heading_level: number | null, heading_path: string[] | null |
lib/mcp/formatters/search.ts:173-175 (ChunkSearchResultSchema Zod) | runtime validation | Already .nullable() — z.string().nullable(), z.number().nullable(), z.array(z.string()).nullable() |
lib/mcp/formatters/search.ts:214-217 (formatChunkSearchResults) | render | Tolerant — r.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 :679 | Tolerant 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-196 | direct table read in get_content_item chunk fetch | Tolerant — 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 above | Already nullable — heading_text: string | null, heading_level: number | null, heading_path: string[] |
lib/mcp/formatters/content.ts:112-116 (formatContentItemChunks) | render | Tolerant — chunk.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-write | RETIRED 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)”| Site | Read / write | Status |
|---|---|---|
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-102 | tests of the legacy heading-split writer | retire with chunk.py |
scripts/tests/test_cocoindex_chunking.py:16-19,299-311 | asserts 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.)
§3. Option (a) — keep nullable legacy
Section titled “§3. Option (a) — keep nullable legacy”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).
§4. Option (b) — drop the columns
Section titled “§4. Option (b) — drop the columns”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:
content_chunkscolumn-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 indexesidx_content_chunks_heading(supabase/migrations/20260416102457_pre_squash_reconciliation.sql:4623) andidx_content_chunks_parent(:4631) and the self-ref FKcontent_chunks_parent_chunk_id_fkey(:5331).DROP COLUMN parent_chunk_idcascades the FK automatically. Viasupabase migration new+supabase db pushonly (CLAUDE.md DDL discipline).search_content_chunksRPC redeclare. Removeheading_text,heading_level,heading_pathfrom theRETURNS 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.- Generated-types regen —
database.types.tsregen; thecontent_chunksRow and the RPC Returns both lose the three/four columns. lib/mcp/formatters/search.ts— removeheading_text/heading_level/heading_pathfromChunkSearchResult(:151-153), fromChunkSearchResultSchema(:173-175), and reworkformatChunkSearchResults(:214,216-217) which currently renderssectionTitlefromheading_textand thePath:line fromheading_path.join(' > '). The## N. {sectionTitle}heading and the**Path:**line lose their data source — a UX decision (drop the lines, or substituteposition-based labelling).lib/mcp/tools/content.ts:184,194-196— remove the three columns from the raw.select(...)projection and the row map.lib/mcp/formatters/content.ts:97-105,112-116— remove fromContentItemChunkand reworkformatContentItemChunks(currently indents byheading_leveland titles byheading_text). “Document Sections” rendering loses its structure source.- MCP App contract check —
mcp-app-contracts.test.ts(perlib/mcp/CLAUDE.md) asserts apptypes.tsmatch formatter interfaces; if any MCP App surfaces chunk sections, its types and the contract test must be updated. - Tool description copy — the
search_content_chunkstool 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 : 408heading_text_populated : 0heading_level_populated : 0heading_path_populated : 0 (non-NULL AND <> '{}')parent_chunk_id_populated: 0Query 2 — breakdown by parent content_type + archived state:
content_type | chunk_count | heading_text_pop | heading_path_pop | archived_parent_chunksarticle | 408 | 0 | 0 | 0Findings:
- On staging, 100% of the 408
content_chunksrows 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 toarticle-type parents. The PRODUCT hypothesis that soft-archivedq_a_pairrows 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.
§7. Overall recommendation + recommended default
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):
- 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. - The one required migration is NOT unique to dropping. The
search_content_chunksRPC 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). - 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.
- 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.
parent_chunk_idhas 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_idoncontent_chunks. Land only thesearch_content_chunksRPC 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_chunksDROP COLUMNmigration + 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_idalone 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.
§8. Sources cited
Section titled “§8. Sources cited”- Live schema DDL:
supabase/migrations/20260416102457_pre_squash_reconciliation.sql:3450-3466,4623,4631,5331. - Latest
search_content_chunksRPC: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) viadocs/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 stagingturayklvaunphgbgscat(§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.