Table Editing Robustness: Cell/Row Ops and Sort-Key Extraction

Not yet implemented — this is a proposal/idea note capturing a 2026-08-08 discussion prompted by an editing mistake on fitness/david-lloyd-clubs, where a batch replace_block with a hand-counted block ID clobbered a table block instead of the intended paragraph.

1. Prefer Narrow Table Ops Over Block-ID Guessing

When the goal is a single cell or a single new row, reaching for replace_block (or worse, hand-counting positions in a flat block_ids array) is unnecessarily risky — exactly this caused the 2026-08-08 incident. Prefer:

table_op set_cell addressed by column name + row index for single-cell edits, not whole-block replacement.

table_op insert_row/append_row with values as a {column: value} dict rather than a positional array — immune to silent breakage if column order ever changes.

append_row (order-agnostic) followed by a table_op sort when a row needs to land in a specific position, rather than computing an insert index by hand.

• Always read back after the edit — an "ok" status is not verification (this is already a stated rule in MCP Note Editing Guide, worth re-emphasising for table edits specifically since a wrong block ID can replace an entire table with a paragraph and still return status: ok).

2. Sorting by a Value That Isn't Directly Sortable

Motivating case: a drive-time column holding strings like "~50-55 min (M4)" isn't sortable as-is. Three approaches were discussed, in order of preference:

Rejected: Hidden/Shadow Sort-Key Column

Store a derived numeric column (e.g. drive_min) alongside the display text. Rejected: nothing enforces that the hidden column stays in sync if the display text is edited later, so the sort can silently go stale/wrong. Two representations of one fact is the core problem, not a detail to manage around.

Rejected as a default: Naive Strip-Non-Digit Transform

Strip everything but digits, then sort/compare numerically. Breaks on ranges: "~50-55 min""5055" and "~5-9 min""59" — the concatenation isn't a meaningful number, it just doesn't clash by coincidence in small same-width datasets. Any transform that discards structure (which number, which bound) without saying so is an invisible assumption baked into the data rather than a stated rule.

Preferred: Explicit Extraction at Sort Time (PCRE), Single Source of Truth

Keep the display text as the only stored value. Extract a sort key transiently, at the point of sorting, via an explicit pattern rather than an ambient assumption. Proposed shape for a future table_op sort extension:

pattern — a PCRE applied to each cell's text.

group — which capture group supplies the sort key.

agg — required (no silent default) when a pattern can match more than once per cell: first, last, min, or max. For a range like "~50-55 min", agg=max sorts by worst-case drive time, which is probably what you want — but the point is that the caller states this, rather than the tool picking silently.

• A stated rule for cells with no match at all (e.g. "-" or "not yet open" text) — sort first, sort last, or error — rather than undefined behaviour.

For genuinely single-valued numeric cells (a plain "42 min", no ranges), a simpler numeric=true flag on sort (extract the first run of digits, compare numerically) would cover the common case without needing full PCRE machinery.

3. Confirmed Working (2026-08-08, on fitness/david-lloyd-clubs)

Live-tested table_op for the first time in this session, prompted directly by being asked whether it had actually been used (it hadn't — every prior edit had gone through whole-document put instead, including one case where table.set_cell was clearly the better tool). Findings:

table_op requires operation names prefixed table. (e.g. table.set_cell, not set_cell) — not obvious from the tool schema; the error message lists valid ops.

table.set_cell correctly scoped a single-cell change with no risk to the rest of the table — confirms the core recommendation in section 1.

• The tool schema describes value as a "JSON primitive", but a JSON-encoded link object ({"link": {"href": ..., "text": ...}}) was accepted and rendered as a real link, not a string — the schema description undersells what it actually supports. Worth relying on for link-bearing cells, but re-verify if this stops working, since it isn't documented behaviour.

table_op (as with patch/batch) does not bump version/updated automatically — a separate patch_meta call is needed afterwards to keep metadata in sync.

Status

Sort-key extraction (section 2) remains idea only. table_op sort currently accepts only by/ascending; no pattern/extraction support exists. If implemented, prefer the PCRE-at-sort-time approach over any hidden-column scheme, and make agg and no-match handling mandatory rather than defaulted, so the extraction rule stays visible rather than becoming another silent assumption. Section 1 (narrow ops over block-ID guessing) and section 3 (confirmed working) are not proposals — they're current, verified tool behaviour.

4. Broader Idea: Point Tool Schemas at Notes Instead of Duplicating Docs

Discovered by the table.-prefix gap above: the tool schema for table_op doesn't state the prefix, and separately describes set_cell's value parameter as a "JSON primitive" when it actually also accepts a JSON-encoded rich block like a link. Both gaps got fixed by hand in README/mcp-tools after the fact — but the schema itself still doesn't say either thing, so the next agent hits the same surprise.

Proposed idea: rather than trying to keep full parameter docs, examples, and gotchas synced in the tool schema itself (which is small and concise by design — see Connection Bootstrap), the schema's description field for a tool/operation could simply say something like "see README/mcp-tools for operation names and parameters" and rely on the agent reading that note before use. This matches how this notes system already works everywhere else — TRIGGERS routes to detailed notes rather than inlining everything — and it means one edit (updating the note) fixes the documentation for every future agent, instead of the fix living only in a note that an agent has to already know to read.

Trade-off worth naming: a schema that just says "see note X" is only as good as the agent's discipline about actually reading X before calling the tool — it doesn't help an agent that skips the read, the way an inline schema description at least attempts to. It also adds one extra round-trip (read the note) before the first correct call, versus a schema that's simply complete. The case for it is strongest for tools like table_op where the full operation surface is large and changes over time; a static schema field is guaranteed to drift, where a linked note can be corrected once and stay correct.

created 2026-08-08  ·  tags ideas, notes-system, table_op, proposal  ·  updated 2026-08-08  ·  version 5