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:
@@ -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.
|
||||
Reference in New Issue
Block a user