Files
fusion/.github/workflows/pr-checks.yml
gsxdsm 4e2f52ce8f feat(gate): ratchet move-target literals at zero — #3150's population had nothing holding it (#3246)
Closes the gap I flagged when re-measuring #3150: that population is at
**0**, and nothing was holding it there.

## Why this surface has no gate today

The lifecycle census parses **comparisons**. A move destination is a
call **argument**:

```ts
await store.moveTask(id, "in-review");   // never counted by anything
```

#3150 measured 31 of these across four files. They are now 0 — I
verified that on current main before writing this — but the comparison
backlog drifted **787 → 854** during the window its own ratchet was
unwired, and this population never had one.

## The failure mode is louder than the guards'

A wrong lane **guard** silently answers "no". A wrong move **target** is
rejected by `moveTaskInternal` with `TransitionRejectionError:
unknown-column` — so on a board that renamed its review lane, every task
finishing implementation **threw** instead of reaching review. Loud at
runtime, invisible to any test on the default board.

## AST, not grep — and that is measured, not stylistic

| scan | result |
|---|---|
| comment-naive grep of `self-healing.ts` | 1 hit — **JSDoc prose**: `*
could call moveTask("in-review")` |
| #3150's own SQL survey by grep | 37 hits against **12** real sites (25
comments) |

Comments are not AST nodes, so that false-positive class cannot occur
here in either direction.

## Controls — all four run, because a gate that only reports 0 proves
nothing

| probe | expected | got |
|---|---|---|
| real `moveTask(id, "in-review")` injected | fail | **exit 1**, names
the file |
| identical call as JSDoc prose | pass | **exit 0** (AST ignores
comments) |
| legacy target + leading `DELIBERATE-LITERAL` | pass | **exit 0**
(marker honored) |
| probe removed | pass | **exit 0** |

The third is the #1411 `recoveryRehome` safe-landing path, where the
legacy id genuinely *is* the target. Marker must be **leading** — the
census already learned that an inline marker attaches to the wrong node
and is silently ignored.

## Ratchet semantics match the census

Fails on a **drop** as well as a rise. A stale allowance is a hole a
re-added target can return through while the gate stays green — exactly
what let the comparison baseline drift.

## Measured

| check | result |
|---|---|
| this gate | scans **1816** files, reports **0** |
| `check:lifecycle-columns` / `check:sql-column-literals` | 0 / 0 |
| `check:fnxc-future-dates` / `check:lane-wiring` | 0 / 0 |
| eslint / `pnpm test:gate` | clean / exit 0 |

Wired into `pr-checks.yml` beside the sibling ratchets, named to match
("Move-target ratchet").

## Scope

Gate only — **no production code touched**, and no conversions in this
PR. The population was already empty; this makes "31 → 0" an invariant
instead of a snapshot, which is the caveat I attached when recommending
#3150 for closure.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Quality Improvements**
  * Added automated validation for task-movement configuration values.
  * Pull request checks now detect unexpected changes in tracked values.
* Added baseline tracking with strict validation to identify both
additions and removals.
  * Added support for explicitly documenting intentional exceptions.
  * Improved reporting for file-discovery and source-reading failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:56:07 -07:00

274 lines
12 KiB
YAML

name: PR Checks
# The thin trusted merge gate (docs/plans/2026-06-04-001-refactor-fast-trusted-test-gate-plan.md).
# Blocking checks are exactly: Lint, Typecheck, Build, Gate.
#
# BRANCH-PROTECTION CUTOVER: required status checks are matched by job name.
# When this file changes job names, update the repo's branch-protection
# required checks to exactly [Lint, Typecheck, Build, Gate] — a stale required
# name (e.g. "Test shard 1/4") that no longer reports will block every PR
# with "Expected — waiting for status". Open PRs must rebase onto main after
# the cutover so they run this workflow shape.
#
# Everything that used to run here as shards / slow tier / inventory guard is
# non-blocking and lives in full-suite.yml (push to main).
on:
pull_request:
branches: [main]
concurrency:
group: pr-checks-${{ github.ref }}
cancel-in-progress: true
# Least-privilege token: every job here only reads the repo (checkout + cache).
permissions:
contents: read
# FN-4863: Opt JavaScript actions into Node 24 ahead of GitHub's forced cutover on 2026-06-02.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
- name: Lint
run: pnpm lint
- name: Changeset format
run: pnpm check:changesets
- name: Dashboard route modularity
run: pnpm check:routes-modular
# The lifecycle-column ratchet was advisory until now: the census existed only as
# `pnpm census:lifecycle-columns` (no --strict) and nothing ran it, so three PRs
# lowered counts without re-recording and left allowances the deleted guards could
# return through while this gate stayed green. ~2s over ~1950 files.
- name: Lifecycle-column ratchet
run: pnpm check:lifecycle-columns
- name: SQL-column ratchet
run: pnpm check:sql-column-literals
- name: Move-target ratchet
run: pnpm check:move-target-literals
- name: Inert lane/flag seams
run: pnpm check:inert-flag-seams
- name: FNXC stamp dates
run: pnpm check:fnxc-future-dates
- name: Lane-wiring ratchet
run: pnpm check:lane-wiring
- name: Plugin interop declarations match the dashboard API
run: pnpm check:plugin-interop-drift
# AGENTS.md states the deletion ratchet as policy — "A quarantined test is DELETED after 14
# days (quarantinedAt + 2 weeks) unless rescued" — and nothing enforced it. Same shape as the
# lifecycle-column note above: the script supports `--strict`, the package script omitted it,
# and no workflow ran it, so it could not fail however long an entry sat there. It fires 5 days
# BEFORE the deadline so the choice is still delete-or-rescue rather than an overdue entry.
- name: Quarantine deletion ratchet
run: pnpm check:quarantine-ledger
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
# FNXC:CIGateSpeed 2026-07-23-00:05:
# tsc incremental buildinfo cache. tsconfig.base.json enables
# `incremental` with per-package dist/.tsbuildinfo; a restored buildinfo
# is SELF-VALIDATING (tsc hashes every input against it and re-checks
# whatever changed), so restore-keys can never let a stale check through
# — it only shrinks the re-checked set. Cold typecheck was ~4 min of the
# PR critical path. Keyed by SHA so every commit saves a fresh snapshot;
# restore-keys picks the nearest prior one (same PR, or main via the
# warm-gate-build-cache job in full-suite.yml, which must keep an
# identical path list — actions/cache versions caches by path list).
- name: Cache TypeScript incremental buildinfo
uses: actions/cache@v6
with:
path: |
packages/*/dist/.tsbuildinfo
packages/dashboard/dist/.tsbuildinfo-app
plugins/*/dist/.tsbuildinfo
key: typecheck-tsbuildinfo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
typecheck-tsbuildinfo-${{ runner.os }}-
- name: Typecheck
run: pnpm typecheck
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
# FNXC:CIGateSpeed 2026-07-23-00:05:
# RESTORE-ONLY tap of the gate's incremental dist cache (see the gate
# job's cache block for the full safety rationale: `pnpm build` always
# runs and reconciles a near-match restore per package by content hash).
# Restore-only (actions/cache/restore, never save) because this job runs
# FULL CLI packaging (CI=true → desktop + bundled plugins + DTS), and
# saving that shape would swap the cache's canonical fast-CLI contents
# out from under the Gate job. Full CLI packaging itself is never skipped
# regardless of cache state (ensureFullPackageCliPlanned force-plans the
# CLI in full mode), so this job's distinctive coverage is preserved.
# Path list must stay byte-identical to the gate job's block.
- name: Compute dist source hash
id: dist-hash
run: echo "hash=$(node scripts/ensure-test-artifacts.mjs --print-source-hash)" >> "$GITHUB_OUTPUT"
- name: Restore built dist artifacts (incremental, restore-only)
uses: actions/cache/restore@v6
with:
path: |
packages/core/dist
packages/dashboard/dist
packages/engine/dist
packages/plugin-sdk/dist
packages/cli/dist
plugins/fusion-plugin-dependency-graph/dist
plugins/fusion-plugin-hermes-runtime/dist
plugins/fusion-plugin-openclaw-runtime/dist
plugins/fusion-plugin-paperclip-runtime/dist
.fusion/cache/plugin-build-cache.json
key: gate-dist-${{ runner.os }}-${{ steps.dist-hash.outputs.hash }}
restore-keys: |
gate-dist-${{ runner.os }}-
- name: Build
run: pnpm build
# The only merge-blocking TEST signal (R3). Runs the boot smoke (the app
# starts and serves) plus the curated engine-core suite and the CI-shape
# test — see `test:gate` in the root package.json. Gate membership is the
# explicit allow-list in packages/engine/vitest.config.ts (engine-core
# project); a flaky gate test is evicted by removing it from that list.
gate:
name: Gate
runs-on: ubuntu-latest
# FNXC:FixPgTestsAndCi 2026-06-26-09:10:
# Provision a PostgreSQL service container so the postgres/*.pg.test.ts
# suites (pgDescribe) run in the merge gate. The pg-test-harness probe
# detects reachability via a TCP probe on localhost:5432 and skips when
# unavailable, so this service is what makes the 57 PG twin tests actually
# execute instead of being silently skipped.
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
# Mark the service healthy only when pg_isready succeeds on the mapped
# port, so job steps don't start before Postgres accepts connections.
options: >-
--health-cmd "pg_isready -h localhost -p 5432 -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
# Point the PG test harness at the service container. psql admin DDL
# (CREATE/DROP DATABASE) runs against this URL's maintenance database.
FUSION_PG_TEST_URL_BASE: "postgresql://postgres:postgres@localhost:5432"
PGPASSWORD: "postgres"
# The gate's value is speed; without a job timeout a hung build or
# deadlocked vitest worker blocks every PR for GitHub's default 6 hours.
# Expected runtime is ~3-5 min.
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
# FNXC:CIGateSpeed 2026-07-22-23:30:
# Gate-scoped INCREMENTAL dist cache (`gate-dist-*` namespace, distinct from
# the shard jobs' exact-match `dist-*` contract). Unlike the shard jobs —
# where restore-keys is forbidden because tests consume restored dist
# WITHOUT a build (stale dist was the FN-4232/FN-4605 failure mode) — the
# gate always runs `pnpm build` after restore. build-workspace.mjs verifies
# every package's git content hash against .fusion/cache/plugin-build-cache.json
# (cached below alongside dist) and rebuilds anything changed, missing, or
# unhashed, so a near-match restore can only speed the build up, never let
# stale dist through. Measured before this change: every PR missed the
# exact key and paid a full ~6-8 min build for ~45s of actual gate tests.
# NEVER add node_modules here (breaks Windows pnpm junctions elsewhere).
# The warm-gate-build-cache job in full-suite.yml saves this same cache
# (identical path list — actions/cache versions caches by path list, so
# the two blocks must stay in sync) on every push to main so a PR's FIRST
# gate run restores main's build instead of building cold.
- name: Compute dist source hash
id: dist-hash
run: echo "hash=$(node scripts/ensure-test-artifacts.mjs --print-source-hash)" >> "$GITHUB_OUTPUT"
- name: Cache built dist artifacts (incremental)
id: dist-cache
uses: actions/cache@v6
with:
path: |
packages/core/dist
packages/dashboard/dist
packages/engine/dist
packages/plugin-sdk/dist
packages/cli/dist
plugins/fusion-plugin-dependency-graph/dist
plugins/fusion-plugin-hermes-runtime/dist
plugins/fusion-plugin-openclaw-runtime/dist
plugins/fusion-plugin-paperclip-runtime/dist
.fusion/cache/plugin-build-cache.json
key: gate-dist-${{ runner.os }}-${{ steps.dist-hash.outputs.hash }}
restore-keys: |
gate-dist-${{ runner.os }}-
# Only seed the mtime-defeating artifact hash-cache on an EXACT hit; on a
# restore-keys near-hit the restored dist may be stale for changed
# packages, and `pnpm build` below is what reconciles it.
- name: Seed artifact hash-cache on cache hit
if: steps.dist-cache.outputs.cache-hit == 'true'
run: node scripts/ensure-test-artifacts.mjs --seed-artifact-cache
# FNXC:CIGateSpeed 2026-07-22-23:30:
# Boot smoke needs the built workspace including the CLI. Fast CLI
# packaging (bin.js + extension.js, no desktop/bundled-plugin/DTS staging)
# is sufficient for boot smoke + test:gate and is the same shape
# `pnpm verify:fast` proves locally; CI=true would otherwise force the
# multi-minute full packaging tail on every gate run. Full CLI packaging
# coverage stays blocking in the separate Build job.
- name: Build
run: pnpm build
env:
FUSION_CLI_FULL_PACKAGE: "0"
- name: Boot smoke (app starts and serves)
run: node scripts/boot-smoke.mjs
- name: Gate tests (curated engine-core + CI-shape)
run: pnpm test:gate
# Advisory desktop-packaging validation lives in its OWN workflow (desktop-packaging.yml) so this
# thin gate stays exactly [Lint, Typecheck, Build, Gate] — the job set here maps 1:1 to the
# branch-protection required checks (CI-shape test enforces the invariant).