docs(U9): safeguard baseline — six merge safeguards verified by mutation (#2511)

**U9, PR3.** Docs-only, one new file, zero production changes. No
changeset (internal docs). Independent of #2504.

This is the six-row safeguard table required as a U9 artifact —
delivered **verified**, replacing the partial, unearned version I put in
#2494.

## Every row proven by mutation

Break the guard in production, run the cited tests, confirm red,
restore. A cited test that does not fail is not evidence.

| # | Safeguard | Consulted at | Result under mutation |
|---|---|---|---|
| 1 | user pause | `project-engine.ts:645` (merge admission filter) |
**11 failed** / 1865 passed |
| 2 | `autoMerge:false` | `project-engine.ts:2797`
`allowsAutoMergeProcessing` | **9 failed** / 250 passed |
| 3 | dependency gating | `task-merge.ts:402` unresolved-dependency
reason | **5 failed** / 94 passed |
| 4 | capacity | `project-engine.ts:3178` single-flight `mergeRunning` |
**11 failed** / 1445 passed |
| 5a | merge-proof (pre-enqueue) | `project-engine.ts:2609`
`getTaskMergeBlocker` consult | **1 failed** / 272 passed |
| 5b | merge-proof (file scope) | `merger-file-scope.ts:200`
`FileScopeViolationError` | **6 failed** / 172 passed |
| 6 | at-most-once | `project-engine.ts:2730` `mergeActive` dedupe | **3
failed** / 263 passed |

**All six hold. Nothing is currently broken.** Per-row test attribution
is in the doc.

## Finding 1 — one of nine safeguard test files runs in blocking CI

Only `merger-merge-lifecycle.test.ts` is in the `engine-core`
allow-list. The core gate is two PG tests (`test:pg-gate`) and does not
include `task-merge.test.ts`.

AGENTS.md: CI blocks on Lint/Typecheck/Build/Gate, and "a red
non-blocking run is information, not a merge stopper."

So a change breaking **user pause on merge admission, dependency gating,
capacity single-flight, or the file-scope invariant** does not block a
PR today — it goes red in full-suite, after the merge.

Acceptable for a lane nobody is rewriting. Wrong for the lane U9
rewrites next. **Recommendation: admit the highest-value safeguard tests
to the gate before conversion begins, with the budget cost measured** —
engine-core is 5.36s against a ~60s ceiling, so there is room, but I
won't assume it. Proposed as PR4.

## Finding 2 — safeguard 5a rests on a single non-gate test

Removing the pre-enqueue merge-blocker consult fails exactly one test.
Thinnest of the six, on a destructive-risk gate. Its sibling 5b is well
covered (6 tests), so the invariant isn't unguarded — but the consult
that keeps a blocked task out of the queue very nearly is.

## Methodology note, because it cost an hour

**Rows 1 and 3 initially measured ZERO failures and looked like coverage
gaps. Both were wrong** — the test selection was too narrow. Widening
row 1 from three files to
`project-engine|merge|concurrency|self-healing` turned 0 failures into
11. Row 3's real coverage lives in `@fusion/core`'s suite, which `pnpm
--filter @fusion/engine` never runs, even though the engine config
aliases `@fusion/core` to source so the mutation *was* live.

I nearly reported two false gaps. Recorded as two rules: a narrow
mutation run cannot prove absence of coverage, and cross-package guards
need cross-package runs.

## Scope

New file only — deliberately **no** edit to
`docs/workflow-policy-ownership-map.md`, because #2504 already edits
that file at the same anchor and I want both PRs independently
revertable. Cross-link follows once both are on main.

Verified: no production diff, all mutations restored, `pnpm lint` clean.

## Not covered, stated rather than implied

Reviewer-lane safeguards; FN-7720 operator bypass; FN-8492
orphaned-pending-step rewrite; branch-group promotion sequencing. Each
needs its own verified row before the matching conversion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-28 17:15:50 -07:00
committed by GitHub
parent 4eaa509024
commit aeef592187

View File

@@ -0,0 +1,94 @@
---
title: "U9 safeguard baseline: what actually holds merge safe today"
type: characterization
status: measured
date: 2026-07-28
measured_against: "main @ 46f35323c"
origin: docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md
---
# U9 safeguard baseline
U9 converts review and merge onto graph nodes. Merge is where irreversible things
happen, so the conversion needs a baseline that is **verified, not asserted**: for
each safeguard, where it is consulted today, and which test actually proves it.
Every row below was verified by **mutation** — break the guard in production code,
run the cited tests, confirm they go red, restore. A cited test that does not fail
is not evidence, and this program has already shipped four guards that looked like
enforcement and were not.
## The six safeguards
| # | Safeguard | Consulted at | Mutation applied | Result | Proven by |
|---|---|---|---|---|---|
| 1 | **user pause** | `project-engine.ts:645` — merge admission provider filters `task.paused \|\| task.userPaused` | dropped both pause checks | **11 failed** / 1865 passed | `merge-error-recovery.test.ts`, `self-healing.test.ts` |
| 2 | **`autoMerge:false`** | `project-engine.ts:2797` — `allowsAutoMergeProcessing(task, settings) \|\| isLiveSharedBranchGroupMemberIntegration(...)` | forced `return true` | **9 failed** / 250 passed | `automerge-toggle-legacy-advisory.test.ts`, `executor-live-branch-group-auto-merge-hold.test.ts`, `merger-merge-lifecycle.test.ts`, `project-engine.test.ts` |
| 3 | **dependency gating** | `task-merge.ts:402` — `getTaskCompletionBlocker` returns the unresolved-dependency reason | returned `undefined` | **5 failed** / 94 passed | `packages/core/.../task-merge.test.ts` |
| 4 | **capacity** | `project-engine.ts:3178` — `if (this.mergeRunning) return;` single-flight merge pump | removed the guard | **11 failed** / 1445 passed | `merge-error-recovery.test.ts`, `project-engine.test.ts` |
| 5a | **merge-proof** (pre-enqueue) | `project-engine.ts:2609` — `if (this.options.getTaskMergeBlocker?.(task)) return false;` | removed the consult | **1 failed** / 272 passed | `project-engine.test.ts` — a single test |
| 5b | **merge-proof** (file scope) | `merger-file-scope.ts:200` — `throw new FileScopeViolationError(...)` | removed the throw | **6 failed** / 172 passed | `merger-file-scope-invariant.test.ts` |
| 6 | **at-most-once merge** | `project-engine.ts:2730` — `mergeActive` membership rejects a duplicate enqueue | let the duplicate through | **3 failed** / 263 passed | `merge-active-status.test.ts`, `merger-merge-lifecycle.test.ts`, `project-engine.test.ts` |
All six hold. Nothing here is currently broken.
## The two findings that matter for U9
### 1. One of nine safeguard test files runs in blocking CI
Of the nine test files above, **exactly one — `merger-merge-lifecycle.test.ts` — is in
the `engine-core` merge-gate allow-list.** The core gate is two PG tests
(`test:pg-gate`), and `task-merge.test.ts` is not among them.
Per AGENTS.md the merge gate is "thin and trusted": CI blocks on Lint, Typecheck,
Build, and Gate, and "a red non-blocking run is information, not a merge stopper."
So today a change that breaks **user pause on merge admission, dependency gating,
capacity single-flight, or the file-scope invariant** does not block a PR. It goes
red in full-suite, after the merge.
That is a safe-enough posture for a lane nobody is rewriting. It is the wrong
posture for the lane U9 is about to rewrite. **Recommendation: admit the
highest-value safeguard tests to the gate before the conversion begins**, with the
budget cost measured rather than assumed (the gate's stated ceiling is ~60s
wall-clock; engine-core is currently 5.36s).
### 2. Safeguard 5a rests on a single non-gate test
Removing the pre-enqueue merge-blocker consult fails exactly **one** test, in
`engine-default`. Of the six safeguards it has the thinnest coverage, and it is one
of the two flagged destructive-risk. Its sibling 5b (file scope) is well covered at
6 tests, so the *invariant* is not unguarded — but the specific consult that keeps a
blocked task out of the queue in the first place very nearly is.
## Methodology note — read this before trusting a "no failures" result
Rows 1 and 3 **initially measured zero failures** and looked like coverage gaps.
Both were wrong: the test selection was too narrow. Widening row 1 from three files
to the `project-engine|merge|concurrency|self-healing` set turned 0 failures into 11.
Row 3's real coverage lives in `@fusion/core`'s suite, which an engine-only
`--filter` never runs.
Two rules follow, and they cost an hour to learn:
- **A narrow mutation run cannot prove absence of coverage.** Widen before
concluding a gap exists, and say which selection produced the number.
- **Cross-package guards need cross-package runs.** `packages/engine/vitest.config.ts`
aliases `@fusion/core` to *source*, so a core mutation is live in engine tests —
but the tests that actually assert on it may sit in the core package, which
`pnpm --filter @fusion/engine` will not run.
Harness used: mutate → run cited tests → record → restore, asserting the patch
actually applied (a silently non-applying patch reports a false "no failures").
## What this baseline does not cover
- **Reviewer-lane safeguards.** This is the merge lane only. The review nodes
(verdict routing, provider-outage hold-in-place) need the same treatment before
the reviewer converts.
- **FN-7720 operator bypass** and **FN-8492 orphaned-pending-step rewrite** are
named U9 invariants but are not safeguards on this table; they need their own
verified rows.
- **Branch-group member integration / promotion sequencing** (the FN-5819 scoped
exception to safeguard 2) is covered incidentally by row 2's mutation but has no
dedicated verified row yet.