Skip to content

fix(validate): state sg-regex-needs-kind in the ast-grep rule schema - #682

Merged
theCodeDrift merged 3 commits into
mainfrom
fix/sg-regex-needs-kind-schema
Nov 12, 2026
Merged

theCodeDrift merged 3 commits into
mainfrom
fix/sg-regex-needs-kind-schema

Conversation

@theCodeDrift

@theCodeDrift theCodeDrift commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Stack (root → tip):

Closes #671

What changed

  • The pairing lives in the schema. astGrepRuleJsonSchema (in src/schemas/ast-grep-rule.ts) layers if: {required: [regex]}, then: {$ref: TasklessPinsKind} onto both rule definitions in ast-grep's vendored schema (SerializableRule, Relation). The generated file is untouched, so upgrades keep working, and the layering throws if a vendor bump renames either definition. getSchemaPayload() now returns the augmented schema.
  • What anchors a regex (TasklessPinsKind): a sibling kind; a sibling any whose every branch pins a kind (recursively); or an all with at least one branch that pins one. A kind inside not or a relation does not count.
  • zod check. z.fromJSONSchema throws on if/then, so astGrepRuleSchema adds a check that walks the data against the same JSON Schema. That gives the same paths by construction, including positions the old walker missed (stopBy, nthChild.ofRule, utility arguments, fix.expandStart/expandEnd, rewriters). It is built with when: () => true because a plain .superRefine is skipped when another field already failed. Issues carry params.constraintId, so validate attributes sg-regex-needs-kind from the schema layer. The old Layer 2 walker is gone.
  • Message names the fix, the sibling-any option, and how to find the kind (--debug-query=ast, and what a constraint's metavariable captures).
  • Docs. The constraint summary/rationale and the create-sg-rule recipe (topic v15) say "every regex, at any depth". There is a new Quirks row and section, and assets/reference.json is regenerated.

Tests

  • New test/ast-grep-rule-schema.test.ts: constraints, utils, not, nested has/inside, precedes/follows, any/all items, the rarer positions, and the sibling-any cases (accepted; refused with an unpinned or empty branch). Also all, nested pins, zod attribution, the issue still being reported alongside an unrelated schema failure, and the JSON Schema augmentation leaving everything else in the upstream schema unchanged.
  • Vendor contract (ast-grep 0.51.1) pins the recipe's claims: a bare top-level regex is an engine config error, a bare regex in a sub-rule runs, the sibling-any constraint example works, and $OBJ.$M() captures a property_identifier.
  • I also checked the augmented JSON Schema with Ajv (2020-12), outside the repo, against 11 cases. The paths it flags matched findRegexWithoutKind exactly. Ajv is not added as a dependency.

Notes for review

  • Behavior change: the refusal moved from the requirements layer to the schema layer, so when it fires, the engine's own single-rule parse (which only runs on a schema pass) is skipped for that round.
  • Nothing ships this schema to editors today (no $schema comment and no package export). The schema now states the pairing, but how an editor picks it up is a separate decision.

@theCodeDrift

Copy link
Copy Markdown
Member Author

@claude /review

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @theCodeDrift's task in 5m 35s —— View job


Review complete

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json held no review summaries and no review threads; the only entry in comments is the author's own @claude /review trigger text, not a prior finding. Treating this as the first substantive review of this PR.

  • Read prior review data (none found — first review)
  • Fetch PR diff and changed files (gh pr diff, gh pr view)
  • Read packages/cli/src/schemas/ast-grep-rule.ts and packages/cli/src/rules/verify.ts in full, including the untouched parts they interact with
  • Trace the new schema-walking algorithm (visit/pinsKind) against the actual vendored ast-grep JSON Schema (src/generated/ast-grep-rule-schema.json) for several non-trivial paths (nthChild.ofRule, matches.<util>.<ARG>, stopBy, nested any/all)
  • Cross-check constraints.ts, assets/reference.json, create-sg-rule.md, and the test suite for consistency with the new behavior
  • Post findings

Summary

This is a well-scoped, carefully-documented fix. The core idea — stating the regex/kind pairing as if/then directly in the published JSON Schema, then deriving a zod-side check from a schema-walker (findRegexWithoutKind) that is driven by that same augmented schema — is a good way to guarantee the human-facing schema and the enforcement logic can't drift apart. I hand-traced the visit()/pinsKind() logic against the real vendored schema for several of the gnarlier positions (nthChild.ofRule, matches.<util>.<ARG>, Relation.stopBy, nested any/all composition) and it produces the paths the new test suite expects in each case. The empty-any vacuous-truth trap is correctly guarded against (any.length > 0 before .every()), and it's mirrored in the JSON Schema side via minItems: 1. The "regex inside not/a relation doesn't count" rule is correctly enforced by requiring kind on the same object, not a nested one.

Cross-file consistency is good: constraints.ts's summary/rationale, assets/reference.json, create-sg-rule.md's Quirks row/section, and test/constraints.test.ts's expected message all agree with each other and with the new REGEX_NEEDS_KIND_HINT text. No stale references to the removed findRegexWithoutKind(object, path) walker or the old astGrepJsonSchema import remain anywhere in src/ or test/.

[New] One fragility concern flagged inline (packages/cli/src/schemas/ast-grep-rule.ts, the regexNeedsKindCheck definition): it reaches into zod/v4/core's underscore-prefixed _check, which is conventionally "internal, not covered by semver," while package.json allows zod to float across ^4.3.6. The code comment explains why — the public .refine()/.superRefine() skip once a prior check already failed, and this check is specifically built to run anyway — so the choice looks deliberate rather than accidental, but it's still worth a maintainer's eyes: either pin zod exactly, or leave a short note for the next reader. If it does break on a zod bump, the new test suite (ast-grep-rule-schema.test.ts) would fail loudly rather than silently, which limits the downside.

Other notes (not findings, just context for the author):

  • The author's own PR description flags the behavior change where parseSgRuleAlone's single-rule engine parse is now skipped whenever the regex/kind schema check fires (since the issue is now folded into schemaErrors rather than validateRequirements's separate violations list). That's called out explicitly and looks like an accepted, understood tradeoff rather than an oversight — matches what I see in verify.ts.
  • Noticed test/ast-grep-vendor-contract.test.ts's file-level header comment still says ast-grep is "exact-pinned at 0.45.3," while package.json and this PR's own new tests pin/measure 0.51.1. That line predates this PR and isn't touched by the diff, so it's out of scope here, but it's worth a follow-up cleanup since it's actively misleading in a file whose entire purpose is pinning assumptions to a measured version.

@theCodeDrift

Copy link
Copy Markdown
Member Author

Re: @github-actions — "Claude finished @theCodeDrift's task in 5m 35s — Review complete…"
#682 (comment)

The _check fragility finding now has an explanatory note in ast-grep-rule.ts (650b440), with exact zod pinning left to the maintainer. The stale 0.45.3 pin in the ast-grep-vendor-contract.test.ts header now reads 0.51.1.

— AI Coding Agent

The regex/kind pairing was enforced only by a hand-written walker in
validate's requirements layer, so the rule schema said nothing about it
and a rule looked valid until validate refused it.

The pairing now lives in the schema. astGrepRuleJsonSchema layers an
if/then onto every rule definition (SerializableRule, Relation) that
points at one TasklessPinsKind definition: a sibling kind, a sibling
`any` whose every branch pins a kind, or an `all` with a branch that
does. zod cannot read if/then, so astGrepRuleSchema mirrors it with a
check that walks the data against the same JSON Schema, reaching every
position a rule can sit (including stopBy, nthChild.ofRule, utility
arguments, fix.expandStart/expandEnd and rewriters). The check runs even
when another field fails, and its issues carry the constraint id, so
validate attributes them without matching on wording. getSchemaPayload
now hands out the augmented schema.

The message says how to find the kind (--debug-query=ast, and what a
constraint's metavariable captures). The constraint summary and the
create-sg-rule recipe (topic v15) now say "every regex, at any depth",
with a Quirks entry, and the vendor contract pins the ast-grep behavior
that entry describes.

Closes #671
Also correct the vendor contract test header, which still named ast-grep 0.45.3 as the pinned version.
astGrepRuleSchema no longer reaches for zod's internal core._check to
attach a `when` to the pairing. It is now z.unknown().superRefine(),
which parses the upstream ast-grep schema with safeParse, forwards its
issues, and then adds one sg-regex-needs-kind issue per unanchored
regex. Because the outer schema accepts any input, the pairing still
runs when another field fails, without depending on `when`.

The public `.refine(fn, { when })` was the other candidate, but a
refinement raises a single issue with a fixed path and params, and the
pairing needs one issue per offending rule object.

Issues, their order, codes, paths, messages and params are unchanged.
The tests now pin that: the pairing is reported next to an unrelated
failure, and upstream failures match a plain z.fromJSONSchema parse.
@theCodeDrift
theCodeDrift force-pushed the fix/sg-regex-needs-kind-schema branch from 180912e to 06935ee Compare November 12, 2026 04:31
@theCodeDrift
theCodeDrift merged commit 64f9187 into main Nov 12, 2026
4 checks passed
@theCodeDrift
theCodeDrift deleted the fix/sg-regex-needs-kind-schema branch November 12, 2026 04:36
theCodeDrift added a commit that referenced this pull request Nov 12, 2026
…nd issue

#682 moved sg-regex-needs-kind into the ast-grep rule schema, so the
validateRequirements loop that appended the `taskless ast <file>` pointer
is gone. Apply the pointer in validateSchema's formatter instead, to both
the layer error and its attributed violation, and point the recipe's
regex/kind section at `taskless ast` rather than --debug-query=ast.
theCodeDrift added a commit that referenced this pull request Nov 12, 2026
…nd issue

#682 moved sg-regex-needs-kind into the ast-grep rule schema, so the
validateRequirements loop that appended the `taskless ast <file>` pointer
is gone. Apply the pointer in validateSchema's formatter instead, to both
the layer error and its attributed violation, and point the recipe's
regex/kind section at `taskless ast` rather than --debug-query=ast.
theCodeDrift added a commit that referenced this pull request Nov 12, 2026
Rebasing onto main brought in text from #682, #683, #686 and #691 that
pushed three outputs past 8 KiB:

- create-sg-rule outline: the `taskless ast` guidance moves to an
  on-demand Reading kinds section, step 4 points at it, and step 5 drops
  its quirk enumeration (the Quirks table lists them).
- create-sg-rule pattern-quirks: the relational entries (follows/precedes,
  has/inside, not) move to a Relational quirks section; validate-rule
  names it beside the other two.
- route outline: #686's note on recipes that limit questions moves to an
  on-demand Limits on asking section, with a one-line pointer in
  Preconditions.
theCodeDrift added a commit that referenced this pull request Nov 12, 2026
Rebasing onto main brought in text from #682, #683, #686 and #691 that
pushed three outputs past 8 KiB:

- create-sg-rule outline: the `taskless ast` guidance moves to an
  on-demand Reading kinds section, step 4 points at it, and step 5 drops
  its quirk enumeration (the Quirks table lists them).
- create-sg-rule pattern-quirks: the relational entries (follows/precedes,
  has/inside, not) move to a Relational quirks section; validate-rule
  names it beside the other two.
- route outline: #686's note on recipes that limit questions moves to an
  on-demand Limits on asking section, with a one-line pointer in
  Preconditions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sg-regex-needs-kind: move the regex/kind pairing into the rule schema, with a zod refinement

1 participant