Skip to content

v0.8.2 — escaped pipes in SD cells, header-based column mapping

Latest

Choose a tag to compare

@tranquocthong tranquocthong released this 13 Aug 01:09
· 1 commit to main since this release

A | inside an SD table cell silently corrupted the trace built from it. Found reviewing an SD whose §5.1 describes a signature payload joined by | and whose §12.2 trigger names a status enum — both perfectly ordinary things for a requirement to say, and both unreadable to the engine.

splitRow() split on every |, including an escaped \|. The escape is what markdown requires to keep a literal pipe inside a cell, so a correctly-written SD was exactly the case that broke: the row gained columns, and every reader after the pipe landed one cell off. This repo's own contract-shim SD is the demonstration — its ERR_INVALID_STATUS row parsed as 12 cells against a 6-column header, with Trigger cut short at the first escaped pipe and the rest of the enum scattered across columns that do not exist. Downstream, trace-build stored the truncated requirement and read priority/source out of their neighbours, and route scored complexity on the cut-short text before picking fast/expand/deep. splitRow now splits on (?<!\\)\| and unescapes \| to | on read, so the value that reaches code, YAML, or a payload is the plain U+007C character — the backslash is a rendering artifact and never leaves the document.

Pass-1 was the source: genSd interpolated raw SRS prose straight into cells. New core.mdCell() escapes on write at all 13 interpolation sites (revision history, §5.1 from AC / edges / BL rules / the ID-prefix fallback, §5.2 NFR, §10.4 state, §12.2 error triggers, §13.2 TC, glossary). It round-trips losslessly with splitRow and does not double-escape an SRS that already writes \| itself. The generated §5.1 now carries a one-line note that the escape is markdown-only and the value is U+007C — a backslash copy-pasted out of a rendered cell into code is a silent data bug that no test catches.

trace-build resolved all five SD tables by column position. New core.resolveCols(table, spec) claims each column by header name, falling back to the canonical Pass-1 position (and never to -1, which would read undefined cells). Applied to the FR / TC / error / state / NFR tables in trace-build and to route, which was destructuring [id, req, prio, src]. The 0.8.0 tcExpIdx special case folds into it, keeping its length-aware fallback (6-col enriched to index 4, 4-col skeleton to 3). route and trace-build now share one FR column spec: they must agree on which cell is the Requirement, or a routed FR and its trace node describe different things.

A shape-broken table now says so. core.tableShapeWarnings() flags any row whose cell count differs from its header — naming the offending row id and its real count, and pointing at the unescaped | — surfaced by trace-build (all five tables) and route (§5.1). It warns, never blocks: the SD stays readable to a human, and the trace built from it is wrong without looking wrong, which is the half nobody catches by eye. route's Result gains a warnings field.

The rule is written down where SDs are authored. agents/sd-author.md now requires escaping a literal | as \| in cell content, and requires a line under the table whenever a requirement or payload genuinely uses | as a delimiter, stating that the delimiter is the plain character. templates/sd-template.md §5.1 carries the same convention for hand-written SDs.

Upgrade note: an SD that already escapes its pipes will now produce a different (correct) trace.json when trace-build is re-run, and an SD with a raw | in a cell will surface a new warning from trace-build and route. Neither blocks.

790 Node tests green (8 new), 65 Python tests green.