* test(#4130): failing-first regression for phase-prefixed decision IDs Add the #4130 matrix: D4-01/D12-01 across all three bullet forms, tags, discretion, wrapped lead-ins, gate-level plan/verify end-to-end rows, and parity properties (well-formed digit-prefixed ids parse to their exact id; a non-digit injected into the prefix fails loud). Update the #2347 non-D-prefix fixture from D5-NN (now a legal grammar) to DEC-NN, and graduate the representative d5-prefix corpus fixture from could-not-parse to parsed-but-uncovered. All new rows are RED against origin/next; they go green with the parser fix in the next commit. * fix(#4130): parse phase-prefixed decision IDs (D4-01) The three declaration grammars, the parse-miss guard, the #3939 join regexes, and the token evidence all anchored on the literal 'D-' (or '**D-'), so an ID carrying a digit-run phase prefix between the leading letter and the hyphen matched nothing — while the #2347 shape detector correctly called those bullets decision-shaped, collapsing the whole CONTEXT.md to could-not-parse with 0 extracted instead of a coverage verdict. Derive the extractor ID grammar from one shared DECISION_ID_SOURCE ('D[0-9]*-' + the existing alnum tail, full id captured), widen the guard/join anchors to ID_ATTEMPT_SOURCE (bare 'D-' or a digit-initial prefix run, so a typo'd 'D4x-01' fails loud while letter-initial prose like 'Deferred-until' stays none-present), and align the bare-token evidence. Both gates and the gap-checker share the parser, so all three surfaces read phase-prefixed decisions now; the gate messages name the accepted forms including the phase-prefixed one. * docs(#4130): document the phase-prefixed decision identifier form The canonical CONTEXT.md reference said decisions carry 'a sequential D-NN identifier' with no mention of the optional phase-number prefix the parser now accepts (D4-01) or the alphanumeric tail it always accepted (D-INFRA-01). Name both in the Decision identifier format section, EN and ja-JP. * chore(#4130): changeset * chore(#4130): backfill PR number in changeset --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -329,12 +329,15 @@ function cmdDecisionCoveragePlan(projectDir: string, args: string[], raw: boolea
|
||||
uncovered: [],
|
||||
message: partialParse
|
||||
? 'Decision coverage gate: decisions could not be fully parsed — one or more ' +
|
||||
'`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator). ' +
|
||||
'Fix the bullet format so all D-NN decisions can be read before re-running the gate.'
|
||||
'`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
|
||||
'prefix that is not a digit run, e.g. `D4x-01`). Fix the bullet format so all decisions ' +
|
||||
'can be read before re-running the gate.'
|
||||
: 'Decision coverage gate: could not parse decisions — possible format mismatch. ' +
|
||||
'The CONTEXT.md appears to be decision-shaped (has a <decisions> block, a decisions heading, ' +
|
||||
'or D- tokens) but no D-NN bullets could be extracted. Check the formatting of the decisions ' +
|
||||
'block and ensure bullets follow the `- **D-NN:** text` or `- **D-NN — title** body` form.',
|
||||
'or D- tokens) but no decision bullets could be extracted. Check the formatting of the decisions ' +
|
||||
'block and ensure bullets follow the `- **D-NN:** text`, `- **D4-NN:** text` (phase-prefixed), ' +
|
||||
'or `- **D-NN — title** body` form. An ID grammar the parser does not support (e.g. `DEC-01`) ' +
|
||||
'also lands here.',
|
||||
}, raw, undefined);
|
||||
return;
|
||||
}
|
||||
@@ -433,9 +436,11 @@ function cmdDecisionCoverageVerify(projectDir: string, args: string[], raw: bool
|
||||
not_honored: [],
|
||||
message: partialParse
|
||||
? 'Decision coverage verify (warning): decisions could not be fully parsed — one or more ' +
|
||||
'`- **D-NN ...**` bullets appear malformed. Fix the bullet format in the CONTEXT.md decisions block.'
|
||||
'`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
|
||||
'prefix that is not a digit run). Fix the bullet format in the CONTEXT.md decisions block.'
|
||||
: 'Decision coverage verify (warning): could not parse decisions — possible format mismatch. ' +
|
||||
'Check the formatting of the CONTEXT.md decisions block.',
|
||||
'Check the formatting of the CONTEXT.md decisions block (accepted forms: `- **D-NN:** text`, ' +
|
||||
'`- **D4-NN:** text` (phase-prefixed), `- **D-NN — title** body`).',
|
||||
}, raw, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* truth). Behaviour is preserved byte-for-behaviour from the prior hand-written
|
||||
* .cjs; only types are added.
|
||||
*
|
||||
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
|
||||
* Accepts numeric (D-42), alphanumeric (D-INFRA-01), and phase-prefixed
|
||||
* (D4-01 — an optional digit-run between the leading letter and the hyphen,
|
||||
* #4130) IDs.
|
||||
* Returns {id, text, category, tags, trackable} per decision.
|
||||
* CJS callers that only use {id, text} safely ignore the extra fields.
|
||||
*
|
||||
@@ -57,23 +59,56 @@ const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
|
||||
// ─── Bullet parsers (decisions-specific grammar) ─────────────────────────────
|
||||
|
||||
/**
|
||||
* Colon form: `- **D-NN[ [tags]]:** text`
|
||||
* (#1343: `[^:*]*` subsumes any pre-colon prose, stops at `:**`)
|
||||
* #4130: the ID grammar every extractor regex below shares, as ONE source.
|
||||
* `D`, an OPTIONAL digit-run phase prefix, a hyphen, then the pre-existing
|
||||
* alphanumeric tail — so `D-01` (bare), `D4-01`/`D12-01` (phase-prefixed,
|
||||
* the reporter's multi-phase convention where bare D-01 collides across
|
||||
* phases), and `D-INFRA-01` (alnum tail) are all the same grammar now.
|
||||
* #2347 had already taught the shape DETECTOR to call `D4-01` decision-shaped
|
||||
* while the EXTRACTOR still anchored on the literal `**D-` — the disagreement
|
||||
* that made a whole phase-prefixed CONTEXT.md report could-not-parse. Deriving
|
||||
* the three grammars (and the token evidence below) from this one constant is
|
||||
* the parity pin: the extractor's ID universe cannot drift from the declared
|
||||
* grammar again without editing this line, which the #4130 property tests
|
||||
* watch from the other side.
|
||||
*/
|
||||
const bulletColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^:*]*:\*\*\s*(.*)$/;
|
||||
const DECISION_ID_SOURCE = 'D[0-9]*-[A-Za-z0-9][A-Za-z0-9_-]*';
|
||||
|
||||
/**
|
||||
* Em-dash form: `- **D-NN[ [tags]] — title** body`
|
||||
* #4130: the bold lead-in that ATTEMPTS the ID grammar above — used by the
|
||||
* parse-miss guard and the #3939 join regexes, where recognising MORE shapes
|
||||
* is the conservative direction (an over-broad match can only make a
|
||||
* malformed bullet fail loud). The prefix run is either empty (bare `D-`) or
|
||||
* DIGIT-INITIAL (`4`, `4x` — a phase prefix with a typo still counts as an
|
||||
* attempted ID, so `D4x-01` reaches the guard and fails loud instead of
|
||||
* vanishing), but never letter-initial: `D` + letters + `-` (`Deferred-until`)
|
||||
* is a prose word, and prose must stay `none-present` (#2347's law).
|
||||
*/
|
||||
const ID_ATTEMPT_SOURCE = 'D(?:[0-9][A-Za-z0-9]*)?-';
|
||||
|
||||
/**
|
||||
* Colon form: `- **D[phase]-NN[ [tags]]:** text`
|
||||
* (#1343: `[^:*]*` subsumes any pre-colon prose, stops at `:**`)
|
||||
* Group 1 captures the FULL id including any phase prefix (#4130).
|
||||
*/
|
||||
const bulletColonRe = new RegExp(
|
||||
`^\\s*-\\s+\\*\\*(${DECISION_ID_SOURCE})(?:\\s*\\[([^\\]]+)\\])?[^:*]*:\\*\\*\\s*(.*)$`,
|
||||
);
|
||||
|
||||
/**
|
||||
* Em-dash form: `- **D[phase]-NN[ [tags]] — title** body`
|
||||
* The em-dash (U+2014) or its lookalike separates the ID+tags group from a title
|
||||
* that lives inside the bold markers; the body (which may be empty) follows
|
||||
* outside the closing `**`. This form was not handled pre-T1 (bug #1364).
|
||||
*
|
||||
* Accepts both U+2014 em-dash (—) and U+2013 en-dash (–) for robustness.
|
||||
*/
|
||||
const bulletEmDashRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?[^*]*[—–][^*]*\*\*\s*(.*)$/;
|
||||
const bulletEmDashRe = new RegExp(
|
||||
`^\\s*-\\s+\\*\\*(${DECISION_ID_SOURCE})(?:\\s*\\[([^\\]]+)\\])?[^*]*[—–][^*]*\\*\\*\\s*(.*)$`,
|
||||
);
|
||||
|
||||
/**
|
||||
* Titled-colon form: `- **D-NN[ [tags]]: Title.** body`
|
||||
* Titled-colon form: `- **D[phase]-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
|
||||
@@ -83,16 +118,35 @@ const bulletEmDashRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+
|
||||
* 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*(.*)$/;
|
||||
const bulletTitledColonRe = new RegExp(
|
||||
`^\\s*-\\s+\\*\\*(${DECISION_ID_SOURCE})(?:\\s*\\[([^\\]]+)\\])?[^:*]*:[^:*]*\\*\\*\\s*(.*)$`,
|
||||
);
|
||||
|
||||
/**
|
||||
* #4130: the parse-miss guard's probe — a line whose bold lead-in ATTEMPTS the
|
||||
* ID grammar (see `ID_ATTEMPT_SOURCE`) but failed all three bullet patterns
|
||||
* above. Bare `D-` attempts behave exactly as before #4130; a digit-initial
|
||||
* prefix run (`D4-`… including a typo'd `D4x-`) is new evidence of an attempt,
|
||||
* so the malformed-prefixed bullet fails loud instead of silently vanishing.
|
||||
*/
|
||||
const parseMissGuardRe = new RegExp(`^\\s*-\\s+\\*\\*${ID_ATTEMPT_SOURCE}`);
|
||||
|
||||
/**
|
||||
* #4130: bare-token evidence of decision-shaped content — a `D…-<alnum>` token
|
||||
* in running text. `D-01` matched before; the digit-run phase prefix (`D4-01`)
|
||||
* is added so token evidence agrees with the extractor's ID grammar
|
||||
* (DECISION_ID_SOURCE) instead of silently ignoring prefixed mentions.
|
||||
*/
|
||||
const decisionTokenRe = new RegExp(`\\bD[0-9]*-[A-Za-z0-9]`, 'm');
|
||||
|
||||
/**
|
||||
* #2347: format-agnostic evidence that a block/section holds real decision
|
||||
* ENTRIES the parser could not read — a bullet whose bold lead-in is an
|
||||
* ID-SHAPED token (uppercase prefix, optional digits, hyphen, alnum), whatever
|
||||
* the exact ID grammar. The three parser grammars above all require a `D-`
|
||||
* prefix; #1365's fail-loud guard reused that same `\bD-` test as its "is this
|
||||
* decision-shaped?" evidence, so any other prefix (e.g. `D5-01`) was invisible
|
||||
* to BOTH parser and guard, collapsing `could-not-parse` into a clean
|
||||
* the exact ID grammar. #1365's fail-loud guard originally reused the parser's
|
||||
* own `\bD-` test as its "is this decision-shaped?" evidence, so any prefix the
|
||||
* parser could not read (e.g. `D5-01` then, `DEC-01` now) was invisible to
|
||||
* BOTH parser and guard, collapsing `could-not-parse` into a clean
|
||||
* `none-present` pass.
|
||||
*
|
||||
* The ID-shape requirement (not "any bold bullet") is deliberate: a decisions
|
||||
@@ -100,26 +154,36 @@ const bulletTitledColonRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([
|
||||
* bullets with bold labels (`- **Scope:** …`, `- **Why:** …`, `- **Note:** …`).
|
||||
* Those are NOT decision entries and must stay `none-present` — a false
|
||||
* `could-not-parse` hard-blocks the plan gate. `[A-Z]+[0-9]*-[A-Za-z0-9]` matches
|
||||
* `D-01` / `D5-01` / `DEC-01` but not `Scope:` / `Why:` / `Follow-up:` (mixed
|
||||
* `D-01` / `D4-01` / `DEC-01` but not `Scope:` / `Why:` / `Follow-up:` (mixed
|
||||
* case) / `TODO:` (no `-<alnum>` id) — mirroring the parser's own `D-<alnum>`
|
||||
* shape without hardcoding the `D`.
|
||||
*
|
||||
* #4130 parity note: for the D-prefixed universe this detector's grammar
|
||||
* (`D` + digit-run + `-` + alnum) is exactly `DECISION_ID_SOURCE` above, so a
|
||||
* well-formed bullet the detector calls decision-shaped is now always one the
|
||||
* extractor can read. The detector stays WIDER on purpose (`DEC-01` is still
|
||||
* evidence): an ID grammar outside the parser's universe must keep failing
|
||||
* loud, never silently passing. The #4130 property tests pin both directions.
|
||||
*/
|
||||
const boldLeadInBulletRe = /^\s*-\s+\*\*[A-Z]+[0-9]*-[A-Za-z0-9]/m;
|
||||
|
||||
/**
|
||||
* #3939: a decision bullet's DECLARATION line — the `- **D-NN … **` bold lead-in
|
||||
* the three grammars above anchor on — may wrap across a line break. Physical
|
||||
* line breaks inside a bullet are markdown-insignificant, and GSD's own
|
||||
* #3939: a decision bullet's DECLARATION line — the `- **D[phase]-NN … **` bold
|
||||
* lead-in the three grammars above anchor on — may wrap across a line break.
|
||||
* Physical line breaks inside a bullet are markdown-insignificant, and GSD's own
|
||||
* discuss-phase writer emits the wrapped shape whenever a decision title runs
|
||||
* past the wrap column. All three grammars require the closing `**` in the same
|
||||
* string as the `- **D-` anchor, so a wrapped declaration matched none of them
|
||||
* string as the `- **D…-` anchor, so a wrapped declaration matched none of them
|
||||
* and fell to the #1365 parse-miss guard, forcing `could-not-parse` (which
|
||||
* hard-blocks `check.decision-coverage-plan`) on a well-formed CONTEXT.md.
|
||||
*
|
||||
* The repair is confined to how the LOGICAL bullet is assembled — the grammars
|
||||
* themselves are untouched, so every single-line form parses exactly as before.
|
||||
* #4130: the anchor uses `ID_ATTEMPT_SOURCE` (digit-run phase prefixes join
|
||||
* like bare ones; recognising more start shapes only reassembles the logical
|
||||
* bullet, which then parses or fails loud as itself).
|
||||
*/
|
||||
const decisionBulletStartRe = /^\s*-\s+\*\*D-/;
|
||||
const decisionBulletStartRe = new RegExp(`^\\s*-\\s+\\*\\*${ID_ATTEMPT_SOURCE}`);
|
||||
|
||||
/**
|
||||
* A line that opens a new BLOCK-LEVEL construct, and therefore terminates the
|
||||
@@ -169,7 +233,8 @@ const blockConstructRe = /^(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|>\s|\|)/;
|
||||
* to watch for a splice.
|
||||
*
|
||||
* The id character class is deliberately looser than the grammars' (it admits
|
||||
* an empty id, so a bare `- **D-` still counts as unsettled). This regex only
|
||||
* an empty id, so a bare `- **D-` still counts as unsettled, and — #4130 — a
|
||||
* digit-run phase prefix between the `D` and the first hyphen). This regex only
|
||||
* answers "may an id-adjacent bracket still open here?", where recognising MORE
|
||||
* shapes is the conservative direction: an over-broad match can only make a
|
||||
* malformed bullet fail loud, while a missed one silently re-classifies.
|
||||
@@ -178,7 +243,7 @@ const blockConstructRe = /^(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|>\s|\|)/;
|
||||
* into `tags` (and therefore into `trackable`). A `[` further along the title is
|
||||
* ordinary text and does not restrict the join.
|
||||
*/
|
||||
const tagRegionRe = /^\s*-\s+\*\*D-[A-Za-z0-9_-]*\s*(?:\[([^\]]*))?$/;
|
||||
const tagRegionRe = new RegExp(`^\\s*-\\s+\\*\\*D[0-9]*-[A-Za-z0-9_-]*\\s*(?:\\[([^\\]]*))?$`);
|
||||
|
||||
/**
|
||||
* #3939 (review): would folding `next` onto a lead-in whose `[tags]` bracket is
|
||||
@@ -391,11 +456,11 @@ function parseDecisionLines(block: string): ParseDecisionLinesResult {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Colon form: `- **D-NN[ [tags]]:** text`
|
||||
// Colon form: `- **D[phase]-NN[ [tags]]:** text`
|
||||
const colonMatch = line.match(bulletColonRe);
|
||||
if (colonMatch) {
|
||||
flush();
|
||||
const id = `D-${colonMatch[1]}`;
|
||||
const id = colonMatch[1];
|
||||
const tags = colonMatch[2]
|
||||
? colonMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
|
||||
: [];
|
||||
@@ -405,11 +470,11 @@ function parseDecisionLines(block: string): ParseDecisionLinesResult {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Em-dash form: `- **D-NN[ [tags]] — title** body`
|
||||
// Em-dash form: `- **D[phase]-NN[ [tags]] — title** body`
|
||||
const emDashMatch = line.match(bulletEmDashRe);
|
||||
if (emDashMatch) {
|
||||
flush();
|
||||
const id = `D-${emDashMatch[1]}`;
|
||||
const id = emDashMatch[1];
|
||||
const tags = emDashMatch[2]
|
||||
? emDashMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
|
||||
: [];
|
||||
@@ -422,14 +487,14 @@ function parseDecisionLines(block: string): ParseDecisionLinesResult {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Titled-colon form: `- **D-NN[ [tags]]: Title.** body` (#1639). Checked LAST — it is
|
||||
// Titled-colon form: `- **D[phase]-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 id = titledColonMatch[1];
|
||||
const tags = titledColonMatch[2]
|
||||
? titledColonMatch[2].split(',').map((t) => t.trim().toLowerCase()).filter(Boolean)
|
||||
: [];
|
||||
@@ -439,10 +504,14 @@ function parseDecisionLines(block: string): ParseDecisionLinesResult {
|
||||
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.
|
||||
// Parse-miss guard (FIX B + #1343, grammar widened #4130): a line whose bold
|
||||
// lead-in ATTEMPTS the ID grammar but failed all three patterns — flush,
|
||||
// warn, and record the miss. `ID_ATTEMPT_SOURCE` accepts the bare `D-` form
|
||||
// (as before) plus a digit-initial prefix run, so a typo'd phase prefix
|
||||
// (`D4x-01`) fails loud instead of silently vanishing, while a letter-initial
|
||||
// run (`Deferred-until`) stays prose and stays invisible.
|
||||
// parseMisses > 0 forces could-not-parse even when other decisions parsed.
|
||||
if (/^\s*-\s+\*\*D-/.test(line)) {
|
||||
if (parseMissGuardRe.test(line)) {
|
||||
flush();
|
||||
parseMisses += 1;
|
||||
console.warn(`parseDecisions: ignored unparseable decision bullet: ${trimmed}`);
|
||||
@@ -502,10 +571,10 @@ export function extractDecisions(content: unknown): DecisionExtraction {
|
||||
// FIX A: Block present but 0 extracted and no parse-misses.
|
||||
// Only report could-not-parse when there is genuine evidence of real decisions
|
||||
// that failed to parse: a bold-lead-in bullet (`- **…**`, any ID grammar — #2347),
|
||||
// a \bD- token in the block text, or an unterminated fence. An empty scaffold
|
||||
// a bare `D[phase]-<alnum>` token (#4130) in the block text, or an unterminated fence. An empty scaffold
|
||||
// (<decisions></decisions>) or an all-prose block has no such evidence — treat
|
||||
// as none-present so the gate passes cleanly.
|
||||
const hasDecisionTokenInBlock = /\bD-[A-Za-z0-9]/m.test(combined);
|
||||
const hasDecisionTokenInBlock = decisionTokenRe.test(combined);
|
||||
const hasBoldLeadInBullet = boldLeadInBulletRe.test(combined);
|
||||
if (hasDecisionTokenInBlock || hasBoldLeadInBullet || unterminatedFence) {
|
||||
return { decisions: [], outcome: 'could-not-parse' };
|
||||
@@ -534,10 +603,10 @@ export function extractDecisions(content: unknown): DecisionExtraction {
|
||||
}
|
||||
// FIX A: Heading found but 0 extracted and no parse-misses.
|
||||
// Report could-not-parse when the section body holds a decision-entry-shaped
|
||||
// bold-lead-in bullet (`- **…**`, any ID grammar — #2347) or a D- token. A
|
||||
// bold-lead-in bullet (`- **…**`, any ID grammar — #2347) or a `D[phase]-<alnum>` token (#4130). A
|
||||
// heading with only prose, sub-headings, or all-discretion content (no such
|
||||
// evidence) is a legitimate empty/discretion section → none-present.
|
||||
const hasDecisionTokenInSection = /\bD-[A-Za-z0-9]/m.test(section.body);
|
||||
const hasDecisionTokenInSection = decisionTokenRe.test(section.body);
|
||||
const hasBoldLeadInBulletInSection = boldLeadInBulletRe.test(section.body);
|
||||
if (hasDecisionTokenInSection || hasBoldLeadInBulletInSection) {
|
||||
return { decisions: [], outcome: 'could-not-parse' };
|
||||
@@ -547,8 +616,8 @@ export function extractDecisions(content: unknown): DecisionExtraction {
|
||||
|
||||
// ── Path 3: no blocks, no heading ────────────────────────────────────────────
|
||||
// Apply shape heuristics to distinguish none-present from could-not-parse.
|
||||
// We re-use the already-computed unterminatedFence and check for D- tokens.
|
||||
const hasDecisionToken = /\bD-[A-Za-z0-9]/m.test(stripped);
|
||||
// We re-use the already-computed unterminatedFence and check for decision tokens.
|
||||
const hasDecisionToken = decisionTokenRe.test(stripped);
|
||||
if (unterminatedFence || hasDecisionToken) {
|
||||
return { decisions: [], outcome: 'could-not-parse' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user