feat(backends): astgrep Python backend v0 — renderer + live-fired matrix + CI firing (python-backend-v0 S1) - #960
Merged
Conversation
… FF7001/FF7002 refusals (S1 Task 1)
python-backend-v0 umbrella, S1 Task 1. Adds packages/core/backends/astgrep/:
- render-astgrep.ts: pure renderer, backend name astgrep-python-yaml,
ToolchainBackend<string> conformance (astgrepBackend), mirroring
backends/cargo/render-clippy.ts seams. Routing is the INVERSE of cargo:
selectorClass 'syntax' renders (native per-rule severity — defaultSeverity
projected directly, NO FF7003 degrade path); 'type-aware' refused FF7001
(mypy backend deferred); 'dep-graph' refused FF7001 (import-linter backend
deferred); malformed/missing params refused FF7002. Params contract:
{kind: 'call'|'attribute'|'import', pattern, replacement?}. Rule message
is ALWAYS node.claim (render-clippy.ts:112 parity). Artifact: ast-grep
YAML rule-set string (header + one doc per rule, ---separated, id-sorted).
Emission shape live-verified against @ast-grep/cli@0.44.1
(scan --json: ruleId identity, exit 1 on error findings, exit 0 clean).
- test-fixtures.ts: FIXTURE_NODE = ban datetime.datetime.now (the P5
live-fired case, research-patches/2026-07-02-multi-toolchain-generalization.md:409-423).
- render-astgrep.test.ts: 15 unit tests (TDD red-first) — T15 grammar-gate
self-application, FF7001/FF7002 paired negatives, severity projection
incl. note→hint, byte-for-byte golden, YAML shape + escaping + sort.
SSOT: appends rows #212 (ast-grep as Python rule-emission target, ADOPT),
#213 (mypy, DEFER — trigger: first type-aware Python node), #214
(import-linter, DEFER — trigger: first dep-graph Python node) to
docs/meta-factory/prior-art-evaluations.md (append-only §3 procedure).
Tests: npm --prefix packages/core run test:backends (9 files / 80 tests
green) + test:principles (33 files / 303 tests green).
Prior-art: prior-art-evaluations.md#212 (ast-grep @ast-grep/cli@0.44.1 as the Python syntax-class rule-emission target, ADOPT — #199 render-target lineage, #185 distinct search-surface capability, P5 live-fire evidence).
… parser + capability matrix (S1 Task 2) Firing fixtures (invalid/valid/valid-clean) + firing-contract.json + live-fire firing.test.ts + self-application drift gate + shared JSON-array diagnostic parser + capability-matrix.json with live-fired evidence, for the astgrep-python-yaml backend. - Fixtures: canonical rule bans datetime.datetime.now (FIXTURE_NODE). invalid calls it; valid wraps it in an injected-clock accessor + the `# ast-grep-ignore: <rule-id>` suppression escape hatch; valid-clean is conformant with zero findings. Each fixture's committed rules/no-datetime-now.yml is byte-identical to render(FIXTURE_NODE) — the always-on self-application drift test gates it (runs in CI, no tool needed). - firing.test.ts: presence-check + loud-skip when the pinned ast-grep is unavailable, but NO `!isCI` guard — CI fires for real (owner decision). Live-fire RED (invalid -> ruleId no-datetime-now) / GREEN (valid + valid-clean -> zero findings). - Shared backends/shared/json-array-parse.ts: parseIdentitiesFromJsonArray for the JSON-array diagnostic shape. Justified per the kickoff §1 two-consumer condition — ast-grep (S1) and ruff (S2) both emit JSON arrays with a flat identity field. The cargo NDJSON parser is untouched. - capability-matrix.json: syntax=yes (live-fired evidence, ruleId identity matches the firing contract), type-aware=no FF7001, dep-graph=no FF7001. Toolchain-freshness gate derives the resolving ast-grep version at run time (no version literal in code) and turns RED on a pin bump without evidence regen. Live-fired against @ast-grep/cli@0.44.1 (2026-07-11): invalid -> 1 finding ruleId no-datetime-now severity error (exit 1); valid + valid-clean -> [] (exit 0). Review carry-forwards closed empirically: (m1) a 2-rule `---`-separated multi-document YAML fired BOTH rules (no-datetime-now + no-os-system) — multi-doc parses; (m2) a `severity: hint` finding DOES surface in `scan --json` default output (exit 0, not 1), so the Task-1 note->hint mapping is safe (findings visible, just non-erroring). ast-grep invocation detail encapsulated in fireContract: `scan` anchors its project root to the enclosing git repo, so both `-c <config>` and the scan target are passed as absolute paths under the fixture dir to scope the scan to that fixture's src.py. Prior-art: prior-art-evaluations.md#212 (ast-grep as Python rule-emission target, ADOPT — this commit ships the S1 firing harness + capability matrix for that emission target; SSOT row added in Task 1 commit fb07a5f).
… firing model (S1 Task 3) Wire the astgrep-python-yaml backend's live-fire to fire FOR REAL in CI, and switch the firing model from an npx-resolved pin to a bare PATH binary backed by a hard, exact-pinned CI install (owner binding decision, Task 2 review follow-up). - firing-contract.json `command`: `npx -y -p @ast-grep/cli@0.44.1 ast-grep scan --json` -> `ast-grep scan --json`. The tool is NOT a package.json dependency; it comes via a workflow global install. deriveToolVersion/fireContract token handling is form-agnostic (splits on the `ast-grep` bin token), so no logic change was needed — only the recorded command and the surrounding rationale comments in firing-runner.ts / firing.test.ts / capability-matrix.test.ts. - audit-self.yml (principles-meta-tests job): add `npm install -g @ast-grep/cli@0.44.1` BEFORE the `test:backends` step. Exact pin, no float, no `latest` (ci-tool-pinning.md Rule A); no secrets, no third-party action (zizmor/no-paid-llm clean). Rationale (owner STOP-line): an npx-resolved pin converts a registry flake into a loud-skip — CI goes green without ever firing the backend. A hard install step fails loud-red instead, keeping "CI fires for real" honest. Kickoff: "Tool comes via workflow install, NOT a package.json dependency". No `!isCI` guard is re-introduced (cargo firing.test.ts:31-49 loud-skip shape, kept for the local-absent-tool path only). Evidence unchanged: capability-matrix.json still claims `ast-grep 0.44.1`; a live `ast-grep 0.44.1 scan --json` run against the invalid fixture reproduces the committed capturedDiagnostic byte-for-byte (byteOffset 38-61, ruleId no-datetime-now, severity error) modulo the already-normalized repo-relative `file` field — so no matrix regeneration was required. Verified: `npm --prefix packages/core run test:backends` green with the 3 live-fire tests FIRING (0.44.1 on PATH; 108 passed) and loud-skipping when the binary is absent (3 skipped + drift block green); `test:principles` 303 passed. Prior-art: prior-art-evaluations.md#212 (ast-grep @ast-grep/cli@0.44.1 as the Python rule-EMISSION target, verdict ADOPT — this commit wires that ADOPTed backend's live-fire into CI via a pinned PATH install; no new capability, modifies existing astgrep backend files + the CI gate).
…lobal install zizmor 1.26.1 (CI-pinned) flags any npm install outside a lockfile; a global CLI tool has no lockfile surface and the exact @0.44.1 pin is the determinism mechanism (ci-tool-pinning.md Rule A). Inline-ignore precedent: demo-regen.yml:62. Verified clean against zizmor==1.26.1 locally. Prior-art: skipped — CI suppression comment only, no new capability
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
S1 of the python-backend-v0 umbrella (kickoff, merged in #955): ships
backends/astgrep/— the PRIMARY Python render target of the Convention Compiler and the first non-TS/non-Rust backend. Three commits:fb07a5fc8— rendererastgrep-python-yaml(syntax→rendered with native per-rule severity, type-aware/dep-graph→FF7001, malformed params→FF7002;message=node.claim) + SSOT rows fix(ai-laziness-traps): T20 inline-verdict-without-evidence #212 (ast-grep emission ADOPT) / research(planner-completeness): R-phase prior-art survey for L3/L4/L5 #213 (mypy DEFER) / feat(meta-orchestrator): L1 — extend discovery surface beyond kickoff.md #214 (import-linter DEFER).00ccacdbb— firing harness: fixturesinvalid/valid/valid-clean(canonical bandatetime.datetime.now, suppression# ast-grep-ignore),firing-contract.json(identityruleId), shared JSON-array parser (backends/shared/json-array-parse.ts, second consumer = ruff in S2; cargo NDJSON untouched),capability-matrix.jsonwith live-fired evidence + toolchain-freshness gate, byte-for-byte self-application drift test (T15).ecbba6228— CI live-fire wiring: hard pinnednpm install -g @ast-grep/cli@0.44.1inaudit-self.ymlbeforetest:backends; firing command reconciled to the bare-PATH binary model (an npx-pin would loud-skip on registry flake — converting the owner's "CI fires for real" STOP-line into#hope-as-gate; a failing install step is loud-red instead). NO!isCIguard (owner decision; cargo remains the documented exception).§0 volatile facts re-verified live at ship time (T12):
@ast-grep/cli@0.44.1(npm latest),scan --json= JSON array withruleIdidentity, suppression syntax,sgconfig.ymllayout; ruff facts captured for S2.Tests:
test:backends108/108 (live-fire genuinely firing),test:principles303/303. Cold backward-sweep (T21 agent): 13/13 surfaces, no gaps. Final whole-branch review: MERGE-READY.§1.7 Forward-check applied
This PR introduces the
astgrep-python-yamlbackend — the first non-TS/non-Rust rule-EMISSION target of the Convention Compiler — plus a shared JSON-array diagnostic parser and a bare-PATH pinned-install CI live-fire model. New conventions and where each is enforced at the earliest reachable channel:docs/meta-factory/prior-art-evaluations.md#212ADOPT ast-grep;#213mypy DEFER;#214import-linter DEFER) — enforced by principle 08 (packages/core/principles/08-prior-art-cited.test.ts, broken-ref catch) + principle 11 F1 (Prior-art trailer present on all three capability commits).RenderOutcome;syntax→rendered,type-aware/dep-graph→FF7001, malformed params→FF7002) — enforced byassertEveryNodeResolved(packages/core/backends/astgrep/render-astgrep.ts:971) + paired-negative tests (packages/core/backends/astgrep/render-astgrep.test.ts:706-784).evidence.kind === 'live-fired'whose capturedruleIdmatches the firing contract) —validateMatrix(packages/core/backends/shared/capability-matrix.ts) +packages/core/backends/astgrep/capability-matrix.test.ts:320, gated in CI viatest:backends.checkToolchainFreshness(packages/core/backends/astgrep/capability-matrix.test.ts:178) derives the version at runtime and turns RED on a pin bump without evidence regen.!isCIguard; tool via hard pinned global install, not a package.json dep) —.github/workflows/audit-self.yml:229(npm install -g @ast-grep/cli@0.44.1, ci-tool-pinning.md Rule A) before thetest:backendsstep at.github/workflows/audit-self.yml:238.render(FIXTURE_NODE)byte-for-byte, T15) — always-on block inpackages/core/backends/astgrep/firing.test.ts:557(runs in CI without the tool).§1.7 Backward-check applied
Class of this change = add a new per-toolchain rendering backend (renderer + params contract + capability matrix + firing harness + CI live-fire wiring + SSOT emission-target rows). Surfaces where this class occurs = the other backends under
packages/core/backends/**, the shared contracts they depend on, and the CI workflows that gate them. Per-surface sweep (evidence = file:line; cold backward-sweep-auditor run 13/13 surfaces, no gaps):packages/core/backends/cargo/**— SWEPT-CLEAN. Routing is the deliberate inverse (cargosyntax→refuse FF7001packages/core/backends/cargo/render-clippy.ts:81-93; astgrepsyntax→renderpackages/core/backends/astgrep/render-astgrep.ts:945-968); themessage === node.claiminvariant preserved on both (render-clippy.ts:112↔render-astgrep.ts:961); cargo's NDJSON parser untouched (packages/core/backends/shared/json-array-parse.ts:15copiesgetByJsonPathrather than re-exporting the NDJSON module); cargo's documented!isCIlive-fire exception left in place, not generalized. No gap.packages/core/backends/npm/**— SWEPT-CLEAN. ast-grep's flatruleIdidentity is the same family as npm/eslint's flatruleId(packages/core/backends/astgrep/capability-matrix.test.ts:167); npm's own firing-contract shape ({command, expectedRuleId}) not regressed by astgrep's{command, jsonPath, expectedCode}. No gap.packages/core/backends/shared/**— SWEPT-CLEAN.capability-matrix.ts::validateMatrixreused verbatim via theextractIdentityparam;render-outcome.ts::assertEveryNodeResolvedreused;json-array-parse.tsis net-new and shared by design under the kickoff §1 two-consumer condition (ruff S2 is the second consumer) — additive, disturbs no existing consumer..github/workflows/audit-self.yml— SWEPT-CLEAN. Pinned-install step added beforetest:backends(.github/workflows/audit-self.yml:229-238); adjacent cargo comment corrected for accuracy. No other job touched..github/workflows/discipline-self-check.yml— SWEPT-CLEAN (not edited). Already coverspackages/core/backends/**(.github/workflows/discipline-self-check.yml:23) since fix(composition): CTX Stage 0 quick-wins — A12 separator · A14 orphan · X1 CI paths · §3.3 rule frontmatter · DN-4 FF8004 · DN-5 PR template · A15 auditor · A10 supersession #905, so this PR's §1.7 sections are gated. No change needed.packages/core/ir/types.ts(ConventionNode) — SWEPT-CLEAN. Field-freeze honored, no new field (STOP line).packages/core/validator/gate-rule-tester.ts— SWEPT-CLEAN. L4 ast-grepenginedeferred-marker (packages/core/validator/gate-rule-tester.ts:87-88) intact, not wired (STOP line, T-PY-C).Carried forward (non-blocking)
replacement → fix:YAML emission unit-tested but not live-fired (no fixture withreplacement) — carry into S2/S3 fixture coverage.renderYaml— triaged OK-to-leave by final review.