fix(#4788): code spans in the decision bold lead-in are opaque to the separator grammar (#4883)

* test(#4788): failing-first — code spans in the decision bold lead-in are opaque to the separator grammar

* fix(#4788): the decision lead-in runs are code-span-aware — a backticked span is data, never grammar

* chore(#4788): backfill changeset PR number (4881)

* chore(#4788): correct changeset PR number (4883, was a guessed 4881)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-19 22:46:48 -04:00
committed by GitHub
parent 5906a24ede
commit 09e1110e68
3 changed files with 110 additions and 11 deletions

View File

@@ -115,7 +115,7 @@ 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 `:**`)
* (#1343: the pre-colon run subsumes any pre-colon prose, stops at `:**`; #4788: the run is code-span-aware — a backticked span is consumed whole, so a `:` or `*` INSIDE it is data, never grammar)
* Group 1 captures the FULL id including any phase prefix (#4130).
* The ID is consumed atomically `(?=(…))\1` — see the hardening note above
* the constants (#4130 follow-up); with the tail unable to give back, the
@@ -123,7 +123,7 @@ const ID_ATTEMPT_SOURCE = 'D(?:[0-9][A-Za-z0-9]*)?-';
* linear in line length.
*/
const bulletColonRe = new RegExp(
`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^:*]*:\\*\\*\\s*(.*)$`,
`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?(?:\\u0060[^\\u0060]*\\u0060|[^:*\\u0060])*:\\*\\*\\s*(.*)$`,
);
/**
@@ -146,7 +146,7 @@ const bulletColonRe = new RegExp(
* dash position. The ID is atomic like the other forms (driver #1).
*/
const bulletEmDashRe = new RegExp(
`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^*—–]*[—–][^*]*\\*\\*\\s*(.*)$`,
`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?(?:\\u0060[^\\u0060]*\\u0060|[^*—–\\u0060])*[—–](?:\\u0060[^\\u0060]*\\u0060|[^*\\u0060])*\\*\\*\\s*(.*)$`,
);
/**
@@ -154,17 +154,19 @@ const bulletEmDashRe = new RegExp(
* 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)
* bulletEmDashRe — it only catches bullets those two miss. The title runs are code-span-aware
* (#4788) but still exclude BARE colons and `*`, so a genuinely-malformed bullet with a bare
* colon in the pre-separator run (e.g. `D-07 ratio 3:1:**` — the `:**` supplies the second
* colon) still fails the anchor and falls through to the parse-miss guard — the #1639
* fail-loud discipline: the separator must be the only BARE colon before `**`. A code span's
* `:`/`*` is data, never grammar; an UNTERMINATED backtick now fails loud (previously it was
* an ordinary character) — the deliberate cost of span opacity.
*
* The ID is consumed atomically `(?=(…))\1` like the other forms — the
* hardening note above the constants explains why (#4130 follow-up).
*/
const bulletTitledColonRe = new RegExp(
`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^:*]*:[^:*]*\\*\\*\\s*(.*)$`,
`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?(?:\\u0060[^\\u0060]*\\u0060|[^:*\\u0060])*:(?:\\u0060[^\\u0060]*\\u0060|[^:*\\u0060])*\\*\\*\\s*(.*)$`,
);
/**