Skip to content

fix(ai-laziness-traps): T20 inline-verdict-without-evidence - #212

Merged
artyhoo merged 1 commit into
stagingfrom
fix/recommendation-laziness-iphase-c
May 24, 2026
Merged

fix(ai-laziness-traps): T20 inline-verdict-without-evidence#212
artyhoo merged 1 commit into
stagingfrom
fix/recommendation-laziness-iphase-c

Conversation

@artyhoo

@artyhoo artyhoo commented May 24, 2026

Copy link
Copy Markdown
Owner

Summary

Sub-wave C of recommendation-laziness-discipline-i-phase umbrella.

Adds T20 «inline-verdict-without-evidence» trap to .claude/rules/ai-laziness-traps.md:

  • §2 catalogue: new ### T20 entry at .claude/rules/ai-laziness-traps.md:146-152 with Trigger / Tempted output / Counter sections (cites parent rule phase-research-coverage.md §1.12 + named anti-pattern #recommendation-skips-own-discipline).
  • §3 obligation: T20 added to active-trap enumeration example at .claude/rules/ai-laziness-traps.md:158.
  • §5 promotion criterion: T20 → Class A on 3+ documented incidents at .claude/rules/ai-laziness-traps.md:178; recall-caveat clause prevents any post-hoc grep from claiming catch-rate without manual classification (per narrow-b-benchmark.md §1.3 + §T19).

T-number choice: maintainer pre-resolved T20 for this trap (overrides R-phase §1.4 pre-decision of (b)=T21). Stryker T20 «equivalence-claim-without-evidence» (queued per memory project_stryker_mutation_hardening_done.md) bumps to T21 when it ships.

Origin:

Parent rule: .claude/rules/phase-research-coverage.md §1.12 (introduced 2026-05-22; this trap operationalises its #recommendation-skips-own-discipline anti-pattern for the inline-chat surface).

How to verify

  • Principle 12 test green — npx vitest run principles/12-ai-laziness-traps.test.ts exit=0, 6/6 passed — verified locally before push
  • Principle 09 test green — npx vitest run principles/09-doc-authority-hierarchy.test.ts exit=0, 17/17 passed — verified locally before push
  • All 18 principle tests green — npx vitest run principles/ exit=0, 117/117 passed — verified locally before push
  • T-count = 20 (was 19) — grep -c '^### T' .claude/rules/ai-laziness-traps.mdverified locally
  • T20 mentioned in 3 places (§2 header L146, §3 enumeration L158, §5 promotion L178) — verified locally
  • Parent rule §1.12 cited in trap body at L152 — verified locally
  • No T-collision (single T20 only) — grep -cE '^### T20' = 1 — verified locally
  • File length 186 lines (pre-commit gate <500); markdownlint 0 errors — verified locally
  • Pre-push hooks green: zizmor 0 findings, audit-ai-docs 1719/1719 passed, skill-drift OK — verified at push time
  • CI green — awaiting GitHub Actions (Synthesizer/Validator/Installer/Phase 8 snapshots in progress)

§1.7 Forward-check applied

This PR complies with discipline-bearing rules:

§1.7 Backward-check applied

Scope-extending change, no silent superseding:

  • T1-T19 unchanged. Diff at .claude/rules/ai-laziness-traps.md:146-152 inserts a NEW T20 block between the existing T19 entry (ending at L144) and the existing §3 header (L155 pre-edit, L155 post-edit — header position unchanged because insertion was below). No edits to T1-T19 content. Verified via git diff origin/staging origin/fix/recommendation-laziness-iphase-c -- .claude/rules/ai-laziness-traps.md — only additions in T-catalogue range plus one in-place edit to L158 (§3 enumeration example) and one append at L178 (§5 promotion criterion).
  • §3 obligation enumeration extended, not rewritten. Single change at .claude/rules/ai-laziness-traps.md:158: T1, T3, T4, T7, T11, T13, T15T1, T3, T4, T7, T11, T13, T15, T20. Existing T-numbers preserved.
  • §5 promotion criterion appended. New bullet at .claude/rules/ai-laziness-traps.md:178; existing §5 entries (Trap retirement, Catalogue split, Companion principle test SHIPPED) preserved unchanged.
  • No other rule files modified. git diff --stat origin/staging..origin/fix/recommendation-laziness-iphase-c → single file .claude/rules/ai-laziness-traps.md (+10 / -1). Sub-wave A (inject-session-bootstrap.sh) and Sub-wave D (new recommendation-laziness-discipline.md) explicitly OUT OF SCOPE for this PR — separate Worker / separate PRs per kickoff §1 file-disjoint dispatch matrix.
  • No principle tests modified. packages/core/principles/12-ai-laziness-traps.test.ts (the companion test) unchanged; it validates the new T20 entry by passing 6/6 (header format + obligation + promotion criterion structural checks).
  • Parent rule .claude/rules/phase-research-coverage.md §1.12 unchanged. This PR ships the mechanism layer only; the prose discipline at §1.12 remains source-of-truth (per kickoff §6 anti-scope: «NOT editing parent rule phase-research-coverage.md §1.12»).
  • #recommendation-skips-own-discipline anti-pattern extended, not superseded. T20 operationalises the existing anti-pattern (defined in .claude/rules/phase-research-coverage.md §4) for the inline-chat surface; the anti-pattern itself stays in its parent rule unchanged.
  • Stryker T20 (queued per memory project_stryker_mutation_hardening_done.md) coordination resolved. Maintainer pre-resolved (a) = ship this trap as T20; Stryker bumps to T21 when it ships. No silent override — collision was mutual-coordination (per kickoff §5 S2), not technical, and the maintainer explicitly made the call in this dispatch.
  • R-phase patches (PR research(recommendation-laziness-discipline): R-phase design + binding I-phase scope #206/research(recommendation-laziness-amend): BFR own-stack-blind-spot fix + narrow-B caveat #207/research(narrow-b-benchmark): production-corpus FP measurement for Option B #210) frozen, untouched. Per docs/meta-factory/research-patches/ folder authority (Append-only), no edits to merged patches; their content cited via Prior-art trailers on commit 40f13e5.

Out of scope

  • Sub-wave A (.claude/hooks/inject-session-bootstrap.sh:11 H1 wording append) — separate Worker / separate PR.
  • Sub-wave D (new .claude/rules/recommendation-laziness-discipline.md rule file) — separate Worker / separate PR.
  • Sub-wave B (Stop-hook scan) — explicitly DROPPED per benchmark §1.5 (FP_rate = 84%); not in this PR, not in this umbrella.
  • Promotion to Class A — deferred per kickoff §6 (insufficient incident evidence; catalogue-only is correct first-codification per README.md absolutism research-patch 2026-05-16).

Rollback plan

Revert commit 40f13e5; .claude/rules/ai-laziness-traps.md returns to T19 highest. No downstream dependencies — Sub-waves A and D ship independently of this PR.

Codifies the operational form of #recommendation-skips-own-discipline
(phase-research-coverage.md §4) for the inline-chat surface — AI session
issues ADOPT/BUILD/REJECT/DEFER or «use X» / «pick Y over Z» in dialogue
without running at least one evidence-bearing tool call in the same turn.

Counter (per phase-research-coverage.md §1.12): before any verdict in
dialogue, run ≥1 Bash|Read|Grep|Glob|WebFetch|WebSearch and quote its
output (file:line / command result / fetched excerpt).

§3 enumeration: T20 added to active-trap example list.
§5 promotion criterion: T20 → Class A on 3+ documented incidents; any
post-hoc grep mechanism MUST include manual classification per
narrow-b-benchmark.md §1.3 + §T19 recall caveat (recall=1.0 on n=10
no-match sample is speculative; FP_rate = 84% on automated narrow-dict
matcher rules out gate-class enforcement without semantic enrichment).

Sub-wave C of recommendation-laziness-discipline-i-phase umbrella.
Maintainer pre-resolved T20 = this trap (Stryker T20 «equivalence-claim»
bumps to T21 when it ships).

Prior-art: docs/meta-factory/research-patches/2026-05-24-recommendation-laziness-discipline.md (R-phase design, PR #206/#207) — §1.4 item 1 binding scope, §1.6 backward-check EXTENDS phase-research-coverage.md §1.12 (mechanism layer).
Prior-art: docs/meta-factory/research-patches/2026-05-25-narrow-b-benchmark.md (benchmark, PR #210) — §1.5 verdict Option D = A+C only (B dropped); §1.3 + §T19 recall caveat load-bearing for §5 promotion criterion phrasing.
@artyhoo
artyhoo merged commit eac9dce into staging May 24, 2026
27 of 28 checks passed
@artyhoo
artyhoo deleted the fix/recommendation-laziness-iphase-c branch May 24, 2026 23:59
artyhoo added a commit that referenced this pull request May 25, 2026
Sub-wave D shipped initially with T21 cross-references per R-phase §1.4 (b)
pre-resolution placeholder. Maintainer overrode the pre-resolution in
Sub-wave C dispatch session — T20 picked, shipped via PR #212 (merge commit
eac9dce 2026-05-24T23:59:35Z). This commit amends the three T21 references
(lines 38, 50, 63) to T20 + replaces «expected/amendable» hedging prose with
the now-known reality (per maintainer override + PR# + commit-SHA).

Anticipated by kickoff §1 Sub-wave D «cross-reference to T<N> resolves only
after Sub-wave C commits — Sub-wave D may stage with placeholder and amend».
In-scope umbrella amend, not drive-by per CLAUDE.md PR strategy.

Prior-art: skipped — rule-file prose amend, not a capability commit per
CLAUDE.md «What is a capability commit?» (no new dep, no new file ≥50 LOC
in new packages/core/ subdir, no new ≥80 LOC file under packages/). Evidence
trail: PR #206/#207 (R-phase) + PR #210 (benchmark) + PR #212 (Sub-wave C
T20 ship; the amend target).
artyhoo added a commit that referenced this pull request May 25, 2026
…ayer for §1.12 (I-phase Sub-wave D) (#215)

* docs(rules): recommendation-laziness-discipline Class C — mechanism layer for §1.12

New rule file codifies the inline-chat verdict-without-evidence pattern as a
mechanism layer for parent `.claude/rules/phase-research-coverage.md §1.12`
(prose discipline source-of-truth). Class C: prose-only, promotion criterion
in §6 (3+ in-session violations in 6 months → consider mechanical gate, with
manual classification sample mandatory per benchmark recall caveat).

Sub-wave D of recommendation-laziness-discipline I-phase. Option D = A+C only
per benchmark verdict (Option B explicitly DROPPED — see §4 enforcement
channel section + benchmark §1.5: FP_rate = 84.2%, Wilson CI [62%, 95%]).

Cross-reference to T-trap in ai-laziness-traps.md uses T21 placeholder
(R-phase §1.4 (b) pre-resolution; amendable in follow-up if Sub-wave C lands
with different number).

Prior-art: skipped — rule-file codification (.claude/rules/*.md), not a
capability commit per CLAUDE.md «What is a capability commit?» (no new
explicit dependency, no new file ≥50 LOC in new packages/core/ subdir, no
new ≥80 LOC file under packages/). Evidence base voluntarily cited:
docs/meta-factory/research-patches/2026-05-24-recommendation-laziness-discipline.md
(PR #206/#207, R-phase) + docs/meta-factory/research-patches/2026-05-25-narrow-b-benchmark.md
(PR #210, benchmark verdict).

§1.7: forward-check applied — .claude/rules/recommendation-laziness-discipline.md:1 header follows doc-authority-hierarchy.md §3 (Class+Auth-for+NOT-auth-for blockquote), §1.12 cited at recommendation-laziness-discipline.md:16, B-DROP rationale at recommendation-laziness-discipline.md:41 (84.2% FP, Wilson CI [62%,95%]), T21 placeholder at recommendation-laziness-discipline.md:38; backward-check sweep — phase-research-coverage.md:86 §1.12 unchanged (mechanism extends, does not redefine), no other rule's Authoritative-for overlaps «inline-verdict-without-evidence», new file is additive only.

* fix(rules): recommendation-laziness — T21 → T20 cross-ref amend

Sub-wave D shipped initially with T21 cross-references per R-phase §1.4 (b)
pre-resolution placeholder. Maintainer overrode the pre-resolution in
Sub-wave C dispatch session — T20 picked, shipped via PR #212 (merge commit
eac9dce 2026-05-24T23:59:35Z). This commit amends the three T21 references
(lines 38, 50, 63) to T20 + replaces «expected/amendable» hedging prose with
the now-known reality (per maintainer override + PR# + commit-SHA).

Anticipated by kickoff §1 Sub-wave D «cross-reference to T<N> resolves only
after Sub-wave C commits — Sub-wave D may stage with placeholder and amend».
In-scope umbrella amend, not drive-by per CLAUDE.md PR strategy.

Prior-art: skipped — rule-file prose amend, not a capability commit per
CLAUDE.md «What is a capability commit?» (no new dep, no new file ≥50 LOC
in new packages/core/ subdir, no new ≥80 LOC file under packages/). Evidence
trail: PR #206/#207 (R-phase) + PR #210 (benchmark) + PR #212 (Sub-wave C
T20 ship; the amend target).
artyhoo added a commit that referenced this pull request May 25, 2026
…mplate (#216)

Adds §4b to templates/meta-kickoff.template.md — a path-triggered,
always-present mandate that any Worker whose target file matches a
discipline-bearing path (.claude/rules/, packages/core/principles/,
EXECUTION-PLAN, prior-art-evaluations, CLAUDE.md, packages/core/templates/,
.claude/skills/, agents/) MUST include §1.7 Forward/Backward-check sections
in the PR body with the exact H3 + "applied" shape + ≥40 chars + ≥1
file:line citation per section.

Origin: 6th recurrence of the §1.7-missing-from-PR-body CI gate fail in
~2 weeks (#58/#69/#105/#111/#212/#215). Memory feedback_pr_s17_authoring_checklist
says "memory ≠ enough, execute the grep" — but in-line memory does not
reliably activate inside paste-block Worker prompts. The mandate must be
in the prompt itself.

The §4b block:
- Names all 8 path patterns that trigger the mandate
- Documents 5 hard rules (H3 not H2; "applied" required; ≥40 chars; ≥1
  file:line; "Skipped" escape-hatch needs ≥60-char rationale)
- Provides a copy-paste pre-flight grep block Workers can run BEFORE
  gh pr create
- Lists the 6 historical incident PR numbers as evidence

Self-evaluating: Workers whose target file does NOT match the path list
ignore §4b. The mandate is path-scoped, not all-Workers.

No template placeholder added (no SKILL.md change needed) — block is
static prose, always present in every generated meta-kickoff. Workers
read it and self-apply.

Prior-art: skipped — template prose addition; not a capability commit per
CLAUDE.md «What is a capability commit?» (no new dep, no new file ≥50 LOC
in new packages/core subdir, no new ≥80 LOC file under packages/).
Evidence base: memory feedback_pr_s17_authoring_checklist + recurrence
incidents PR #58/#69/#105/#111/#212/#215.
artyhoo added a commit that referenced this pull request May 25, 2026
… detection) (#220)

Run `bash .claude/skills/meta-orchestrator/helpers/plan-currency-check.sh`
(L2 Stage 3 detection shipped in #217) → 88 UNTRACKED-N entries surfaced
between the 2026-05-22 reconciliation and origin/staging tip (#217). Map
each to an existing §0 / Track row by adding the PR number to its evidence
cell, or to a new §0 row for two umbrellas that landed in full since the
prior snapshot. Re-run helper → 0 UNTRACKED remaining.

Key changes:

- Snapshot date 2026-05-22 → 2026-05-25 (header + §0).
- N8 row: A-phase 🔲 → 🟡 — C1 SSOT-existence (#170), C2 kickoff
  T-enumeration floor (#174), C3 principle 13 §1.7 substance (#178),
  C4 delivery-channel marker (#177), activation #180. C5 + cost-levers
  remain gated on §5.3 utilisation trigger.
- Track M.1 / M.4 → DONE: M.1 codified T20 via #212 (with NB note —
  recommendation-laziness took the T20 slot, mutation-equivalence
  T-bump 20→21 still pending); M.4 6 paired-negative bash-hook tests
  shipped #195/#196/#197/#198/#199/#200.
- Two new §0 rows: Meta-orchestrator skill (Track P) — BUILD #186 +
  audit rounds #192/#193/#194/#201/#202 + UX refactor #203/#204/#205
  + planner-completeness #213/#214/#217 + §1.7 PR-body mandate #216;
  Recommendation-laziness discipline — R-phase #206/#207, benchmark
  #210, I-phase Sub-waves A/C/D #211/#212/#215.
- N7 row: + dogfood research-patch #135 / §4 demotion #166 / live-trial
  verified #171. N4b row: + design #136 / record #118.
- Infra paragraph: PR refs for I.1 follow-ups (#121/#123/#124/#125/
  #128/#130/#131/#143/#145/#146/#147/#148/#149/#172/#187), I.2
  (#139/#142/#154/#175), I.3 DN-4 (#126/#132/#133/#138/#140/#152/
  #159/#161/#162/#167).
- Track 2.3 (channel-earliness audit) → DONE 2026-05-23 (#181); removed
  from "What actually remains".
- Footer subsection: standalone work (#191 satellite-arch / #189
  guard-liveness / #173 storm-readiness / #176 §10 port / #182 cleanup),
  Wave 10 follow-ups (#110/#112/#113), plan-revision history (#108/
  #109/#153/#155/#157/#160/#164/#165/#168/#179/#185).

Verification:
- `bash .claude/skills/meta-orchestrator/helpers/plan-currency-check.sh
  | grep -c '^UNTRACKED'` → 0 (down from 88).
- `npx markdownlint-cli docs/meta-factory/wave-sequencing-plan.md`
  → no violations.
- `npx vitest run packages/core/skills/plan-currency-check.test.ts`
  → 14/14 passed.

Prior-art: skipped — chore, doc reconciliation only, no new capability
or rule introduced.
artyhoo added a commit that referenced this pull request Jul 10, 2026
…rix + CI firing (python-backend-v0 S1) (#960)

* feat(backends): astgrep Python YAML renderer — syntax-class routing + 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).

* feat(backends): astgrep firing harness — fixtures + shared JSON-array 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).

* ci(backends): astgrep live-fire via pinned PATH install — bare-binary 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).

* ci(backends): suppress zizmor adhoc-packages on the pinned ast-grep global 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

---------

Co-authored-by: t <t@t.co>
artyhoo added a commit that referenced this pull request Jul 11, 2026
…live-fired partial matrix (python-backend-v0 S2) (#961)

* feat(backends): ruff flake8-tidy-imports fast-path renderer (TID251/TID253) — S2 Task 1

Add packages/core/backends/ruff/render-ruff.ts: a narrow fast-path backend that
emits a ruff.toml from the SAME ConventionNodes the ast-grep backend renders, for
the 2 import/qualified-name bans inside ruff's closed flake8-tidy-imports vocabulary.

Params contract is shared with the astgrep backend (kind/pattern/replacement on the
frozen IR node):
  - kind 'attribute' (qualified-name / attribute chain) -> TID251 banned-api (.msg = node.claim)
  - kind 'import'    ('import <mod>')                    -> TID253 banned-module-level-imports
  - kind 'call'      (call-with-args, e.g. `a.b($$$ARGS)`) -> refused FF7001 (out of fast-path;
    ruff bans a qualified NAME not a call site — ast-grep #212 is the catch-all)

Refusal split (cargo/astgrep semantics):
  - selectorClass type-aware / dep-graph -> FF7001 (mypy / import-linter deferred)
  - malformed / off-contract params       -> FF7002
  - non-error severity                     -> degraded FF7003, entry STILL emitted
    (ruff has no per-rule severity — FIRST Python-lane backend to exercise FF7003,
    "nothing silent" per spec §4). error is ruff's fixed native level -> rendered.

The narrowness is load-bearing: P2 census measured a 90.9%/100% native-ruff drop
(NO-GO for a native-ruff default; Python enters via the ast-grep escape hatch),
so this backend is a fast-path only, not the Python default. The kind:'call'
FF7001 refusal enforces that boundary (the astgrep FIXTURE_NODE refuses here).

Live-verified against ruff 0.15.21 (uvx): rendered golden ruff.toml validates +
fires (TID253 + TID251, exit 1 on findings, clean on conformant). Reuses the shared
RenderOutcome contract + assertEveryNodeResolved; pure function, zero fs/network.

Adds SSOT row #215 (ADOPT, narrow) in the same commit, citing #212 (S1 astgrep
lineage) + the P2 census NO-GO (:363-378) as the narrowness rationale.

TDD: render-ruff.test.ts (24 cases, RED-first) covers routing, both goldens
byte-for-byte, the FF7003 degrade path, and mapping/escaping/sorting details.

test:backends 132 green; test:principles 303 green; typecheck + format:check clean.

Prior-art: prior-art-evaluations.md#215 (ruff TID fast-path, ADOPT narrow — new capability artifact packages/core/backends/ruff/render-ruff.ts; lineage #212 ast-grep Python default, narrowness per P2 census NO-GO research-patches/2026-07-02-multi-toolchain-generalization.md:363-378).

* feat(backends): ruff firing harness + capability matrix + CI wiring — S2 Task 2

Live-fire the ruff-tidy-imports-toml backend (Task 1 renderer) against committed
fixtures, and gate its honesty matrix.

- fixtures/firing/{invalid,valid,valid-clean}: each carries src.py + a ruff.toml
  that is byte-identical to render([RUFF_TID251_NODE, RUFF_TID253_NODE]) (T15 drift
  gate). invalid violates BOTH TID251 (banned `requests`) + TID253 (module-level
  `import torch`); valid suppresses both via `# noqa`; valid-clean is conforming (zero).
- firing-contract.json: `ruff check --output-format=json`, identity `$.code`,
  expectedCodes [TID251, TID253] (the flake8-tidy-imports family fires two codes).
- firing-runner.ts: bare-PATH `ruff` model (S1 reconciled decision — PATH binary +
  hard CI install, NOT a uvx/pip-run pin in the command). fireContract scopes via
  `--config <abs ruff.toml> <abs src.py>` (overrides ruff's ancestor-config discovery).
  deriveToolVersion re-runs `ruff --version`; REUSES shared/json-array-parse.ts (ruff is
  its second consumer by design).
- firing.test.ts: presence-check loud-skip when ruff absent (NO !isCI guard), live
  RED (invalid -> both codes) / GREEN (valid, valid-clean -> none) + always-on drift test.
- capability-matrix.json + test: syntax=partial (only the TID-shaped slice fires; kind
  'call' refuses FF7001 — caps documents it), type-aware + dep-graph=no FF7001. Evidence
  is a real ruff 0.15.21 TID251 diagnostic; freshness derives the resolving version live.
- render-ruff.ts: dedup TID253 module bans + TID251 banned-api keys (duplicate keys would
  emit invalid TOML) — makes the "deduped-order-stable" comment true; unit tests added.
- audit-self.yml: pinned `pip install ruff==0.15.21` before test:backends (ci-tool-pinning
  Rule A); zizmor 1.26.1 clean (bare pinned pip install not flagged, no suppression needed).

Gates: test:backends 154 green (ruff live-fire actually fires locally via a PATH shim to
the uvx-resolved 0.15.21 binary); test:principles 303 green; tsc clean; format:check clean;
zizmor clean.

Prior-art: prior-art-evaluations.md#215 (ruff TID fast-path, ADOPT narrow) — S2 firing harness + matrix reuse the S1 astgrep pattern (#212 lineage) and the shared json-array parser; no new capability class beyond the Task-1 renderer this exercises.

---------

Co-authored-by: t <t@t.co>
artyhoo added a commit that referenced this pull request Jul 11, 2026
…ive, umbrella closure (python-backend-v0 S3) (#962)

* feat(composition): wire the Python lane (astgrep+ruff) into the demo enforcement line (S3)

python-backend-v0 S3 (composition surface-3 + closure prep). The flagship demo's
`buildDemoRenderFacts()` now feeds both demo nodes to ALL FOUR shipped backends
(cargo, npm, astgrep #212, ruff #215). The Python lane enters as TWO enforcement-line
segments — the enforcement-line mechanism (enforcement-line.ts:72-104) emits one segment
per registered backend; there is no combined "Python segment" by construction. Both demo
nodes are non-Python, so the Python backends honestly refuse: FF7001 (capability-class gap
→ mypy deferred) on the type-aware cargo node, FF7002 (params-contract gap) on the
JS-selector npm node.

Changes:
- root-agents-demo.ts: import renderAstgrep/renderRuff; ASTGREP_BACKEND/RUFF_BACKEND consts;
  register all four backends in outcomesByBackend + matricesByBackend; header docstring
  updated to the 4-backend reality + the FF8003 latent-incoherence design caveat.
- root-agents-demo.test.ts: regenerated GOLDEN_{CARGO,NPM}_NODE_ENFORCED (recomputed from
  buildDemoRegion, not hand-written); NEW T-PY-D pin test — both Enforced lines .toContain(' · ')
  and carry no ", " at a segment boundary (pins the #905 A12 separator on the demo's own
  4-segment goldens) + asserts both Python segments present.
- AGENTS.md: regenerated the two `> Enforced:` lines (byte-ratchet green).
- backends/shared/render-outcome.ts: doc-note on RenderedSurface.content's per-backend
  divergence (cargo table name / npm eslint JSON / astgrep+ruff = node id token) — intentional
  and inert (no code path compares content across backends). Comment only, no behavior change.

Design finding (surfaced, not fixed — STOP-lined for this umbrella): running
runCompositionGate over the demo would fire FF8003 on astgrep/ruff × the syntax node
(both carry a live-fired syntax cell yet refuse the JS-params node; FF8003 is
selectorClass-level, not node-params-aware). No test runs the gate over the demo (parity
with MT S4 #903 — demo validated only by the byte-ratchet), so no test breaks; kickoff's
must-stay-green list (FF8002, FF8004) is unaffected — the gate is untouched. Owner follow-up:
node-params-aware FF8003 or disjoint-class demo nodes.

README widening is maintainer-owned (Artifact Ownership Contract) — prepared draft diff at
scratchpad/readme-draft-diff.patch (git apply --check OK), README not edited here.

### §1.7 Forward-check
Complies with: no-paid-llm-in-ci.md (zero API calls — pure TS + vitest); doc-authority-hierarchy
(no canonical-doc authority change); build-first-reuse-default (REUSE — wires already-ADOPTed
backends #212/#215, zero new capability, no new dependency/module); dual-implementation (n/a —
core TS). Gate + capability matrices untouched; the four principle/composition/backends suites
stay green (45/154/303).

### §1.7 Backward-check
Class of this change = "add a backend to a shipped-backend enumeration surface". Surfaces where
class-X occurs (grep: real multi-backend shipped-set maps): the SOLE enumeration surface is
composition/demo/root-agents-demo.ts:125,131 (outcomesByBackend + matricesByBackend) —
SWEPT-CLEAN (both maps now carry all 4 backends; goldens + AGENTS.md region regenerated to match).
Other grep hits (backends/cargo/render-clippy.ts, backends/npm/from-node.ts + their tests,
backends/shared/toolchain-backend.ts, backends/npm/grammar-gate-in-flow.test.ts) are single-backend
renderers / the generic interface, not shipped-set enumerations → no GAP. Supersedes nothing.

Prior-art: prior-art-evaluations.md#212 (ast-grep as Python rule-emission target, ADOPT) — S3 wires the already-ADOPTed astgrep backend into the composition demo; no new capability.
Prior-art: prior-art-evaluations.md#215 (ruff TID fast-path, ADOPT narrow) — S3 wires the already-ADOPTed ruff backend into the composition demo; no new capability.

* feat(composition): astgrep demo node fires in its own Time-handling section (S3 review fix)

Review fix (task reviewer MAJOR, orchestrator-ratified): Python entered the flagship demo as a
pure refuser, contradicting the MT S4 precedent (each backend entered WITH a node it fires on)
and the umbrella thesis (astgrep = PRIMARY render target, live-fired). Fix: the shipped astgrep
FIXTURE_NODE (`no-datetime-now`, syntax class, kind:'call' — the P5 live-fired case) becomes the
THIRD demo node in its own "time-handling" DocPlan section, mirroring how cargo's node entered
in S4. Per-backend outcomes on the new node (all computed, never hand-written):
astgrep ✅ (syntax live-fired) · cargo — FF7001 (syntax class) · npm — FF7002 (params contract:
no JS selector) · ruff — FF7001 (call-with-args ban outside the TID fast-path).

Changes:
- fixtures/root-agents-demo.docplan.json: second section "time-handling" with no-datetime-now
  (FF8002 stays green — the node is placed in a section; FF8004 stays green — the astgrep
  syntax cell is status:'yes' + live-fired, not 'no').
- root-agents-demo.ts: astgrepDemoNode() (the shipped astgrep FIXTURE_NODE, aliased import);
  DEMO_TIME_SECTION_ID export; buildDemoRenderFacts() feeds all THREE nodes to all FOUR
  backends; buildDemoRegion(sectionId = DEMO_SECTION_ID) generalized to serve both regions;
  header per-node outcome map rewritten; latent-FF8003 caveat arithmetic updated (2 → 4
  would-be diagnostics: astgrep+ruff × the JS-params node, npm+ruff × the Python-params node —
  reviewer-confirmed no clean→dirty threshold crossed, the gate is still not run on the demo).
- root-agents-demo.test.ts: GOLDEN_ASTGREP_NODE_ENFORCED added (computed); byte-ratchet +
  explicit-toBe extended to BOTH regions via regionsMatch/findRegions; T-PY-D pin now covers
  all 3 Enforced lines (` · ` present, no `, ` at segment boundaries, both Python segments on
  every line) + asserts exactly ONE `astgrep-python-yaml ✅` segment (the primary target FIRES
  in the demo); mirrored cargo-pattern test for the astgrep node (syntax class, id, kind:'call',
  backed by the CI-gated live-fired matrix cell).
- AGENTS.md: new "## Time handling" fenced region (section=time-handling, same plan path),
  byte-equal to buildDemoRegion(DEMO_TIME_SECTION_ID); markdownlint clean (0 errors).

README stays untouched (maintainer-owned); the prepared draft diff at
scratchpad/readme-draft-diff.patch was updated to say the astgrep segment renders and fires ✅
live in AGENTS.md (git apply --check OK).

### §1.7 Forward-check
Complies with: no-paid-llm-in-ci.md (pure TS + vitest, zero API calls); doc-authority-hierarchy
(AGENTS.md region is generated machinery, not a canonical-authority change);
build-first-reuse-default (REUSE — re-uses the shipped astgrep FIXTURE_NODE #212 verbatim, zero
new capability); ai-laziness-traps T-PY-D (separator pin extended to the new line). Gate +
matrices + backends untouched; suites green (composition 46 / backends 154 / principles 303).

### §1.7 Backward-check
Class of this change = "demo section/node addition + goldens/region regen". Surfaces where
class-X occurs: (1) fixtures/root-agents-demo.docplan.json — SWEPT (new section added,
schema-conformant); (2) demo builder root-agents-demo.ts — SWEPT (nodes list + section builder
generalized); (3) demo ratchet root-agents-demo.test.ts — SWEPT (both regions byte-gated, 3
goldens); (4) committed AGENTS.md regions — SWEPT (time-handling region added byte-equal);
(5) sibling per-backend test-fixtures (cargo/astgrep FIXTURE_NODE sources) — SWEPT-CLEAN
(consumed verbatim, unmodified: git diff shows no backends/** change); (6) composition
gate/enforcement-line — SWEPT-CLEAN (untouched; gate paired-negative suite unchanged, 46 green).
Supersedes nothing.

Prior-art: prior-art-evaluations.md#212 (ast-grep as Python rule-emission target, ADOPT) — the demo now exercises the ADOPTed backend's own live-fired fixture node; no new capability.
Prior-art: prior-art-evaluations.md#215 (ruff TID fast-path, ADOPT narrow) — the new node demonstrates the documented fast-path boundary (kind:'call' refuses FF7001); no new capability.

---------

Co-authored-by: t <t@t.co>
artyhoo added a commit that referenced this pull request Jul 11, 2026
…yte-drift gate, pure-bash delivery layer (python-delivery-v0) (#991)

* docs(orchestrator): python-delivery-v0 meta-launch kickoff (launch-table + stage gates)

Generated by /pipeline python-delivery-v0 (plan-currency: kickoff on staging #967,
no in-flight S1, umbrella open). Orchestration record for the S1-S3 dispatch.

Prior-art: skipped — orchestration bookkeeping doc, no new capability

* feat(python-starter): curated Python starter ConventionNode[] set (S1 Task 3)

Adds packages/core/python-starter/starter-nodes.ts — a curated Python
starter node set authored in the frozen neutral IR (ir/types.ts, no field
added) and rendered through the existing ADOPTED backends: renderAstgrep
(#212, syntax-class default) and renderRuff (#215, TID251/TID253
fast-path). Colocated vitest unit tests (RED-first) assert: grammar-gate
pass, getff-* id namespacing (probe-proven: duplicate ids = ast-grep
exit 8), render-through-both-backends without error, the probe-proven
firing shape, and the datetime caveat — the rendered YAML covers BOTH
`datetime.now()` and `datetime.datetime.now()` via two literal-pattern
nodes (a single pattern cannot match both; a `$MOD.now()` metavariable
over-fires on `foo.now()` — live-verified against @ast-grep/cli@0.44.1).

Set content (all severity error): ban datetime.now()/
datetime.datetime.now() calls (flagship P5), ban eval()/os.system()
calls (bandit ADOPT-VOCABULARY; ruff refuses FF7001 — ast-grep
catch-all), ban datetime.datetime.utcnow (TID251), ban module-level
import tensorflow (TID253).

SSOT row 217 appended to docs/meta-factory/prior-art-evaluations.md in
this same commit (register tail verified: last row was 215; 216 is
reserved for the delivery-layer task). vitest.config.ts registers the
new python-starter/ include glob.

Verified: python-starter 12/12, test:backends 154/154, test:principles
303/303, tsc --noEmit clean.

Prior-art: prior-art-evaluations.md#217 (curated Python starter node set — BUILD own content in the neutral IR; ruff has no canonical "recommended" starter preset, bandit's catalog is engine-coupled with no neutral-IR export, Factory plugin #4 is JS/ESLint-coupled — ADOPT-VOCABULARY the ban semantics, author the set ourselves).

* feat(python-delivery): commit-time render script + pre-rendered templates + byte-drift gate (S1 T4)

Render the curated Python starter node set (packages/core/python-starter/starter-nodes.ts,
#217) through the pure ADOPTED backends into static shipped templates under
packages/core/templates/python/ (Model A). The renderers stay pure; all fs writes live in
packages/core/python-starter/render-python-templates.ts (planPythonTemplates() is pure and is
what the drift gate imports).

Single-owner lane per rule (coordinator Decision #5), partitioned by node kind — a rule ships
in exactly ONE lane so a consumer never gets a duplicate report:
  - ast-grep lane (call-kind): getff-no-datetime-now, getff-no-datetime-datetime-now,
    getff-no-eval, getff-no-os-system — one single-doc YAML file per rule under
    .getff/astgrep-rules/ (idiomatic ast-grep layout; a multi-doc stream fails the repo's own
    single-document pre-commit yaml.safe_load gate — empirically verified).
  - ruff lane (attribute/import-kind): getff-no-utcnow (TID251), getff-no-tensorflow-module-import
    (TID253) — one ruff.toml for the whole lane.

Our own sgconfig.yml template (Decision #4, no `ast-grep new` shellout) references the rules dir
from the consumer's perspective (ruleDirs: [.getff/astgrep-rules]). Every generated file carries a
framework-generated header comment.

Byte-drift gate: packages/core/backends/python-templates-drift.test.ts asserts committed templates
== fresh render, byte-for-byte, at the pre-push channel (test:backends / hooks/pre-push.ts §5c).
Idempotent: re-running the render script produces zero diff. Live-fired end-to-end against the
pinned @ast-grep/cli@0.44.1 + ruff==0.15.21 on the exact consumer tree layout (both lanes fire;
no rule double-reports).

Templates land under a NEW packages/core/templates/python/ dir NOT in install.sh's enumerated
copy-list — no install-fingerprint / shipped-doc / principle-test surface is touched.

Prior-art: prior-art-evaluations.md#212 (ast-grep render backend, ADOPT) and prior-art-evaluations.md#215 (ruff fast-path render backend, ADOPT) — this commit renders the #217 curated node set through the already-ADOPTED backends; no new render target, the emission verdicts stand.

* feat(python-delivery): pure-bash delivery layer + augment-first collision policy + tmp-dir tests (S1 T5)

setup.d/45-python.sh ships the pre-rendered Python lint bundle (ast-grep rules +
sgconfig.yml + ruff config, S1 Task 4) into a consumer Python repo with an augment-first
collision policy — merge into what the consumer authored, never silently clobber. INERT
on the npm flow (gated on GETFF_TOOLCHAIN=python; S2 wires the ./setup python entry) —
install.sh:564 auto-sources it as a guarded no-op, so byte-identical.test.sh stays 8/8.

Collision matrix (probe-decided, .superpowers/sdd/task-2-report.md):
- fresh dir             -> whole-file copy
- sgconfig.yml exists   -> structural merge into the block-list ruleDirs (idempotent);
                           unproven shapes (flow-list / multi-key) REFUSE-LOUDLY (a naive
                           second ruleDirs: key = duplicate-field exit 8; a clever-but-
                           unsafe merge is a STOP-line violation)
- ruff.toml exists      -> REFUSE (a sibling ruff.toml wins entirely + silently disables
                           theirs); ship getff-ruff.toml + `extend` instructions (scalar
                           caveat surfaced if they already use extend)
- pyproject [tool.ruff] -> REFUSE (a sibling ruff.toml SILENTLY overrides [tool.ruff]);
                           ship getff-ruff.toml + [tool.ruff.lint] merge steps
- re-run                -> delivered config artefacts byte-identical (idempotent)
Every degrade path is printed AND appended to <consumer>/.getff-python-install.log. A
pre-existing .prettierignore gets `.getff/` appended idempotently; none is created.

tests/install-sh/python-delivery.test.sh covers every cell + npm-inertness/activation as
first-class fixtures (RED-before-GREEN on the merge/refuse logic), plus GATED live-fire:
all 4 shipped ast-grep rules + both ruff TID bans fire RED on planted violations (pinned
@ast-grep/cli@0.44.1 + ruff==0.15.21; SKIP when unobtainable, like cargo firing.test.ts).
Wired into audit-self.yml (meta-all-wired). refresh-covers-full-delivery.test.sh made
lane-aware — the Python lane is inert on npm and has its own S2 refresh semantics, out of
the npm-refresh-parity gate's population (same rationale as its skill/merge/yq exclusions).

SSOT row 216 appended (delivery-mechanism BUILD verdict).

Prior-art: prior-art-evaluations.md#216 (thin pure-bash delivery writer — BUILD; no headless tool copies a pre-rendered multi-tool lint bundle into an existing repo with augment-first collision. ADAPT the skip/version-stamp concepts from #124/#125, REFERENCE the `ast-grep new project` #212/#185 scaffold precedent; reuses install.sh copy_safe/merge machinery, zero new deps).

* fix(python-delivery): sgconfig idempotency survives trailing-comment edits + .ruff.toml fixture + empty-array guard

Review-round-1 findings addressed (commit 9e9b5bac9, setup.d/45-python.sh + tests):
- Important: _py_sgconfig_merge's idempotency grep required an exact-match line, so a
  consumer-added trailing comment on our entry (`- .getff/astgrep-rules  # our rules`)
  was not recognised as already-present — a re-run inserted a DUPLICATE entry, tripping
  ast-grep exit 8 on the consumer's next scan. Fixed by stripping trailing #-comments
  before the compare (same shape as the existing rest="${rest%%#*}" ruleDirs: value
  check two lines away), bash 3.2 compatible. TDD: added a failing cell-(ii) fixture
  first (RED: 2 entries instead of 1), watched it fail against the unfixed code, then
  fixed and watched it pass.
- Important: setup.d/45-python.sh:153's `.ruff.toml` (dotfile) collision branch had no
  fixture. Added cell-(iii-dotfile): consumer has .ruff.toml only → layer REFUSEs
  loudly, ships getff-ruff.toml, prints extend instructions, logs the refusal. Passed
  immediately (branch already existed and worked).
- Minor: tests/install-sh/refresh-covers-full-delivery.test.sh's NPM_LANE_LAYERS array
  expansion under `set -u` would crash on bash 3.2 if the array were ever empty. Added
  the repo-precedent length guard (setup.d/lib.sh:281-283 shape) before expansion.

Prior-art: skipped — review-fix round, no new capability

---------

Co-authored-by: t <t@t.co>
artyhoo added a commit that referenced this pull request Jul 13, 2026
…e shared ConventionNode core (#1005)

* feat(live-generation): S1 Inc1 — neutral live-research → ConventionNode bridge + python Tier-0 keys

The one shared BUILD of the live-generation umbrella (research-patch §0 Plane 3): a
thin, stack-neutral projection «a researched practice → a ConventionNode (frozen IR)»
that python (this stage) and rust (LG-S3) both reuse. It authors ONE neutral node from
ONE researched practice and hands it to the existing pure renderAstgrep backend — it is
NOT a pipeline, does not render to disk, and touches no network.

Bridge (packages/core/synthesizer/research-to-node.ts):
  - Defines its OWN input interface (AstgrepResearchedPractice — GenerateCandidate-shaped
    + the frozen-IR `kind:'call'|'attribute'|'import'` discriminator the backend requires;
    no existing type carries {kind,pattern}). Only the OUTPUT ConventionNode is frozen — no
    IR field added.
  - Two honesty lines: (1) MAJOR-1 degrade-not-inert (§Qb) — a practice that does not reduce
    to a single literal call/attribute/import ban is dropped to a research-only finding BEFORE
    node construction, never an inert node; (2) the bridge validates provenance ITSELF
    (validateProvenance, Tier-0 default; tiered when a ResolveCtx is supplied) — the grammar
    gate does not check the provenance host, so an untrusted source is a research-only finding,
    never a trusted node.

Python Tier-0 allowlist keys (packages/core/research/allowlist.ts — DATA, the
`#allowlist-as-code-not-data` discipline, parallel to how react-native/expo were added):
  - python.official → docs.python.org, peps.python.org
  - pyyaml → pyyaml.org (the getff-no-yaml-load flagship's security-guidance source)

Tests (research-to-node.test.ts, all committed fixtures — $0, never a live MCP call):
  AC1 getff-no-yaml-load → valid node (grammar gate passes + provenance resolves);
  AC2 node → renderAstgrep yields rule.pattern "yaml.load($$$ARGS)";
  AC3 mutable-default-arg / bare-except → research-only, no node/rule (non-vacuous: with the
      MAJOR-1 filter disabled the inexpressible practice emits an inert node → the assertion
      goes RED — RED-before-GREEN verified);
  AC4 expressible practice with a non-Tier-0 host → host-tier reject (FF2006), NOT an
      unknown-key/schema failure; no-provenance is fail-closed.

Suites green: test:principles 311, test:backends 171, test:composition 46, research 175,
to-node byte-locks 19, bridge 10; tsc --noEmit clean.

Prior-art: prior-art-evaluations.md#219 (stack-agnostic autogeneration core — BUILD thin, composed over shipped IR + backends + research/trust core; no upstream neutral-multi-backend generator, ast-grep/ruff/antfu confirmed negative 2026-07-11) and prior-art-evaluations.md#218 (LintConfig ADOPT-VOCABULARY coverage-classification + REFERENCE doc->config precedent).
Prior-art: prior-art-evaluations.md#183 (rule-research bridge, the shipped JS reference adapter this umbrella generalizes to the neutral ConventionNode plane) and #212/#215 (astgrep/ruff render targets, ADOPT — reused verbatim for the python adapter).

* fix(live-generation): S1 Inc1 review — drop untested tiered provenance seam + cover gate-failed + kind-drift guard

Review round 1 on commit 91d5332 (the neutral research→ConventionNode bridge):

FIX 1 (drop broken/untested tiered seam): remove ResolveCtx/ResearchToNodeOptions
param+type, the resolveCtx provenance branch, and the tiered
validateProvenanceTiered/resolveAllowedSources imports it used. The Tier-1 path was
structurally unreachable (no package field on AstgrepResearchedPractice) AND had zero
test coverage — YAGNI + a T20 unverified-code-path risk. The bridge keeps ONLY the
Tier-0 validateProvenance path the ACs cover; the Tier-1 python adapter is DEFERRED to
LG-S4/rust per research-patch §Qc, where it lands with a package field + Tier-1-reachable
tests. Doc-comments corrected: the tiered-seam mentions removed; with allowlist-resolver.ts
no longer directly imported the "same purity contract as the renderers" claim is now accurate.

FIX 2 (cover the third degrade path): add gate-failed coverage — a degenerate-pair
practice (expressible + valid Tier-0 provenance, but examples.bad === examples.good) trips
the grammar gate's FF6001 on the built node, asserting status research-only + reason
gate-failed. A non-vacuity companion test shows the same practice with distinct examples
becomes a node, isolating the degenerate pair as the cause.

FIX 3 (kind-drift guard, test-only): export VALID_KINDS from render-astgrep.ts (clean
export, no behavior change) and assert EXPRESSIBLE_KINDS set-equals it, so a future kind
added to the bridge but not the renderer (node builds then FF7002-refuses) fails at
assert time.

research-to-node.test.ts: 10 -> 13 tests. test:principles/backends/composition + tsc green.

Prior-art: skipped — INC-1 review fixes (drop unused/broken tiered seam + add gate-failed + drift coverage), no new capability

* feat(live-generation): S1 Inc2 — python Model A′ render driver + committed getff-researched-no-yaml-load artifact + firing proof

The python adapter's author + session-side render (Model A′) half. A committed
researched-practice record for the flagship getff-researched-no-yaml-load rule is
routed through the INC-1 bridge (researchedPracticeToNode) → renderAstgrep by a new
session-side driver, and BOTH the input record and the rendered ast-grep artifact are
committed (Model A′ pre-render, §Qa — the consumer install path stays Node-free).

- synthesizer/render-researched-astgrep.ts — the driver: pure planResearchedAstgrep /
  planFromCommittedRecords (compose bridge + pure renderer), impure writeResearchedAstgrep
  + a --check drift CLI. Renderers stay pure; all fs writes live in the driver (mirrors
  render-python-templates.ts plan-vs-write and cargo write-clippy.ts vs render-clippy.ts).
- synthesizer/fixtures/live-generation/ — the committed record (.practice.json, real Tier-0
  `pyyaml` provenance from INC-1), the rendered artifact (firing/rules/*.yml), and RED/CLEAN
  firing fixtures sharing one ../rules dir so the SAME committed bytes are drift-gated + fired.
- backends/astgrep/live-generation-research.test.ts — AC1 record→node, AC2 Model-A byte-drift
  gate (committed === renderAstgrep([node]), with a paired-negative teeth check), AC3 live-fire
  via the EXISTING firing-runner harness against pinned ast-grep@0.44.1 (RED on yaml.load / clean
  on yaml.safe_load), AC4 MAJOR-1 degrade-not-inert through the driver. Home = backends/ so CI's
  test:backends fires it with the installed pinned binary.
- rule id namespaced getff-researched-* (§Qd) so a delivered researched rule never collides with a
  starter getff-*. call-kind → ast-grep lane ONLY (no ruff render, python-delivery-v0 decision #5).

STOP-lines honored: frozen IR (no new ConventionNode field); render-astgrep.ts behavior untouched;
NO render-from-research in CI (CI only drift-checks + fires the committed artifact — $0, principle 17
/ no-paid-llm-in-ci). Delivery to a scratch consumer + $0 e2e RED is INC-3, out of scope here.

Prior-art: prior-art-evaluations.md#219 (stack-agnostic autogeneration core — this is the "session-side render + firing artifact" residue: committed researched practice → INC-1 bridge → renderAstgrep → committed ast-grep artifact; #219 landed in INC-1, cited not re-added) and #212 (ast-grep render target, ADOPT — reused verbatim).

* fix(live-generation): S1 Inc2 review — real drift-teeth test + dup-entryId loud guard + docstring

Replace the vacuous String.replace/not.toBe 'teeth' test (never called any drift code — attention-is-not-a-mechanism/#discipline-theatre) with two real ones: mutate the committed artifact on disk, run checkResearchedAstgrepDrift(), assert byte-mismatch / missing, restore verbatim bytes in finally (tree left byte-identical). Add a loud dup-entryId/path guard to planResearchedAstgrep (writeResearchedAstgrep would else silently clobber the first), mirroring planPythonTemplates' lane guard + a covering test. Fix planFromCommittedRecords JSDoc PURE -> PURE (read-only fs).

Prior-art: skipped — INC-2 review fixes (real drift-teeth test + dup-entryId loud guard + docstring), no new capability

* feat(live-generation): S1 Inc3 — deliver researched rule to a scratch consumer + flagship RED proof

The LG-S1 headline "live generation WORKS for python": the INC-2 committed researched
rule (getff-researched-no-yaml-load) is delivered to a FRESH mkdtemp consumer through the
python-delivery-v0 setup.d/45-python.sh augment-first seam — reused VERBATIM via its
PY_TEMPLATE_DIR env override (zero seam edit) — and then FIRES RED via the pinned
@ast-grep/cli@0.44.1: reports the ruleId + exit 1 on a planted yaml.load(raw), CLEAN
(exit 0, zero findings) on yaml.safe_load(raw).

- AC1 (always-on): the researched rule joins the consumer's single ruleDirs
  [.getff/astgrep-rules] additively, byte-identical to the INC-2 artifact; second run
  idempotent; STOP-line — the rule is NEVER baked into the shipped templates/python
  starter set (Model A′ per-consumer, §Qe genuinely-researched-not-starter).
- AC2 (skipIf(!toolPresent)): fireContract (firing-runner.ts, NOT the npx arm) asserts
  the delivered rule fires RED; a non-vacuity test delivers WITHOUT it and fires the same
  fixture → not reported (the RED is delivery-gated, not ambient).
- AC3 $0: committed fixtures only, no paid MCP/LLM; lands in test:backends where
  audit-self.yml installs the free pinned ast-grep binary.
- AC4: 11 install baselines byte-identical (PY_TEMPLATE_DIR delivery touches nothing
  shipped) — no regen.

Home backends/astgrep/ mirrors the INC-2 sibling live-generation-research.test.ts:
test:backends is the only CI suite with the pinned ast-grep on PATH.

Prior-art: skipped — test-only: exercises the existing python-delivery-v0 seam (setup.d/45-python.sh, unedited) + the existing astgrep firing-runner harness + the committed INC-2 artifact; no new capability/dependency/module (flagged only by the >=80-LOC new-file heuristic).

* docs(live-generation): S1 census — expressibility research-patch (OWNER-FORK-1 data)

Records the LG-S1 sub-deliverable 7 census: 15 real python conventions classified by frozen-IR expressibility with real ast-grep@0.44.1 firing (flat 40% / flat+relational 93.3%; require-via-ban 3/3 positives). DATA-only OWNER-FORK-1 record; the unfreeze decision is a separate MT-plane umbrella.

Prior-art: skipped — docs-only OWNER-FORK-1 census data (research-patch), no new capability/dependency/module

* chore(live-generation): S1 — regen synth-and-wire bundle for the python Tier-0 allowlist keys

Byte-consequence of INC-1's allowlist.ts python Tier-0 keys (research/load.ts imports allowlist.ts → bundled transitively via synth-and-wire). The increments touched allowlist.ts but did not rebuild the bundle; pre-push synth-bundle drift gate caught it. Diff is exactly python.official + pyyaml, --check now in-sync.

Prior-art: skipped — build-artifact regen (byte-derived from the reviewed allowlist.ts change), no new capability/dependency/module

---------

Co-authored-by: t <t@t.co>
artyhoo added a commit that referenced this pull request Jul 21, 2026
…iring test + ruff FF7001 refuse (#1079)

* feat(astgrep): render relational tree as ast-grep YAML sibling arm (OWNER-FORK-1 S2)

Insertion point: render-astgrep.ts renderRule() (:26 known-bound). RuleEntry gains
optional `relational?: RelationalRule`, populated from n.relational in the render
loop. renderRule() still emits `rule:` + the mandatory `pattern: <dq(pattern)>` line
FIRST (the atomic positive matcher — proven live @ast-grep 0.44.1: bare
has/not/all/any parse-error, exit 8, "Rule must have one positive matcher"; the
RelationalRule union has no atomic-positive leaf), then appends the translated arm
as a SIBLING key under `rule:` only when relational is present.

renderRelationalNode translation (fixed key order, byte-lock determinism):
- has  -> `has:` then (kind first, if present) `pattern:`, then `stopBy: end`
  (ALWAYS emitted — a backend-render decision, not an IR-shape change: ir/types.ts:51
  declares `has` as descendant search, but ast-grep's `has` defaults to
  direct-child-only without stopBy; live-verified `has:{pattern:'return $V'}` (no
  stopBy) against a body 2 levels deep finds ZERO matches, the same rule with
  stopBy:end finds it).
- not (1 child)  -> `not:` + nested render(child), 1:1 native not:{has:...}.
- not (N children) -> `not:{any:[...]}`. DECISION: NOR(c1..cn) = not(c1 or ... or cn)
  = not-any, so the implicit combinator over multiple `not` children is `any`, NEVER
  `all` (`not:{all:[...]}` means "at least one fails" - a different, wrong semantics).
  Both forms parse live @0.44.1; only not:{any} matches the declared NOR — verified
  with a 3-function fixture (only the function with NEITHER a `return` NOR a `raise`
  statement flags under not:{any}; not:{all} would have flagged a different set).
- all -> `all:` block-sequence (conjunction; empirically verified AND semantics —
  only a function with BOTH children present flagged).
- any -> `any:` block-sequence (disjunction; empirically verified OR semantics —
  every function with EITHER child present flagged).

Second backend (ruff, render-ruff.ts): refuses a present node.relational via FF7001,
checked BEFORE the kind fast-path (so an otherwise-bannable attribute/import shape
carrying a relational constraint still refuses). ruff's flake8-tidy-imports
vocabulary has zero relational surface (no has/not/all/any) — a relational tree
cannot be partially/lossily expressed, so no FF7003 "rendered-with-loss" path
applies (that degrade is for severity, not structure); this is a hard capability
gap, refused honestly via the exact FF7001 idiom already used for the call-kind
refusal in the same file.

Fixture + firing test (RELATIONAL_FIXTURE_NODE, id `require-return-type-hint`,
census require-via-ban #1). The kickoff's literal example anchor+arm
(pattern:'def $NAME($$$ARGS): $$$BODY' + has:{pattern:'-> $T'}) is LIVE-PROVEN
BROKEN — adopted the advisor-flagged fix instead, per two independently
live-verified defects against ast-grep 0.44.1 (the CI pin):
  1. VACUOUS ARM: that anchor pattern ALONE (arm deleted) already matches ONLY the
     un-annotated function — ast-grep pattern matching requires exact structural-
     shape equivalence, so a pattern omitting the return-type child structurally
     fails to match a function that HAS one. Deleting the not:has arm reproduces
     byte-identical exit codes on the two single-function fixtures. Fixed anchor:
     `def $NAME($$$ARGS)$$$TAIL: $$$BODY` ($$$TAIL occupies the optional
     return-type grammar slot) — debug-query verified to match BOTH shapes.
  2. BROKEN LEAF PATTERN: `has:{pattern:'-> $T'}` matches NOTHING at any stopBy
     setting — `-> $T` is not a valid standalone ast-grep fragment (--debug-query
     shows it parses to `unary_operator - ERROR(>) MetaVar`, garbage). Fixed leaf:
     `has:{kind:'type', pattern:'$T'}` (tree-sitter-python's `return_type: type`
     field, --debug-query=ast verified) + the stopBy:end fix above.
Both fixes verified together: BAD fixture (no `-> int`) -> exit 1, ruleId
`require-return-type-hint` present; GOOD fixture (`-> int`) -> exit 0, zero
findings. An additional arm-deleted control (relational-firing.test.ts) proves the
arm is load-bearing on a MIXED file (not just the single-function fixtures): WITH
the arm -> 1 finding (only the un-annotated def); WITHOUT it -> 2 findings (both).

HARD CONSTRAINTS held: node.relational is read-only (S1 owns ir/types.ts — 0 lines
changed there in this commit); byte-lock synthesizer/to-node.test.ts stays green
(legacy scalar nodes emit zero relational lines — unchanged golden, verified);
render-astgrep.ts does not flow into the synth bundle (grep on
synth-and-wire.bundle.mjs for renderAstgrep/RELATIONAL = 0 hits), so
build-synth-bundle.sh --check / install-sh snapshot compare are not applicable to
this commit; exit codes ARE the firing proof, a green render alone is not; no rust
coverage claimed.

Full packages/core suite: 225 files / 2633 passed / 2 pre-existing skips
(unrelated runtime-bridge-dispatch ZCode cases). tsc --noEmit clean.

Prior-art: prior-art-evaluations.md#212 (ast-grep as the Python rule-EMISSION
target, ADOPT) — this commit extends the SAME artifact (render-astgrep.ts) #212
already covers, adding relational-tree translation using ast-grep's OWN native
has/not/all/any operators (no new tool, no new dependency). The DEDICATED new SSOT
row for the "relational IR = ADAPT of ast-grep operators" verdict (ir/types.ts:35-43's
own framing, distinct from #212's base render-target adoption) stays deferred to S4
per .claude/orchestrator-prompts/ir-unfreeze/kickoff.md line 37 ("S4 — hardening...
the capability-commit Prior-art: trailer + SSOT row") — S1's own commit (e6c2272)
established this S4-deferral for the umbrella. Unlike S1 (all 9 files MODIFIED, no
capability-commit trip), THIS commit adds new files >=80 LOC
(relational-firing.test.ts) and DOES trip prior-art.ts's detectCapabilityReason, so
the escape-hatch form is unavailable here by design: checkTrailerBody treats any
"skipped" trailer as an automatic substance failure (code 2) once a commit is
flagged as capability — a real positive citation is required, hence #212 above.

Co-authored-by: t <t@t.co>

* fix(astgrep): escape RelationalHas.kind in ast-grep YAML — closes injection BLOCKER (ir-unfreeze S2 REVISE)

QUALITY REVISE finding (BLOCKER, render-astgrep.ts:225): renderRelationalNode()'s
'has' arm emitted `kind: ${rule.kind}` completely unescaped, unlike every other
free-form string in this renderer (`pattern:`, `message:`, `id:` all go through
yamlDq()). RelationalHas.kind is schema-typed as a bare `{"type":"string"}`
(ir/convention-node.schema.json) with no charset restriction, so nothing upstream
constrains its content.

Reproduced (BEFORE fix) via a standalone js-yaml round-trip of the reviewer's exact
repro (`kind: "type\n    severity: off"`): the rendered `has:` map parsed back to
`{kind:'type', severity:'off', pattern:'y', stopBy:'end'}` — an injected sibling
key, exactly as the review described.

Fix: wrap `rule.kind` in the existing `yamlDq()` helper (render-astgrep.ts:229),
matching the treatment already given to `pattern` two lines below. Verified
(AFTER fix) with the same js-yaml round-trip: the `has:` map now parses to exactly
`{kind, pattern, stopBy}` (sorted) with no `severity` key — YAML double-quoted-
scalar line-folding keeps the embedded newline inside the SAME logical value,
it does not need separate escaping (same non-escaping treatment `pattern` already
had for this class of input — no new gap introduced).

Test changes:
- New regression test 'has: kind is escaped identically to pattern — ... (BLOCKER
  regression)' in render-astgrep.test.ts: renders a `kind` value containing an
  embedded colon+newline, asserts the byte-for-byte golden, then round-trips the
  output through a REAL js-yaml parse (createRequire idiom, same as
  principles/24-plugin-manifest-integrity.test.ts:39-40) and asserts the `has` map
  has exactly the three intended keys with no injected `severity`.
- Three pre-existing goldens updated for the now-quoted `kind` value: 'has: emits
  kind...', 'not (2 children) -> NOR...', 'P-relational: RELATIONAL_FIXTURE_NODE...'.
- Regenerated (via the actual renderAstgrep() call, not hand-edited) the two
  committed fixture YAMLs fixtures/firing/relational-{invalid,valid}/rules/
  require-return-type-hint.yml so the self-application byte-lock
  (relational-firing.test.ts) still holds.
- Updated the renderRelationalNode doc comment (:198) to state `kind` is
  dq()-escaped, keeping the comment truthful.

STOP-lines re-verified, not just re-asserted:
- ir/types.ts + ir/gates/grammar.ts: `git diff --stat origin/staging..HEAD` on both
  paths is EMPTY — no IR-shape change in this fix (nor in the S2 commit it revises).
- Firing = real, not render==fire: live-invoked the pinned ast-grep 0.44.1 binary
  by hand against both regenerated fixture dirs — relational-invalid exits 1 with
  ruleId `require-return-type-hint` in the JSON findings; relational-valid exits 0
  with an empty JSON array. Matches the pre-fix behaviour exactly (the quoted
  `kind: "type"` is semantically identical to ast-grep, confirmed live).
- Byte-lock: `vitest run packages/core/synthesizer/to-node.test.ts` -> 19/19 pass
  (this file is untouched by the S2 diff; unaffected as expected).
- No rust/cargo file touched, no rust coverage claimed.

Full evidence re-run (real exit codes captured):
- `tsc --noEmit -p packages/core/tsconfig.json` -> exit 0.
- `vitest run packages/core/backends/astgrep packages/core/backends/ruff` ->
  9 files / 118 passed (was 117 before this fix's +1 new test), exit 0.
- `vitest run packages/core/backends/astgrep/relational-firing.test.ts` -> live
  RED/GREEN tests genuinely executed against the pinned binary (22ms/19ms, not
  skipped); self-application (committed-fixture-== render) tests green; arm-deleted
  control (relational arm load-bearing, not vacuous) green.
- Full `packages/core` suite: 225/226 files, 2644/2645 tests pass; the 1 failure
  (principles/11-build-first-reuse-default.test.ts F1) is the documented
  full-suite-load timeout flake (memory reference_vitest_fullsuite_timeout_flake,
  PR #848 precedent) — re-run standalone: 8/8 pass in 22.71s. Unrelated to this
  diff (that test scans capability-commit git history; this diff touches none of
  its inputs).

Scope check: this fix modifies 4 already-tracked files (0 new files, no
package.json edit) — does not trip prior-art.ts's detectCapabilityReason (no new
file >=50/80 LOC, no new dependency).

Prior-art: skipped — bug fix (YAML-injection escaping in an existing renderer), no
new capability surface; the fixed helper (yamlDq) and the file it lives in both
predate this change.

* fix(astgrep): add assertNever default arm to relational switch (ir-unfreeze S2 REVISE)

QUALITY REVISE finding (MAJOR, render-astgrep.ts:223): renderRelationalNode()'s
`switch (rule.op)` over the 4-arm RelationalRule union had no `default` case and
no `assertNever` call, unlike the codebase's own established idiom for the
IDENTICAL union: ir/gates/grammar.ts:56-59 defines `function assertNever(x: never):
never { throw ... }` and its own `switch (rule.op)` (grammar.ts:70-98) ends with
`default: return assertNever(rule);`. ir/types.ts:43's own doc comment documents
this as the expected contract ("switch (rule.op) over the 4 arms is exhaustively
checkable (assertNever in the default), giving the author-time guarantee") — the
astgrep renderer's switch did not match it.

Not a live type-safety hole today (TS compile-time exhaustiveness is preserved
regardless, since the function has an explicit `string[]` return type and no
fall-through code — a 5th union member forces TS2355 at build time even without
assertNever), but a real runtime-robustness regression relative to the sibling
module: a relational op that bypasses the type system (e.g. an unvalidated cast,
or a caller that skips runGrammarGate — synthesizer/render-researched-astgrep.ts's
planResearchedAstgrep already calls renderAstgrep without an intervening grammar-
gate call, so the precedent for an unvalidated-node caller already exists in this
codebase) would fall through silently and implicitly return `undefined`, surfacing
as an opaque `TypeError: ... is not iterable` at the spread call site
(renderRule():184, `lines.push(...renderRelationalNode(...))`) or inside
renderRelationalList — instead of a clear diagnostic.

Fix: added a local `assertNever(x: never): never` helper (mirroring
grammar.ts:56-58 byte-for-byte in intent) plus a `default: return assertNever(rule);`
arm at the end of the switch (render-astgrep.ts:263-264), matching the sibling
module's pattern exactly.

No test changes needed: the current 4 relational ops are already fully covered by
existing goldens; the new default arm is unreachable by construction under a
type-valid tree and only fires as a defensive guard.

Full evidence re-run (real exit codes captured):
- `tsc --noEmit -p packages/core/tsconfig.json` -> exit 0.
- `vitest run packages/core/backends/astgrep packages/core/backends/ruff` ->
  9 files / 118 tests passed, exit 0 (unchanged from pre-fix — this is a
  defensive-only addition, no behavioural delta on the 4 covered ops).
- `vitest run packages/core/backends/astgrep/relational-firing.test.ts` -> 5/5
  passed; re-verified real-fire (not render==fire) by hand-invoking the pinned
  ast-grep 0.44.1 binary directly: relational-invalid/src.py -> exit 1, one
  finding ruleId `require-return-type-hint`; relational-valid/src.py -> exit 0,
  empty JSON array `[]` — byte-for-byte identical to the pre-fix evidence (the
  fix touches only the unreachable-by-construction default branch).
- Byte-lock: `vitest run packages/core/synthesizer/to-node.test.ts` -> 19/19
  pass; this file is untouched by the S2 diff.
- STOP-lines re-verified: `git diff --stat origin/staging...HEAD -- packages/core/ir`
  and `-- packages/core/synthesizer` both EMPTY (no IR-shape change, no synth
  touch); no rust/cargo file in the diff, no rust coverage claimed.

Scope check: this fix modifies 1 already-tracked file (0 new files, no
package.json edit) — does not trip prior-art.ts's detectCapabilityReason.

Prior-art: skipped — defensive exhaustiveness-guard fix mirroring an existing
sibling-module idiom (ir/gates/grammar.ts's assertNever), no new capability
surface.

---------

Co-authored-by: t <t@t.co>
artyhoo added a commit that referenced this pull request Jul 21, 2026
… (honest capability boundary) (#1081)

Adds relational-tree refusal branches to the two backends whose vocabularies have
no relational surface, mirroring S2's ruff FF7001 refuse verbatim. Neither backend
can express a RelationalRule tree, so both refuse honestly and route to ast-grep
(#212) — the OPPOSITE of a coverage claim.

CARGO (render-clippy.ts): new FF7001 guard inserted AFTER the syntax/dep-graph
FF7001 block and BEFORE the "type-aware from here on" comment — crucially BEFORE
isValidParams, so a relational node whose scalar {kind,path} params LOOK bannable is
STILL refused (closes the pre-S3 silent drop where such a node rendered into the
toml, dropping the tree). clippy.toml's disallowed-methods/-types/-macros tables are
a FLAT qualified-name ban list with ZERO relational surface (no has/not/all/any, not
even a selector language); a structural composition cannot be partially/lossily
rendered (FF7003 "rendered-with-loss" is for severity only, not structure). CRITICAL
framing: clippy refusing IS the honesty boundary ("clippy cannot express this") — NO
rust relational-coverage is claimed; the R-patch §3 rust GAP stands untouched.

NPM (from-node.ts): DOUBLE guard because the module has two entry points.
(1) renderNpmDeclarative gains an FF7001 relational guard after the type-aware/
dep-graph block and before the syntax render path (again BEFORE isValidParams, so an
otherwise-renderable {selector,presence} node is still refused — closes today's
silent drop). (2) the EXPORTED nodeToSynthesizedRule gains a throw guard alongside
the existing non-syntax throw — defense-in-depth for direct callers of the exported
projection (renderNpmDeclarative's FF7001 gates only the batch path; the projection
is exported and would otherwise silently drop the tree). Honest rationale (precise,
not over-claimed): esquery — what no-restricted-syntax consumes — IS a JS-AST
selector language WITH relational operators (:has/:not/combinators), so this is NOT
"eslint can't do relational at all". The narrower, defensible claim: the
RelationalRule LEAF (RelationalHas.pattern) is an ast-grep metavariable pattern over
PYTHON AST (e.g. `return $V`), not an esquery selector; there is no faithful
ast-grep-pattern→esquery translator, and fabricating selector-composition would be a
"works"-without-evidence claim. So refuse honestly and route to ast-grep (#212).

Both header routing comments updated to add the relational-refuse row. Reuses
diag+FF7001+BACKEND_NAME already imported in each file; no RelationalRule import
(n.relational is a presence check).

RE-BASELINE (honest outcome): the five named drift/byte-lock/snapshot tests needed
NO re-baseline — all GREEN as-is with ZERO committed golden/byte-lock/snapshot
change (the refuse branches fire ONLY on n.relational !== undefined, inert for every
legacy scalar fixture; JSON.stringify of the absent key is unchanged). synth-and-
wire.bundle.mjs DID require regeneration — but as a genuine source-compile artifact,
NOT a fabricated re-baseline: the bundle compiles nodeToSynthesizedRule (reachable
from the install path), so my new throw-guard is bundled in. Verified the fresh-
build diff is EXACTLY the 5-line relational throw-guard (the batch renderers +
render-clippy tree-shake out); the install-sh snapshot compare stayed 11/11
byte-identical, proving the bundle change altered NO consumer-facing render output.
At clean HEAD (stashed) build-synth-bundle.sh --check was in-sync, confirming the
drift is caused solely by this diff.

Tests (RED-before-GREEN, paired-negative, mirroring ruff R8/R8b):
- render-clippy.test.ts: R-rel (relational -> refused FF7001 even with bannable
  type-aware params) + R-rel-b (no-leak: toml lacks std::env::var).
- from-node.test.ts: Rel-1 (relational -> refused FF7001, rules[] empty) + Rel-2
  (nodeToSynthesizedRule throws on a relational node).
RED confirmed first (exit 1, 4 failed — code rendered the nodes silently); GREEN
after the guards (exit 0). Full packages/core suite: 226 files / 2650 tests pass.
tsc --noEmit clean. build-synth-bundle.sh --check in-sync post-regen. install-sh
snapshot compare 11/11 byte-identical.

HARD CONSTRAINTS held: no IR-shape change (ir/types.ts, schema, grammar untouched —
S1 owns); no render-astgrep.ts change (S2 owns); byte-lock synthesizer/to-node.test.ts
green (19/19 within the 29-test drift set); clippy refusing IS NOT rust coverage; no
fabricated re-baseline (the only regenerated file genuinely changed).

Capability-commit check: modifications only (5 files, all status M; 0 new files, no
package.json/dependency edit) — detectCapabilityReason returns null (all three
triggers require status 'A' or a new dep), so this does NOT trip prior-art.ts.

Prior-art: skipped — modifications-only commit adding FF7001 relational-refuse
branches to two existing renderers (render-clippy.ts, from-node.ts) reusing the
existing selectorClass-refusal idiom; no new tool, dependency, file, or capability
surface. The dedicated "relational IR = ADAPT of ast-grep operators" SSOT row stays
deferred to S4 per the ir-unfreeze kickoff (same deferral S1/S2 recorded).

Co-authored-by: t <t@t.co>
artyhoo added a commit that referenced this pull request Jul 21, 2026
…OR live-fire + Prior-art #225 (#1084)

S4 (umbrella closure) discharges the BFR §3 obligation for the ir-unfreeze
relational-IR capability (landed across S1–S3) and closes the S2 carry-forward M3
(the multi-child not:{any} fold had render-golden coverage only, no committed
live-fire). No IR-shape re-decision, no consumer migration.

Sweep (confirm-not-migrate): `git grep -l ConventionNode -- 'packages/core/**/*.ts'
| grep -v test` = 24 consumers; `tsc --noEmit -p packages/core/tsconfig.json` exit 0
proves every consumer type-checks under the Option B discriminated union.
Bucket A (7 read the relational plane): ir/types.ts (the definition), ir/gates/
grammar.ts (FF6004 switch+assertNever), backends/astgrep/render-astgrep.ts
(renderRelationalNode switch+assertNever), backends/{cargo,npm,ruff} (FF7001
presence-refuse — the honest S3 boundary, not a gap), diagnostics/registry.ts
(FF-code registration). Exhaustiveness rests on the 2 op-switch sites (grammar.ts,
render-astgrep.ts) whose assertNever(x: never) default forces TS2355 on any
unhandled arm; the 3 refuse-sites read presence only. Bucket B (17 never read
.relational): correct-by-construction — the field is OPTIONAL and absent on every
legacy scalar node. ZERO of the 24 files edited for migration; byte-lock
(synthesizer/to-node.test.ts) stays green.

Multi-child live-fire (closes S2 M3): census-grounded NOR
"require-return-type-or-docstring" (flag functions with NEITHER a return-type hint
NOR a docstring). MULTI_CHILD_FIXTURE_NODE renders not:{any:[has type $T, has string
$DOC]}. Real @ast-grep/cli 0.44.1 scan: RED (neither) exit 1 / GREEN (-> int) exit 0;
the NOR control proves the any-fold is behaviourally load-bearing — a docstring-only
source under the rendered not:{any} = 0 findings, under the wrong not:{all} swap = 1
finding. Arm B string-kind IMPLEMENTER-PINNED live (T12); the raise-based fallback was
not needed.

SSOT: append-only row #225 (ADAPT — ast-grep's any/all/not/has vocabulary borrowed,
the type-safe recursive discriminated-union shape is ours; distinct capability-area
from #212 emission-target ADOPT). Umbrella closure: done.md is NOT written in
this feature commit — per CLAUDE.md "Umbrella closure convention" + repo precedent
(762b2d3 PR #904, c035a94 PR #691) the merging session authors it as a separate
post-merge commit carrying the real Final PR number, not a TBD placeholder baked here.

python-data-only; rust expressibility remains a stated GAP (never a coverage claim).

Prior-art: prior-art-evaluations.md#225 (relational ConventionNode IR = ADAPT of ast-grep's any/all/not/has relational vocabulary — vocabulary borrowed, the type-safe discriminated-union shape is ours; distinct capability-area from #212 ast-grep emission-target ADOPT).

Co-authored-by: t <t@t.co>
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.

1 participant