Harden \stgroup and tighten the callout budget (review follow-up)

- Bracket ink is part of the fit: \st@facebody drops -inkw/-inke extreme
  coordinates and \stface/\ststack register them with the enclosing
  group/col/row fit, so a group outline can no longer be crossed by a
  member's bracket arms
- \stlink inside \stgroup or \stcol is now a package error: sub-flow
  members never terminate a pending connector, so the arrow was dropped
  silently while the label still rendered
- \stgroup requires role= (explicit role=neutral for mixed groups) and
  must bind at least two members or one \stcol partition; a lone stack
  or face inside a group is a dirty-build warning
- lint: default budget is one \stcallout per figure; the
  allow-multiple-callouts directive relaxes it to one per band
- build.sh: clean-build hint no longer names hue budget (lint owns it)
- tests/group-callout.tex reworked: multi-member group with a bracketed
  member as a regression probe, single callout; new negative fixtures
  group-link, group-norole, group-single, callout-budget
- api.md, checklist.md, style.md, layout.md, SKILL.md updated to match
This commit is contained in:
dela
2026-08-05 17:03:14 +08:00
parent 866173a831
commit de917a2fbd
13 changed files with 181 additions and 54 deletions
+14 -2
View File
@@ -114,10 +114,13 @@ def lint(path: Path) -> list[str]:
)
# A callout hangs off a BAND. Anchored to a face it becomes the floating
# commentary card between two operands that layout.md forbids, and two of
# them on one band is a dashboard, not a figure.
# commentary card between two operands that layout.md forbids. The default
# budget is ONE callout for the whole figure: a card per band is already a
# dashboard, and at thumbnail size the cards are unreadable anyway. The
# allow-multiple-callouts directive relaxes this to one per band.
band_names = set(ROW_NAME_RE.findall(source))
callout_anchors: dict[str, str] = {}
first_callout: str | None = None
for match in CALLOUT_RE.finditer(source):
name, anchor = (part.strip() for part in match.groups())
line = line_of(source, match.start())
@@ -131,7 +134,16 @@ def lint(path: Path) -> list[str]:
f"line {line}: callout {name!r} is the second card on band {anchor!r} "
f"(after {callout_anchors[anchor]!r}); one aside per band"
)
elif first_callout is not None and "allow-multiple-callouts" not in allowed:
errors.append(
f"line {line}: callout {name!r} is the second card in the figure "
f"(after {first_callout!r}); the budget is one callout per figure — "
f"move the text to \\stmeaningbox, or add the "
f"allow-multiple-callouts exemption"
)
callout_anchors.setdefault(anchor, name)
if first_callout is None:
first_callout = name
for row_number, row in enumerate(ROW_RE.finditer(source), start=1):
row_roles = set(ROLE_KEY_RE.findall(row.group(1)))