Two remainders from #2025.
## date_scan.cpp was unguarded
hooks/pre-commit knew about four Ragel outputs; mux/lib/date_scan.cpp was
absent from GENERATED_GENS entirely, so it could be hand-edited and
committed without its .rl and nothing objected -- while being listed in
docs/generated-files.md and shipped via unix/TOC.patchable. Added to all
three parallel arrays, which now hold 27 each.
Verified by alignment rather than by count: equal lengths prove nothing
about correspondence, so the check confirms every ragel triple's basenames
match and its rule lives in the same directory as its output. Then
exercised: staging date_scan.cpp alone is now blocked, naming date_scan.rl
as its source.
## Six more outputs carried #line
#2025's scope was right for what the mux build regenerates and dounix.sh
ships, but these have live rules of their own and could still dirty a tree:
testcases/tools/unformat.c 28 testcases/tools/Makefile (%.c: %.rl)
testcases/tools/reformat.c 23 same
ragel/trigger_match.c 16 ragel/Makefile
tools/ansify/ansify.c 19 tools/ansify/Makefile (make regen)
client/tf/src/script_lex.cpp 67 client/tf/CMakeLists.txt
client/tf/src/input_lex.cpp 62 same
The issue reported "no rule found" for trigger_match.c and ansify.c. Both
were wrong: ragel/Makefile:25 generates trigger_match.c on dependency, and
tools/ansify/Makefile has an explicit `regen` target. Neither is inert --
ansify's is opt-in rather than implicit, which is why it reads as absent.
Each regenerated through its OWN rule rather than by running sed by hand,
so the rule is what is under test. All six are idempotent across a second
regeneration, and the diffs are 215 deletions, every one a #line, nothing
added.
ragel/color_ops.c is generated from the same ../mux/lib/color_ops.rl as the
shipped copy but is not tracked, so it could not dirty anything; its rule
gets the strip anyway, so the two generations of one source cannot disagree
if it is ever checked in.
client/tf could not be built here -- src/regex_utils.h needs pcre2.h, which
is not on that target's include path on macOS -- but that is a pre-existing
gap unrelated to this change. The Ragel custom commands themselves DID run
(cmake configure succeeds once NCURSESW_LIB is pointed at a macOS ncurses),
and both outputs came back stripped, so the CMake edit is exercised rather
than assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ragel's #line values depend on the Ragel build rather than on our source,
so regenerating with a different Ragel rewrites dozens of #line-only lines
and leaves the tree dirty after any pull that touches a .rl. That is not
cosmetic: dounix.sh packages from the working tree, not from a git export,
so the churn ships -- into the tarball and into the generated .patch.gz.
Stripping the directives makes each output a function of its .rl alone.
Same input, same file, every box. The cost is that a debugger reports
positions in the generated .c rather than the .rl, which is the accepted
trade for an artifact that can be compared by hash.
Applied to all five Ragel outputs, not the four in docs/generated-files.md
-- date_scan.cpp is also Ragel-generated and had 102 directives.
color_ops.c 194 art_scan.cpp 24
date_scan.cpp 102 ast_scan.cpp 43
muxescape.cpp 16 total 379
sed rather than sed -i: the -i spelling differs between GNU and BSD, and
this has to run on Linux, macOS and FreeBSD. Deleting whole lines is safe
here -- all 379 directives are anchored at column 0 and none carries
trailing code, both checked before the change.
Makefile.in is hand-edited to match rather than regenerated: this box has
automake 1.18.1 and the tree's Makefile.in came from 1.16.5, so
autoreconf would churn whole files for a five-line change (#1477). The
inserted lines are identical to the Makefile.am ones.
The pre-commit hook needed teaching, and this commit is the example: it
rejects a generated file staged without its source, but changing how a
file is generated rewrites the output while the .rl stays untouched. The
build rule now counts as a source. Verified all three ways -- a bare
generated file is still blocked, and staging it with either its .rl or its
Makefile.am is accepted.
Verified on macOS/arm64:
- regeneration is idempotent: all five byte-identical across two runs
- the generated diffs are 379 deletions, every one a #line, nothing added
- outputs return to mode 444, no .tmp files left behind
- make test 35 passed / 1 skipped / 0 failed
(jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hook used `declare -A` (associative array, bash 4+ only). On
macOS where /bin/bash is still 3.2, the declaration silently failed
and the loop iterated zero entries — so the hook printed a parse
error to stderr but skipped its actual check, defeating the
protection against committing generated files without their source.
Rewrite with parallel arrays (GENERATED_GENS / GENERATED_SRCS
indexed in lockstep), which work on bash 3.2 onward. Verified the
hook executes cleanly under /bin/bash 3.2.57.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prevent accidental edits to machine-generated files (Ragel scanners, Unicode
tables, autoconf, protobuf, SQLite amalgamation) with three enforcement layers:
- hooks/pre-commit: blocks commits of generated output without its source
- Makefile chmod a-w: Ragel outputs made read-only after generation
- make hooks: auto-installs hook via core.hooksPath on first build
docs/generated-files.md is the shared reference for all agents and developers.
CLAUDE.md and AGENTS.md both point to it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>