plugin-inventory counts hooks/README.md and hooks.json as executable LOC, inflating script_coverage #10

Closed
opened 2026-07-29 18:16:56 +03:00 by oleks · 2 comments
Owner

Found during the Phase 7 acceptance sweep (kotkan/claude-plugin-inference-arbitrage#7). Not fixed there deliberately — it shifts script_coverage for every target, and the S1–S8 numbers had just been stabilised. Small and self-contained.

What

walk_hooks in bin/plugin-inventory adds every regular file under hooks/ to the hook list, and build_aggregate sums their line counts into script_loc:

script_loc = sum(s["loc"] for s in scripts) + sum(h["loc"] for h in hooks)

So documentation and configuration are counted as shipped executable volume:

Target Non-executable files counted LOC wrongly counted
memory hooks/README.md, hooks/hooks.json 115
anxious hooks/hooks.json 51
worktree-discipline hooks/hooks.json 50

memory also lists README.md as a hook with "event": "unknown" in the JSON output, which is simply wrong — it is not a hook.

Why it matters

FR-2.3 defines script coverage as prose-instruction volume vs shipped executable volume. A README is prose; counting it on the executable side of that ratio is a category error, and it biases the metric in the one direction that matters — it makes a prose-heavy plugin look better cut than it is. script_coverage is one of the four static smells the offload-audit skill and offload-analyst gather candidates from, so the error propagates into candidate selection.

The effect is small on the targets audited so far (a few percent), but it scales with how much documentation a plugin keeps beside its hooks.

Fix

Filter walk_hooks to actual hook scripts. The information needed is already parsed there — .claude-plugin/hooks.json is read to build events_by_file. Reasonable rule: count a file only if it is referenced by hooks.json, or is executable, or has a #! shebang; keep hooks.json itself out of script_loc regardless, since it is configuration rather than logic.

Note detect_lang already reads shebangs for bin/, so the same test is available.

Test

tests/inventory.test.sh — add a fixture hook directory containing one real hook script, a README.md and a hooks.json, and assert script_loc counts only the hook script, and that the README does not appear in hooks[].

Watch out

tests/calibration/mask-worktree-audit.py recomputes script_loc and must keep matching build_aggregate — that consistency was itself the second bug fixed in 105e9f5, and the regression test added there (script_loc still counts hooks) will catch a divergence.

Found during the Phase 7 acceptance sweep (kotkan/claude-plugin-inference-arbitrage#7). Not fixed there deliberately — it shifts `script_coverage` for every target, and the S1–S8 numbers had just been stabilised. Small and self-contained. ## What `walk_hooks` in `bin/plugin-inventory` adds **every regular file** under `hooks/` to the hook list, and `build_aggregate` sums their line counts into `script_loc`: ```python script_loc = sum(s["loc"] for s in scripts) + sum(h["loc"] for h in hooks) ``` So documentation and configuration are counted as shipped executable volume: | Target | Non-executable files counted | LOC wrongly counted | |---|---|---| | `memory` | `hooks/README.md`, `hooks/hooks.json` | 115 | | `anxious` | `hooks/hooks.json` | 51 | | `worktree-discipline` | `hooks/hooks.json` | 50 | `memory` also lists `README.md` as a hook with `"event": "unknown"` in the JSON output, which is simply wrong — it is not a hook. ## Why it matters FR-2.3 defines script coverage as *prose-instruction volume vs shipped executable volume*. A README is prose; counting it on the executable side of that ratio is a category error, and it biases the metric in the one direction that matters — it makes a prose-heavy plugin look better cut than it is. `script_coverage` is one of the four static smells the `offload-audit` skill and `offload-analyst` gather candidates from, so the error propagates into candidate selection. The effect is small on the targets audited so far (a few percent), but it scales with how much documentation a plugin keeps beside its hooks. ## Fix Filter `walk_hooks` to actual hook scripts. The information needed is already parsed there — `.claude-plugin/hooks.json` is read to build `events_by_file`. Reasonable rule: count a file only if it is referenced by `hooks.json`, or is executable, or has a `#!` shebang; keep `hooks.json` itself out of `script_loc` regardless, since it is configuration rather than logic. Note `detect_lang` already reads shebangs for `bin/`, so the same test is available. ## Test `tests/inventory.test.sh` — add a fixture hook directory containing one real hook script, a `README.md` and a `hooks.json`, and assert `script_loc` counts only the hook script, and that the README does not appear in `hooks[]`. ## Watch out `tests/calibration/mask-worktree-audit.py` recomputes `script_loc` and must keep matching `build_aggregate` — that consistency was itself the second bug fixed in `105e9f5`, and the regression test added there (`script_loc still counts hooks`) will catch a divergence.
oleks added the agent-wip label 2026-07-30 05:22:11 +03:00
Author
Owner

Confirmed real, fixed in eb4b945.

Root cause: walk_hooks in bin/plugin-inventory added every regular file under hooks/ to the hook list with no filter, and build_aggregate summed hooks[].loc straight into script_loc. hooks/README.md and .claude-plugin/hooks.json are prose/config, not shipped executable logic, so they inflated script_coverage in the direction that makes a plugin look better cut than it is — exactly as the issue described. Issue body was accurate as filed; nothing since (checked git log --oneline -- bin/plugin-inventory: last touches were 07b5637/caa2a8c/bf88742, all Phase 2, well before this issue was opened 2026-07-29T18:16 — none of today's other fixes (6b634fd, e3b67e9, ba445cf) touched this file).

Repro before fix (~/projects/claude-plugins/memory hooks dir has README.md + hooks.json alongside 3 real hooks):

$ python3 bin/plugin-inventory ~/projects/claude-plugins/memory --json | python3 -c "..."
hooks: README.md(75), hooks.json(40), precompact-hook.sh(58), session-start-hook.sh(68), stop-hook.sh(321)
script_loc 1202   script_coverage 0.616

Matches the issue's table exactly: 75+40=115 wrongly-counted LOC for memory. Same for anxious (hooks.json=51) and worktree-discipline (hooks.json=50).

Fix: walk_hooks now counts a file only if it is referenced by .claude-plugin/hooks.json's command paths, is executable (os.access(X_OK)), or detect_lang() recognizes a shebang/.py extension — matching the rule the issue proposed. hooks/hooks.json is unconditionally excluded regardless of those checks, since it's configuration. README.mds with no shebang/exec bit are now dropped entirely (no more "event": "unknown" bogus hook entries for them).

After fix, same repro:

hooks: precompact-hook.sh, session-start-hook.sh, stop-hook.sh   (README.md, hooks.json gone)
script_loc 1087   script_coverage 0.557

1202 − 115 = 1087, exactly the delta the issue predicted. Checked anxious and worktree-discipline too — hooks.json (51/50 LOC) no longer counted, real hook scripts unaffected (anxious's test-issuer-ladder-guard.py stays counted since it's executable — that's a legitimate call under the issue's own proposed rule, not a new bug).

Test: added hooks/README.md and hooks/hooks.json to tests/fixtures/sample-plugin, plus three new assertions in tests/inventory.test.sh (only guard.sh appears in hooks[], script_loc excludes the README, LOC still matches). Ran the full suite:

$ bash tests/run-all.sh

All green, no FAIL lines (verified via grep -iE "fail|error" — only benign test-label matches like "T4 fails -> low").

Watch-out from the issue (mask-worktree-audit.py must keep matching build_aggregate): verified — it recomputes script_loc by summing d["hooks"][].loc from the already-filtered JSON output, so it stayed consistent automatically with no code change needed there. Confirmed live:

$ python3 bin/plugin-inventory ~/projects/claude-plugins/memory --json | python3 tests/calibration/mask-worktree-audit.py
_mask: script_loc_before=1087, script_loc_after=1087, script_coverage_after=0.557

Bumped plugin.json to 0.6.2 (patch, per this repo's convention that a shipped fix requires a version bump for cache invalidation). Committed as eb4b945, pushed to main, working tree clean and in sync with origin/main.

No new bugs surfaced during this work. Nix/worktree/deploy standing rules don't apply here — no Nix build step, no deploy step, work done directly on main in the primary checkout (not a worktree).

Confirmed real, fixed in `eb4b945`. **Root cause**: `walk_hooks` in `bin/plugin-inventory` added every regular file under `hooks/` to the hook list with no filter, and `build_aggregate` summed `hooks[].loc` straight into `script_loc`. `hooks/README.md` and `.claude-plugin/hooks.json` are prose/config, not shipped executable logic, so they inflated `script_coverage` in the direction that makes a plugin look better cut than it is — exactly as the issue described. Issue body was accurate as filed; nothing since (checked `git log --oneline -- bin/plugin-inventory`: last touches were `07b5637`/`caa2a8c`/`bf88742`, all Phase 2, well before this issue was opened 2026-07-29T18:16 — none of today's other fixes (`6b634fd`, `e3b67e9`, `ba445cf`) touched this file). **Repro before fix** (`~/projects/claude-plugins/memory` hooks dir has README.md + hooks.json alongside 3 real hooks): ``` $ python3 bin/plugin-inventory ~/projects/claude-plugins/memory --json | python3 -c "..." hooks: README.md(75), hooks.json(40), precompact-hook.sh(58), session-start-hook.sh(68), stop-hook.sh(321) script_loc 1202 script_coverage 0.616 ``` Matches the issue's table exactly: 75+40=115 wrongly-counted LOC for `memory`. Same for `anxious` (hooks.json=51) and `worktree-discipline` (hooks.json=50). **Fix**: `walk_hooks` now counts a file only if it is referenced by `.claude-plugin/hooks.json`'s command paths, is executable (`os.access(X_OK)`), or `detect_lang()` recognizes a shebang/`.py` extension — matching the rule the issue proposed. `hooks/hooks.json` is unconditionally excluded regardless of those checks, since it's configuration. `README.md`s with no shebang/exec bit are now dropped entirely (no more `"event": "unknown"` bogus hook entries for them). **After fix**, same repro: ``` hooks: precompact-hook.sh, session-start-hook.sh, stop-hook.sh (README.md, hooks.json gone) script_loc 1087 script_coverage 0.557 ``` 1202 − 115 = 1087, exactly the delta the issue predicted. Checked `anxious` and `worktree-discipline` too — hooks.json (51/50 LOC) no longer counted, real hook scripts unaffected (anxious's `test-issuer-ladder-guard.py` stays counted since it's executable — that's a legitimate call under the issue's own proposed rule, not a new bug). **Test**: added `hooks/README.md` and `hooks/hooks.json` to `tests/fixtures/sample-plugin`, plus three new assertions in `tests/inventory.test.sh` (only `guard.sh` appears in `hooks[]`, `script_loc` excludes the README, LOC still matches). Ran the full suite: ``` $ bash tests/run-all.sh ``` All green, no FAIL lines (verified via `grep -iE "fail|error"` — only benign test-label matches like "T4 fails -> low"). **Watch-out from the issue** (`mask-worktree-audit.py` must keep matching `build_aggregate`): verified — it recomputes `script_loc` by summing `d["hooks"][].loc` from the *already-filtered* JSON output, so it stayed consistent automatically with no code change needed there. Confirmed live: ``` $ python3 bin/plugin-inventory ~/projects/claude-plugins/memory --json | python3 tests/calibration/mask-worktree-audit.py _mask: script_loc_before=1087, script_loc_after=1087, script_coverage_after=0.557 ``` Bumped `plugin.json` to 0.6.2 (patch, per this repo's convention that a shipped fix requires a version bump for cache invalidation). Committed as `eb4b945`, pushed to `main`, working tree clean and in sync with `origin/main`. No new bugs surfaced during this work. Nix/worktree/deploy standing rules don't apply here — no Nix build step, no deploy step, work done directly on `main` in the primary checkout (not a worktree).

Fixed by eb4b945 (walk_hooks now excludes hooks.json unconditionally and only counts other hook files if referenced by hooks.json, executable, or shebang-detected). Verified: full test suite (bash tests/run-all.sh) passes clean on current origin/main (1a14456), including the 3 new regression assertions in tests/inventory.test.sh guarding this fix.

Fixed by eb4b945 (walk_hooks now excludes hooks.json unconditionally and only counts other hook files if referenced by hooks.json, executable, or shebang-detected). Verified: full test suite (bash tests/run-all.sh) passes clean on current origin/main (1a14456), including the 3 new regression assertions in tests/inventory.test.sh guarding this fix.
admin closed this issue 2026-07-30 05:51:58 +03:00
admin removed the agent-wip label 2026-07-30 05:51:59 +03:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kotkan/claude-plugin-inference-arbitrage#10