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
7 changes: 7 additions & 0 deletions .github/workflows/loop-tools-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ jobs:
# workflow produced (#2327). Pure Python, no compiler, no corpus.
run: python3 scripts/ci/test_damage_repair_snapshot_required.py

- name: An empty match is not a number
# #3067. `tri wave` printed "lesson last -> next 1" against a 242 KB
# document whose own last wave loop is 898, because the matcher found
# nothing and empty-plus-exit-0 is not a failure. Runs here: no compiler
# is needed, and the test builds its own two-document fixture.
run: python3 scripts/ci/test_an_empty_match_is_not_a_number.py

- name: An absent compiler is "could not run", not "the check failed"
# #3045. This job has no compiler, which is what makes it the right
# place: the assertion is about the no-build path and nowhere else can
Expand Down
26 changes: 26 additions & 0 deletions docs/now/2026-09-04-an-empty-match-is-not-a-number.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# NOW -- An empty match is not a number (2026-09-04)

## `tri wave` said the next lesson is 1; the document's last one is 898

- `.claude/skills/t27-wave-loop.md` is 242 KB and present, its worked examples
are headed `## Worked example -- Wave Loop 898`, and
`grep -cE '^\*\*[0-9]+\.'` on it is **0**. The format moved past the matcher.
- `last_lesson_no` tested that the file EXISTS and then returned whatever the
matcher produced. Empty output with exit 0 is not a failure, so
`$(last_lesson_no || echo '-')` never substituted the `-`, and
`$(( "" + 1 ))` is 1.
- The write path took the same route. `tri lesson` carries the guard written for
this -- `no="$(last_lesson_no)" || die "no numbered lessons found"` -- and
`bash -x` shows `no=` then `next=1` with the die SKIPPED. Nothing was written
only because an unrelated downstream step failed on the same empty anchor.
- Both readers now return 1 when the matcher produced no number, so the `-` and
the `die` work again, and `tri wave` says `UNREADABLE -- no line matched in
<file>` rather than inventing a number.
- `theorem` is fixed the same way although it reads correctly today: the two
halves of one display should not differ in whether they can lie.
- Not done, deliberately: widening the matcher to the `## … Wave Loop N` form.
Which heading is canonical belongs to whoever owns the document; saying "I
cannot read this" has to land first either way.
- Two mutants: the old reader kills the three defect assertions, a reader that
always fails kills all three CONTROLS -- a document that does carry lessons
must still be read and written, or the test cannot fail.
115 changes: 115 additions & 0 deletions scripts/ci/test_an_empty_match_is_not_a_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""#3067: a matcher that matched nothing is not a reading.

`last_lesson_no` tested that the file EXISTS and then returned whatever the
matcher produced -- empty, with exit 0, when the file was present and the format
had moved past the pattern. So `$(last_lesson_no || echo '-')` never substituted
the '-', and `no="$(last_lesson_no)" || die` never died.

Measured on the real document: `.claude/skills/t27-wave-loop.md` is 242 KB and
present, its worked examples are headed `## Worked example -- Wave Loop 898`,
and `grep -cE '^\\*\\*[0-9]+\\.'` on it is 0. `tri wave` printed

lesson last -> next 1

where the document's own last wave loop is 898. `tri lesson` reached `next=1`
the same way -- traced with `bash -x` -- and wrote nothing only because an
unrelated downstream step failed on the same empty anchor.

Each assertion has a control: a document that DOES carry lessons must still be
read and written correctly, or this file cannot fail.
"""

import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
FAILURES = []

NO_LESSONS = """# Wave loop

## Worked example -- Wave Loop 898

Body text, with no `**N.` lesson line anywhere.
"""

HAS_LESSONS = """# Wave loop

**41. An existing lesson.** body

## Next heading

more
"""


def check(name, ok, detail=""):
print(f" {'ok ' if ok else 'FAILED '}{name}")
if not ok:
FAILURES.append(f"{name}: {detail}")


def tree(doc):
d = tempfile.mkdtemp(prefix=f"tri-wave-{os.getpid()}-")
shutil.copytree(REPO / "scripts", os.path.join(d, "scripts"))
os.makedirs(os.path.join(d, ".claude/skills"))
with open(os.path.join(d, ".claude/skills/t27-wave-loop.md"), "w") as f:
f.write(doc)
subprocess.run(["git", "init", "-q", "."], cwd=d, capture_output=True)
return d


def tri(d, *args):
return subprocess.run(["bash", os.path.join(d, "scripts", "tri"), *args],
capture_output=True, text=True, cwd=d)


def main():
d = tree(NO_LESSONS)
try:
w = tri(d, "wave")
check("a matcher that matched nothing does not become a number",
"next 1" not in w.stdout, f"stdout={w.stdout!r}")
check("and says the line is unreadable, naming the file",
"UNREADABLE" in w.stdout and "t27-wave-loop.md" in w.stdout, f"stdout={w.stdout!r}")

l = tri(d, "lesson", "A brand new lesson", "with a body")
check("tri lesson refuses instead of numbering from an empty match",
l.returncode != 0, f"rc={l.returncode} out={(l.stdout+l.stderr)!r}")
check("and the refusal is the one written for it",
"no numbered lessons found" in (l.stdout + l.stderr), f"out={(l.stdout+l.stderr)!r}")
doc = Path(d, ".claude/skills/t27-wave-loop.md").read_text()
check("and nothing was written", "**1." not in doc, "a lesson numbered 1 was inserted")
finally:
shutil.rmtree(d, ignore_errors=True)

# CONTROLS. Without these, a `last_lesson_no` that always fails passes above.
d = tree(HAS_LESSONS)
try:
w = tri(d, "wave")
check("control: a document with lessons is still read",
"last 41 -> next 42" in w.stdout, f"stdout={w.stdout!r}")
l = tri(d, "lesson", "A brand new lesson", "with a body")
check("control: and a lesson is still written, with the next number",
l.returncode == 0, f"rc={l.returncode} out={(l.stdout+l.stderr)!r}")
doc = Path(d, ".claude/skills/t27-wave-loop.md").read_text()
check("control: numbered 42, not 1", "**42. A brand new lesson.**" in doc, f"doc={doc!r}")
finally:
shutil.rmtree(d, ignore_errors=True)

print()
if FAILURES:
print("FAILED:")
for f in FAILURES:
print(f" - {f}")
return 1
print("ok: an empty match refuses; a real one still reads and writes.")
return 0


if __name__ == "__main__":
sys.exit(main())
42 changes: 36 additions & 6 deletions scripts/tri
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,38 @@ lesson_numbers() { grep -oE '^\*\*[0-9]+\.[[:space:]]' "$WAVE_SKILL" 2>/dev/null
# theorems: T709, T709a, T709b are three distinct entries, not a triple collision.
theorem_tokens() { grep -oE '^#{2,4} T[0-9]+[a-z]*' "$THEORY_DOC" | awk '{ print $2 }'; }

# AN EMPTY MATCH IS NOT A NUMBER.
#
# These returned empty-and-exit-0 when the file existed and the matcher found
# nothing, so `$(last_lesson_no || echo '-')` never substituted the '-' and
# `no="$(last_lesson_no)" || die` never died. Measured on the real document:
# `.claude/skills/t27-wave-loop.md` is 242 KB and present, its worked examples
# are headed `## Worked example -- Wave Loop 898`, and `grep -cE '^\*\*[0-9]+\.'`
# on it is **0**. `tri wave` therefore printed
#
# lesson last -> next 1
#
# where the document's own last wave loop is 898. `tri lesson` took the same
# path to `next=1` -- traced with `bash -x`: `no=` then `next=1`, with the die
# skipped -- and wrote nothing only because an unrelated downstream step failed
# on the same empty anchor. The guard that exists for this could not fire.
#
# The `-f` test only catches an ABSENT file. A present file whose format has
# moved past the matcher is the case that actually happened.
last_lesson_no() {
[ -f "$WAVE_SKILL" ] || return 1
lesson_numbers | sort -n | tail -1
local n
n="$(lesson_numbers | sort -n | tail -1)"
[ -n "$n" ] || return 1
printf '%s\n' "$n"
}

last_theorem_no() {
[ -f "$THEORY_DOC" ] || return 1
theorem_tokens | tr -dc '0-9\n' | sort -n | tail -1
local n
n="$(theorem_tokens | tr -dc '0-9\n' | sort -n | tail -1)"
[ -n "$n" ] || return 1
printf '%s\n' "$n"
}

origin_slug() {
Expand Down Expand Up @@ -138,10 +162,16 @@ case "$cmd" in
printf 'branch %s @ %s (%s dirty)\n' "$branch" "$head" "$dirty"
printf 'disk %s GiB free (floor %s GiB)%s\n' "$free" "$DISK_FLOOR_GIB" \
"$(awk -v f="$free" -v fl="$DISK_FLOOR_GIB" 'BEGIN { print (f + 0 < fl + 0) ? " <<< BELOW FLOOR" : "" }')"
printf 'lesson last %s -> next %s\n' "$lesson" \
"$([ "$lesson" = '-' ] && echo '-' || echo $((lesson + 1)))"
printf 'theorem last T%s -> next T%s\n' "$theorem" \
"$([ "$theorem" = '-' ] && echo '-' || echo $((theorem + 1)))"
if [ "$lesson" = '-' ]; then
printf 'lesson UNREADABLE -- no line matched in %s\n' "${WAVE_SKILL#"$REPO_ROOT"/}"
else
printf 'lesson last %s -> next %s\n' "$lesson" "$((lesson + 1))"
fi
if [ "$theorem" = '-' ]; then
printf 'theorem UNREADABLE -- no heading matched in %s\n' "${THEORY_DOC#"$REPO_ROOT"/}"
else
printf 'theorem last T%s -> next T%s\n' "$theorem" "$((theorem + 1))"
fi
printf 't27c %s\n' "${t27c_path:-not built (compiler commands unavailable)}"
exit 0
;;
Expand Down
Loading