Skip to content

fix(drift-gate): shipped-eslint-rules --check detects orphans + runs at pre-push - #990

Merged
artyhoo merged 2 commits into
stagingfrom
claude/festive-cohen-9888a0
Jul 11, 2026
Merged

fix(drift-gate): shipped-eslint-rules --check detects orphans + runs at pre-push#990
artyhoo merged 2 commits into
stagingfrom
claude/festive-cohen-9888a0

Conversation

@artyhoo

@artyhoo artyhoo commented Jul 11, 2026

Copy link
Copy Markdown
Owner

What

Brings scripts/build-shipped-eslint-rules.sh --check up to the repo's drift-gate invariant. Cold backward-sweep (2026-07-11, python-delivery-v0 S1) found two deviations:

  1. No orphan detection — the check loop iterated only from .ts sources, so deleting a rule source left its committed .mjs/.d.ts silently shipping (install.sh keeps copying a rule nobody authors anymore). Added an orphan walk: any committed *.mjs/*.d.ts in a RULE_DIRS entry without a matching shippable .ts source = RED (scripts/build-shipped-eslint-rules.sh:87-101), mirroring the python-template drift gate's committed-artifacts-without-source walk.
  2. CI-only channel — the gate ran solely in audit-self.yml:273, one channel later than its drift-gate siblings (synth-bundle --check, manifest render). Wired as pre-push section 3g (packages/core/hooks/pre-push.ts:849-870): exit 2 (tsc absent) → warn-skip degrade, mirroring 3f's esbuild skip; exit 1 → block. CI gate kept as the last-resort channel (unchanged).

RED-then-GREEN evidence

Scratch run with packages/preset-react-spa/eslint-rules/require-error-boundary.ts moved aside:

=== RED run ===
ORPHAN: packages/preset-react-spa/eslint-rules/require-error-boundary.mjs has no matching rule source ... (delete the artifact or restore the source)
ORPHAN: packages/preset-react-spa/eslint-rules/require-error-boundary.d.ts has no matching rule source ...
✗ shipped-rule drift detected (22 checks). Re-run: scripts/build-shipped-eslint-rules.sh
exit=1
=== GREEN run ===
✓ shipped compiled rules in sync with .ts sources (24 checks).
exit=0

Also verified under macOS /bin/bash 3.2.57 (#989 array-expansion precedent) — green; npm --prefix packages/core run test:hooks — 716/716 green; the push of this branch exercised the new 3g section live in the full pre-push hook — green.

§1.7 Forward-check applied

  • no-paid-llm-in-ci: the gate is deterministic bash + tsc recompile diff, zero LLM calls (scripts/build-shipped-eslint-rules.sh:59-108).
  • ci-tool-pinning §1-§2: no tool installs added; script still requires the repo-root node_modules/.bin/tsc (scripts/build-shipped-eslint-rules.sh:23-24), no bare pip/npm install lines.
  • language-discipline §1: all new comments/messages English (packages/core/hooks/pre-push.ts:849-854).
  • dual-implementation-discipline: pre-push wiring mirrors sibling 3f's degrade convention (exit 2 → warn-skip, packages/core/hooks/pre-push.ts:857-861); consumer-safe via existsSync guard + tsc-absent degrade — never hard-blocks a consumer push.
  • Capability-commit gate (CLAUDE.md): not a capability commit (no new dependency, no new file, no new module) — escape-hatch Prior-art: trailer carried on the commit.

§1.7 Backward-check applied

Class = "compiled/generated-artifact drift gate must (a) detect orphans, (b) run at pre-push, not CI-only". Cold backward-sweep agent (given only the class, never the diff) enumerated ALL drift-gate surfaces:

  • scripts/build-shipped-eslint-rules.sh:87-101 — the only per-source-artifact gate where orphan-class applies; NOW SWEPT-CLEAN (this PR).
  • scripts/build-synth-bundle.sh:55-68 — single fixed artifact from single source; orphan-class N/A, missing-case covered; pre-push wired (packages/core/hooks/pre-push.ts:791-792) — SWEPT-CLEAN.
  • packages/core/render/render-rules.ts:101-117 — full re-render + diff of a single target; orphan cannot linger; pre-push wired (packages/core/hooks/pre-push.ts:875-886) — SWEPT-CLEAN.
  • scripts/render-rule-index.mjs:42-47 — full block re-render; a deleted rule's row disappears on diff; pre-push wired (packages/core/hooks/pre-push.ts:894-901) — SWEPT-CLEAN.
  • scripts/check-skill-drift.sh:203-206 — refs/frontmatter validator, not an artifact gate; orphan-class N/A; pre-push §3b (packages/core/hooks/pre-push.ts:732-734) — SWEPT-CLEAN.
  • scripts/render-harness-config.mjs:8-17 + render-rule-channels.mjs:21-35 — install-time consumer-harness renderers, no committed per-source artifact in this repo → outside the audited class (behaviour covered by rule-channel-degradation.test.ts).

No GAP found across the class; the sole surface where orphan detection applies is fixed here.

…at pre-push

Two deviations from the drift-gate invariant (cold backward-sweep,
2026-07-11, python-delivery-v0 S1):

1. No orphan detection — the check loop iterated only from .ts sources,
   so deleting a rule source left its committed .mjs/.d.ts silently
   shipping. Added an orphan walk (committed artifact without a matching
   shippable source = RED), mirroring the python-template drift gate's
   committed-artifacts-without-source walk.
2. CI-only channel — the gate ran solely in audit-self.yml, one channel
   later than its pre-push siblings (synth-bundle --check, manifest
   render). Wired as pre-push section 3g (exit 2 = tsc absent → degrade
   to warn-skip, mirroring 3f's esbuild skip).

RED-then-GREEN evidence: with require-error-boundary.ts moved aside,
--check emits ORPHAN for both .mjs and .d.ts and exits 1; restored,
exits 0 (24 checks). Verified under macOS bash 3.2 (#989 precedent) and
npm --prefix packages/core run test:hooks (716/716 green).

Prior-art: skipped — drift-gate hardening of an existing script + pre-push wiring; no new dependency, module, or capability
…hifted by section 3g

Only delta across all 8 fingerprints is the packages/core/hooks/pre-push.ts
sha (shipped file). SNAPSHOT_MODE=capture + local compare 8/8 green.

Prior-art: skipped — snapshot baseline regeneration after shipped-file edit, no new capability
@artyhoo
artyhoo merged commit 66123c3 into staging Jul 11, 2026
36 checks passed
artyhoo added a commit that referenced this pull request Jul 12, 2026
…firing proof, fingerprint row (python-delivery-v0) (#996)

* feat(install): getff Python toolchain entry lane + refresh-parity + firing self-check (python-delivery-v0 S2 T1)

`install.sh python` / `./setup python` route to the pure-bash Python delivery
(setup.d/45-python.sh) instead of the npm stack pipeline: an explicit `python`
positional always wins; else pyproject.toml present + no package.json → OFFER
(interactive default No; non-interactive / --dry-run decline to the npm lane). The
lane bypasses the npm package.json precondition + stack pick and early-exits before
the setup.d layer loop, so no npm-assuming step fires. The npm flow stays
byte-identical (all 8 fingerprint baselines green; python logic is gated on
TOOLCHAIN=python which the npm fixtures never trigger).

--refresh re-delivers framework-owned Python artefacts (GETFF_TOOLCHAIN_REFRESH →
refresh_safe overwrite of the rules dir + our-own sgconfig/ruff), auto-detected via
the .getff-python-install.log / .getff/astgrep-rules marker on a bare `--refresh`.
Augment-merged / refused consumer files stay untouched (never clobbered).
refresh-covers-full-delivery.test.sh Check 4 replaces the S1 blanket 45-python.sh
exclusion with real source-token refresh parity (RED before the refresh_safe
branches landed, GREEN after — TDD).

Post-install firing self-check (always-run + graceful degrade, matching the
99-finalize.sh capstone self-verify UX — no separate opt-in flag): plants a
violation in an OS temp dir ONLY (mktemp -d, never under the consumer tree — STOP
line), runs the delivered ast-grep rules + ruff config against it, asserts BOTH fire
RED, then removes the temp dir. An absent tool → loud degrade printing the exact
pinned manual command, never silently green (attention-is-not-a-mechanism.md §1).

Tests: tests/install-sh/python-entry-lane.test.sh (20 arms — detection matrix,
no-package.json bypass, npm-untouched, explicit override, --refresh overwrite +
marker auto-detect, self-check fire + tool-absent degrade), wired into
audit-self.yml. S1 python-delivery (43), refresh-covers (6), byte-identical (8/8),
principles (303) all green.

Prior-art: prior-art-evaluations.md#216 (BUILD the thin bash writer — no headless tool delivers a pre-rendered, multi-tool, augment-first lint bundle into an existing repo; the S2 entry lane + firing self-check realise #216's delivery-mechanism verdict, reusing install.sh copy_safe/refresh_safe + the 99-finalize self-verify UX, zero new deps).

* feat(install): getff Python consumer CI template + fingerprint matrix python row (python-delivery-v0 S2 T2)

Consumer CI template:
- Ship packages/core/templates/python/github-actions-ci.yml — pinned @ast-grep/cli@0.44.1 +
  ruff==0.15.21 installs (ci-tool-pinning.md Rule A; both pins re-verified LATEST 2026-07-12
  via npm registry dist-tags + PyPI JSON, matching the framework CI pins audit-self.yml:232,242),
  failing gates `ast-grep scan` + `ruff check .`. Action SHAs reuse repo-verified pins
  (checkout v4.2.2, setup-node v4.4.0, setup-python v5.5.0).
- Delivered by the python lane (setup.d/45-python.sh _py_deliver_ci) to a getff-NAMESPACED
  .github/workflows/getff-python.yml — never clobbers the consumer's own ci.yml; loud REFUSE on
  a non-getff file at our path; refresh-aware (source-token parity keeps refresh-covers Check 4
  green). Same collision-discipline class as the S1 config cells (STOP-line: no silent clobber).

Fingerprint snapshot matrix python row:
- snapshot.sh: python row with its OWN seeding (pyproject.toml, no package.json) + collision-cell
  brownfield variants (pre-existing ruff.toml → cell iii refuse; pre-existing sgconfig.yml →
  cell ii structural merge), decoupled from the npm {greenfield,brownfield} matrix — NOT a naive
  STACKS append (the npm seeding cannot seed a pyproject / exercise the augment-first cells).
- Exclude the timestamped .getff-python-install.log from the fingerprint (running audit trail,
  not a delivered config artefact — already excluded from the layer's own (v) idempotency
  checksum) so the python row is deterministic; no-op for npm stacks.
- New baselines tests/install-sh/baselines/python/{greenfield,brownfield-ruff,brownfield-sgconfig}
  .fingerprint. Existing npm baselines byte-identical (proven — no npm baseline regen).

Tests: python-delivery.test.sh +8 CI-collision arms (fresh/no-ci.yml-clobber/refuse/refresh) =
50/0; byte-identical.test.sh 11/0 (8 npm + 3 python) + shellcheck 16 clean; determinism proven
(SNAPSHOT_MODE=compare ×2 = 11/0 idempotent); refresh-covers Check 4 6/0; principles 303/303.

Prior-art: prior-art-evaluations.md#216 (getff Python consumer delivery mechanism — the CI
template + its collision-safe delivery lane are part of the same augment-first consumer-install
capability this SSOT row justifies; no upstream analog for pinned multi-tool lint CI delivery).

* fix(python-delivery): EOF-safe OFFER prompt + explicit-arg refresh precedence + drop dead TOOLCHAIN_EXPLICIT

Addresses S2 entry-lane review findings on install.sh (commit 9ad9dd5be):

- Important: the interactive OFFER-lane `read` prompt for a pyproject-only repo
  aborted under `set -euo pipefail` at EOF (non-tty invocation, closed stdin) —
  a bare `read` returns non-zero at EOF, so `set -e` killed the script right
  there with a message-less exit 1, instead of falling through to the
  documented "decline -> npm lane -> clean no-package.json abort" path.
  `read -rp ... || _py_ans=""` makes the read EOF-safe.
- Minor: the `.getff/astgrep-rules` dual-consumer `--refresh` marker branch
  fired before any explicit stack arg was considered, so
  `install.sh ts-server --refresh` on a repo with both package.json and a
  stale/prior python marker silently rerouted to the python-only refresh and
  exited 0, skipping the npm refresh with no error. Gated the marker branch
  on `[ -z "$STACK_EXPLICIT" ]` so an explicit npm stack/toolchain arg always
  wins over the marker auto-route.
- Minor: removed the dead `TOOLCHAIN_EXPLICIT` variable (set, never read —
  "explicit wins" was already achieved via the `[ -z "$TOOLCHAIN" ]` guard on
  the auto-detect block) and fixed the misleading comment describing it.

Extended tests/install-sh/python-entry-lane.test.sh: arm (4) now asserts the
clean "No package.json found" message + exit 1 (RED before the fix: message
absent, script died silently at the read line); new arm (7b) asserts an
explicit `ts-server --refresh` on a repo with a python marker present
refreshes the npm stack, not the python-only path (RED before the fix: wrong
banner + no npm artefact delivered). 26/26 in that file (was 20); the three
sibling suites (python-delivery.test.sh, refresh-covers-full-delivery.test.sh,
byte-identical.test.sh) stay green with no fingerprint drift (11/11: 8 npm +
3 python).

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

* fix(python-delivery): CI template enforces getff ruff bans via isolated --config gate (ruff-collision cells)

S2-T2 review Important (coordinator-confirmed fix-now): the shipped consumer CI template ran a bare
`ruff check .` with no `--config`. In a ruff-collision cell (consumer has their own ruff.toml or
pyproject [tool.ruff]), the getff TID bans ship as a NON-discovered getff-ruff.toml, so `ruff check .`
discovers the CONSUMER's config and the getff bans are SILENTLY unenforced — a green gate enforcing
nothing (#discipline-theatre, attention-is-not-a-mechanism.md §1, the exact failure this project
exists to prevent).

Probe-proven (pinned ruff 0.15.21, live, not from memory): `ruff check <dir> --config <getff.toml>
--no-cache` enforces ONLY the getff config's rules — `--config` REPLACES config discovery (does not
merge) — is self-contained (no consumer config needed, exit 0 on a clean tree), and fires RED on a
getff violation regardless of any consumer ruff config present.

Fix:
- setup.d/45-python.sh: the ruff lane ALWAYS delivers the getff bans to a stable, cell-independent
  .getff/ruff-bans.toml (fresh dir AND every collision cell) via _py_copy_or_refresh (framework-owned,
  getff-header-marked, refresh-aware). Source token $tpl/ruff.toml already has copy+refresh parity, so
  refresh-covers Check 4 is unchanged. Firing self-check now prefers .getff/ruff-bans.toml (proves the
  exact file CI points at). S1 no-clobber discipline intact — no consumer file touched.
- packages/core/templates/python/github-actions-ci.yml: the bare `ruff check .` becomes two steps —
  discovered-config check (unchanged) + an isolated getff-bans gate
  `ruff check . --config .getff/ruff-bans.toml --no-cache` that fires RED on our bans in EVERY cell.
- _py_deliver_ci REFUSE hint: comment noting its pins intentionally mirror the template + hint kept in
  sync with the two-step gate (S2-T2 Minor).

Tests (TDD, RED->GREEN documented in the report): +8 arms in python-delivery.test.sh (58 pass, was 50),
3 deterministic + 1 tool-gated live-fire proving bare check is BLIND while the getff gate fires RED in
a collision cell. python-entry-lane 26/0, refresh-covers 6/0, byte-identical 2/0 (11/11 + shellcheck
16 clean), test:principles 303/0. Python fingerprint baselines regenerated (python rows only, +1 file
each = .getff/ruff-bans.toml; npm baselines byte-identical, drift = 0).

Prior-art: skipped — review-fix round, no new capability (delivers an existing artifact to a stable path + a CI gate step).

* fix(python-delivery): register hand-authored CI template as non-rendered in S1 drift gate (rebase-integration)

Rebasing S2 onto staging pulled in S1's python byte-drift gate (#991,
backends/python-templates-drift.test.ts), which treats EVERY file under
packages/core/templates/python/ as a byte-identical render of the curated
starter nodes. S2 T2 hand-authored github-actions-ci.yml there (following the
repo-wide convention — ts-server/presets ship their CI workflow inside their
own templates/<stack>/ dir), so the now-present drift gate flagged it as an
orphan. S2 was reviewed against pre-S1 base #990, so this cross-stage
interaction could not surface before the rebase.

Fix: register github-actions-ci.yml in a new NON_RENDERED_TEMPLATE_FILES
allowlist. checkPythonTemplateDrift() no longer orphan-flags it AND now asserts
it EXISTS (a silent deletion still surfaces as `missing` — the exclusion is not
a blind pass, per attention-is-not-a-mechanism). The template's byte-integrity
remains guarded by tests/install-sh/byte-identical.test.sh (delivered
.github/workflows/getff-python.yml hash) + python-delivery.test.sh (cmp
template↔delivered) — no coverage hole. No template bytes changed; byte-identical
stays 11/11.

Prior-art: skipped — rebase-integration fix reconciling S2's hand-authored CI template with the S1 render-drift gate that arrived via rebase; no new capability area.

* fix(python-delivery): arm-9 degrade test uses a research-tool-excluded PATH (CI-robust)

The firing-self-check degrade arm forced PATH=/usr/bin:/bin to simulate 'no
tools', assuming that strips ast-grep/ruff/uvx. That holds on macOS (node +
ast-grep live in homebrew) but NOT on CI runners that ship ast-grep in
/usr/bin — there `command -v ast-grep` still resolved, the ast-grep lane FIRED
instead of degrading, and the arm never exercised the both-tools-absent degrade
summary it exists to prove (S2 CI-only fail 2026-07-12: PASS=24 FAIL=2).

Fix: build a coreutils-only bindir that symlinks everything EXCEPT the research
tools (ast-grep/sg/ruff/uvx/npx/node/pip), point the self-check PATH at it. Now
`command -v` genuinely fails for both lanes on any runner → both degrade →
pinned manual commands + no-green summary assert as intended. Production degrade
code unchanged (it was correct; pins are hardcoded constants). Local: 26/0.

Prior-art: skipped — test-only environment-robustness fix, no new capability

---------

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants