# RI-1-002 — Publish-gate negative controls (SDLC-D-034 second half)

- Task: RI-1-002 (docs/release-integrity workstream, PRD item RI-N1), issue ref #1275
- Branch: `test/ri-050-publish-gate-negative` (base `origin/next` @ d8e0aec9 = PR #1277, RI-1-001)
- Budget: worker estimate ~45K tokens; keep scoped to the two test files + scratchpad.

## Objective

Checked-in negative-control tests that PROVE the publish gate fails when it must:

1. Broken mandatory check blocks every publish step (structural DAG proof from `.woodpecker/publish.yml`).
2. Bypass shapes fail the checker: missing edge, hidden effect (non-`publish` name), detached verify, always-pass verify (`failure: ignore` / `success` override), conditional verify (`when`).
3. Exact-commit identity: no HEAD-moving step between verify and publish effects; legitimate re-checkout requires verify to re-run after it.
4. `verify-release.mjs` composition control: a SUBSET stage list fails the composition check.

## Plan

- NEW `scripts/publish-gate-structure.test.mjs` — self-contained structural checker (`assertPublishGateBlocksOnVerify`) + positive control on the real pipeline + one negative-control test per bypass shape (S1–S6, documented in file header) + positive control for the legitimate re-checkout shape.
- EXTEND `scripts/verify-release.test.mjs` — refactor the stage-mirror test body into `assertStagesMirrorCi(stages, ci)`; add negative control dropping each stage one at a time (subset must throw).

## Conventions confirmed

- Root `test:checkout` = `node --test scripts/*.test.mjs` → new file auto-joins `pnpm test`.
- Test-enumeration guard population is `*test*.sh` under `packages/mosaic/framework/tools/` only → unaffected.
- Root eslint covers only `**/*.{ts,tsx}` → .mjs files need Prettier style only (printWidth 100, singleQuote, semi, trailingComma all).
- Do NOT touch docs/TASKS.md, docs/release-integrity/TASKS.md, docs/scratchpads/.

## Progress log

- [x] Base verified: publish.yml `verify` step + verify-release.mjs present; HEAD contains origin/next.
- [x] Wrote scripts/publish-gate-structure.test.mjs
- [x] Extended scripts/verify-release.test.mjs (mirror fn + subset negative control)
- [x] Gates: node --test scripts (31 tests pass), prettier clean on touched files, pnpm typecheck PASS, pnpm lint PASS, pnpm format:check PASS
- [x] Committed ff585b88 + pushed, PR #1305 → next (no conflicts). Stopped before merge per task instruction.

## Evidence

- `node --test scripts/verify-release.test.mjs scripts/publish-gate-structure.test.mjs` → 31 tests, 0 fail.
- Mutation sanity: temporarily removing the `verify` edge from build-gateway in publish.yml → structure test goes red (verified manually during dev, then reverted).
- Gates run from repo root on this worktree; results in Progress log.

## Risks / notes

- Effect detection (`isPublishCommand`) is deliberately over-broad (any npm/pnpm/yarn command mentioning `publish`, any kaniko/docker-push/`--destination`) — fail-closed: a false positive forces justification, a false negative is the actual hazard.
- `git fetch` flagged as HEAD-moving even though fetch alone doesn't move HEAD — fail-closed on the classic `fetch && reset` pair.
