Skip to content

fix(pipeline): gate redesign integrity before success and handoff - #208

Merged
cryptoxdog merged 7 commits into
mainfrom
claude/audit-f1-redesign-gate-order
Sep 19, 2026
Merged

cryptoxdog merged 7 commits into
mainfrom
claude/audit-f1-redesign-gate-order

Conversation

@cryptoxdog

Copy link
Copy Markdown
Collaborator

Summary

Closes audit website-bot-core-pipeline-2026-09-17 finding F1: the redesign integrity gate ran after the release was recorded as succeeded and after the external handoff POST, so it could only reject a run that had already been published. It now runs after visual-qa and before release-receipt-finalizer and handoff-emitter. Also fixes a defect the audit did not find, which would have made F1's fix inert on the durable path.

Type of change

  • type:bugfix — bug fix

Area

  • area:backend — src/, packages/

Checklist

  • npm run verify:all passes locally
  • npx tsc --noEmit passes (no type errors)
  • No secrets or credentials committed
  • ADR added/updated if this is an architectural decision — not applicable, no new architecture; the audit's preservation obligation P2 forbids new stages, receipt types or handoff contracts, and none are added
  • VALIDATION.md updated if validation artifacts changed — not applicable
  • DECISION_LOG.md updated if relevant — not applicable

Related issues / ADRs / contracts

Commits

Commit Change
a355629 Reorder the redesign integrity gate ahead of success finalization and handoff
b6605f6 Pass buildIntent into the durable pipeline's execution plan
6906e3f Lock the redesign stage order with a regression assertion
ecc2786 Correct the hard-coded stage tail in the golden fixture builder
67e357c Stop site:validate dirtying a tracked report

What the reorder does and does not change

ReleaseReceiptFinalizerStage writes status: "succeeded"; HandoffEmitterStage performs the external POST to SEO-Bot (externalMutation: true). A gate positioned after them cannot prevent an invalid redesign being published, only complain afterwards.

The gate deliberately stays outside the end-to-end guard: REDESIGN_ADDED_MANDATORY makes redesign-integrity-receipt mandatory in every mode, so folding it into that branch would make requireMandatoryConvergence throw on plan, local-proof and publish-proof redesign runs.

Two intended behavioural consequences:

  • On the visual-QA-not-passed path a redesign end-to-end run now fails as REDESIGN_PIPELINE_INCOMPLETE rather than RELEASE_EVIDENCE_INCOMPLETE. That is fail-closed-earlier, not a regression.
  • executed_stages in the emitted receipt no longer lists release-receipt-finalizer or handoff-emitter, since it snapshots ctx.stageResults at emit time. Nothing validates that field.

The defect the audit missed

src/inngest/website-pipeline.ts computed ctx.buildIntent from the spec and then built its execution plan without passing it, so redesign was always false there. A REDESIGN_IMPROVE spec submitted through Inngest ran the COPY topology — no preflight, no competitive intelligence, no redesign authority chain, and no redesign-integrity-receipt stage at all. Reordering the gate fixes the plan builder, but on that path the gate was never in the plan, so F1's fix would have been inert.

Why a .gitignore change is in a pipeline PR

validation/validation_report.yaml is per-run output of scripts/validation-executor.ts but was tracked, so every site:validate — and therefore every verify:all and every make pr — rewrote it. That hard-blocks the publish path: the release receipt is bound to a tree digest, so the gate dirtied the tree it was about to verify and refused its own push. Untracking follows the repo's existing convention (.gitignore already ignores validation/launch_env_report.json and validation/deploy_secrets_report.json). Nothing reads the committed copy; CI generates and uploads its own.

Verification

npm run verify:all — 588 tests, all passing, on 67e357c.

Two rendered-site-browser.test.ts cases initially failed on a missing Playwright browser (the container ships chromium_headless_shell-1194; the pinned Playwright 1.62.1 wants build 1234). That was fixed by installing the correct browser build so the tests genuinely run — they were not skipped, weakened or mocked.

The new ordering assertion was verified to discriminate: it fails against the parent commit's plan builder (8 pass / 1 fail) and passes against the reordered one (9 pass). The pre-existing COPY end-to-end assert.deepEqual is deliberately untouched and passes unmodified — it is the evidence that the reorder did not disturb the non-redesign topology.

Publication note

make pr ran the full gate — L4 release check, main-bound ancestry, PR overlap probe (disjoint from #207), and pr-check — and pushed this branch. It could not compose the PR body itself: open_pr_after_gate.sh requires ops/config/root-file-protection.json under the workspace root, and Website-Bot has no ops/ directory. That is a consumer-side governance wiring gap, reported separately rather than worked around by inventing a root-file policy for this repo.


Generated by Claude Code

buildFactoryExecutionPlan pushed RedesignIntegrityReceiptStage after
ReleaseReceiptFinalizerStage and HandoffEmitterStage. For an end-to-end
REDESIGN_IMPROVE run the emitted order was:

  ... visual-qa, release-receipt-finalizer, handoff-emitter,
      redesign-integrity-receipt, terminal-convergence

The finalizer writes status: "succeeded" to the release receipt and the
emitter performs the external POST to SEO-Bot plus recordWebsiteRelease
(externalMutation: true). A gate in that position can only reject a run
that has already been recorded as successful and handed off — it cannot
prevent one. An invalid redesign was therefore publishable.

The gate now runs after visual-qa, whose ctx.qualityEvidence.visualQa it
reads, and before the finalizer and emitter. It stays outside the
end-to-end guard on purpose: REDESIGN_ADDED_MANDATORY makes
redesign-integrity-receipt mandatory in every mode, so folding it into
that branch would make requireMandatoryConvergence throw on plan,
local-proof and publish-proof redesign runs.

No stage, receipt type, or handoff contract is added — this is a reorder.
The gate reads only context produced upstream of its new position and
declares no evidence block, so it imposes no runner ordering constraint.
The COPY end-to-end stage list is unchanged; the existing deepEqual
assertion in tests/unit/factory-execution-plan.test.ts passes unmodified.

Two intended consequences:

- On the visual-QA-not-passed path a redesign end-to-end run now fails as
  REDESIGN_PIPELINE_INCOMPLETE rather than RELEASE_EVIDENCE_INCOMPLETE.
  That is the fail-closed-earlier behaviour, not a regression.
- executed_stages in the emitted receipt no longer lists
  release-receipt-finalizer or handoff-emitter, since it snapshots
  ctx.stageResults at emit time. Nothing validates that field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hmr3SKRhYQ56omTzNFZ4L
…plan

The durable path computed ctx.buildIntent from the spec and then built its
execution plan without it, so buildFactoryExecutionPlan always derived
redesign === false there.

A REDESIGN_IMPROVE spec submitted through Inngest therefore ran the COPY
topology: no seo-build-intelligence-preflight, no competitive-intelligence,
the legacy content/schema stages instead of the redesign authority chain,
and no redesign-integrity-receipt stage at all. The build context said one
thing and the plan built another.

This also bounds the preceding commit. Reordering the integrity gate ahead
of the finalizer and emitter fixes the plan builder, but on this path the
gate was never in the plan to begin with, so that fix would have been inert
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hmr3SKRhYQ56omTzNFZ4L
There was no regression coverage for the terminal ordering of a redesign
plan. The two existing REDESIGN_IMPROVE tests assert only the head of the
pipeline (preflight before competitive-intelligence, competitive-intelligence
before design-intelligence), and redesign-integrity-receipt.test.ts asserts
that the stage is present, not where. All of them passed with the gate sitting
after the finalizer and the external handoff.

This asserts the chain that actually matters:

  visual-qa < redesign-integrity-receipt < release-receipt-finalizer
            < handoff-emitter < terminal-convergence

Verified to discriminate: against the parent commit's plan builder it fails
(8 pass / 1 fail); against the reordered one it passes (9 pass). A test that
cannot fail proves nothing, so it was run both ways before being committed.

The COPY end-to-end deepEqual assertion is deliberately left untouched — it
is the byte-identity guard proving the reorder did not disturb the non-redesign
topology, and editing it would destroy the evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hmr3SKRhYQ56omTzNFZ4L
executed_stages and the STAGES list that drives synthetic checkpoint
timestamps both hard-code the pipeline's terminal order as static data
rather than deriving it from buildFactoryExecutionPlan. They still carried
the pre-fix tail, with redesign-integrity-receipt trailing the finalizer
and the handoff emitter.

Nothing fails as a result — the golden oracle checks a relative ordered
subsequence that omits both of those stages, so it is satisfied by either
order. That is precisely the problem: the fixtures would have gone on
describing an ordering the code no longer has, and a reviewer re-deriving
a fixture would have been misled by it.

Data-only; no generator logic changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hmr3SKRhYQ56omTzNFZ4L
validation/validation_report.yaml is per-run output of
scripts/validation-executor.ts, but it was tracked. Every `npm run
site:validate` — and therefore every `verify:all` and every `make pr` —
rewrote it with a fresh run_id, fresh timestamps, the local absolute
target_roots and the running user's git identity.

That is not cosmetic. It hard-blocks the sanctioned publish path: the L4
release receipt is bound to a tree digest, so `make pr` runs `pr-check`,
`pr-check` dirties this file, and the L4 remote check then rejects the
push as a stale receipt. The gate invalidates the authorization it is
about to verify, and no amount of re-authorizing escapes it, because the
next run dirties the tree again.

Untracking it is the repo's own established convention, not a new one:
.gitignore already ignores validation/launch_env_report.json and
validation/deploy_secrets_report.json, which are the same class of
per-run artifact from the same directory.

Nothing consumes the committed copy. scripts/validation-executor.ts only
writes it, no test or script reads it, and build-and-validate.yml
generates its own at the repository root and uploads that as a workflow
artifact.

The file stays on disk; only the index entry is removed. As a side
benefit the committed copy stops leaking one contributor's worktree path
and email address into the repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hmr3SKRhYQ56omTzNFZ4L
Copilot AI lite review requested due to automatic review settings September 17, 2026 20:24
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

This repo is npm-only: .github/workflows/l9-lint-test.yml and
l9-lint-test-node.yml both pin PACKAGE_MANAGER="npm", regen-lockfile.yml
knows only `npm install --package-lock-only`, and four package-lock.json
files are tracked. .gitignore carried no lockfile entries at all, so a
stray pnpm-lock.yaml sat untracked and committable.

It is container debris — never tracked on any ref, and its mtime matches
every other file in the clone rather than any commit. Left unignored it is
one `git add -A` away from landing, and the dormant pnpm/yarn arms of the
lint workflows would treat it as load-bearing the moment PACKAGE_MANAGER
moved.

Appended at end of file rather than in the secrets block: PR #207 deletes
.gitignore:41, and a hunk there would overlap one written nearby.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hmr3SKRhYQ56omTzNFZ4L
(cherry picked from commit 3bf384caca440a43d4e5466957c671b35c353057)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Both hard-coded stage lists in the safehaven fixture builder went
seo-build-intelligence-preflight -> competitive-intelligence, skipping
design-reference-acquisition. The real REDESIGN_IMPROVE plan puts it between
those two (FactoryExecutionPlan.ts:275-279), and it is listed in
REDESIGN_ADDED_MANDATORY, so it is mandatory for every redesign run in every
mode.

The fixture declares mode: "end-to-end" with build_intent:
"REDESIGN_IMPROVE" and claims to represent a green run of exactly that plan,
so a stage the plan cannot omit was missing from the record. executed_stages
fed the emitted receipt and STAGES drives the synthetic checkpoint files and
the SQLite stage_runs rows, so both the receipt and the simulated stage
history described a run that could not occur.

Verified against the plan builder rather than by eye: an end-to-end
REDESIGN_IMPROVE plan emits ["seo-build-intelligence-preflight",
"design-reference-acquisition", "competitive-intelligence"] at that point,
which is now what both lists contain.

Found by Copilot's review on the stacked PR #210. It is a pre-existing gap
rather than one the preceding fixture commit introduced — that commit
corrected the tail of these lists — but the file is this PR's to get right,
and a fixture that misdescribes the pipeline is worse than no fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hmr3SKRhYQ56omTzNFZ4L
@sonarqubecloud

Copy link
Copy Markdown

@cryptoxdog
cryptoxdog merged commit 02829f0 into main Sep 19, 2026
18 checks passed
@cryptoxdog
cryptoxdog deleted the claude/audit-f1-redesign-gate-order branch September 19, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants