Semantic search over a Markdown knowledge base, served over MCP.
Since v1.18.0 (.docx) and v1.19.0 (.pptx) a table row is one line of a
chunk, its cells separated by tabs. A reader that shows an excerpt, such as
grooveseek-desktop, has to guess which lines are table rows from the text alone,
and the guess fails both ways: a paragraph outside a table can hold a tab, and a
row whose first or last cell is empty starts or ends with a tab that a trim
removes. Only the parser knows which lines it wrote as rows, and it knows it per
chunk.
ADR-0028 and ADR-0029 kept the schema unchanged and caught existing indexes up
through policy keys. .docx now needs a fourth key, which meets ADR-0029’s
revisit condition (“one format needs a fourth key of this kind”).
.docx and .pptx chunk, so every one of them is re-embedded.
Rejected.chunks on every write and delete path. Rejected.chunks. Taken: NULL says “not computed” as it is,
and the value goes with its row, which upsert_document already deletes and
rewrites as a whole. ADR-0028 and ADR-0029 kept the schema unchanged so that
no row had to say which rules wrote it; this column holds what a chunk
contains (where its rows are), not a generation of rules, so it does not run
against that.For the catch-up:
Chunk::table_rows: Option<Vec<TableRowSpan>>, where TableRowSpan { table,
start, end } is a byte range into content without the row’s newline and the
table’s number in the chunk by first appearance. The .docx and .pptx
parsers return Some (an empty list when there is no row); every other format
and the text fallback return None.chunks.table_rows TEXT holds its JSON, NULL meaning not computed.
SearchResult::table_rows returns it after checking it fits the content
(each span inside it, covering whole lines, in order, the tables numbered
from 0 without a gap); a value that does not reads as None. SearchHit does not carry it.docx_table_rows_policy and pptx_table_rows_policy (row-spans) join the
predicate of ADR-0029, each written last among its format’s keys in the run’s
final transaction.BEGIN IMMEDIATE and reads the hash and the text again before it
writes, so no row keeps table rows computed from a text it does not hold. A
re-read writes the column only while the row’s hash is the hash of the bytes
it parsed: a row another writer rehashed in between is left as it is and stays
unsettled, so a later run reads it again. A changed file whose chunk text
matches and whose row moved in between gets the column NULL, since that write
replaces the hash..docx and .pptx once and
writes the column; nothing is re-embedded and nothing is counted as updated.--force.SearchResult has a new field, so Rust code that builds one with a struct
literal needs table_rows: None..docx needs a fifth key of this kind, a third format needs a
catch-up, or a second per-chunk column is needed.grooveseek/src/parser/docx.rs, grooveseek/src/parser/pptx.rs
and grooveseek/src/parser/ooxml.rs (the spans), grooveseek/src/parser/mod.rs
(table_rows_fit, the check a span must pass), grooveseek/src/db.rs (the
column, the read check, the keys), grooveseek/src/indexer.rs (the
column-only write and its recheck, the notices) and
grooveseek/tests/index_table_rows_policy.rs (the runs).