* 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:
5
.changeset/noble-bears-munch.md
Normal file
5
.changeset/noble-bears-munch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 1665
|
||||
---
|
||||
**`check.decision-coverage-plan` no longer false-passes when CONTEXT.md decisions use the titled-colon bullet form** — `parseDecisions` recognized the colon-immediate (`- **D-NN:** text`) and em-dash (`- **D-NN — title** body`) forms but dropped the titled-colon form (`- **D-NN: Title.** body`, where a title sits between the colon and the closing `**`) via the parse-miss guard. When all decisions used the titled convention, the parser returned 0 decisions and the coverage gate passed vacuously. A third per-form regex (checked last, a strict superset of the colon form) now parses the titled-colon form; id and `[tags]` trackability are honored.
|
||||
@@ -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.
|
||||
|
||||
@@ -791,3 +791,40 @@ describe('FIX D: gap-checker surfaces decision could-not-parse even when require
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Regression #1639: titled-colon bullet form '- **D-NN: Title.** body' ─────
|
||||
// Both bulletColonRe (':**' anchor) and bulletEmDashRe (em-dash) miss the form where a
|
||||
// title sits between the colon and the closing **, so it was dropped by the parse-miss
|
||||
// guard and check.decision-coverage-plan passed vacuously when all decisions were titled.
|
||||
describe('parseDecisions — titled-colon bullet form (#1639)', () => {
|
||||
test('titled-colon bullet is parsed, not dropped', () => {
|
||||
const md = '## Locked decisions\n- **D-01: Default sandbox ON.** body\n- **D-02: Reject unsigned.** body two\n';
|
||||
const out = parseDecisions(md);
|
||||
assert.equal(out.length, 2, 'should extract both titled-colon decisions (not 0)');
|
||||
assert.equal(out[0].id, 'D-01');
|
||||
assert.equal(out[1].id, 'D-02');
|
||||
});
|
||||
|
||||
test('titled-colon coexists with colon-immediate and em-dash forms', () => {
|
||||
const md = '## Locked decisions\n- **D-01:** plain colon\n- **D-02 — emdash** body\n- **D-03: Titled.** body\n';
|
||||
const out = parseDecisions(md);
|
||||
assert.equal(out.length, 3);
|
||||
assert.deepEqual(out.map((d) => d.id), ['D-01', 'D-02', 'D-03']);
|
||||
});
|
||||
|
||||
test('titled-colon with [tags] still parses id and tags', () => {
|
||||
const md = '## Locked decisions\n- **D-01 [informational]: Title.** body\n';
|
||||
const out = parseDecisions(md);
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].id, 'D-01');
|
||||
assert.ok(out[0].tags.includes('informational'), `tags should include informational, got ${JSON.stringify(out[0].tags)}`);
|
||||
});
|
||||
|
||||
test('all-titled CONTEXT.md parses every decision (no vacuous 0)', () => {
|
||||
// The reporter case: 13 decisions all titled → previously all dropped → gate passed vacuously.
|
||||
let md = '## Locked decisions\n';
|
||||
for (let i = 1; i <= 13; i++) md += `- **D-${String(i).padStart(2, '0')}: Decision ${i}.** body\n`;
|
||||
const out = parseDecisions(md);
|
||||
assert.equal(out.length, 13, 'all 13 titled-colon decisions must parse (not vacuously 0)');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user