fix(#1639): parseDecisions handles the titled-colon bullet form (#1665)

* fix(#1639): parseDecisions handles titled-colon bullet form

bulletColonRe anchors on ':**' (colon immediately before close-bold) and bulletEmDashRe
requires an em-dash, so the titled-colon form '- **D-NN: Title.** body' (title between the
colon and the closing **) matched neither and was dropped by the parse-miss guard. When all
decisions used the titled convention, parseDecisions returned 0 and check.decision-coverage-
plan passed vacuously — the same false-coverage failure mode as #1343/#1364/#1365. Add a
third per-form regex bulletTitledColonRe, checked LAST (strict superset of bulletColonRe,
so it only catches bullets the other two miss — minimal blast radius); id + [tags]
trackability honored. Regression folded into decisions.test.cjs: titled-colon parses,
coexists with colon/em-dash, tags, all-titled-13 no longer vacuously 0.

* fix(#1639): tighten titled-colon title to [^:*]* so malformed pre-colon-run bullets still reject

The first cut's title run [^*]* was too permissive: it matched a genuinely-malformed
bullet with a colon in the pre-separator freeform run (e.g. 'D-07 ratio 3:1:**') by
treating the 3:1 colon as the separator, regressing the #1343 parse-miss guard tests.
Tighten the title to [^:*]* (no colon, no star) so the separator colon remains the only
colon permitted before ** — matching bulletColonRe's existing [^:*]* discipline. Valid
titled forms (colon-free titles) still parse; the malformed colon-in-freeform case still
falls through to the parse-miss guard.

* chore(#1639): backfill changeset pr ref to 1665
This commit is contained in:
Tom Boucher
2026-06-24 13:26:25 -04:00
committed by GitHub
parent 4631923982
commit b205e4c2b2
3 changed files with 71 additions and 0 deletions

View File

@@ -71,6 +71,19 @@ const bulletColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)
*/
const bulletEmDashRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^*]*[—–][^*]*\*\*\s*(.*)$/;
/**
* Titled-colon form: `- **D-NN[ [tags]]: Title.** body`
* A title sits between the colon and the closing `**` (so the `:**` anchor of
* bulletColonRe fails, and there is no em-dash for bulletEmDashRe). This is a strict
* superset of the colon-immediate form, so it MUST be checked AFTER bulletColonRe and
* bulletEmDashRe — it only catches bullets those two miss. The title run is `[^:*]*` (no
* colon, no `*`) so a genuinely-malformed bullet with a colon in the pre-separator run
* (e.g. `D-07 ratio 3:1:**`) still fails the anchor and falls through to the parse-miss
* guard — matching bulletColonRe's `[^:*]*` discipline that the separator colon is the
* only colon permitted before `**`. (#1639)
*/
const bulletTitledColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:[^:*]*\*\*\s*(.*)$/;
interface ParseDecisionLinesResult {
decisions: Decision[];
parseMisses: number;
@@ -150,6 +163,22 @@ function parseDecisionLines(block: string): ParseDecisionLinesResult {
continue;
}
// Titled-colon form: `- **D-NN[ [tags]]: Title.** body` (#1639). Checked LAST — it is
// a strict superset of bulletColonRe, so it only catches bullets the colon-immediate
// and em-dash forms missed (minimal blast radius). id + [tags] trackability honored;
// the body after the closing bold run is reported as text.
const titledColonMatch = line.match(bulletTitledColonRe);
if (titledColonMatch) {
flush();
const id = `D-${titledColonMatch[1]}`;
const tags = titledColonMatch[2]
? titledColonMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
: [];
const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
current = { id, text: titledColonMatch[3] || '', category, tags, trackable };
continue;
}
// Parse-miss guard (FIX B + #1343): a line that looks like a `D-NN` decision
// bullet but failed both patterns — flush, warn, and record the miss.
// parseMisses > 0 forces could-not-parse even when other decisions parsed.