Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions spec_eval/authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,44 @@ def _document_or_none(md, note):
_KEYWORD_LINE_RE = re.compile(r"^\s*(?:end|subgraph|direction|classDef|linkStyle|click)\b")


_SEQUENCE_BLOCK_RE = re.compile(r"```mermaid\n(\s*sequenceDiagram\b.*?)```", re.S)
# A message or a note: everything after the first `:` is free text, and that grammar has no quoting,
# so a `;` there is read as a STATEMENT SEPARATOR and splits the line in two.
_SEQ_TEXT_RE = re.compile(r"^(\s*(?:(?:Note|note)\s+(?:over|left of|right of)\s+[^:]+|[^:\n]*?"
r"(?:-->>|->>|-->|->|--x|-x|--\)|-\))[^:\n]*?)\s*:)(.*)$")


def escape_sequence_text(md):
"""Replace `;` in sequenceDiagram message and note text. Returns `(markdown, fixed)`.

A semicolon is a STATEMENT SEPARATOR in Mermaid, and a sequence diagram's message text is
unquoted, so `A->>B: one thing; another` parses as two statements and the block fails. The
second half becomes a new statement whose first word is read as an actor.

Flowchart labels are unaffected because they are quoted, so this is scoped to sequence blocks.

**Why an em dash and not an entity:** `&#59;` does make the block parse, but it renders
literally as `&;` — the diagram is then readable-but-wrong, which is worse than a clear
substitution. A semicolon in this prose always joins two clauses, and an em dash joins them
correctly."""
fixed = 0

def _fix_block(m):
nonlocal fixed
out = []
for line in m.group(1).split("\n"):
hit = _SEQ_TEXT_RE.match(line)
if hit and ";" in hit.group(2):
text = re.sub(r"\s*;\s*", " \u2014 ", hit.group(2)).rstrip()
out.append(hit.group(1) + text)
fixed += 1
else:
out.append(line)
return "```mermaid\n" + "\n".join(out) + "```"

return _SEQUENCE_BLOCK_RE.sub(_fix_block, md or ""), fixed


def _sub_outside_quotes(pattern, repl, line):
"""Substitute only outside double-quoted spans.

Expand Down Expand Up @@ -675,6 +713,7 @@ def _repo_overview():
return None, guard_note
md, downgraded = verify_scanner_labels(md, ep)
md, _renamed = rename_reserved_node_ids(md)
md, _escaped = escape_sequence_text(md)
md, fixed, broken = repair_links(md, repo, "OVERVIEW.md")
if syscontext.evidence_block(ctx): # stamp the fingerprint this System context section was
md = md.rstrip() + "\n\n" + syscontext.stamp_comment(ctx) # rendered from, so `--check` can
Expand Down Expand Up @@ -784,6 +823,7 @@ def diagram_block(repo, config, model, on_progress=None):
single_pass=True) # one picture, drawn once, over every module
md, downgraded = verify_scanner_labels(md, ep) # the diagram's own provenance claims, held to the scan
md, _renamed = rename_reserved_node_ids(md) # a keyword as a node id breaks the whole block
md, _escaped = escape_sequence_text(md) # a `;` in message text splits the statement
sliced = len(_pack(items, reduce_cap)) > 1 # the same packing the pass just force-fitted
note = _cap_note(
f"{len(items)} module intents exceeded the reduce cap — each was sliced to an equal share for the "
Expand Down
56 changes: 56 additions & 0 deletions tests/contract/test_diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,59 @@ def test_every_reserved_id_is_covered_not_just_the_one_that_broke():
def test_the_rubric_also_tells_the_model_to_avoid_them():
"""Belt and braces: the rewrite is the guarantee, the instruction reduces how often it fires."""
assert "reserved" in ARCH_DIAGRAM_RUBRIC.lower()


# --- a `;` in sequence message text is a statement separator ----------------------------------------------

def _sequence(body):
return "```mermaid\nsequenceDiagram\n" + body + "```"


def test_a_semicolon_in_a_message_is_replaced():
"""`;` separates statements in Mermaid, and sequence message text is unquoted — so
`A->>B: one; two` parses as two statements and the block fails, with `two`'s first word read as
an actor."""
md, fixed = authoring.escape_sequence_text(_sequence(
" A->>B: corpus mode; without --execute it prices and stops\n"))
assert fixed == 1
assert ";" not in md
assert "corpus mode — without --execute it prices and stops" in md


def test_a_semicolon_in_a_note_is_replaced():
md, fixed = authoring.escape_sequence_text(_sequence(
" Note over A: one thing; another thing\n"))
assert fixed == 1 and "one thing — another thing" in md


def test_the_arrow_and_actors_are_untouched():
md, _ = authoring.escape_sequence_text(_sequence(" CLI-->>U: a; b\n"))
assert "CLI-->>U:" in md


def test_a_colon_inside_the_text_is_not_a_boundary():
"""Message text routinely contains a colon — a file:line citation, a label. Splitting on the
last colon instead of the first would move the replacement into the wrong half."""
md, _ = authoring.escape_sequence_text(_sequence(
" A->>B: loads m0, m1: prompt and provenance; then runs\n"))
assert "loads m0, m1: prompt and provenance — then runs" in md


def test_a_clean_sequence_block_is_returned_untouched():
src = _sequence(" A->>B: plain text with --execute inside it\n")
md, fixed = authoring.escape_sequence_text(src)
assert md == src and fixed == 0


def test_flowchart_labels_are_left_alone():
"""A flowchart label is QUOTED, so a `;` there is safe. Replacing it would rewrite prose to fix
a problem that does not exist in that grammar."""
src = "```mermaid\nflowchart LR\n a[\"stage one; stage two\"]\n```"
md, fixed = authoring.escape_sequence_text(src)
assert md == src and fixed == 0


def test_a_participant_declaration_is_not_treated_as_a_message():
src = _sequence(" participant CLI as tool (console script)\n")
md, fixed = authoring.escape_sequence_text(src)
assert md == src and fixed == 0
Loading