Skip to content

parseDecisionsMarkdown() is fence-blind: headings inside code fences are parsed as decisions #1839

Open

Description

@bradygaster

Summary

parseDecisionsMarkdown() treats every trimmed ### line as a decision boundary without tracking fenced code blocks. A ### heading inside a ```markdown fence is therefore parsed as a separate decision record, splitting the enclosing record in two.

Found by copilot-pull-request-reviewer on #1838 and independently verified.

Location

packages/squad-sdk/src/config/markdown-migration.ts:417-422

Reproduction

.squad/decisions.md contains a RETRO record whose body includes a normative block intended to be pasted into workflows/squad.md:

````markdown
### Shell input security contract [MANDATORY]

The `/squad` parser treats issue bodies ... as attacker-controlled.
````

DecisionsCollection.list() splits the RETRO record at that heading and returns the fenced example as its own decision.

The content is correct. A normative example legitimately contains headings. The parser is the defect — it must not be worked around by mangling the example.

Why this matters beyond cosmetics

Decision counts are the integrity check for Scribe's archival path. Rule C exists precisely because byte size is blind to content loss, so archival correctness is asserted by counting entries (N removed from source / N added to destination).

A parser that miscounts entries corrupts the only check standing between the archival path and silent data loss — the failure mode already seen twice (#1782, and the recurrence tracked in #1836).

Concretely, on the #1838 branch a naive ^### count reports 59 decisions. The true distinct count is ~54. The inflation comes from three sources, only one of which is this bug:

Cause Count Status
Dated wrapper + undemoted duplicate source heading 3 content defect, fixed in #1838
Addendum subsection using ### 1 content defect, fixed in #1838
Heading inside a fenced block 1 this issue — parser defect

The first two are content-side and repairable by Scribe. This one is not: no amount of correct authoring prevents it, because a fenced example is supposed to contain headings.

Proposed fix

Track fence state while scanning. Handle both ``` and longer runs ( ````), since a longer fence is exactly how a block containing triple-backticks is nested — which is the case here. A closing fence must be at least as long as the opener.

Acceptance criteria

  • parseDecisionsMarkdown() ignores ### inside fenced blocks
  • Handles fences longer than three backticks (4+), and nested/inner triple-backtick blocks
  • Regression test using the real RETRO record shape: an outer ```` fence containing an inner yaml ``` block and a ### heading — asserting the record is not split
  • Test proven capable of failing: revert the fence tracking, confirm the test goes red naming the offending heading, restore
  • Enumerate callers of parseDecisionsMarkdown() and confirm none depend on the current fence-blind behavior

Notes

Related to #1836 (archival gate reports outcomes it did not measure). Same thesis: a check that cannot observe the thing it counts is not a check.

Touches packages/squad-sdk/src/ → requires a changeset.

Activity

  1. added
    squadSquad triage inbox — Lead will assign to a member
    on Aug 23, 2026
  2. github-actions commented on Aug 23, 2026

    @github-actions
    Contributor

    🏗️ Squad Triage — Flight (Lead)

    Issue: #1839 — parseDecisionsMarkdown() is fence-blind: headings inside code fences are parsed as decisions
    Assigned to: FIDO (Quality Owner)
    Reason: Issue relates to testing/quality work

    @copilot evaluation: 🔴 Not suitable — issue involves work outside the coding agent's capability profile.

    Team roster:

    • Flight (Lead) → label: squad:flight
    • Procedures (Prompt Engineer) → label: squad:procedures
    • EECOM (Core Dev) → label: squad:eecom
    • FIDO (Quality Owner) → label: squad:fido
    • PAO (DevRel) → label: squad:pao
    • CAPCOM (SDK Expert) → label: squad:capcom
    • CONTROL (TypeScript Engineer) → label: squad:control
    • Surgeon (Release Manager) → label: squad:surgeon
    • Booster (CI/CD Engineer) → label: squad:booster
    • GNC (Node.js Runtime) → label: squad:gnc
    • Network (Distribution) → label: squad:network
    • RETRO (Security) → label: squad:retro
    • INCO (CLI UX & Visual Design) → label: squad:inco
    • GUIDO (VS Code Extension) → label: squad:guido
    • Telemetry (Aspire & Observability) → label: squad:telemetry
    • VOX (REPL & Interactive Shell) → label: squad:vox
    • DSKY (TUI Engineer) → label: squad:dsky
    • Sims (E2E Test Engineer) → label: squad:sims
    • Handbook (SDK Usability) → label: squad:handbook
    • Ralph (Work Monitor) → label: squad:ralph
    • @copilot (Coding Agent) → label: squad:copilot

    To reassign, remove the current squad:* label and add the correct one.

  3. bradygaster commented on Aug 23, 2026

    @bradygaster
    CollaboratorAuthor

    Verified against the source, plus two details not in the original report

    Read markdown-migration.ts:417-441 directly and replicated the loop against the live .squad/decisions.md.

    1. The regex matches ##, not just ###

    const trimmed = line.trim();
    const headingMatch = trimmed.match(/^(###?)\s+(.+)/);

    ###? is ## plus an optional third # — so H2 is also a decision boundary. Worth stating explicitly in the fix, because a fence-tracking patch that only reasons about ### would still split records on a fenced ##.

    Confirmed #### does not match, which is why demoting a heading to #### is the correct content-side remedy.

    2. .trim() runs before the match, so indentation offers no protection

    An indented heading inside an indented code block still matches. Not currently triggered in this repo (measured: 0), but it widens the blast radius — a fix that tracks only fenced blocks and ignores indented code blocks would still be incomplete.

    Measured on the current file

    Measure Count
    Boundaries the parser sees 59
    — of which ## 0
    — of which ### 59
    — inside a fenced block 1 ← this bug
    Fence-aware boundary count 58
    #### headings (correctly invisible) 81

    Method: replicated /^(###?)\s+(.+)/ against .trim()ed lines while tracking ```-or-longer fence state, where a closing fence must be at least as long as its opener.

    Of the 59, exactly 1 is this parser bug. The other 4 phantoms are content defects being fixed in #1838. True distinct decisions: ~54.

    That the ## count is 0 confirms Scribe's ## demotion rule is being applied correctly — this is specifically a ###-and-fences problem, not a general heading-hygiene problem.

    Suggested addition to acceptance criteria

    • Fence tracking applies to ## boundaries as well as ###
    • Test covers an indented fenced block, not just a column-0 one
  4. github-actions commented on Sep 10, 2026

    @github-actions
    Contributor

    🔄 Ralph — Auto-Triage

    Assigned to: CONTROL
    Reason: Matched module path "src/config/" to primary owner "CONTROL 👩‍💻"
    Source: module-ownership

    Ralph auto-triaged this issue using routing rules.
    To reassign, swap the squad:* label.

  5. github-actions commented on Sep 10, 2026

    @github-actions
    Contributor

    🔄 Ralph — Auto-Triage

    Assigned to: CONTROL
    Reason: Matched module path "src/config/" to primary owner "CONTROL 👩‍💻"
    Source: module-ownership

    Ralph auto-triaged this issue using routing rules.
    To reassign, swap the squad:* label.

  6. github-actions commented on Sep 11, 2026

    @github-actions
    Contributor

    🔄 Ralph — Auto-Triage

    Assigned to: CONTROL
    Reason: Matched module path "src/config/" to primary owner "CONTROL 👩‍💻"
    Source: module-ownership

    Ralph auto-triaged this issue using routing rules.
    To reassign, swap the squad:* label.

  7. github-actions commented on Sep 11, 2026

    @github-actions
    Contributor

    🔄 Ralph — Auto-Triage

    Assigned to: CONTROL
    Reason: Matched module path "src/config/" to primary owner "CONTROL 👩‍💻"
    Source: module-ownership

    Ralph auto-triaged this issue using routing rules.
    To reassign, swap the squad:* label.

  8. github-actions commented on Sep 13, 2026

    @github-actions
    Contributor

    🔄 Ralph — Auto-Triage

    Assigned to: architect
    Reason: No module, routing, or role keyword match — routed to Lead/Architect
    Source: lead-fallback

    Ralph auto-triaged this issue using routing rules.
    To reassign, swap the squad:* label.

  9. added
    squad:sdk-runtime-devAssigned to sdk-runtime-dev (SDK Runtime Engineer)
    and removed
    squad:architectAssigned to architect (Lead Architect)
    on Sep 24, 2026
  10. github-actions commented on Sep 24, 2026

    @github-actions
    Contributor

    📋 Assigned to sdk-runtime-dev (SDK Runtime Engineer)

    Issue: #1839 — parseDecisionsMarkdown() is fence-blind: headings inside code fences are parsed as decisions

    sdk-runtime-dev will pick this up in the next Copilot session.

    For Copilot coding agent: If enabled, this issue will be worked automatically.
    Otherwise, start a Copilot session and say:
    sdk-runtime-dev, work on issue #1839

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    go:needs-researchNeeds investigationsquadSquad triage inbox — Lead will assign to a membersquad:sdk-runtime-devAssigned to sdk-runtime-dev (SDK Runtime Engineer)wave1:P4Wave 1 PR group P4wave:1-nextWave 1 — immediate next sprint

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions