fix(tests): teach the CLI-verb linter to read cli_called verbs - #2995
fix(tests): teach the CLI-verb linter to read cli_called verbs#2995alexandrujircan wants to merge 1 commit into
Conversation
`check-cli-verbs.py` only ever parsed `command_pattern`, so a verb named by a `cli_called` criterion was checked by nothing. Every task migrated to `cli_called` (#2565, #2994 — 39 criteria naming 40 verb spellings today) quietly left the catalog linter's reach, and the migrations were verified by hand instead. A stale ixp verb in a migrated task would have been caught by no gate in the repo. `cli_called` names its verb in `verb` / `verb_any_of` as literal whitespace-separated chains, so there is no regex to walk and no dynamic case. Only criteria carrying `tool: uip` are checked: `record_cli` shims several executables into ONE shared log and `tool` is what separates them, so a verb under any other tool is not a uip verb. An ABSENT `tool` is also skipped — it matches any executable, so nothing can be claimed about it. Both skips are silent, matching how a non-uip `command_pattern` is already dropped. The catalog lookup had to change to make this bite. `classify` walks progressively shorter prefixes, which a `command_pattern` verb needs because the regex may have captured a trailing flag fragment. A `cli_called` verb is a complete chain, and the catalog holds group prefixes as their own entries — so under the prefix fallback the typo `ixp projects lisst` matched the group `ixp projects` and scored reachable. The first cut of this change shipped that bug; caught by asserting on a deliberately misspelled leaf. `classify` now takes `exact=`, used only by the `cli_called` path, which accepts a deliberately short group verb while still failing a typo'd leaf. Renames stay prefix-matched in both modes, so `flow validate` is still reported as retired via its `flow` ancestor and keeps its canonical suggestion. Both paths now emit findings through one `record()` helper so severities and message shapes cannot drift apart. Findings carry a new `source` field (`command_pattern` | `cli_called`); `command_pattern` stays the display key for both, because write_report and the stdout printer read it and the High histogram parses `unmatched: [...]` out of the message. The display label reads `cli_called:` rather than `pattern:` where that is what it is. Verification: 8 new regression tests (22 pass in the file). Differential over all 1284 task YAMLs, old checker vs new — 102 findings each, identical on every field, no key added or dropped, so nothing already in the repo changes verdict. The four classification paths were exercised end to end: typo'd leaf -> High, group prefix -> clean, retired verb -> Medium with suggestion, non-uip tool -> no verdict; and --report renders a cli_called High into the verb histogram. Docs updated where they stated the old scope: `audit-verbs.md`'s report description and `lint-task.md`'s CLI-verb axis. Not fixed here, pre-existing and unrelated: the Medium message's `→` crashes on a Windows cp1252 console, and test_runtime_payload_key_casing.py fails the same way at HEAD. Both are Windows-locale only; CI is UTF-8 Linux. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @alexandrujircan's task in 2m 45s —— View job PR Review: fix(tests): teach the CLI-verb linter to read cli_called verbs
SummaryExtends Change-by-Change Review1.
|
| Test | What it pins |
|---|---|
test_cli_called_verb_is_linted_at_all |
Basic: typo'd verb → High, source == cli_called |
test_cli_called_typoed_leaf_not_rescued_by_group_prefix |
The key regression: exact vs prefix on classify |
test_cli_called_deliberately_short_verb_stays_reachable |
Group-level verb is legal under exact matching |
test_cli_called_verb_any_of_reachable_if_any_spelling_is |
Mixed reachable/unreachable in verb_any_of |
test_cli_called_retired_verb_is_medium_with_suggestion |
Rename prefix-match still works under exact |
test_cli_called_non_uip_tool_gets_no_verdict |
Both tool: curl and absent tool skip cleanly |
test_cli_called_verbless_criterion_is_skipped |
Flags-only criterion produces no finding |
test_cli_called_verb_whitespace_is_normalised |
Irregular spacing normalises to catalog key |
The test_cli_called_typoed_leaf_not_rescued_by_group_prefix test is especially valuable — it asserts both exact=True (unknown) and exact=False (reachable) on the same input, directly pinning the bug the PR description warns about.
3. .claude/commands/audit-verbs.md
Severity: OK
Line 12: The output description now mentions cli_called verb / verb_any_of alongside command_executed patterns. Accurate and complete.
4. .claude/commands/lint-task.md
Severity: Low
Line 163: The primary paragraph is well-updated — mentions both criterion types, the source field, and the exact-matching behavior. However, the severity passthrough descriptions at lines 167–169 still use command_pattern-centric language ("pattern's verb path", "pattern matches only retired verbs"). These descriptions are now shared across both criterion types and could be mildly misleading for cli_called findings. Not incorrect (the PR description explains command_pattern is the display key), but "criterion" would be more accurate than "pattern" in those bullets.
What's Missing
- Info finding
sourcefield: A one-line addition ("source": "command_pattern") at line 393 would make the--jsonschema uniform across all severities. Trivial but worth doing. Fix this → - Nothing else identified — the PR is well-scoped.
Area Ratings
| Area | Status | Notes |
|---|---|---|
| Frontmatter | N/A | No skill files changed |
| E2E Tests | N/A | Script tests, not skill tests |
| Skill Body | N/A | No skill files changed |
| References & Assets | OK | Slash-command docs updated accurately |
| Repo Hygiene | OK | Scoped to linter + tests, no secrets, no drive-by changes |
Issues for Manual Review
- Differential claim: The PR states "102 findings each, identical on every field" across old vs new checker over all 1284 task YAMLs. I was unable to run the tests locally (missing permissions), so this claim could not be independently verified. CI should confirm.
- The two pre-existing Windows-locale issues (
→on cp1252,UnicodeDecodeErrorintest_runtime_payload_key_casing.py) are noted in the PR as not-fixed-here. They affect Windows only and are outside this PR's scope.
Conclusion
Solid, well-motivated change. The exact parameter on classify is the correct design — it closes the prefix-fallback blind spot for cli_called while preserving the fallback where command_pattern genuinely needs it. The regression test that pins both directions of that distinction is the strongest evidence the fix works. The 8 new tests cover the meaningful edge cases.
Two Low nits: the missing source field on Info findings and the "pattern"-centric language in lint-task.md severity descriptions. Neither blocks merge.
Approve — no blocking issues.
|
Closes the follow-up flagged in #2994.
scripts/check-cli-verbs.pyonly ever parsedcommand_pattern, so a verb named by acli_calledcriterion was checked by nothing. Every task migrated tocli_called(#2565, #2994 — 39 criteria naming 40 verb spellings today) quietly left the catalog linter's reach, and both migrations were verified by hand instead. A stale ixp verb in a migrated task would have been caught by no gate in this repo.What it does now
cli_callednames its verb inverb/verb_any_ofas literal whitespace-separated chains, so there is no regex to walk and no dynamic case.Only criteria carrying
tool: uipare checked.record_clishims several executables into one shared log andtoolis what separates them, so a verb recorded under any other tool is not a uip verb. An absenttoolis also skipped — it matches any executable, so nothing can be claimed about it. Both skips are silent, matching how a non-uipcommand_patternis already dropped. No criterion in the repo is affected today: all 39 carrytool: uip.The catalog lookup had to change
This is the part worth reviewing.
classifywalks progressively shorter prefixes, which acommand_patternverb genuinely needs — the regex may have captured a trailing flag fragment. Acli_calledverb is a complete chain, and the catalog holds group prefixes as their own entries (ixp,ixp projects,ixp projects list).So under the prefix fallback, the typo
ixp projects lisstmatched the groupixp projectsand scored reachable. The first cut of this change shipped exactly that bug — the new check ran, found nothing, and looked like it worked. Caught by asserting on a deliberately misspelled leaf.classifynow takesexact=, used only by thecli_calledpath. It accepts a deliberately short group verb (legal —verbis an ordered prefix of the argv, so amax_count: 0guard onixp projectsis meant to fire on every subcommand) while still failing a typo'd leaf. Renames stay prefix-matched in both modes, soflow validateis still reported retired via itsflowancestor and keeps its canonical suggestion.Shape of the output
Both paths emit through one
record()helper, so severities and message shapes cannot drift apart. Findings carry a newsourcefield (command_pattern|cli_called).command_patternstays the display key for both —write_reportand the stdout printer read it, and the High histogram parsesunmatched: [...]out of the message — but the printed label readscli_called:where that is what it is.Verification
tests/scripts/test_verb_checkers.py. One of them pins the prefix-vs-exact distinction in both directions, so the bug above cannot come back.--reportrenders acli_calledHigh into the verb histogram correctly (theunmatched: [...]parse still lands).Merge order
Independent of #2994 — branched off
main, changes no task YAMLs, and #2994's verbs are all valid so neither PR turns the other red. Merge in either order. Landing this one first is what I'd suggest, so the remaining ~25 migrations are linted as they arrive rather than by hand.Not fixed here
Two pre-existing, unrelated Windows-locale issues, left alone to keep this one logical change:
→crashes a Windows cp1252 console. Present at HEAD (check-cli-verbs.py:316), and this PR adds a new way to reach a Medium finding without introducing the character.tests/scripts/test_runtime_payload_key_casing.py::test_no_raw_lowercase_runtime_key_readsfails with aUnicodeDecodeError. Confirmed identical at HEAD with these changes stashed.Both are Windows-only; CI is UTF-8 Linux and unaffected.
🤖 Generated with Claude Code