From 44a0a4fb598497cc148e9eccb96c4dd703b910ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 03:11:19 +0000 Subject: [PATCH] fix(pm): drop the phantom `#` from os-regen-merge.sh's .gitattributes reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `grep 'merge=os-regen' .gitattributes | awk '{print $1}'` also matched the header comment that quotes the literal in prose and took ITS first field, so the run-time path list carried a pathspec that was literally `#`. Harmless by luck — `#` matches no tracked path, so both `git diff --name-only … -- "${regen_paths[@]}"` calls ignored it — but the pattern COUNT printed to the operator is the only check that the script is reading the right surface, and it was off by one in the one place the design deliberately keeps no second copy. Measured on origin/main 95d5cbb31: 19 entries produced, 1 literally `#`, 18 real patterns. The repaired reader answers 18, matching both the 18 rows in the file and `scripts/git-merge-regen.mjs`'s independent reader. The grammar was measured, not assumed (git 2.43.0, scratch repos): a leading-`#` line is a comment even when indented, and there is NO inline trailing comment — git rejects such a row whole and routes nothing. Hence comment lines are dropped before the field split, and `merge=os-regen` is anchored as a whitespace- delimited token. Pinned in the script's own `--self-test`: a bait `.gitattributes` with prose quoting the literal plus three real rows, asserting the exact printed COUNT (so an empty population reds too), the git-grammar measurement with a firing control, and a discriminating mutation that disables the comment skip and watches the phantom come back. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/pm/os-regen-merge.sh | 133 ++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/scripts/pm/os-regen-merge.sh b/scripts/pm/os-regen-merge.sh index 5d78dd31d8..562c20f19e 100644 --- a/scripts/pm/os-regen-merge.sh +++ b/scripts/pm/os-regen-merge.sh @@ -213,11 +213,51 @@ mode_run() { # the form #12142 standardised; its own note attributes the trap to the empty # list, which measures clean — an all-empty read leaves the body unexecuted # and the `while` at status 0.) + # + # The READER admits only what git itself routes, and .gitattributes' grammar + # was MEASURED for this rather than assumed (git 2.43.0, scratch repos): + # + # a leading-`#` line is a COMMENT, and leading whitespace does not undo + # that — ` # foo bar` assigns nothing to a file named `#`, while the + # escaped-pattern control `\# foo bar` on that same file assigns `foo` and + # `bar`. So comment lines must be dropped BEFORE the field split. + # + # there is NO inline trailing comment. `x merge=os-regen # note` does not + # route x with a note on the end; git rejects the LINE — `# is not a valid + # attribute name: .gitattributes:1` on stderr, and `git check-attr merge -- + # x` then answers `unspecified`. Nothing to strip, because no such row can + # ever be live. Pinned in the self-test against real git, so the day git + # grows one this decision reddens instead of rotting. + # + # attributes are WHITESPACE-delimited tokens, so `merge=os-regenX` is a + # different driver and not this one — hence the token anchor below rather + # than a bare substring. + # + # The spelling this replaced was `grep 'merge=os-regen' .gitattributes | awk + # '{print $1}'`, which also matched the header comment that quotes the + # literal in prose and took ITS first field, so the list carried a pathspec + # that was literally `#`. Harmless by luck — `#` matches no tracked path, so + # both `git diff`s below ignored it — but the COUNT printed just below is the + # operator's only check that this script is looking at the right surface, and + # it was off by one in the one place the design deliberately keeps no second + # copy to compare against. Measured at the repair on origin/main: 19 entries + # produced, 1 of them literally `#`, 18 real patterns. It had already been + # wrong at two different values (18 then 19) across an intervening edit, so + # the impurity is in the construction, not in one snapshot of the file. + # + # `scripts/git-merge-regen.mjs`'s own reader (`reconcileAttributes`) already + # skipped comment lines; this is that same rule, in awk. + # + # `[ \t]` rather than `[[:space:]]`: the bash 3.2 floor above is a macOS + # floor, and that host's awk is not gawk. regen_paths=() regen_line='' while IFS= read -r regen_line; do if [[ -n "$regen_line" ]]; then regen_paths+=("$regen_line"); fi - done < <(grep 'merge=os-regen' .gitattributes | awk '{print $1}') + done < <(awk ' + /^[ \t]*#/ { next } + /(^|[ \t])merge=os-regen([ \t]|$)/ { print $1 } + ' .gitattributes) if [ "${#regen_paths[@]}" -eq 0 ]; then echo "✗ no merge=os-regen entries found in .gitattributes — refusing to guess" >&2 exit 1 @@ -558,6 +598,32 @@ DRIVER git fetch -q origin main } +# Build the standard fixture in $1, then replace its .gitattributes with one +# shaped like the REAL file: prose that quotes the literal `merge=os-regen`, +# and exactly THREE real rows. Leaves the cwd in the clone, on `feature`, with +# the new routing committed. +# +# Two prose shapes, on purpose, because they fail the reader differently: +# the header line quotes the literal in BACKTICKS — caught by the token +# anchor even with comment-skipping off; +# the indented line mentions it whitespace-delimited — caught ONLY by +# comment-skipping, which is what makes it the discriminator for 8b. +# The real .gitattributes carries the backticked shape (its line 36); the +# indented one is the near neighbour that a future edit can add for free. +st_fixture_list_bait() { + st_fixture "$1" >/dev/null 2>&1 + cat > .gitattributes <<'ATTRS' +# Routed generated artifacts. `merge=os-regen` hands these to the driver. +# + # an indented comment that also mentions merge=os-regen in prose +gen/** merge=os-regen +docs/generated-guide.md merge=os-regen +data/*.json merge=os-regen +ATTRS + git add -A + git commit -qm 'route three paths, with prose that quotes the literal' +} + mode_self_test() { tmp="$(mktemp -d "${TMPDIR:-/tmp}/os-regen-merge-selftest.XXXXXX")" trap 'rm -rf "$tmp"' EXIT INT TERM @@ -737,6 +803,71 @@ mode_self_test() { "$(printf '%s' "$out" | grep -c 'Do not resolve generated files textually' || true)" 0 cd "$here" + # --- 8. THE LIST ITSELF. The printed pattern count is the operator's ONLY + # check that this script is reading the right surface — the header + # says so, and says why there is deliberately no second copy to check + # it against. So it is pinned here, against a fixture .gitattributes + # carrying the prose shape that used to enter the list as a pathspec + # literally `#`. + # + # The assertion is an EXACT COUNT, never "no entry equals `#`": a + # count also reds the day the read silently matches ZERO lines, which + # an absence assertion passes with flying colours. + st_fixture_list_bait "$tmp/h" + out="$(bash "$SELF" 2>&1)" && rc=0 || rc=$? + st_case 'a run over the bait .gitattributes exits 0' "$rc" 0 + st_case 'the pattern count counts ROWS, not prose that quotes the literal' \ + "$(printf '%s' "$out" | sed -n 's/^→ os-regen paths (from .gitattributes, \([0-9]*\) patterns):$/\1/p')" 3 + st_case 'and no phantom # pathspec is listed' \ + "$(printf '%s' "$out" | grep -c '^ #$' || true)" 0 + st_case 'and the three real rows all are' \ + "$(printf '%s' "$out" | grep -cE '^ (gen/[*][*]|docs/generated-guide[.]md|data/[*][.]json)$' || true)" 3 + # The fixture really is bait: the pre-repair spelling over-counts it by two. + st_case 'the pre-repair spelling over-counts the same file (fixture is bait)' \ + "$(grep 'merge=os-regen' .gitattributes | awk '{print $1}' | wc -l | tr -d ' ')" 5 + cd "$here" + + # --- 8a. THE GRAMMAR the reader was decided against, pinned against REAL git + # rather than assumed. .gitattributes has no inline trailing comment: + # git rejects the whole row and routes nothing. That measurement is + # why the reader strips comment LINES only and does not try to trim a + # trailing `#`. If git ever grows one, this reddens and the decision + # gets revisited instead of rotting. + st_fixture "$tmp/i" >/dev/null 2>&1 + printf 'inline.txt merge=os-regen # trailing note\n' > .gitattributes + : > inline.txt + st_case 'git does NOT admit an inline trailing comment — the row routes nothing' \ + "$(git check-attr merge -- inline.txt 2>/dev/null)" 'inline.txt: merge: unspecified' + st_case 'and names # as the invalid attribute name' \ + "$(git check-attr merge -- inline.txt 2>&1 >/dev/null | grep -c 'is not a valid attribute name' || true)" 1 + # Firing control for the two absences above: the same row WITHOUT the trailing + # comment does route. Without it, a git that stopped reading .gitattributes at + # all would pass both cases above. + printf 'inline.txt merge=os-regen\n' > .gitattributes + st_case 'control: the same row without the note DOES route' \ + "$(git check-attr merge -- inline.txt 2>/dev/null)" 'inline.txt: merge: os-regen' + cd "$here" + + # --- 8b. THE DISCRIMINATING MUTATION for case 8. Disable the reader's + # comment-line skip and watch the count come back one too high — the + # phantom is exactly what that rule removes. Same perl/\Q..\E literal + # replacement 6b uses, keyed off the awk rule rather than a message. + mutated_list="$tmp/mutated-list-os-regen-merge.sh" + MUT_ANCHOR=' /^[ \t]*#/ { next }' \ + MUT_INSERT=' /^[ \t]*#ZZ-NEVER-MATCHES-ZZ/ { next }' \ + perl -0777 -pe 's/\Q$ENV{MUT_ANCHOR}\E/$ENV{MUT_INSERT}/' "$SELF" > "$mutated_list" + st_case 'the list mutation actually changed the script text' \ + "$(diff -q "$SELF" "$mutated_list" >/dev/null 2>&1; echo $?)" 1 + st_case 'and the mutated script still parses' \ + "$(bash -n "$mutated_list" >/dev/null 2>&1; echo $?)" 0 + st_fixture_list_bait "$tmp/h-mutated" + mut_out="$(bash "$mutated_list" 2>&1)" && mut_rc=0 || mut_rc=$? + st_case 'mutated: the phantom # is back in the list (proves case 8 bites)' \ + "$(printf '%s' "$mut_out" | grep -c '^ #$' || true)" 1 + st_case 'mutated: and the count is one too high' \ + "$(printf '%s' "$mut_out" | sed -n 's/^→ os-regen paths (from .gitattributes, \([0-9]*\) patterns):$/\1/p')" 4 + cd "$here" + if [ "$st_fail" -ne 0 ]; then printf '✗ os-regen-merge self-test: %d case(s) failed.\n' "$st_fail" return 1