fix(cli): require both fixture buckets for an ast-grep rule - #156
Conversation
|
@claude /review |
|
Claude finished @thecodedrift's task in 3m 57s —— View job Review in progress
|
bd9d73f to
ee1a034
Compare
|
@claude /review The earlier review on this PR stopped before posting findings (the checklist above is unfinished), and the branch has changed since — it was rebased onto a corrected base and carries new commits. Please review the current head. |
|
Claude finished @thecodedrift's task in 2m 45s —— View job Review completeReview mode: incremental — read 0 prior review thread(s) before reviewing.
Findings posted inline (all
|
3ecd0cd to
b1eaaa7
Compare
ee1a034 to
44f4850
Compare
Three review findings on #156, all in the same shape: a claim stated twice diverges, and the second copy is the one nobody reads. `fixtureCoverage` counted a test file by filename alone, while `sg test --filter ^<id>$` resolves cases against the file's `id:` field. A draft copied from another rule — right filename, wrong `id:` — had its buckets counted toward coverage while ast-grep never ran them, reopening the "never shown to fire" gap this branch exists to close through the filename door rather than the empty-bucket one. The "what counts as this rule's test file" predicate was also written twice in one `verifyRule()` pass. It now lives in one `discoverRuleTestFiles` used by both `validateRequirements` and `fixtureCoverage`. Also drops the duplicate `describe("the language field")` suite that the rebase left behind. The base branch carries the fuller three-test version; commit 44f4850 removed the duplicate `atLanguage` helper but missed the suite itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms
This is the in-progress checklist from a run that stopped mid-review; it was superseded by the completed run in #issuecomment-5391681373, whose three inline findings are all addressed in 5c19a47. Acknowledging so a re-run does not resurface it. — AI Coding Agent |
All three inline findings verified against the code and fixed in 5c19a47: the duplicate — AI Coding Agent |
b1eaaa7 to
af57c0e
Compare
`verify` Layer 2 checked that a rule's `-test.yml` existed and never read what was in it, and `ast-grep test` reports an empty `invalid:` bucket as `1 passed; 0 failed` and exits zero. A rule whose fixtures were all `valid:` therefore reported `ok: true, ran: true` while `check` found nothing anywhere. `test` now counts the `valid:`/`invalid:` entries across every test file a rule owns and requires both, mirroring `ValeFixtureCoverage` state for state with ast-grep's own vocabulary. This rejects sg rules that passed before; the changeset says so. Also pins the `$$$` separator behaviour behind #152 as a vendor contract: a comma-adjacent `$$$` is not "zero or more", because the comma is itself a node that must match under the default `smart` strictness. Upstream calls this intended and 0.45.2 is identical, so `verify --schema` gains a curated `strictness: ast` example rather than a version bump. Fixes #152 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms
…e is written `create-sg-rule` sent an author to upstream for the rule format and said nothing about the two fields that fail without local feedback. The `$$$` separator gets its own step, with the arity table measured against the pinned binary: a comma-adjacent `$$$` is not "zero or more", because the comma is itself a node that must match under the default `smart` strictness. Both remedies are stated as the different things they are — `strictness: ast` inside a pattern object for a trailing `$$$` (moving the boundary from two arguments to one, not to zero), `any` per arity for a leading one — and the fixture step now asks for a case on each side of an arity boundary, since a one-sided rule reports `1 passed; 0 failed` and exits zero. `language:` gets the same treatment in the authoring step, rendered from `AST_GREP_LANGUAGES` rather than transcribed. Nothing local validates that field, and it fails two ways: a spelling ast-grep does not recognize takes the whole scan down, and `TypeScript` over a `.tsx` tree reports nothing and looks clean. Both are now pinned in the vendor contract, so the prose is quoting the binary rather than describing it. `improve-rule` carries the two notes that only matter on a rewrite: read the pattern for a comma-adjacent `$$$` before reporting a rule as too narrow, and re-check both fixture buckets after the service returns a narrowed rule. Refs #152 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms
The suite and its `atLanguage` helper moved down to fix/engine-capabilities, so that the recipe claim in route.txt and the binary behaviour pinning it land in the same change. Rebasing brought them here, leaving a duplicate declaration. The $$$ arity suite stays — that one pins claims made by this branch's own recipe prose in create-sg-rule.txt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms
Three review findings on #156, all in the same shape: a claim stated twice diverges, and the second copy is the one nobody reads. `fixtureCoverage` counted a test file by filename alone, while `sg test --filter ^<id>$` resolves cases against the file's `id:` field. A draft copied from another rule — right filename, wrong `id:` — had its buckets counted toward coverage while ast-grep never ran them, reopening the "never shown to fire" gap this branch exists to close through the filename door rather than the empty-bucket one. The "what counts as this rule's test file" predicate was also written twice in one `verifyRule()` pass. It now lives in one `discoverRuleTestFiles` used by both `validateRequirements` and `fixtureCoverage`. Also drops the duplicate `describe("the language field")` suite that the rebase left behind. The base branch carries the fuller three-test version; commit 44f4850 removed the duplicate `atLanguage` helper but missed the suite itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms
5c19a47 to
a3fa92b
Compare

Stack (root → tip):
verifyLayer 2 checked that a-test.ymlexists, never what was in it — andast-grep testpasses an emptyinvalid:bucket (1 passed; 0 failed, exit 0). So a rule that flags nothing reported success:We already close this on the Vale side.
rules/vale/verify.tshasValeFixtureCoverageandinspect.tsfails any rule whosefixtures !== "both"— "half a claim". The sg branch of that same function had no equivalent, andcli-rule-validationspecified the Vale rule with no sg counterpart, so the spec actively endorsed the gap.Now symmetric: a rule whose
invalid:bucket is empty across all its.tests/*-test.ymlfiles has never been shown to fire and does not pass. It reads the author's own YAML with the parser already imported inverify.ts— derives nothing, guesses at nothing — and generalizes past$$$to any rule that matches nothing.minoris defensible;patchwas chosen (pre-1.0) with the warning carried in the changeset body.Fixes #152
What #152 reported vs. what is true
The report was that a trailing
$$$does not match a zero-argument call. That is false as worded — and a worse variant is real. Measured against the bundled 0.41.0 and now pinned:foo(),foo(1),foo(1,2),foo(1,2,3)foo($$$)foo()foo($A, $$$)foo(1,2),foo(1,2,3)— notfoo(1)foo($$$, $A)foo(1)foo($A, $$$, $B)foo(1,2)The pattern's
,is itself an AST node, and under defaultsmartstrictness every pattern node must match, so a call with no comma cannot match a pattern that has one.$$$matching zero nodes is fine — the separator is what fails.$$$and$$$ARGSbehave identically; statement blocks are unaffected because;lives inside the statement node.Upstream closed this as working-as-intended (ast-grep/ast-grep#1365) and 0.45.2 behaves identically, so there is no version to wait for.
The remedy is
strictness: astinside a pattern object — and note it moves the boundary from ≥2 to ≥1, not to zero, since$Astill has to bind something.strictnessat rule level is rejected as an unknown field and fails the whole scan, so placement is load-bearing; a test pins that too. For a leading$$$,strictness: astdoes not help andany:with one branch per arity is the fix.The coverage gate is what surfaces this. It forces an
invalid:case, that case fails against the original pattern, andstrictness: astfixes it — the trap becomes visible at authoring time instead of in production.Zero shipped rules were affected
Every
$$$in this repo is the sole element of its argument list. The exposure was entirely prospective: no recipe mentioned$$$at all, andcreate-sg-rule.txtpointed agents at the upstream docs, which say only "zero or more AST nodes" — the omission that makes this a trap.Recipes
create-sg-rule.txtgains a step for the separator trap with the measured table and both remedies, and the fixture step now states that both buckets must be non-empty and that a rule with an arity boundary needs a case on each side of it.improve-rule.txtgains a note to check for a comma-adjacent$$$before assuming a pattern is merely too narrow, and to re-confirm both buckets after the service rewrites a rule — narrowing a pattern to kill a false positive can leaveinvalid:with nothing the rule still matches.Inserting a step shifted four numbers; three in-file cross-references were corrected, and every recipe was grepped for
step [0-9]to catch others. (A previous change here shipped an off-by-one exactly this way.)verify-examples.tsgains astrictness: astentry, so the safe form reaches agents throughverify --schema— it previously showed only the standalone$$$.Tests
One case per coverage state in
verify.test.ts(nothing covered an empty bucket before), the$$$behavior pinned inast-grep-vendor-contract.test.tsincluding rule-levelstrictnesshard-failing, and two cases pinning the language-field claims added to the recipe: an unrecognized spelling takes the whole scan down, andTsx/TypeScriptare distinct parsers rather than aliases.OpenSpec delta on
cli-rule-validationadding the sg bucket scenario. Not archived; not the tip.Built on top of #155
An agent reading
route.txtcould not tell whether a language was buildable locally or had to escalate toruntime, which needs a login. The reporter's case: two GitHub Actions YAML rules were routed toruntimebecause nothing said whether ast-grep handles YAML.It does. The pinned
@ast-grep/cli@0.41.0lists 26 languages includingYaml, and a realpr-workflow-no-branches-filterrule (language: Yaml) passesverifyandtest. Both Actions rules belong increate-sg-rulewith no login.Refs #151
Derived, not transcribed
There was no source of truth for either engine's reach.
$defs.Languagein the vendored schema is a bare string with no enum,verifynever validateslanguage, anddetect --jsonreports the repository's languages in a different vocabulary. The binaries were the only authority, androute.txtsaid nothing.Transcribing two lists into a
.txtwould drift on a version bump with nothing to catch it, and stale prose is worse than the current silence — silence at least makes the agent ask. So: pinned constants insrc/rules/capabilities.ts, substituted into recipes as%(KEY)s, with vendor-contract tests asserting the constants still match the binaries. That is the patternast-grep-vendor-contract.test.tsalready exists for — exact-pinned binary plus a test that fires on the bump.capabilities.tsis deliberately import-free:prompts/recipes.tsimports it, and theassert-prompts-graphvite plugin fails the build if that chunk reaches a host capability.Rejected alternative: extending
detect --jsonwith anenginesblock.cli-detect's spec framesdetectas an offline repo scan, engine reach is knowable at build time, and it would cost a third subprocess turn in a recipe already spending two.Vale's reach was measured, not read
Vale self-reports no format list, so every tier here comes from probing the shipped 3.17.1 binary — and the measurement corrected the starting assumptions in five places:
.cssis comment-tier, not plaintext. The first probe said otherwise because it fed CSS a//comment, which is not one..sassis comment-tier;.scssis not..Ris comment-aware,.PY/.JS/.TSare not;.cc/.cxx/.pyw/.pmare,.hh/.hxxare not..htm,.xhtml).--no-exitdoes not suppress the converter tier'sE100exit 2.Each tier is pinned by its discriminating property, because on ordinary prose all three readable tiers look identical: markup by a construct only a parser skips, comment-tier by the negative (a bare non-comment line must yield zero), converter-tier by non-zero exit plus
E100plus the converter's name. A test also asserts the fixture map covers the markup constant exactly, so an unprobed entry cannot be added to a constant.create-vale-rule.txtwas teaching the crashIts worked example globbed
[*.{md,mdx}]..mdxneedsmdx2vast, which this CLI does not ship, and one such file fails the entire Vale pass — so any rule following that example silently disabled every other Vale rule. Now[*.{md,markdown}], with the point stated plainly: a matcher like that is not a wider[*.md], it is a broken one. (The guard that makes this non-fatal is in the PR above this one.)A correction, included deliberately
The last commit walks back a claim this branch originally shipped. Both the recipe and the constants said a plausible
C++oryaml"passesverifyand fails inside the binary." Measured at 0.41.0,C++andcppare accepted aliases that resolve toCpp, so an off-list spelling is not reliably an error.The two failures that are real, neither caught locally: a name ast-grep does not recognize (
C#forCSharp) aborts config parsing, taking every other rule's report down with it; and a valid name for the wrong parser (TypeScriptover a.tsxtree) reports nothing and reads as a clean codebase. The advice to copy from the list is unchanged; the reason for it is now the accurate one. The vendor-contract assertions were already right — they pin the canonical list, not what the binary rejects — but their comment said otherwise.Also
route.txtnow states that a language on neither list does not route toruntimeby default:create-legacy-rulecomes first, since the repo's own linter may already speak it. And a new note separates reach (a property of the pinned version) from availability (a property of the host), which the existing "availability you can assert" bullet was conflating while giving the agent no way to assert it.OpenSpec delta on
cli-rule-routing— engine reach becomes a fourth thing the recipe is specified to state, with the normative core being that it is derived from the pinned versions rather than transcribed. Not archived; this is not the tip of the stack.