Files
gstack/lib/result-codes-release.ts
T
Garry Tanandcapy-ai[bot] 576839cb8c v1.91.256.0 feat(ship): review loop converges (frozen scope, 3-pass cap, NO FINDINGS) and receipts carry across conflict-only merges (#3481)
* feat(ship): review loop converges (frozen finding scope, 3-pass cap, NO FINDINGS first-class) and receipts carry across conflict-only merges

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(ship): keep the Step 9 first-entry and Fix-First phrasing the skip/requeue contract asserts; fill the review record's pass, review_scope and range_base placeholders in the caller template test; narrow a test type

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (pass 1) and the review-loop budget wording

Two blocking findings from /ship's own pass 1, both reproduced in the QA smoke before the fix:
gstack-ship-receipt carry hashed the product diff with git patch-id --stable, which strips
whitespace, so a space moved in a reviewed shell line carried as "no product change"; and it
classed package.json and every lockfile as release metadata, so a dependency swap carried too.
The patch-id is now --verbatim, package.json and VERSION are metadata only while the change is a
version stamp (lib/pregate/stamp-only.ts), lockfiles are product unless the policy names them,
the test globs name code extensions, a dirty working tree refuses before anything is compared,
and a receipt carried twice keeps the reviewed head as carry.from.

gstack-review-scope: an unresolvable base is POLICY_SOURCE_UNAVAILABLE instead of a silent
working-tree read; --range-base is validated; git runs with core.quotePath=false; range_base is
null on pass 1; a blocking finding with no path is fix; category "testing" is a test finding
below the floor; defer searches once, files nothing on a failed search and exits 0 unfiled;
summary refuses zero matching passes and the receipt write is chained to it with &&; a finding
the diff causes elsewhere names the changed line in `cause` and classify follows it.

Review-loop budget wording (ideas from tstack's sol-loop and pstack's interrogate, rewritten;
NOTICE.md): the blocker / should / nit glossary and two stop rules in Step 9.4, the checklist
and docs/review-policy.md; a lead-judgment stage in the finding merge both /review and /ship
render. The core read diffs $RANGE_BASE from pass 2 on. Caps and pins re-measured with reasons.

Step 7 coverage audit: four tests added in existing files, the --review-loop case moved to its
own test (the shared test tripped bun's 5 s default).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pin the renumbered review-army stages and point the wording test at product code

CI at c3e27fc failed three free-tier tests, all this branch's own: the
workflow-required-steps fixture and shared-libs-rendering still pinned the
pre-Lead-judgment stage numbers, and test-of-test-ratchet counted
review-loop-budget-wording.test.ts as test-only code because it read only
generated markdown. The fixture and rendering test now pin stages 1-8, and the
wording test renders both armies through generateReviewArmy and reads the
authored ship template as well as the generated sections.

Pass-2 maintainability notes folded in: docs/troubleshooting.md names the two
new RECEIPT_CARRY_REFUSED causes (dirty tree, unreadable status) and the two new
REVIEW_SCOPE_INPUT causes (non-hex --range-base, summary with no matching
pass); gstack-review-scope's help states that classify reads the base branch's
policy and refuses an unresolvable base unless --policy is passed.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: date a review-log row from its binding start when it has no timestamp

The Step 9.4 template asks the writer to fill `timestamp`; the logger stamps
`review_binding.started_at` but never `timestamp`. `gstack-review-scope
summary` dropped such a row silently, so this PR's own pass 2 was missing from
REVIEW_LOOP (passes=1 for a two-pass loop). A row now dates from `timestamp`,
else from its binding start; a row with neither is still not counted.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pass-4 review fixes (carry stamp rule, deleted-file locus, scanned defer, diff prefixes, row time)

Five findings at the blocking floor from the Step 11 outside voice (gpt-6.1-sol)
and the native adversarial pass, fixed as a fourth pass approved by GStack
Minder under the three-pass policy:

- carry: a path the policy names under stamp_files, stamp_paths or
  release_outputs is metadata only while its change is a stamp (versionStampOnly
  for a manifest kind, stampOnlyText otherwise), the rule the tree receipt
  applies; a logic change in a stamped source file is product. Every
  metadata-listed path is read at both heads and a non-stamp change drops its
  globs from the product exclusion.
- review-scope: parseHunks keeps a deleted file as a whole changed location, so
  a cause anchored in a removed export is the diff's to fix, not deferred.
- defer: issues are filed through lib/gstack-post's publication scan; a HIGH
  finding in the title or body refuses and a MEDIUM one needs a human, both
  recorded as unfiled with the reason; --repo names the destination.
- classify: both diffs pin --src-prefix=a/ --dst-prefix=b/ --no-textconv, so
  diff.mnemonicPrefix cannot send every finding outside.
- summary: a row dates from review_binding.started_at before the writer-typed
  timestamp, so a date-only value cannot drop a real pass.

Docs: ship-policy.md#receipt-carry, review-policy.md, the Step 9.4 template
and the CHANGELOG gate-integrity note record the rule and the pass.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(review-army): assert the Red Team branch the run took in the N+1 case

Under the blocking floor the Performance specialist rates a 12-line N+1
INFORMATIONAL, so the conditional Red Team does not activate; the case
encoded the old severity (N+1 is CRITICAL, a Red Team always follows). It
now asserts the branch the run took: a Red Team that ran must follow the
Performance specialist; no Red Team is correct only when the report's own
count says 0 critical and its Red Team line names that reason. The N+1
detection assertion is unchanged. The free contract test gains the
informational-not-activated and informational-claimed controls and the
budget test's synthetic report carries the count line.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.256.0 stamp

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-26 20:48:01 +00:00

233 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Result codes for the release path: gstack-ship-queue and the ship-policy
* floor, pregate timing and its working-tree fingerprint, regen with missing
* dependencies, approval classes and
* the state-machine matrix. Kept beside lib/result-codes.ts (which spreads
* this table into RESULT_CODES) so
* the owner module stays within the module-size ratchet; the same rules apply:
* one stable docs/troubleshooting.md anchor, a summary and a fix clause each.
*/
import type { ResultCode } from './result-codes';
export const RELEASE_RESULT_CODES = {
// Field list 2, W1: gstack-ship-queue and the ship-policy compatibility floor.
POLICY_GSTACK_TOO_OLD: {
anchor: 'policy-gstack-too-old',
summary: 'the ship policy sets min_gstack_version above this gstack, so this gstack refuses to read it',
fix: 'upgrade gstack (bin/gstack-upgrade, or the session install with --for <skill>) to the version named, then rerun',
},
QUEUE_NOT_READY: {
anchor: 'queue-not-ready',
summary: 'the lane is not ready to report: the working tree, HEAD or CI state does not match the pushed PR head',
fix: 'commit and push the work (or wait for CI on that exact head), then rerun gstack-ship-queue ready',
},
QUEUE_PROOF_MISSING: {
anchor: 'queue-proof-missing',
summary: 'a proof input the queue step needs is missing or does not match (gated head, base, stamp journal, version or tree)',
fix: 'supply the named input (fetch the gated head, pass the expected sha), or regate; nothing was written',
},
QUEUE_STAMP_NOT_STAMP_ONLY: {
anchor: 'queue-stamp-not-stamp-only',
summary: 'the stamp commit differs from the gated head by more than stamps, so the gate does not cover it',
fix: 'inspect the printed residual paths; drop the stray edit, or run the gate again on the stamped head',
},
QUEUE_VERIFY_MISMATCH: {
anchor: 'queue-verify-mismatch',
summary: 'the coordinator could not reproduce the lane’s stamp claim: the PR head, the recomputed gate-ahead merge or the gated tree differs',
fix: 'do not merge; ask the lane to rerun gstack-ship-queue stamp (or gate-ahead) on the current queue and report again',
},
QUEUE_GATE_MISSING: {
anchor: 'queue-gate-missing',
summary: 'the policy sets full_e2e and no passing full-gate record is bound to this tree',
fix: 'run the gate the policy names (gstack-ship-queue gate-ahead, then --record) or pass the record of a passing run on this head',
},
GATE_AHEAD_ATTEMPT_INVALID: {
anchor: 'gate-ahead-attempt-invalid',
summary: 'the gate-ahead attempt is unknown, already recorded, or its worktree no longer holds the gated commit',
fix: 'list attempts with gstack-ship-queue gate-ahead --list, then record the live one, or prepare a new attempt',
},
// GSTA-49 lane T1: merge trains in gstack-ship-queue (docs/ship-policy.md#run-a-merge-train).
QUEUE_TRAIN_OFF: {
anchor: 'queue-train-off',
summary: 'the base revision’s .gstack/ship-policy.json declares no `train` block, so the merge-train surface is off and the per-PR queue applies',
fix: 'use the per-PR queue (ready, stamp, verify), or land a `train` block with min_gstack_version on the base branch first',
},
QUEUE_TRAIN_UNTRUSTED: {
anchor: 'queue-train-untrusted',
summary: 'a PR’s head repository is not the base repository; fork work never runs in a train (the train branch and its dispatch carry repository secrets)',
fix: 'leave the PR out of the train (gstack-ship-queue train build --prs <list without it>); fork work stays on the trusted-branch path',
},
QUEUE_TRAIN_MOVED: {
anchor: 'queue-train-moved',
summary: 'a PR’s head (or the train branch) is no longer the SHA the train pinned, so the train would not test what merges',
fix: 'ask the lane for a fresh READY on the new head, then rebuild the train with the new pin (train build --prs <n>@<new sha>,...)',
},
QUEUE_TRAIN_CONFLICT: {
anchor: 'queue-train-conflict',
summary: 'merging a PR onto the previous prefix conflicts outside the version files and the CHANGELOG; nothing was dispatched and no branch pushed',
fix: 'run the printed train build without that PR; the PR’s lane merges main (with the earlier PRs once they land) and reports READY again',
},
QUEUE_TRAIN_FREE_RED: {
anchor: 'queue-train-free-red',
summary: 'the free tests dispatched on train/<nonce> are red, missing, cancelled or ran on another SHA, so the paid gate was not dispatched',
fix: 'read the free-tests run; a PR-caused red drops that PR (train build --prs <list without it>), a flaky one goes to its owner before the next build',
},
QUEUE_TRAIN_DISPATCH_UNKNOWN: {
anchor: 'queue-train-dispatch-unknown',
summary: 'a train dispatch was attempted but its run could not be found by nonce, so whether it started is unknown; it is never re-dispatched blindly',
fix: 'run gstack-ship-queue train status --train <nonce>: it finds the run by nonce, or marks the dispatch absent so train build --train <nonce> may dispatch it',
},
QUEUE_TRAIN_WAITING: {
anchor: 'queue-train-waiting',
summary: 'the train is below its size and its oldest READY PR has waited less than the policy’s fill time, so it does not dispatch yet',
fix: 'add READY PRs, rerun once the printed time has passed, or pass --urgent for a one-PR train',
},
QUEUE_TRAIN_PENDING: {
anchor: 'queue-train-pending',
summary: 'the train’s run (free tests, the full gate or the bisect) has not finished, so there is no verdict yet',
fix: 'rerun the printed NEXT: command later; nothing is dispatched again while a run is pending',
},
QUEUE_TRAIN_INCOMPLETE: {
anchor: 'queue-train-incomplete',
summary: 'the train gate did not complete its census (INFRA or INCOMPLETE panels, an unbound, partial or cancelled shard, a slice killed before upload, or no published record), so it is never green',
fix: 'when every gap is machine-classified INFRA or INCOMPLETE, take the one re-dispatch EVAL_POLICY.infraRedispatch allows (train rebuild --train <nonce> --infra) and report both runs; otherwise diagnose the run',
},
QUEUE_TRAIN_RED: {
anchor: 'queue-train-red',
summary: 'the train’s full gate is red on the train tree; its PRs do not merge, and nothing is retried to change the verdict',
fix: 'bisect the red cases (train bisect --train <nonce> --spend-cap <usd>); after a rebuild’s second red, route the cases to a repair lane',
},
QUEUE_TRAIN_RED_ON_MAIN: {
anchor: 'queue-train-red-on-main',
summary: 'the bisect found the red cases red on every prefix, main (T0) included: main is broken, not the train’s PRs',
fix: 'fix forward on main first (a one-PR train), then rebuild this train unchanged',
},
QUEUE_TRAIN_UNATTRIBUTED: {
anchor: 'queue-train-unattributed',
summary: 'the bisect pattern blames no PR and not main (all green, or not monotone); one trial cannot tell a flake from a break',
fix: 'all green: the one preregistered rebuild (train rebuild --train <nonce>), reporting both runs; any other pattern: a repair lane, then a changed train',
},
QUEUE_TRAIN_BISECT_CAPPED: {
anchor: 'queue-train-bisect-capped',
summary: 'the bisect’s reserved price (every case×prefix trial, unknown costs at the policy reservation) passes the spend cap; completed prefixes are kept and the outcome stays unattributed',
fix: 'resume with a higher cap (train bisect --train <nonce> --spend-cap <usd>); only prefixes without a result run',
},
QUEUE_TRAIN_PREFIX_INVALID: {
anchor: 'queue-train-prefix-invalid',
summary: 'a bisect prefix is not an ancestor of the train head on train/<nonce>, or a red case is not in the gate’s plan, so the bisect inputs are refused',
fix: 'check that train/<nonce> still points at the recorded head (train status); a moved branch needs a new train',
},
QUEUE_TRAIN_REBUILD_REFUSED: {
anchor: 'queue-train-rebuild-refused',
summary: 'the rebuild is not allowed: the bisect was not all green, the train was already rebuilt (or re-dispatched), or there is no bisect or INFRA record',
fix: 'route the red cases to a repair lane; the train is rebuilt only once the repair merges (a changed train)',
},
QUEUE_TRAIN_TREE_MISMATCH: {
anchor: 'queue-train-tree-mismatch',
summary: 'main’s tree is neither the train’s prefix before this PR nor after it (modulo stamps), so the merge does not publish what the gate tested',
fix: 'stop merging; if earlier PRs of the train merged, the queue is frozen (QUEUE_TRAIN_FROZEN); otherwise rebuild the train on current main',
},
QUEUE_TRAIN_FROZEN: {
anchor: 'queue-train-frozen',
summary: 'a train stopped partway through publication, so main holds a prefix no gate covered on its own; ordinary trains are refused until a repair lands',
fix: 'admit one repair PR at its exact head (train build --repair --prs <n>@<sha>); the freeze clears when verify finds main’s tree after that merge in a green record',
},
// CREW rule 22 (Garry, 2026-10-25): /ship runs before every READY; the queue refuses a PR whose body has no current receipt.
QUEUE_RECEIPT_MISSING: {
anchor: 'queue-receipt-missing',
summary: 'the PR body carries no valid /ship receipt, so the PR was never shipped through gstack and cannot be READY’d, admitted to a train or merged',
fix: 'run /ship on the branch (unattended it writes the receipt into an open PR at HEAD with gstack-ship-receipt write --pr <n> --body …, else leaves it in .gstack/tmp/ship-receipt.md for the parent to post), then gstack-ship-queue ready again; there is no flag to skip this',
},
QUEUE_RECEIPT_STALE: {
anchor: 'queue-receipt-stale',
summary: 'the PR body’s /ship receipt is for another head than the one being READY’d or merged (a push after /ship, or a receipt copied from another PR)',
fix: 'rerun /ship on the current head so the receipt names it, then gstack-ship-queue ready again; a slot’s stamp commit keeps the receipt written for its pinned head',
},
QUEUE_SCREEN_STALE_REF: {
anchor: 'queue-screen-stale-ref',
summary: 'GitHub’s refs/pull/<n>/merge is missing or still merges an older head than the PR’s: the PR conflicts with the base (a CHANGELOG-only conflict counts), so a screen dispatched on it would plan no cases and report success',
fix: 'resolve the conflict with the base branch (or wait for GitHub to recompute the merge ref), push, then gstack-ship-queue ready --pr <n> --screen again; a lane never READYs on a screen that planned nothing',
},
READY_SCREEN_CHOICE_REQUIRED: {
anchor: 'ready-screen-choice-required',
summary: 'the base policy turns trains on, so READY needs an explicit screen choice: --screen (pay for a diff-relevant screen now) or --no-screen',
fix: 'rerun gstack-ship-queue ready with the printed recommendation (--screen or --no-screen); say in the READY block if focused local cases already ran',
},
// Field list 2 W4 (pregate `timing`).
PREGATE_TIMING_ASSERTION: {
anchor: 'pregate-timing-assertion',
summary: 'an added or changed test line compares an elapsed wall-clock or CPU time against a bound, or sleeps and then asserts (a warning, not a block)',
fix: 'assert on the outcome, an injected clock or a forced deadline instead; when the bound is the contract, add `gstack-timing-ok: <reason>` in a comment on the line or the line above',
},
// Lane D (GSTA-112): the no-policy warning and the environment-cause verdict.
PREGATE_NO_POLICY: {
anchor: 'pregate-no-policy',
summary: 'the repository has no pregate policy yet (no .gstack/pregate.json on the base branch and no user-owned file) and a touched file maps to no test lane; a warn, with a starter policy printed',
fix: 'review the printed starter, write it with `gstack-pregate init`, edit the TODO entries, then commit .gstack/pregate.json on the base branch; from then on an unmapped file is PREGATE_NO_LANE',
},
PREGATE_ENVIRONMENT: {
anchor: 'pregate-environment',
summary: 'a check could not run because of the machine, not the diff: a runner or guard binary missing from PATH, a network error or quota refusal before any test ran, a full disk, or an unverified runtime; incomplete, never pass',
fix: 'read the `environment:` line (kind and evidence), repair that on this machine or run the lane remotely (`--remote`, `gstack-pregate clear`), then rerun; a waiver cites the kind, never a test name',
},
// Field list 2, W2 (approval classes) and W3 (state-machine round-trip matrix).
APPROVAL_CLASSES_INVALID: {
anchor: 'approval-classes-invalid',
summary: '`.gstack/approval-classes.json` is malformed, a class matches nothing, or a path escapes the repository',
fix: 'keep `classes` a non-empty list of uniquely named classes, each with `paths`, `patterns` or `migrations: true`, and every glob repo-relative (`gstack-approval-classes validate` checks the working-tree copy)',
},
APPROVAL_CLASS_FOUND: {
anchor: 'approval-class-found',
summary: 'the branch changes something a declared approval class covers',
fix: 'get the owner\'s approval and put `Approved by <owner> <YYYY-MM-DD> via <plan sha|comment or review URL> for <class>@<digest12>` in the PR body, using the APPROVAL_SCOPE token the check printed',
},
APPROVAL_CITATION_MISSING: {
anchor: 'approval-citation-missing',
summary: 'a declared approval class has no citation that resolves and binds to its current effects, so no receipt was written',
fix: 'add an `Approved by` line whose plan (with --approval-run <dir>) or maintainer comment names the APPROVAL_SCOPE token or the class and every effect path; for evidence gstack cannot read, attest it with `for <class>@<digest12>` on the line. Only the owner approves: never write the line without their approval',
},
APPROVAL_SOURCE_UNAVAILABLE: {
anchor: 'approval-source-unavailable',
summary: 'origin/<base> is not a resolvable commit, so the committed .gstack/approval-classes.json cannot be read and nothing was checked',
fix: 'git fetch origin <base> (classes are never read from the working tree)',
},
STATE_MATRIX_INCOMPLETE: {
anchor: 'state-matrix-incomplete',
summary: 'the state-machines specialist output lacks a kind × owner action cell, maps a cell to a test that does not exist, or leaves an untested cell without a CRITICAL finding',
fix: 'rerun the state-machines specialist on the same diff; its review coverage stays incomplete until gstack-state-matrix check passes',
},
// Fix wave 2026-10-15, PR 5 (item 10b, #3197).
WTREE_UNAVAILABLE: {
anchor: 'wtree-unavailable',
summary: 'cannot fingerprint the working tree: bin/gstack-wtree could not start (on Windows, no Git Bash was found) or it ran and failed',
fix: 'on Windows point GSTACK_GIT_BASH at Git Bash\'s bash.exe (Git Bash: export GSTACK_GIT_BASH=\'C:/Program Files/Git/bin/bash.exe\'; PowerShell: $env:GSTACK_GIT_BASH = \'C:\\Program Files\\Git\\bin\\bash.exe\'), or install Git for Windows (winget install --id Git.Git -e); elsewhere rerun ./setup so bin/gstack-wtree is present; then rerun gstack-pregate',
},
// Fix wave W5 (GSTA-31): pregate regen without installed dependencies.
REGEN_DEPS_MISSING: {
anchor: 'regen-deps-missing',
summary: 'a regen command exited 0 with different outputs while package.json declares dependencies and node_modules is missing, so the outputs were not verified',
fix: 'install the dependencies (the printed command, such as bun install), then rerun the check',
},
// The /ship review loop's finding scope (gstack-review-scope) and the receipt carry (gstack-ship-receipt carry).
REVIEW_SCOPE_INPUT: {
anchor: 'review-scope-input',
summary: 'gstack-review-scope was given input it cannot classify: findings that are not a JSON array of objects, a --pass or --policy outside its values, or a pass 2+ call without --range-base',
fix: 'pass the Step 9.3 findings as a JSON array (--findings <file> or stdin), --pass <n> from the invocation record, and from pass 2 on --range-base <sha> (HEAD at the start of the previous pass)',
},
REVIEW_SCOPE_RANGE: {
anchor: 'review-scope-range',
summary: 'gstack-review-scope could not read the diff it scopes findings against: the base or the range base does not resolve, or the range base is not an ancestor of HEAD',
fix: 'fetch origin/<base>, check --range-base against the saved pass heads, and start a full pass (--pass 1) when history was rewritten',
},
RECEIPT_CARRY_REFUSED: {
anchor: 'receipt-carry-refused',
summary: 'gstack-ship-receipt carry could not carry the /ship receipt to HEAD: the PR body has no receipt, the receipt head is not an ancestor of HEAD (rebase or force-push), or the product diff could not be compared',
fix: 'merge the base in instead of rebasing, keep the reviewed head in HEAD\'s history, then rerun carry; otherwise run /ship on the new head',
},
RECEIPT_CARRY_PRODUCT_CHANGED: {
anchor: 'receipt-carry-product-changed',
summary: 'the product diff against the base changed between the receipt\'s head and HEAD (a product file differs beyond a base merge, a test-only fix or release metadata), so the reviewed verdict does not cover HEAD',
fix: 'run /ship on the new head; the receipt carries only when every changed path is a test, release metadata or the base merge and the product patch-id is unchanged',
},
} as const satisfies Record<string, ResultCode>;