fix(ai-laziness-traps): T20 inline-verdict-without-evidence - #212
Merged
Conversation
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
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).
3 tasks
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.
This was referenced May 25, 2026
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>
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
Sub-wave C of
recommendation-laziness-discipline-i-phaseumbrella.Adds T20 «inline-verdict-without-evidence» trap to
.claude/rules/ai-laziness-traps.md:### T20entry at .claude/rules/ai-laziness-traps.md:146-152 with Trigger / Tempted output / Counter sections (cites parent rulephase-research-coverage.md §1.12+ named anti-pattern#recommendation-skips-own-discipline).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:
docs/meta-factory/research-patches/2026-05-24-recommendation-laziness-discipline.mddocs/meta-factory/research-patches/2026-05-25-narrow-b-benchmark.md(verdict: Option D = A+C only, narrow-B Stop-hook DROPPED at FP_rate=84%).Parent rule:
.claude/rules/phase-research-coverage.md §1.12(introduced 2026-05-22; this trap operationalises its#recommendation-skips-own-disciplineanti-pattern for the inline-chat surface).How to verify
npx vitest run principles/12-ai-laziness-traps.test.tsexit=0, 6/6 passed — verified locally before pushnpx vitest run principles/09-doc-authority-hierarchy.test.tsexit=0, 17/17 passed — verified locally before pushnpx vitest run principles/exit=0, 117/117 passed — verified locally before pushgrep -c '^### T' .claude/rules/ai-laziness-traps.md— verified locallygrep -cE '^### T20'= 1 — verified locally§1.7 Forward-check applied
This PR complies with discipline-bearing rules:
.claude/rules/doc-authority-hierarchy.md §3— T20 mirrors T19's existing### T19 — Titleem-dash header format at .claude/rules/ai-laziness-traps.md:146; the file's Authoritative-for header is unchanged (catalogue extension, not header-bearing edit)..claude/rules/phase-research-coverage.md §1.12— parent rule explicitly cited in T20 Counter section at .claude/rules/ai-laziness-traps.md:152: «per parent rulephase-research-coverage.md §1.12». This is the mechanism-layer operationalisation of §1.12 prose discipline (per R-phase backward-checkEXTENDS §1.12)..claude/rules/phase-research-coverage.md §4 #recommendation-skips-own-discipline— named anti-pattern cited at .claude/rules/ai-laziness-traps.md:152 as the parent of this inline-chat surface specialisation..claude/rules/no-paid-llm-in-ci.md §1— §5 promotion criterion at .claude/rules/ai-laziness-traps.md:178 frames any future post-hoc grep as deterministic + MANUAL classification, NOT an LLM-in-CI gate; explicitly citesnarrow-b-benchmark.md §1.5(FP_rate=84%) as the rationale ruling out automated gate-class enforcement..claude/rules/build-first-reuse-default.md— Prior-art trailers present on commit 40f13e5: R-phase patch (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) + benchmark patch (PR research(narrow-b-benchmark): production-corpus FP measurement for Option B #210) cited as evidence base. Pre-push hook accepted trailers without escape-hatch invocation..claude/rules/ai-laziness-traps.md §3— T20 added to the existing active-trap enumeration example (T1, T3, T4, T7, T11, T13, T15, T20); matches existing comma-list format..claude/rules/ai-laziness-traps.md §2 T15self-application — Worker quoted actual command output (exit codes, test counts 6/6, 17/17, 117/117) in REPORT before claiming success; no «green ✅» without evidence quotation..claude/rules/ai-laziness-traps.md §2 T19own-cold-QA — orchestrator (this session) ran independentgh pr view --json statusCheckRolluppost-push, caught the §1.7 substance-gate FAILURE that Worker missed; CI ≠ design review; «merge is maintainer's decision, QA is mine».§1.7 Backward-check applied
Scope-extending change, no silent superseding:
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).T1, T3, T4, T7, T11, T13, T15→T1, T3, T4, T7, T11, T13, T15, T20. Existing T-numbers preserved.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 (newrecommendation-laziness-discipline.md) explicitly OUT OF SCOPE for this PR — separate Worker / separate PRs per kickoff §1 file-disjoint dispatch matrix.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)..claude/rules/phase-research-coverage.md §1.12unchanged. 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 rulephase-research-coverage.md §1.12»).#recommendation-skips-own-disciplineanti-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.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.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
.claude/hooks/inject-session-bootstrap.sh:11H1 wording append) — separate Worker / separate PR..claude/rules/recommendation-laziness-discipline.mdrule file) — separate Worker / separate PR.README.md absolutism research-patch 2026-05-16).Rollback plan
Revert commit 40f13e5;
.claude/rules/ai-laziness-traps.mdreturns to T19 highest. No downstream dependencies — Sub-waves A and D ship independently of this PR.