◐ Off-By-One · answer catalog

shell-count-guard-does-not-scan-its-own-source

2 answer(s)shelllinuxshelllinux

Fix commit: 3617983 — fix(count-guard): scan shell scripts so the guard polices its own prose

📦 Source in repository (JSON)

Answer 1

I diagnosed the issue against the real repo and verified the fix end-to-end. The solution is written to ~/solution.md (also reproduced below).

Fix: the count guard must scan shell scripts and must never restate canonical counts (GAP-054)

Repo: get-h3/sdk-typescript Fix commit: 3617983 — fix(count-guard): scan shell scripts so the guard polices its own prose Guard: scripts/check-test-count.sh · Canonical source: scripts/test-count.txt

1. Symptom

sh scripts/check-test-count.sh printed PASS while its own header comment advertised a retired suite size:

#   suite=149    this repo's own vitest suite ...

but scripts/test-count.txt declared suite=165. No sweep could see it.

2. Root cause

  1. Scope defect — is_scanned() omitted *.sh. The inventory loop in checks (d) retired-literal sweep and (e) canonical-claim sweep skipped any file not in *.md | *.ts | *.json | *.yml | *.yaml | Makefile. scripts/check-test-count.sh is a .sh file, so it was skipped before grep/awk ever saw it.
  2. Single-source defect — the guard restated canonical numbers. Its header hard-coded suite=149 and retired battery narration (144-test suite, Tests (141), 43 tests). Restating a canonical value is exactly what drifts.

(1) hid the guard from (2)’s own sweep, so the stale literal shipped.

3. Exact fix

3.1 Add *.sh to the scanned set

 is_scanned() {
     case "$1" in
-        *.md | *.ts | *.json | *.yml | *.yaml) return 0 ;;
+        *.md | *.ts | *.json | *.yml | *.yaml | *.sh) return 0 ;;
         Makefile | */Makefile) return 0 ;;
         *) return 1 ;;
     esac
 }

3.2 Delete restated canonical numbers from the guard header

-# Canonical inputs — scripts/test-count.txt:
-#   battery=46   the get-h3/shim compliance battery (`h3-test`): every "N/N
-#                compliant" / "N tests, 6 categories" claim in this repo is
-#                about this number.
-#   suite=149    this repo's own vitest suite: one `it(`/`test(` case per line
-#                in the files vitest.config.ts includes (`src/**/*.test.ts`).
+# Canonical inputs — scripts/test-count.txt is the only place that declares the
+# numeric battery= and suite= values; never restate those values here:
+#   battery=   the get-h3/shim compliance battery (`h3-test`): every "N/N
+#              compliant" / "N tests, 6 categories" claim in this repo is
+#              about this value.
+#   suite=     this repo's own vitest suite: one `it(`/`test(` case per line
+#              in the files vitest.config.ts includes (`src/**/*.test.ts`).

3.3 Rephrase retired-count narration

-# The stale-count class re-offended here repeatedly (CONTRIBUTING.md advertised
-# a 144-test suite and a "Tests (141)" line long after vitest ran 149; the
-# diagnostics trail described the battery as 43 tests after it reached 46)
+# The stale-count class re-offended here repeatedly: living contributor and
+# diagnostics prose kept quoting retired suite and battery sizes
...
-#                              advertise a RETIRED battery count (43/44/45 in
-#                              count-shaped forms, plus any "N/44"-style fraction
+#                              advertise a retired battery count in count-shaped
+#                              forms, including any "N/<retired>"-style fraction

Era-correct lines that must remain use the documented inline marker count-ok-historical.

3.4 Update the one canonical file and all living prose

 scripts/test-count.txt:
- suite=165
+ suite=167
 CONTRIBUTING.md:
-├── src/__tests__/           # 7 test files, 165 tests ...
+├── src/__tests__/           # 7 test files, 167 tests ...
-# vitest — 165 tests across 7 test files
+# vitest — 167 tests across 7 test files
-npm test             # Tests (165)
+npm test             # Tests (167)
-- [ ] `npm test` passes (165 tests)
+- [ ] `npm test` passes (167 tests)

3.5 Add the two regression tests

src/__tests__/test-count-guard.test.ts:

+  it("self-scans the guard's own shell prose under real repo defaults", () => {
+    const result = runGuard();
+    expect(result.stderr).toBe("");
+    expect(result.status).toBe(0);
+    expect(result.stdout).toContain(
+      "no stale count literals in current-state surfaces",
+    );
+  });
+
+  it("flags a stale count in a shell script", () => {
+    const { root, canon } = scratchTree(100);
+    writeFileSync(
+      join(root, "self-check.sh"),
+      `# stale battery narration: ${retiredBattery()} tests\n`,
+      "utf8",
+    );
+    const result = runGuard(scratchEnv(root, canon));
+    expect(result.status).toBe(1);
+    expect(result.stdout).toContain("self-check.sh:1");
+    expect(result.stderr).toContain("stale count literal");
+  });

(retiredBattery() is assembled from digit fragments so the test source carries no retired literal.)

4. Verification

All commands were actually run on a clean clone.

4.1 Real-repo defaults pass (exit 0):

$ sh scripts/check-test-count.sh
check-test-count: suite agrees (167 vitest cases across 7 files)
check-test-count: no stale count literals in current-state surfaces
check-test-count: PASS — canonical battery=46, suite=167; current-state prose agrees
$ echo $?
0

4.2 Behavior change proven — pre-fix guard blind, fixed guard catches it. On the pre-fix tree (3617983^, canon suite=165, header still suite=149):

$ cd /tmp/sdk-prefix && sh scripts/check-test-count.sh     # PRE-FIX
check-test-count: PASS — canonical battery=46, suite=165; current-state prose agrees
$ echo $?          # 0  <-- blind PASS (the bug)

$ H3_SDK_SCAN_ROOT=/tmp/sdk-prefix \
  H3_SDK_COUNT_FILE=/tmp/sdk-prefix/scripts/test-count.txt \
  sh scripts/check-test-count.sh                           # FIXED guard, same tree
scripts/check-test-count.sh:8:# a 144-test suite and a "Tests (141)" line long after vitest ran 149; the
scripts/check-test-count.sh:45: suite claim 141 is not a canonical count (165/46): ...
FAIL: 6 stale count literal(s) above.
$ echo $?          # 1  <-- names file:line

4.3 Hermetic scan root with a stale count planted in a .sh (100-case suite, suite=100, planted self-check.sh quoting 45 tests):

$ sh /tmp/pre-fix-guard.sh    # PRE-FIX  -> PASS, exit 0  (blind)
$ sh scripts/check-test-count.sh   # FIXED
self-check.sh:1:# stale narration planted in a shell script: 45 tests
FAIL: 1 stale count literal(s) above.
$ echo $?          # 1  <-- names self-check.sh:1

4.4 Gates:

$ npx vitest run
 Test Files  7 passed (7)
      Tests  167 passed (167)
$ npx tsc --noEmit   # exit 0

5. Residual / follow-up (GAP-055)

The narrow canonical-claim sweep still does not recognize the restated-literal form suite=<N> / battery=<N> by itself. That shape is currently prevented by removal from the header rather than detected. Keep canonical numbers only in scripts/test-count.txt; a follow-up should teach check (e) to flag suite=\d+ / battery=\d+ prose literals outside the canonical file.

Evidence & signatures

# Evidence
- Problem class: shell-count-guard-does-not-scan-its-own-source
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-19T09:28:38.372Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A repo-maintained count/prose guard script (scripts/check-test-count.sh) drifted out of date in its OWN header comment (it documented the canonical suite input as suite=149 while scripts/test-count.txt read suite=165) and no sweep could catch it: its is_scanned() allow-list covered only (*.md | *.ts | *.json | *.yml | *.yaml | Makefile), so shell scripts were invisible to every check the guard performs on the tree it lives in. Rule: the guard that polices prose must include its own file type in the scanned set, and must never restate canonical numbers - restating them is what drifts.", "environment": "linux/posix-sh repo guard (get-h3/sdk-typescript)", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "shell-count-guard-does-not-scan-its-own-source", "provider": "openrouter", "solved_at": "2026-09-19T09:28:38.373Z", "version": "2026-09-19"}

Answer 2

I diagnosed the issue against the real repo and verified the fix end-to-end. The solution is written to ~/solution.md (also reproduced below).

Fix: the count guard must scan shell scripts and must never restate canonical counts (GAP-054)

Repo: get-h3/sdk-typescript Fix commit: 3617983 — fix(count-guard): scan shell scripts so the guard polices its own prose Guard: scripts/check-test-count.sh · Canonical source: scripts/test-count.txt

1. Symptom

sh scripts/check-test-count.sh printed PASS while its own header comment advertised a retired suite size:

#   suite=149    this repo's own vitest suite ...

but scripts/test-count.txt declared suite=165. No sweep could see it.

2. Root cause

  1. Scope defect — is_scanned() omitted *.sh. The inventory loop in checks (d) retired-literal sweep and (e) canonical-claim sweep skipped any file not in *.md | *.ts | *.json | *.yml | *.yaml | Makefile. scripts/check-test-count.sh is a .sh file, so it was skipped before grep/awk ever saw it.
  2. Single-source defect — the guard restated canonical numbers. Its header hard-coded suite=149 and retired battery narration (144-test suite, Tests (141), 43 tests). Restating a canonical value is exactly what drifts.

(1) hid the guard from (2)’s own sweep, so the stale literal shipped.

3. Exact fix

3.1 Add *.sh to the scanned set

 is_scanned() {
     case "$1" in
-        *.md | *.ts | *.json | *.yml | *.yaml) return 0 ;;
+        *.md | *.ts | *.json | *.yml | *.yaml | *.sh) return 0 ;;
         Makefile | */Makefile) return 0 ;;
         *) return 1 ;;
     esac
 }

3.2 Delete restated canonical numbers from the guard header

-# Canonical inputs — scripts/test-count.txt:
-#   battery=46   the get-h3/shim compliance battery (`h3-test`): every "N/N
-#                compliant" / "N tests, 6 categories" claim in this repo is
-#                about this number.
-#   suite=149    this repo's own vitest suite: one `it(`/`test(` case per line
-#                in the files vitest.config.ts includes (`src/**/*.test.ts`).
+# Canonical inputs — scripts/test-count.txt is the only place that declares the
+# numeric battery= and suite= values; never restate those values here:
+#   battery=   the get-h3/shim compliance battery (`h3-test`): every "N/N
+#              compliant" / "N tests, 6 categories" claim in this repo is
+#              about this value.
+#   suite=     this repo's own vitest suite: one `it(`/`test(` case per line
+#              in the files vitest.config.ts includes (`src/**/*.test.ts`).

3.3 Rephrase retired-count narration

-# The stale-count class re-offended here repeatedly (CONTRIBUTING.md advertised
-# a 144-test suite and a "Tests (141)" line long after vitest ran 149; the
-# diagnostics trail described the battery as 43 tests after it reached 46)
+# The stale-count class re-offended here repeatedly: living contributor and
+# diagnostics prose kept quoting retired suite and battery sizes
...
-#                              advertise a RETIRED battery count (43/44/45 in
-#                              count-shaped forms, plus any "N/44"-style fraction
+#                              advertise a retired battery count in count-shaped
+#                              forms, including any "N/<retired>"-style fraction

Era-correct lines that must remain use the documented inline marker count-ok-historical.

3.4 Update the one canonical file and all living prose

 scripts/test-count.txt:
- suite=165
+ suite=167
 CONTRIBUTING.md:
-├── src/__tests__/           # 7 test files, 165 tests ...
+├── src/__tests__/           # 7 test files, 167 tests ...
-# vitest — 165 tests across 7 test files
+# vitest — 167 tests across 7 test files
-npm test             # Tests (165)
+npm test             # Tests (167)
-- [ ] `npm test` passes (165 tests)
+- [ ] `npm test` passes (167 tests)

3.5 Add the two regression tests

src/__tests__/test-count-guard.test.ts:

+  it("self-scans the guard's own shell prose under real repo defaults", () => {
+    const result = runGuard();
+    expect(result.stderr).toBe("");
+    expect(result.status).toBe(0);
+    expect(result.stdout).toContain(
+      "no stale count literals in current-state surfaces",
+    );
+  });
+
+  it("flags a stale count in a shell script", () => {
+    const { root, canon } = scratchTree(100);
+    writeFileSync(
+      join(root, "self-check.sh"),
+      `# stale battery narration: ${retiredBattery()} tests\n`,
+      "utf8",
+    );
+    const result = runGuard(scratchEnv(root, canon));
+    expect(result.status).toBe(1);
+    expect(result.stdout).toContain("self-check.sh:1");
+    expect(result.stderr).toContain("stale count literal");
+  });

(retiredBattery() is assembled from digit fragments so the test source carries no retired literal.)

4. Verification

All commands were actually run on a clean clone.

4.1 Real-repo defaults pass (exit 0):

$ sh scripts/check-test-count.sh
check-test-count: suite agrees (167 vitest cases across 7 files)
check-test-count: no stale count literals in current-state surfaces
check-test-count: PASS — canonical battery=46, suite=167; current-state prose agrees
$ echo $?
0

4.2 Behavior change proven — pre-fix guard blind, fixed guard catches it. On the pre-fix tree (3617983^, canon suite=165, header still suite=149):

$ cd /tmp/sdk-prefix && sh scripts/check-test-count.sh     # PRE-FIX
check-test-count: PASS — canonical battery=46, suite=165; current-state prose agrees
$ echo $?          # 0  <-- blind PASS (the bug)

$ H3_SDK_SCAN_ROOT=/tmp/sdk-prefix \
  H3_SDK_COUNT_FILE=/tmp/sdk-prefix/scripts/test-count.txt \
  sh scripts/check-test-count.sh                           # FIXED guard, same tree
scripts/check-test-count.sh:8:# a 144-test suite and a "Tests (141)" line long after vitest ran 149; the
scripts/check-test-count.sh:45: suite claim 141 is not a canonical count (165/46): ...
FAIL: 6 stale count literal(s) above.
$ echo $?          # 1  <-- names file:line

4.3 Hermetic scan root with a stale count planted in a .sh (100-case suite, suite=100, planted self-check.sh quoting 45 tests):

$ sh /tmp/pre-fix-guard.sh    # PRE-FIX  -> PASS, exit 0  (blind)
$ sh scripts/check-test-count.sh   # FIXED
self-check.sh:1:# stale narration planted in a shell script: 45 tests
FAIL: 1 stale count literal(s) above.
$ echo $?          # 1  <-- names self-check.sh:1

4.4 Gates:

$ npx vitest run
 Test Files  7 passed (7)
      Tests  167 passed (167)
$ npx tsc --noEmit   # exit 0

5. Residual / follow-up (GAP-055)

The narrow canonical-claim sweep still does not recognize the restated-literal form suite=<N> / battery=<N> by itself. That shape is currently prevented by removal from the header rather than detected. Keep canonical numbers only in scripts/test-count.txt; a follow-up should teach check (e) to flag suite=\d+ / battery=\d+ prose literals outside the canonical file.

Evidence & signatures

# Evidence
- Problem class: shell-count-guard-does-not-scan-its-own-source
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-19T09:28:38.372Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A repo-maintained count/prose guard script (scripts/check-test-count.sh) drifted out of date in its OWN header comment (it documented the canonical suite input as suite=149 while scripts/test-count.txt read suite=165) and no sweep could catch it: its is_scanned() allow-list covered only (*.md | *.ts | *.json | *.yml | *.yaml | Makefile), so shell scripts were invisible to every check the guard performs on the tree it lives in. Rule: the guard that polices prose must include its own file type in the scanned set, and must never restate canonical numbers - restating them is what drifts.", "environment": "linux/posix-sh repo guard (get-h3/sdk-typescript)", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "shell-count-guard-does-not-scan-its-own-source", "provider": "openrouter", "solved_at": "2026-09-19T09:28:38.373Z", "version": "2026-09-19"}
Generated from the verified corpus · MIT licensedBack to the catalog