bloom-sheet: refuse a split marker, fill markers in one pass, document the byte-equal caveat

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Weksa 2026-11-11 20:15:40 +00:00
commit 9b13b498e4
3 changed files with 17 additions and 4 deletions

View file

@ -16,7 +16,7 @@ gets a Soul pass before merge.
| `tools/testparse.sh` | Sourced library: failing test names and pass/fail/skip counts from cargo, dotnet, vitest, jest and pytest logs, and test files that failed to load (`LOADFAIL`). Shared by the summary and the kill table. | `tools/stopgap/ygg-verify.sh`, `tools/mutation-table.sh` | not named by a brief; it lives and dies with them |
| `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runners' host drivers and container halves (`tools/mutation-table.sh`, `tools/cargo-mutants.sh`) under a mock docker, a fake `cargo mutants` and real slot locks. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) |
| `tools/context-pack.py` | `py -3 tools/context-pack.py <spec file or -> <worktree> [--since <sha>] [--only <k>\|r<k>[,...]] [--max-lines <n>]`. Carries a cut_spec's `file_changes` and `reads` anchors (read rows `r<k>`; a read absent at base is MISSING) from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED, MISSING, plus SYMBOL_MISSING when an anchor's `symbol` is absent from its window) and windows of at most 100 lines numbered in worktree lines, then one capped caller list per symbol (`git grep`, hits outside the windows), within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable. The spec is the document `huginn view --raw <cut_spec id>` prints, saved to a file or piped to `-`; context-pack never fetches it, and an Unavailable from huginn is the caller's one retry. `--since <sha>` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | `references/briefs.md` (Imagination map, Hands, Soul), `SKILL.md` (step 1) |
| `tools/bloom-sheet/bloom_sheet.py` | `bloom-sheet notes.md -o sheet.png [--visitor a.svg]...`: a markdown document as one neon A4 page. The first top-level heading is the title, every other heading a card sized by its text; a plant grows through what's left, and each SVG visitor gets a disc it grows around and is drawn with the plant's pen, so it must be flat strokes or fills that touch; large fills become a wash and a heavy outline. `--theme NAME` swaps the page file for `themes/NAME.tex` (`calligraphic`: ink on parchment). PDF or PNG, about 2.5 minutes. `grow.py` owns the plant, `page.tex` the look, `test_visitors.py` what a visitor may be, `test_theme.py` what a theme must be. | Minds and Self, for info renders | the `bloom-sheet` skill; `bloom-sheet` on Yggdrasil's PATH |
| `tools/bloom-sheet/bloom_sheet.py` | `bloom-sheet notes.md -o sheet.png [--visitor a.svg]...`: a markdown document as one neon A4 page. The first top-level heading is the title, every other heading a card sized by its text; a plant grows through what's left, and each SVG visitor gets a disc it grows around and is drawn with the plant's pen, so it must be flat strokes or fills that touch; large fills become a wash and a heavy outline. `--theme NAME` swaps the page file for `themes/NAME.tex` (`calligraphic`: ink on parchment); byte-equal rebuilds need `SOURCE_DATE_EPOCH=1 FORCE_SOURCE_DATE=1`, and a PDF's `/ID` differs every run regardless. PDF or PNG, about 2.5 minutes. `grow.py` owns the plant, `page.tex` the look, `test_visitors.py` what a visitor may be, `test_theme.py` what a theme must be. | Minds and Self, for info renders | the `bloom-sheet` skill; `bloom-sheet` on Yggdrasil's PATH |
## From a Self seat on Yggdrasil

View file

@ -18,6 +18,8 @@ opacity is ignored. All of it must touch, reading as one thing; a closed
outline counts as covering its inside. Text, images and gradients are refused.
--theme NAME swaps the page file for themes/NAME.tex (`calligraphic`: ink on parchment, Zapf Chancery).
Builds are byte-equal across runs only with SOURCE_DATE_EPOCH=1 FORCE_SOURCE_DATE=1 set, and then only once the
PDF's /ID is blanked: LaTeX writes a fresh /ID every run, even for unchanged code. PNGs compare equal with both set.
Needs python3-numpy, python3-markdown-it, LuaLaTeX with tikz and tcolorbox,
pdftoppm for PNG, the Ubuntu, IBM Plex Mono and Montserrat fonts, and for
@ -356,6 +358,16 @@ def check_markers(text, name):
if n > 1 or (n == 0 and mark != "@@FONTS@@"):
sys.exit(f"bloom-sheet: {name} has {mark} {n} times, not once "
"(a marker inside a TeX comment is replaced too)")
rest = re.sub("|".join(map(re.escape, MARKS)), "", text)
for mark in MARKS:
if mark in rest:
sys.exit(f"bloom-sheet: {name} has {mark} split by another marker "
"(removing the markers leaves it whole)")
def fill(page, values):
"""Substitute every marker in one pass, so a replacement's text is never searched for markers."""
return re.sub("|".join(map(re.escape, values)), lambda m: values[m.group(0)], page)
def build(args, source):
@ -400,8 +412,7 @@ def build(args, source):
(d / "spec.json").write_text(json.dumps(
{"seed": seed, "panels": panels, "title_box": title_box, "swash": swash, "visitors": visitors}), encoding="utf8")
page = page_source(args.theme)
for mark, value in (("@@FONTS@@", FONTS), ("@@CONTENTS@@", "\n".join(contents)), ("@@TITLE@@", title_tex)):
page = page.replace(mark, value)
page = fill(page, {"@@FONTS@@": FONTS, "@@CONTENTS@@": "\n".join(contents), "@@TITLE@@": title_tex})
(d / "sheet.tex").write_text(page, encoding="utf8")
grown = run([sys.executable, str(HERE / "grow.py"), "spec.json", "layer.tex"], d, args.verbose)
for line in grown.splitlines():

View file

@ -6,7 +6,7 @@ from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from bloom_sheet import FONTS, MARKS, check_markers, page_source
from bloom_sheet import FONTS, MARKS, check_markers, fill, page_source
fails = 0
@ -45,6 +45,8 @@ good = "@@CONTENTS@@ @@TITLE@@"
check_markers(good, "good.tex")
refused("marker in a comment", lambda: check_markers(good + "\n% fills @@TITLE@@ here", "bad.tex"), "@@TITLE@@ 2 times")
refused("marker missing", lambda: check_markers("@@CONTENTS@@", "bad.tex"), "@@TITLE@@ 0 times")
refused("split marker", lambda: check_markers("@@CONTENTS@@ @@CONT@@TITLE@@ENTS@@", "bad.tex"), "@@CONTENTS@@ split by")
check("fill is one pass", fill("@@CONTENTS@@ @@TITLE@@", {"@@CONTENTS@@": "x@@TITLE@@", "@@TITLE@@": "T"}) == "x@@TITLE@@ T")
refused("unknown theme", lambda: page_source("nope"), "no theme 'nope'")
refused("path as theme", lambda: page_source("../page"), "no theme")
sys.exit(1 if fails else 0)