Merge main e85005482 (v1.93.27.0, train 94) into wave/review-53cfb5bfe011 (rule 19 grant)

Conflicts resolved keeping both sides: bin/gstack-review-dispatch (main's targetedRead record plus the wave's exclude digest on the rendered event); bin/gstack-ship-receipt (main's receiptTargetedAtHead and targetedClassesSince plus the wave's receiptReviewAtHead and receiptExclude; both help paragraphs); docs/troubleshooting.md (main's targeted-refusal fix plus the wave's review_exclude, REVIEW_DIFF_TOO_LARGE, REVIEW_DIFF_UNREADABLE and REVIEW_EXCLUDE_REFERENCE entries); ship/DRIVER.md.tmpl (main's line, DRIVER.md 12,241 B); test/review-packets-removed.test.ts (both replay lists, the wave's placeholder brief-body hashes under main's GST-986 test). test/review-targeted.test.ts follows main's DRIVER wording ([--review-targeted @<f>]), which main's own copy of that check in test/review-targeted-record.test.ts pins.
This commit is contained in:
garrytan committed 2026-11-02 23:50:02 +00:00
commit feeec2e760
74 files changed
+1618 -316

No files matched your search

+1 -1
View File
@@ -59,7 +59,7 @@ describe('factory-scorecard: recorded window', () => {
expect(values.merged).toBe('3 (crew 2, outside 1)');
expect(values.lead_time).toBe('median 8.0h, p90 5.0d');
expect(values.ready_to_merge).toBe('median 1.0h, p90 2.0h (2 of 3 PRs with a READY comment)');
expect(values.review_passes).toBe('median 2, p90 3 (2 of 3 PRs report a pass count; 1 at the cap of 3)');
expect(values.review_passes).toBe('median 2, p90 3 (2 of 3 PRs report a pass count; 2 at the cap of 2)');
expect(values.gates).toBe('2 / 1 / 1; known cost $43.00 on 2 of 4 runs');
expect(values.cost_per_merge).toBe('$14.33 (2 runs without a known cost)');
expect(values.red_train).toBe('50% (1 red of 2 trains; 0 red backstop, 0 red screens)');
+115
View File
@@ -0,0 +1,115 @@
{
"calls": [
{
"preReview": true,
"observedAtMs": 76003,
"call": {
"sessionId": "4767bbfe-f301-4d0c-9cfb-eeff94eb0850",
"toolUseId": "toolu_01S2K6zpLEy8wZXVMiG7teSA",
"questions": [
{
"question": "D1 — Review focus for the dashboard plan\nProject/branch/task: main, design review of the User Dashboard Page plan (review-input.md).\nELI10: I've rated this plan 4/10 on design completeness. The data and safety rules are solid, but it never says what the user sees first, how the three panels sit on phone vs desktop, or what the empty/error/toast/modal moments actually say and do. I'll generate visual mockups next, then review all 7 design dimensions. You can narrow that to specific areas instead.\nStakes if we pick wrong: Narrowing too early leaves gaps like toast accessibility or mobile layout for the implementer to guess at, and they ship as generic defaults.\nRecommendation: A because the gaps span hierarchy, states, responsive and a11y, so a partial pass would miss real issues.\nCompleteness: A=10/10, B=6/10\nPros / cons:\nA) Full review, all 7 passes (recommended)\n ✅ Covers hierarchy, states, journey, slop risk, design system, responsive and a11y\n ✅ Every gap gets its own decision so nothing is silently assumed\n ❌ More questions to answer, one per real gap found across the passes\nB) Focus on specific areas only\n ✅ Fewer questions if you only care about, say, states and mobile layout\n ✅ Faster path to an amended plan for the areas you name\n ❌ Unreviewed dimensions stay at their current low rating and ship as guesses\nNet: Full coverage with more decisions, versus speed with known blind spots.",
"header": "Focus",
"multiSelect": false,
"options": [
{
"label": "Full review, all 7 (recommended)",
"description": "Mockups, then all 7 passes. ✅ Covers every dimension. ✅ Each gap decided individually. ❌ More questions overall."
},
{
"label": "Focus on specific areas",
"description": "Name the areas in a note. ✅ Fewer questions. ✅ Faster. ❌ Other dimensions remain unreviewed guesses."
}
]
}
],
"answered": true,
"failed": false,
"answers": {
"D1 — Review focus for the dashboard plan\nProject/branch/task: main, design review of the User Dashboard Page plan (review-input.md).\nELI10: I've rated this plan 4/10 on design completeness. The data and safety rules are solid, but it never says what the user sees first, how the three panels sit on phone vs desktop, or what the empty/error/toast/modal moments actually say and do. I'll generate visual mockups next, then review all 7 design dimensions. You can narrow that to specific areas instead.\nStakes if we pick wrong: Narrowing too early leaves gaps like toast accessibility or mobile layout for the implementer to guess at, and they ship as generic defaults.\nRecommendation: A because the gaps span hierarchy, states, responsive and a11y, so a partial pass would miss real issues.\nCompleteness: A=10/10, B=6/10\nPros / cons:\nA) Full review, all 7 passes (recommended)\n ✅ Covers hierarchy, states, journey, slop risk, design system, responsive and a11y\n ✅ Every gap gets its own decision so nothing is silently assumed\n ❌ More questions to answer, one per real gap found across the passes\nB) Focus on specific areas only\n ✅ Fewer questions if you only care about, say, states and mobile layout\n ✅ Faster path to an amended plan for the areas you name\n ❌ Unreviewed dimensions stay at their current low rating and ship as guesses\nNet: Full coverage with more decisions, versus speed with known blind spots.": "Full review, all 7 (recommended)"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-11-02T08:39:03.879Z"
}
},
{
"preReview": true,
"observedAtMs": 106367,
"call": {
"sessionId": "4767bbfe-f301-4d0c-9cfb-eeff94eb0850",
"toolUseId": "toolu_01AqE7XDKvovVJAvKF31kq8z",
"questions": [
{
"question": "Want outside design voices before the detailed review?\nD2 — Outside design voices\nProject/branch/task: main, design review of the User Dashboard Page plan.\nELI10: Optionally, two other reviewers read the plan before my 7 passes: Codex checks hard design rules and a litmus scorecard, and an independent Claude subagent checks completeness. Their disagreements get raised in the relevant pass.\nStakes if we pick wrong: Skipping means one reviewer's eye; running costs a few minutes and may only get one voice if Codex is missing.\nRecommendation: No, proceed without — this is a taste call, no strong preference either way\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons: see option descriptions.\nNet: A cross-model check versus the shortest path to a fixed plan.",
"header": "Voices",
"multiSelect": false,
"options": [
{
"label": "Yes, run outside design voices",
"description": "✅ Codex hard-rule + litmus scorecard plus an independent Claude completeness read. ✅ Disagreements surface with both perspectives in the relevant pass. ❌ Adds a few minutes; Codex may be missing here, in which case only the Claude voice runs and it's tagged single-model."
},
{
"label": "No, proceed without (recommended)",
"description": "✅ Straight into the 7 passes with the approved mockup as anchor. ✅ Fewest steps to a fixed plan. ❌ Findings rest on one reviewer; no cross-model check on slop risk or hierarchy."
}
]
}
],
"answered": true,
"failed": false,
"answers": {
"Want outside design voices before the detailed review?\nD2 — Outside design voices\nProject/branch/task: main, design review of the User Dashboard Page plan.\nELI10: Optionally, two other reviewers read the plan before my 7 passes: Codex checks hard design rules and a litmus scorecard, and an independent Claude subagent checks completeness. Their disagreements get raised in the relevant pass.\nStakes if we pick wrong: Skipping means one reviewer's eye; running costs a few minutes and may only get one voice if Codex is missing.\nRecommendation: No, proceed without — this is a taste call, no strong preference either way\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons: see option descriptions.\nNet: A cross-model check versus the shortest path to a fixed plan.": "No, proceed without (recommended)"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-11-02T08:39:34.676Z"
}
},
{
"preReview": false,
"observedAtMs": 132955,
"call": {
"sessionId": "4767bbfe-f301-4d0c-9cfb-eeff94eb0850",
"toolUseId": "toolu_01PDy4qdecNenP7T1QhxRkFR",
"questions": [
{
"question": "D3 — 1: What does a just-logged-in user see first, second, third?\nProject/branch/task: main, User Dashboard plan, Pass 1 Information Architecture.\nELI10: The plan has three panels but no order. If we don't pick, the engineer puts them in a grid of three equal boxes and nobody knows where to look. Only notifications ask the user to do something, so they're the natural anchor.\nStakes if we pick wrong: Users skim past the one thing needing attention, or the page feels like a generic widget grid.\nRecommendation: 1A because unread notifications are the only actionable content, and Krug says each screen needs one obvious next step.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Action row, then Notifications primary, Activity secondary (recommended)\n ✅ The one actionable thing (unread items) gets the wide main column and first scan\n ✅ Quick actions sit in the page header as a compact row, one primary button\n ❌ Activity feed is visually demoted; heavy activity users scroll or look right\nB) Activity primary, Notifications in a side rail\n ✅ Suits products where 'what changed' matters more than inbox triage\n ✅ Notifications rail stays visible while scanning activity history\n ❌ Unread items compete in a narrow rail and are easier to ignore\nNet: Anchor on what needs action (notifications) or on what changed (activity).",
"header": "Hierarchy",
"multiSelect": false,
"options": [
{
"label": "1A Notifications primary (recommended)",
"description": "Header + quick-action row first, Notifications wide main column second, Activity secondary column third.",
"preview": "lg (≥1024px)\n+------------------------------------------------+\n| Dashboard [Open workspace] [Review notifications (3)] |\n+-------------------------------+----------------+\n| Notifications 3 unread | Recent activity|\n| [Mark all as read] | • edited ... |\n| ● New comment on Q4 plan 2m | • shared ... |\n| ● ... | • ... |\n| ... | |\n+-------------------------------+----------------+\n 1st: page title + primary action\n 2nd: unread notifications\n 3rd: recent activity"
},
{
"label": "1B Activity primary",
"description": "Header + quick-action row first, Activity wide main column second, Notifications side rail third.",
"preview": "lg (≥1024px)\n+------------------------------------------------+\n| Dashboard [Open workspace] [Review notifications (3)] |\n+-------------------------------+----------------+\n| Recent activity | Notifications |\n| • You edited Q4 roadmap 2m | 3 unread |\n| • ... | [Mark all read]|\n| • ... | ● ... |\n+-------------------------------+----------------+\n 1st: page title + primary action\n 2nd: recent activity\n 3rd: notifications rail"
}
]
}
],
"answered": true,
"failed": false,
"answers": {
"D3 — 1: What does a just-logged-in user see first, second, third?\nProject/branch/task: main, User Dashboard plan, Pass 1 Information Architecture.\nELI10: The plan has three panels but no order. If we don't pick, the engineer puts them in a grid of three equal boxes and nobody knows where to look. Only notifications ask the user to do something, so they're the natural anchor.\nStakes if we pick wrong: Users skim past the one thing needing attention, or the page feels like a generic widget grid.\nRecommendation: 1A because unread notifications are the only actionable content, and Krug says each screen needs one obvious next step.\nNote: options differ in kind, not coverage — no completeness score.\nPros / cons:\nA) Action row, then Notifications primary, Activity secondary (recommended)\n ✅ The one actionable thing (unread items) gets the wide main column and first scan\n ✅ Quick actions sit in the page header as a compact row, one primary button\n ❌ Activity feed is visually demoted; heavy activity users scroll or look right\nB) Activity primary, Notifications in a side rail\n ✅ Suits products where 'what changed' matters more than inbox triage\n ✅ Notifications rail stays visible while scanning activity history\n ❌ Unread items compete in a narrow rail and are easier to ignore\nNet: Anchor on what needs action (notifications) or on what changed (activity).": "1A Notifications primary (recommended)"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-11-02T08:40:01.258Z"
}
}
],
"provenance": {
"kind": "public native AskUserQuestion calls and answers only (observation.json fingerprints[].nativeCall, byte-exact)",
"runId": 41414111284,
"artifact": "paid-slice-6-a1",
"prNumber": null,
"headSha": "4a280637792b46576474f3349a1ba2e977c5f5c9",
"testedSha": "4a280637792b46576474f3349a1ba2e977c5f5c9",
"recordedOutcome": "ceiling_reached",
"recordedReviewCount": 1,
"recordedStep0Count": 2,
"recordedPassed": false,
"note": "Step 0D titled \"Review focus\" with header Focus, the 4/10 design-completeness rating in its ELI10 body, and an all-7 option that does not lead with the count (\"Full review, all 7 (recommended)\")"
}
}
+5 -13
View File
@@ -521,7 +521,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
runs items 1 and 3–5; each decision left to you is a `STOP:` naming its rerun flag
(`--bump`, `--accept-incomplete`, `--version <v>`, `--keep-current`, `--answer`).
`ship: queue_mode stamp-at-merge` replaces items 1–4.
`VERSION: skipped` or `VERSION: <v> kept (…; no write)` ends Step 12 with
`NEW_VERSION` unset: Step 13 follows its `NEXT:`; Steps 18 and 20 take NO_VERSION's branch.
1. **Classify state** — pure reader, never writes:
```bash
@@ -642,19 +643,10 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
migrations may stand alone. Under 50 lines across fewer than 4 files may be one commit.
2. Order dependencies first: infrastructure → models/services → controllers/views.
Each commit works alone (no broken imports or missing code).
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body; fixing a
user-named issue adds a `Fixes: #<n>` trailer. Only the final
VERSION/CHANGELOG commit gets the release version and co-author trailer. No Git tag:
```bash
git commit -m "$(cat <<'EOF'
chore: bump version and changelog (vX.Y.Z.W)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
user-named issue adds a `Fixes: #<n>` trailer. Leave VERSION, CHANGELOG/fragment, TODOS.md
and the digest uncommitted: Step 16's `finish` commits them last with
`--trailer 'Co-Authored-By: Claude <noreply@anthropic.com>'`. Do not add a Git tag.
---
+5 -13
View File
@@ -520,7 +520,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
runs items 1 and 3–5; each decision left to you is a `STOP:` naming its rerun flag
(`--bump`, `--accept-incomplete`, `--version <v>`, `--keep-current`, `--answer`).
`ship: queue_mode stamp-at-merge` replaces items 1–4.
`VERSION: skipped` or `VERSION: <v> kept (…; no write)` ends Step 12 with
`NEW_VERSION` unset: Step 13 follows its `NEXT:`; Steps 18 and 20 take NO_VERSION's branch.
1. **Classify state** — pure reader, never writes:
```bash
@@ -641,19 +642,10 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
migrations may stand alone. Under 50 lines across fewer than 4 files may be one commit.
2. Order dependencies first: infrastructure → models/services → controllers/views.
Each commit works alone (no broken imports or missing code).
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body; fixing a
user-named issue adds a `Fixes: #<n>` trailer. Only the final
VERSION/CHANGELOG commit gets the release version and co-author trailer. No Git tag:
```bash
git commit -m "$(cat <<'EOF'
chore: bump version and changelog (vX.Y.Z.W)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
user-named issue adds a `Fixes: #<n>` trailer. Leave VERSION, CHANGELOG/fragment, TODOS.md
and the digest uncommitted: Step 16's `finish` commits them last with
`--trailer 'Co-Authored-By: Claude <noreply@anthropic.com>'`. Do not add a Git tag.
---
+5 -13
View File
@@ -520,7 +520,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
runs items 1 and 3–5; each decision left to you is a `STOP:` naming its rerun flag
(`--bump`, `--accept-incomplete`, `--version <v>`, `--keep-current`, `--answer`).
`ship: queue_mode stamp-at-merge` replaces items 1–4.
`VERSION: skipped` or `VERSION: <v> kept (…; no write)` ends Step 12 with
`NEW_VERSION` unset: Step 13 follows its `NEXT:`; Steps 18 and 20 take NO_VERSION's branch.
1. **Classify state** — pure reader, never writes:
```bash
@@ -644,19 +645,10 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
migrations may stand alone. Under 50 lines across fewer than 4 files may be one commit.
2. Order dependencies first: infrastructure → models/services → controllers/views.
Each commit works alone (no broken imports or missing code).
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body; fixing a
user-named issue adds a `Fixes: #<n>` trailer. Only the final
VERSION/CHANGELOG commit gets the release version and co-author trailer. No Git tag:
```bash
git commit -m "$(cat <<'EOF'
chore: bump version and changelog (vX.Y.Z.W)
Co-Authored-By: OpenAI Codex <noreply@openai.com>
EOF
)"
```
user-named issue adds a `Fixes: #<n>` trailer. Leave VERSION, CHANGELOG/fragment, TODOS.md
and the digest uncommitted: Step 16's `finish` commits them last with
`--trailer 'Co-Authored-By: OpenAI Codex <noreply@openai.com>'`. Do not add a Git tag.
---
+5 -13
View File
@@ -522,7 +522,8 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
runs items 1 and 3–5; each decision left to you is a `STOP:` naming its rerun flag
(`--bump`, `--accept-incomplete`, `--version <v>`, `--keep-current`, `--answer`).
`ship: queue_mode stamp-at-merge` replaces items 1–4.
`VERSION: skipped` or `VERSION: <v> kept (…; no write)` ends Step 12 with
`NEW_VERSION` unset: Step 13 follows its `NEXT:`; Steps 18 and 20 take NO_VERSION's branch.
1. **Classify state** — pure reader, never writes:
```bash
@@ -646,19 +647,10 @@ Make bisectable commits; if already committed, continue to Step 16. Never create
migrations may stand alone. Under 50 lines across fewer than 4 files may be one commit.
2. Order dependencies first: infrastructure → models/services → controllers/views.
Each commit works alone (no broken imports or missing code).
Group VERSION + CHANGELOG + TODOS.md after the feature commits.
3. Use `<type>: <summary>` (feat/fix/chore/refactor/docs) and a brief body; fixing a
user-named issue adds a `Fixes: #<n>` trailer. Only the final
VERSION/CHANGELOG commit gets the release version and co-author trailer. No Git tag:
```bash
git commit -m "$(cat <<'EOF'
chore: bump version and changelog (vX.Y.Z.W)
Co-Authored-By: Factory Droid <droid@users.noreply.github.com>
EOF
)"
```
user-named issue adds a `Fixes: #<n>` trailer. Leave VERSION, CHANGELOG/fragment, TODOS.md
and the digest uncommitted: Step 16's `finish` commits them last with
`--trailer 'Co-Authored-By: Factory Droid <droid@users.noreply.github.com>'`. Do not add a Git tag.
---
+6
View File
@@ -0,0 +1,6 @@
# garrytan/gstack-evals#141: native adversarial findings #4 and #6, head a752186 (tree 0ac70e872724875710ccc47f9c93b7b30a58604a)
Copied verbatim from /home/user/.capy/work/ship-141/run/p2/out/adversarial-native.txt
4. **driver.ts:352, 561 and 618 (the C1 ruling): skill-eval sessions now end with background Agents still running and leave no structured record.** run-host.ts:352 passes no wait. Claude Code 2.1.314 runs Agent calls in the background by default. So a skill-eval `--host claude` session ends and is graded on its "agents launched, waiting" turn, which is the original GST-734 bug. Its record also has no `session_end`, only a line in cli-stderr.log. The CREW rule asks for a per-arm count of sessions that ended with tasks pending, and these records can't give that count. Grades for skills that hand work to Agents may be deflated with nothing marking them. Old cached sessions do not mix in, because adapterSha256 hashes driver.ts. The fix: keep emitting `session_end` (cause `tasks_pending`, `waited_ms` 0) when no wait is set, and leave the hold opt-in. That reverses the C1 test's `"session_end" in r.outcome === false` assertion, so the C1 owner needs to agree. FIXABLE. Severity: Medium. Blocks: no
6. **driver.ts:404-407: the timeout branch for an idle session sets `overdue = true` without checking `stop`.** Here is the case. A background agent's refused request sets a stop while the main agent is idle, such as `cap_stop`, or an infra failure does. The timer then fires during abort teardown. The record ends up with a `cap_stop` or infra termination but `session_end.cause` "deadline". The `watch` interval already checks `stop`. Fix: `else if (idleSince !== null && !stop)`. FIXABLE. Severity: Low. Blocks: no
+1 -1
View File
@@ -1 +1 @@
{"note":"parseFindings labels at main 82e9d7ec (GST-976): every input the review-dispatch and prose-voice-floor tests pass it, every reader fixture here, and generated Severity field shapes. Regenerate only from a frozen main.","main":"82e9d7ec7a2c222f20b04aaf5b965fa50d7b6bdb","entries":[{"source":"pinned","reader":"adversarial-native","output":"FIXABLE: race in a.ts:3\nSeverity: Medium\nBlocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"INVESTIGATE: odd lock in b.ts:9\nSeverity: Low\nBlocks: yes","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"INVESTIGATE: odd lock in b.ts:9\nBlocks: no","main":["INVESTIGATE"]},{"source":"pinned","reader":"adversarial-native","output":"FIXABLE: race in a.ts:3. Severity: Medium. Blocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"1. FIXABLE: race in a.ts:3 — Severity: Low, Blocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"- FIXABLE: race in a.ts:3\n- Severity: Medium\n- Blocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"outside-voice","output":"1. Drops the last row (q.ts:7). Severity: Critical. Blocks: no","main":["Critical"]},{"source":"pinned","reader":"outside-voice","output":"1. Cache evicts sessions (c.ts:4)\n Severity: High","main":["High"]},{"source":"pinned","reader":"outside-voice","output":"1. Retry storm (r.ts:2)\n Severity: Medium\n Blocks: yes","main":["CRITICAL"]},{"source":"pinned","reader":"outside-voice","output":"1. Index drift (i.ts:5). Severity: Low. Blocks: Yes, it loses the row","main":["CRITICAL"]},{"source":"pinned","reader":"outside-voice","output":"1. Naming (n.ts:1)\n Severity: Low\n Blocks: no","main":["Low"]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\nNO FINDINGS\n","main":[]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"a.ts\",\"line\":3,\"category\":\"logic\",\"summary\":\"off by one\",\"specialist\":\"testing\"}\n{\"severity\":\"INFORMATIONAL\",\"path\":\"a.ts\",\"line\":9,\"category\":\"style\",\"summary\":\"name\",\"specialist\":\"testing\"}\n","main":["CRITICAL","INFORMATIONAL"]},{"source":"pinned","reader":"outside-voice","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\nFindings:\n1. **High**: retry loop never backs off (q.ts:78)\n2. Low - log line is noisy\nRecommendation: Fix the retry because it DoSes the pool\n","main":["High","Low"]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"a.ts\",\"line\":3,\"category\":\"logic\",\"summary\":\"x\",\"specialist\":\"testing\"}\n","main":["CRITICAL"]},{"source":"pinned","reader":"red-team","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"b.ts\",\"line\":1,\"category\":\"x\",\"summary\":\"y\",\"specialist\":\"red-team\"}\n","main":["CRITICAL"]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"a.ts\",\"line\":3,\"category\":\"logic\",\"summary\":\"x\",\"specialist\":\"testing\"}\n{\"severity\":\"INFORMATIONAL\",\"path\":\"b.ts\",\"line\":1,\"category\":\"n\",\"summary\":\"y\",\"specialist\":\"testing\"}\n","main":["CRITICAL","INFORMATIONAL"]},{"source":"pinned","reader":"r","output":"NO FINDINGS\n","main":[]},{"source":"pinned","reader":"r","output":"## High\n1. The auth check skips webhooks (a.ts:4)\n - Impact: nested evidence\n **Fix:**\n- second one\n## Notes\n- not a finding\n**Critical findings**\n- real one\n**Recommendations**\n- consider renaming foo\n","main":["High","High","Critical"]},{"source":"pinned","reader":"r","output":"1. **Unbounded retry in queue.ts:78** (FIXABLE): loops on 429\n2. `auth.ts:42` skips token expiry check — INVESTIGATE\n### CRITICAL: missing auth check in api.ts:10\n#### [P1] token not expired\n### High: authorization bypass (auth.ts:14)\n- **HIGH**: leak\n| High | race in queue.ts:4 |\nClassification: INVESTIGATE\n7. **Informational: reads every file**","main":["FIXABLE","INVESTIGATE","CRITICAL","P1","High","HIGH","High","INVESTIGATE","Informational"]},{"source":"pinned","reader":"r","output":"FIXABLE: race in a.ts:3\nINVESTIGATE: odd lock\nRecommendation: Fix the race because x\n","main":["FIXABLE","INVESTIGATE"]},{"source":"pinned","reader":"outside-voice","output":"No CRITICAL issues except the token leak in log.ts:3","main":["CRITICAL"]},{"source":"pinned","reader":"fix-check","output":"No CRITICAL issues except the token leak in lLine truncated
{"note":"parseFindings labels at main 82e9d7ec (GST-976): every input the review-dispatch and prose-voice-floor tests pass it, every reader fixture here, and generated Severity field shapes. Regenerate only from a frozen main.","main":"82e9d7ec7a2c222f20b04aaf5b965fa50d7b6bdb","entries":[{"source":"pinned","reader":"adversarial-native","output":"FIXABLE: race in a.ts:3\nSeverity: Medium\nBlocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"INVESTIGATE: odd lock in b.ts:9\nSeverity: Low\nBlocks: yes","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"INVESTIGATE: odd lock in b.ts:9\nBlocks: no","main":["INVESTIGATE"]},{"source":"pinned","reader":"adversarial-native","output":"FIXABLE: race in a.ts:3. Severity: Medium. Blocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"1. FIXABLE: race in a.ts:3 — Severity: Low, Blocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"adversarial-native","output":"- FIXABLE: race in a.ts:3\n- Severity: Medium\n- Blocks: no","main":["CRITICAL"]},{"source":"pinned","reader":"outside-voice","output":"1. Drops the last row (q.ts:7). Severity: Critical. Blocks: no","main":["Critical"]},{"source":"pinned","reader":"outside-voice","output":"1. Cache evicts sessions (c.ts:4)\n Severity: High","main":["High"]},{"source":"pinned","reader":"outside-voice","output":"1. Retry storm (r.ts:2)\n Severity: Medium\n Blocks: yes","main":["CRITICAL"]},{"source":"pinned","reader":"outside-voice","output":"1. Index drift (i.ts:5). Severity: Low. Blocks: Yes, it loses the row","main":["CRITICAL"]},{"source":"pinned","reader":"outside-voice","output":"1. Naming (n.ts:1)\n Severity: Low\n Blocks: no","main":["Low"]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\nNO FINDINGS\n","main":[]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"a.ts\",\"line\":3,\"category\":\"logic\",\"summary\":\"off by one\",\"specialist\":\"testing\"}\n{\"severity\":\"INFORMATIONAL\",\"path\":\"a.ts\",\"line\":9,\"category\":\"style\",\"summary\":\"name\",\"specialist\":\"testing\"}\n","main":["CRITICAL","INFORMATIONAL"]},{"source":"pinned","reader":"outside-voice","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\nFindings:\n1. **High**: retry loop never backs off (q.ts:78)\n2. Low - log line is noisy\nRecommendation: Fix the retry because it DoSes the pool\n","main":["High","Low"]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"a.ts\",\"line\":3,\"category\":\"logic\",\"summary\":\"x\",\"specialist\":\"testing\"}\n","main":["CRITICAL"]},{"source":"pinned","reader":"red-team","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"b.ts\",\"line\":1,\"category\":\"x\",\"summary\":\"y\",\"specialist\":\"red-team\"}\n","main":["CRITICAL"]},{"source":"pinned","reader":"specialist/testing","output":"REVIEWED_TREE: d0dd42d6a88fbc2a6abca4f6b2dfde7e35520c5a /home/user/.capy/work/gst976/wt\n{\"severity\":\"CRITICAL\",\"path\":\"a.ts\",\"line\":3,\"category\":\"logic\",\"summary\":\"x\",\"specialist\":\"testing\"}\n{\"severity\":\"INFORMATIONAL\",\"path\":\"b.ts\",\"line\":1,\"category\":\"n\",\"summary\":\"y\",\"specialist\":\"testing\"}\n","main":["CRITICAL","INFORMATIONAL"]},{"source":"pinned","reader":"r","output":"NO FINDINGS\n","main":[]},{"source":"pinned","reader":"r","output":"## High\n1. The auth check skips webhooks (a.ts:4)\n - Impact: nested evidence\n **Fix:**\n- second one\n## Notes\n- not a finding\n**Critical findings**\n- real one\n**Recommendations**\n- consider renaming foo\n","main":["High","High","Critical"]},{"source":"pinned","reader":"r","output":"1. **Unbounded retry in queue.ts:78** (FIXABLE): loops on 429\n2. `auth.ts:42` skips token expiry check — INVESTIGATE\n### CRITICAL: missing auth check in api.ts:10\n#### [P1] token not expired\n### High: authorization bypass (auth.ts:14)\n- **HIGH**: leak\n| High | race in queue.ts:4 |\nClassification: INVESTIGATE\n7. **Informational: reads every file**","main":["FIXABLE","INVESTIGATE","CRITICAL","P1","High","HIGH","High","INVESTIGATE","Informational"]},{"source":"pinned","reader":"r","output":"FIXABLE: race in a.ts:3\nINVESTIGATE: odd lock\nRecommendation: Fix the race because x\n","main":["FIXABLE","INVESTIGATE"]},{"source":"pinned","reader":"outside-voice","output":"No CRITICAL issues except the token leak in log.ts:3","main":["CRITICAL"]},{"source":"pinned","reader":"fix-check","output":"No CRITICAL issues except the token leak in lLine truncated
+10 -8
View File
@@ -71,21 +71,23 @@ describe('git fixture template', () => {
expect(() => leaky.copy()).toThrow(/still references its template [^\n]*\n.*config/);
});
test("Capy's git shim log in a fixture home (home/.capy/git-shim.log) is not a leak; any other file naming the template still is", () => {
test("Capy's directory in a fixture home (home/.capy, the git shim's log and anything else under it) is not a leak; any other file naming the template still is", () => {
const plant = (rel: string) => gitFixtureTemplate({ prefix: 'gstack-fixture-template-shim-', build: (root) => {
const meta = build(root);
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), '# capy git shim v2: ts cwd ppid parent verb\n2026-11-02T02:15:30Z\t' + path.join(root, 'work') + '\t1\tbun\tcat-file\n');
return meta;
}, rebind: (copy, tpl) => rebindGitDir(path.join(copy, 'work', '.git'), tpl, copy) });
const shim = plant(path.join('home', '.capy', 'git-shim.log'));
const other = plant(path.join('home', '.capy', 'state.json'));
const skipped = [path.join('home', '.capy', 'git-shim.log'), path.join('home', '.capy', 'git-shim.log.1'), path.join('home', '.capy', 'state', 'run.json')].map(plant);
const leaks = [path.join('home', 'state.json'), path.join('home', '.capy-old', 'state.json'), path.join('home', '.config', '.capy'), path.join('home', 'capy', 'git-shim.log')].map(rel => ({ rel, t: plant(rel) }));
try {
const c = shim.copy();
roots.push(c.root);
expect(git(path.join(c.root, 'work'), 'remote', 'get-url', 'origin')).toBe(path.join(c.root, 'origin.git'));
expect(() => other.copy()).toThrow(/still references its template [^\n]*\n.*state\.json/);
} finally { shim.dispose(); other.dispose(); }
for (const s of skipped) {
const c = s.copy();
roots.push(c.root);
expect(git(path.join(c.root, 'work'), 'remote', 'get-url', 'origin')).toBe(path.join(c.root, 'origin.git'));
}
for (const { rel, t } of leaks) expect(() => t.copy()).toThrow(new RegExp('still references its template [^\\n]*\\n.*' + rel.replace(/[.\\/]/g, m => '\\' + m) + '$'));
} finally { for (const s of skipped) s.dispose(); for (const { t } of leaks) t.dispose(); }
});
test('native, forward-slash, backslash-escaped and Git Bash spellings are each rewritten, and each one left behind is found', () => {
+1
View File
@@ -36,6 +36,7 @@ const KEPT: Record<string, string> = {
'ship-gate.json': '/ship handoff gate record',
'issues.json': '/ship issue links',
'docs-audit.json': '/ship documentation audit record',
'targeted.json': '/ship Step 11 targeted reader record (gstack-review-dispatch verify --targeted-out)',
'tree-receipt.json': 'gstack-tree-receipt output',
'base-red.json': '/ship base-red record',
'review-*': '/ship review loop records',
+1
View File
@@ -92,6 +92,7 @@ export const FREE_FIXTURES: Readonly<Record<string, readonly string[]>> = {
'test/fixtures/design-ui-focus-capture-40664336141.json': ['test/plan-design-with-ui-fixture.test.ts'],
'test/fixtures/design-ui-focus-capture-41178131219.json': ['test/plan-design-with-ui-fixture.test.ts'],
'test/fixtures/design-ui-focus-capture-41186376825.json': ['test/plan-design-with-ui-fixture.test.ts'],
'test/fixtures/design-ui-focus-capture-41414111284.json': ['test/plan-design-with-ui-fixture.test.ts'],
'test/fixtures/design-ui-focus-capture.json': ['test/plan-design-with-ui-fixture.test.ts'],
'test/fixtures/detector-corpora/auto-decide-preserved/**': ['test/detector-corpus-auto-decide-preserved.test.ts'],
'test/fixtures/detector-corpora/plan-ceo-section-loading/**': ['test/detector-corpus-plan-ceo-section-loading.test.ts'],
+5 -4
View File
@@ -54,8 +54,9 @@ export function rebindGitDir(gitDir: string, from: string, to: string): void {
/**
* Files and symlinks under dir (and the template's meta value) that still name templateRoot. Skipped: zlib-compressed object
* storage, and the log Capy's /usr/bin/git shim appends to in whatever HOME it runs under (.capy/git-shim.log), which names
* the template whenever a fixture home ran git during build; nothing reads it back.
* storage, and every .capy directory: Capy's /usr/bin/git shim creates $HOME/.capy in whatever HOME it runs under and logs
* there, so a fixture home that ran git during build holds the machine's writes, not gstack's (install-fixture's tree() skips
* it for the same reason); nothing reads them back.
*/
export function templateReferences(dir: string, templateRoot: string, meta?: unknown): string[] {
const needles = pathSpellings(templateRoot).map(s => Buffer.from(s));
@@ -65,8 +66,8 @@ export function templateReferences(dir: string, templateRoot: string, meta?: unk
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
const p = path.join(d, entry.name);
if (entry.isSymbolicLink()) { if (names(Buffer.from(fs.readlinkSync(p)))) hits.push(p); }
else if (entry.isDirectory()) { if (!(path.basename(d) === 'objects' && /^(pack|[0-9a-f]{2})$/.test(entry.name))) walk(p); }
else if (entry.isFile() && !(entry.name === 'git-shim.log' && path.basename(d) === '.capy') && names(fs.readFileSync(p))) hits.push(p);
else if (entry.isDirectory()) { if (entry.name !== '.capy' && !(path.basename(d) === 'objects' && /^(pack|[0-9a-f]{2})$/.test(entry.name))) walk(p); }
else if (entry.isFile() && names(fs.readFileSync(p))) hits.push(p);
}
};
walk(dir);
@@ -61,10 +61,18 @@ const LATER_COOKIE_ORDER_EDITS: Array<[string, string]> = [
["# view the page; only --verify-auth proves sign-in", "# confirm the intended account"],
];
// The #3338 trust warning (GSTA-84) added one paragraph after step 3's goto
// (two lines longer). Removing it first lets the GSTA-49 reversal match.
const LATER_TRUST_PARAGRAPH = "Tell the user once, before the import: gstack's headless browser trusts the root store built into gstack's bundled Chromium, which still accepts certificates from the compromised eMudhra intermediates. Until gstack moves to a Playwright release that drops them, don't use the imported sessions on a network the user does not trust. That same root's `BROWSER.md`, **TLS trust: the bundled Chromium's root store**, has the details and the check.\n\n";
export function approvedCookieWorkflowSource(source: string): string {
let removedLines = 0;
let removedEntrypointLines = 0;
let reordered = source;
if (reordered.includes(LATER_TRUST_PARAGRAPH)) {
removedEntrypointLines += LATER_TRUST_PARAGRAPH.split('\n').length - 1;
reordered = reordered.replace(LATER_TRUST_PARAGRAPH, '');
}
for (const [later, earlier] of LATER_COOKIE_ORDER_EDITS) {
if (!reordered.includes(later)) continue;
removedEntrypointLines += later.split('\n').length - earlier.split('\n').length;
+1 -1
View File
@@ -107,7 +107,7 @@ function fixtureRepoAt(root: string): FixtureRepo {
const origin = path.join(root, 'origin.git');
const work = path.join(root, 'work');
const home = path.join(root, 'home');
const env = { GSTACK_HOME: home, GSTACK_STATE_DIR: home, HOME: home, GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1' };
const env = { GSTACK_STATE_ROOT: home, GSTACK_HOME: home, GSTACK_STATE_DIR: home, HOME: home, GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1' };
const git = (...args: string[]): string => {
const r = gitArgvIn(work, args, 30_000, { ...process.env, ...env });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: status=${r.status} signal=${r.signal} error=${r.error?.message ?? ''} stderr=${r.stderr?.toString() ?? ''}`);
+1 -1
View File
@@ -46,7 +46,7 @@ export function fixtureRepoAt(root: string): FixtureRepo {
const origin = path.join(root, 'origin.git');
const work = path.join(root, 'work');
const home = path.join(root, 'home');
const env = { GSTACK_HOME: home, GSTACK_STATE_DIR: home, HOME: home, GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1' };
const env = { GSTACK_STATE_ROOT: home, GSTACK_HOME: home, GSTACK_STATE_DIR: home, HOME: home, GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1' };
const git = (...args: string[]): string => {
const r = gitArgvIn(work, args, 30_000, { ...process.env, ...env });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr?.toString()}`);
+1 -1
View File
@@ -99,7 +99,7 @@ export async function capture(change: string, prepared?: PathEligibilityFixture,
branch: fixtureGit(f, 'symbolic-ref', '--quiet', '--short', 'HEAD'), wtree: fixtureWorkingTree(f),
startedAt: receipt.review_start.started_at, finding: current, reusable: change === 'unchanged',
coveredPaths: receipt.snapshot.covered_paths,
...(prepared ? { sources: Object.fromEntries(current!.evidence_paths.map((file: string) => [file, fs.readFileSync(path.join(f.repo, file), 'utf8')])) } : {}) };
sources: Object.fromEntries(current!.evidence_paths.map((file: string) => [file, fs.readFileSync(path.join(f.repo, file), 'utf8')])) };
return { f, current, token, receipt, events, checkAt, finishAt, expected, prepared, resumed, prerequisites, ...(declared ? { questions } : {}) };
}
+13
View File
@@ -125,6 +125,19 @@ describe('paid/free dependency boundary', () => {
expect(ethos.selection.e2e!.length).toBeLessThan(10);
});
// Value: protects=a root guide that only README, docs and free tests read keeps a screen narrow, and a root doc a paid prompt quotes still selects beside it; fails_when=a guide-only PR restores every gate case as a "real unknown dependency" (GST-1027: screens 41246728073 and 41321112436 ran the full suite for it);
// why_new=the README regex in knownNonBehaviorFile names only five root docs; seam=FREE_ONLY_PR_FILES in scripts/test-pr-profile.ts; no_claim=the train gate's full census
test('USING_GBRAIN_WITH_GSTACK.md selects no paid case; ETHOS.md beside it keeps its selection (GST-1027)', () => {
const guide = 'USING_GBRAIN_WITH_GSTACK.md';
expect(fs.existsSync(path.join(ROOT, guide))).toBe(true);
const alone = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [guide] });
expect([alone.coverage?.mode, alone.coverage?.unknownFiles, alone.selection]).toEqual(['pr', [], { e2e: [], judges: [] }]);
const ethos = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: ['ETHOS.md'] });
expect(ethos.selection.e2e!.length).toBeGreaterThan(0);
const both = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [guide, 'ETHOS.md'] });
expect([both.coverage?.mode, both.selection]).toEqual(['pr', ethos.selection]);
});
test('only audited free-only dependencies avoid unknown-dependency fallback', () => {
for (const file of [...FREE_ONLY_PR_FILES, 'scripts\\test-free-shards.ts']) {
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [file] });
+30 -2
View File
@@ -103,6 +103,10 @@ const captured40664336141 = JSON.parse(fs.readFileSync(${JSON.stringify(path.joi
// rated in its ELI10 body, and lettered its options ("A) All 7 passes", "A) All 7 dimensions").
const [captured41178131219, captured41186376825] = ['design-ui-focus-capture-41178131219.json', 'design-ui-focus-capture-41186376825.json'].map(name => JSON.parse(fs.readFileSync(${JSON.stringify(ROOT)} + '/test/fixtures/' + name, 'utf8'))
.calls.map(({ preReview, observedAtMs, call }) => nativePlanCallFingerprint(call, observedAtMs, preReview)));
// Run 41414111284 slice 6 (train 4a280637): header "Focus", rated in its ELI10 body, and the all-7 option
// put the count second ("Full review, all 7 (recommended)").
const captured41414111284 = JSON.parse(fs.readFileSync(${JSON.stringify(path.join(ROOT, 'test/fixtures/design-ui-focus-capture-41414111284.json'))}, 'utf8'))
.calls.map(({ preReview, observedAtMs, call }) => nativePlanCallFingerprint(call, observedAtMs, preReview));
const unnumberedFinding = fp('unnumbered-finding', "Which region is the dashboard's primary anchor, and how is the page composed?", false);
unnumberedFinding.nativeCall.questions[0].options = nativeFinding.nativeCall.questions[0].options;
const finding = fp('finding', 'Which loading feedback should Save show?', false);
@@ -241,6 +245,28 @@ mock.module(${JSON.stringify(path.join(ROOT, 'test/helpers/claude-pty-runner.ts'
expect([0, 2].some(index => opts.isLastStep0AUQ(capture[index]) || opts.isReviewAUQ(capture[index]))).toBe(false);
expect(opts.isReviewAUQ(capture[3])).toBe(true);
}
expect(opts.isLastStep0AUQ(captured41414111284[0])).toBe(true);
expect(opts.isReviewAUQ(captured41414111284[0])).toBe(false);
expect(opts.isLastStep0AUQ(captured41414111284[1]) || opts.isReviewAUQ(captured41414111284[1])).toBe(false);
expect(opts.isReviewAUQ(captured41414111284[2])).toBe(true);
const headed = (id, header, question, labels) => {
const call = labeled(id, question, labels);
call.nativeCall.questions[0].header = header;
return call;
};
const focusBody = 'D1 — Review focus\\nProject/branch/task: main.\\nELI10: I rated this plan 4/10 on design completeness.';
const fullFirst = ['Full review, all 7 (recommended)', 'Focus on specific areas'];
expect(opts.isLastStep0AUQ(headed('focus-header', 'Focus', focusBody, fullFirst))).toBe(true);
for (const [label, header, question, labels] of [
['focus-header-unrated', 'Focus', 'D1 — Review focus\\nELI10: I will review every dimension unless you narrow it.', fullFirst],
['focus-header-out-of-seven', 'Focus', focusBody.replace('4/10', '4/7'), fullFirst],
['focus-header-issue-labeled', 'Focus', focusBody, ['1A Full review, all 7', '1B Focus on hierarchy']],
['focus-header-single-option', 'Focus', focusBody, fullFirst.slice(0, 1)],
['focus-header-no-all-seven', 'Focus', focusBody, ['Every dimension (recommended)', 'States + hierarchy only']],
['focus-header-all-four', 'Focus', focusBody, ['Full review, all 4 (recommended)', 'Focus on specific areas']],
['other-header', 'Hierarchy', focusBody, fullFirst],
['header-names-more', 'Focus order', focusBody, fullFirst],
]) expect(opts.isLastStep0AUQ(headed(label, header, question, labels)), label).toBe(false);
const letteredFocus = 'D2 — Review focus\\nProject/branch/task: main.\\nELI10: I rated this plan 4/10 on design completeness.';
for (const labels of [['A) All 7 dimensions (recommended)', 'B) Focus on specific areas'], ['A. All seven passes (recommended)', 'B. Hierarchy+states only'],
['A: All 7 areas (recommended)', 'B: Only specific areas']]) {
@@ -344,6 +370,8 @@ mock.module(${JSON.stringify(path.join(ROOT, 'test/helpers/claude-pty-runner.ts'
: mode === 'captured-41178131219-no-focus' ? captured41178131219.filter((_, index) => index !== 1)
: mode === 'captured-41186376825' ? captured41186376825
: mode === 'captured-41186376825-no-focus' ? captured41186376825.filter((_, index) => index !== 1)
: mode === 'captured-41414111284' ? captured41414111284
: mode === 'captured-41414111284-no-focus' ? captured41414111284.filter((_, index) => index !== 0)
: mode === 'captured-focus-after-finding' ? [...captured.filter((_, index) => index !== 1), captured[1]]
: mode.startsWith('target-menu') ? [target, fp('other', 'Which artifact should I inspect?', false)]
: mode === 'early-exit' ? [] : mode === 'focus-only' ? [chosenFocus]
@@ -388,12 +416,12 @@ await import(${JSON.stringify(path.join(ROOT, 'test/skill-e2e-plan-design-with-u
} finally { fs.rmSync(directory, { recursive: true, force: true }); }
}
test.each(['source', 'paraphrase', 'native-sequence', 'native-unnumbered', 'native-passes', 'captured', 'captured-rated', 'captured-39298269902', 'captured-39363705692', 'captured-39852003387', 'captured-40664336141', 'captured-41178131219', 'captured-41186376825'])('UI gate seeds the exact target and accepts Design progress with an unselected no-UI alternative (%s)', mode => {
test.each(['source', 'paraphrase', 'native-sequence', 'native-unnumbered', 'native-passes', 'captured', 'captured-rated', 'captured-39298269902', 'captured-39363705692', 'captured-39852003387', 'captured-40664336141', 'captured-41178131219', 'captured-41186376825', 'captured-41414111284'])('UI gate seeds the exact target and accepts Design progress with an unselected no-UI alternative (%s)', mode => {
const result = exercise(mode);
expect(result.code, result.output).toBe(0);
}, 20_000);
test.each(['target-menu', 'target-menu-design-system', 'focus-only', 'captured-no-focus', 'captured-focus-after-finding', 'captured-rated-no-focus', 'captured-39298269902-no-focus', 'captured-39298269902-focus-only', 'captured-39363705692-no-focus', 'captured-39852003387-no-focus', 'captured-40664336141-no-focus', 'captured-41178131219-no-focus', 'captured-41186376825-no-focus', 'native-setup-only', 'native-voices-only', 'early-exit', 'timeout', 'exited', 'unanswered-finding', 'failed-finding', 'no-native-finding'])('UI gate rejects %s and removes its fixture', mode => {
test.each(['target-menu', 'target-menu-design-system', 'focus-only', 'captured-no-focus', 'captured-focus-after-finding', 'captured-rated-no-focus', 'captured-39298269902-no-focus', 'captured-39298269902-focus-only', 'captured-39363705692-no-focus', 'captured-39852003387-no-focus', 'captured-40664336141-no-focus', 'captured-41178131219-no-focus', 'captured-41186376825-no-focus', 'captured-41414111284-no-focus', 'native-setup-only', 'native-voices-only', 'early-exit', 'timeout', 'exited', 'unanswered-finding', 'failed-finding', 'no-native-finding'])('UI gate rejects %s and removes its fixture', mode => {
const result = exercise(mode);
expect(result.code, result.output).toBe(1);
expect(result.output).toContain('plan-design-review with UI scope FAILED');
+10 -5
View File
@@ -16,11 +16,17 @@ import { receiptTokens, resolveReceipts } from '../lib/plan-reality-receipts';
const ROOT = path.resolve(import.meta.dir, '..');
const BIN = path.join(ROOT, 'bin', 'gstack-plan-reality');
function gitOk(cwd: string, ...args: string[]) {
const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 10_000 });
if (r.status !== 0) throw new Error(`git ${args.join(' ')} exited ${r.status} (signal ${r.signal}, error ${r.error?.message ?? 'none'}): ${r.stderr}`);
return r;
}
function fixtureRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'reality-receipts-'));
fs.mkdirSync(path.join(dir, 'lib'));
fs.writeFileSync(path.join(dir, 'lib', 'a.ts'), 'one\ntwo\nthree\n');
spawnSync('git', ['init', '-q'], { cwd: dir, timeout: 10_000 });
gitOk(dir, 'init', '-q');
return dir;
}
@@ -99,10 +105,9 @@ describe('resolveReceipts', () => {
test('a commit sha in the repository resolves; an absent one is unverifiable', () => {
const dir = fixtureRepo();
const git = (...a: string[]) => spawnSync('git', a, { cwd: dir, encoding: 'utf8', timeout: 10_000 });
git('add', '.');
git('-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'seed');
const sha = git('rev-parse', 'HEAD').stdout.trim();
gitOk(dir, 'add', '.');
gitOk(dir, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'seed');
const sha = gitOk(dir, 'rev-parse', 'HEAD').stdout.trim();
const out = resolveReceipts(parseRealityLines(`REALITY: numbers pass ok ${sha.slice(0, 10)} abcdef1234`), { roots: [dir] });
expect(out.map(r => r.outcome)).toEqual(['resolved', 'unverifiable']);
});
+154
View File
@@ -0,0 +1,154 @@
/**
* GST-1024: gstack's own .gstack/pregate.json declares remote_gate_runs, so a green Free Tests run at the pregate tree
* clears pregate.json/remote_gate through `gstack-pregate clear --run`. The declaration's commands run against this
* checkout (the inventory must be exactly the free suite CI plans, the skipped list exactly the paid files
* pregate-remote.sh leaves to the paid gate); the clear runs through buildRunCheck in a fixture repository with this
* repository's real run profile and a fake Actions API shaped like a real Free Tests pull_request run (41484634592).
*/
import { afterEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { bunIsolationArgs } from '../lib/bun-isolation';
import { buildContext } from '../lib/pregate/context';
import { clearDecision, runPregate, type RunCheck } from '../lib/pregate/index';
import { buildRunCheck } from '../lib/pregate/run-check';
import { runInventory, runProfileErrors, type ApiRun, type RemoteGateRuns, type RunProfile } from '../lib/run-evidence';
import { isPaidTestFile } from './helpers/paid-test-set';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, type FixtureRepo } from './helpers/pregate-fixture';
const ROOT = path.resolve(import.meta.dir, '..');
const REAL = JSON.parse(fs.readFileSync(path.join(ROOT, '.gstack/pregate.json'), 'utf8')) as { run_profiles: Record<string, RunProfile>; remote_gate_runs?: RemoteGateRuns };
const LANE = 'pregate.json/remote_gate';
const PROFILE = 'ci-reuse/free-tests';
const PAID = 'test/skill-e2e-ship-gate.test.ts';
const dirs: string[] = [];
const repos: FixtureRepo[] = [];
afterEach(() => {
while (repos.length) cleanup(repos.pop()!);
while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true });
});
const tmp = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gst1024-')); dirs.push(d); return d; };
const lines = (text: string) => text.split('\n').filter(Boolean);
describe.skipIf(process.platform === 'win32')('gstack declares remote_gate_runs (GST-1024)', () => {
test('the declaration names the Free Tests profile; its inventory is exactly the free suite CI plans, and its skipped list is exactly the selected paid files', () => {
const g = REAL.remote_gate_runs;
expect(g?.profiles).toEqual([PROFILE]);
expect(REAL.run_profiles[PROFILE]).toMatchObject({ workflow: 'free-tests.yml', artifact: 'tested-tree', os: 'Linux' });
expect(runProfileErrors(REAL.run_profiles, g)).toEqual([]);
const listed = runInventory(g!.inventory, ROOT);
if ('error' in listed) throw new Error(listed.error);
const planFile = path.join(tmp(), 'plan.json');
const plan = spawnSync('bun', [...bunIsolationArgs(), 'run', 'scripts/test-free-shards.ts', '--ci-plan', planFile, '--shards', '11', '--shard-jobs', '2'], { cwd: ROOT, encoding: 'utf8', timeout: 120_000 });
expect(plan.status).toBe(0);
const planned = (JSON.parse(fs.readFileSync(planFile, 'utf8')) as { shards: Array<{ files: string[] }> }).shards.flatMap(s => s.files);
expect(listed.length).toBeGreaterThan(1000);
expect([...listed].sort()).toEqual([...new Set(planned)].sort());
expect(listed.filter(isPaidTestFile)).toEqual([]);
const selected = path.join(tmp(), 'selected.txt');
fs.writeFileSync(selected, [listed[0], PAID, 'test/codex-e2e-plan-format.test.ts', listed[1]].join('\n') + '\n');
const skipped = runInventory(g!.skipped!, ROOT, { PREGATE_FILES_FILE: selected });
expect(skipped).toEqual([PAID, 'test/codex-e2e-plan-format.test.ts']);
}, 180_000);
});
const INVENTORY = 'printf "test/tool-cli.test.js\\ntest/other.test.js\\n"';
const SKIPPED = 'grep -E "^test/skill-e2e-" "$PREGATE_FILES_FILE" || true';
function fixture(gateRuns: Record<string, unknown>): FixtureRepo {
const repo = makeFixtureRepo({ pregate: { ...BASE_PREGATE, remote_gate: 'node scripts/remote-gate.js', run_profiles: REAL.run_profiles, remote_gate_runs: gateRuns } });
repos.push(repo);
repo.git('checkout', '-q', 'main');
commit(repo, { 'scripts/remote-gate.js': "console.log('PREGATE_REMOTE: tree=' + process.env.PREGATE_TREE + ' tests=3 failed=0');\n" }, 'remote gate');
repo.git('push', '-q', 'origin', 'main');
repo.git('checkout', '-q', 'feature');
repo.git('merge', '-q', '--ff-only', 'main');
commit(repo, { 'lib/util.js': 'exports.add = (a, b) => a + b + 0;\n' }, 'the real change');
return repo;
}
/** A Free Tests pull_request run as GitHub reports it: the profile's jobs, a tested-tree artifact naming the merge commit. */
interface Run { head: string; files: string[]; conclusion?: string; path?: string; headRepo?: string; mergeTree?: string }
function gateCheck(repo: FixtureRepo, run: Run): RunCheck {
const built = buildRunCheck(repo.work, LANE, true, 'main');
if ('error' in built) throw new Error(built.error);
const profile = REAL.run_profiles[PROFILE]!;
const merge = 'f'.repeat(40);
const finished = new Date(Date.now() - 3600_000).toISOString();
const api: ApiRun = {
id: 41484634592, run_attempt: 1, status: 'completed', conclusion: run.conclusion ?? 'success', event: 'pull_request', head_sha: run.head,
path: run.path ?? `.github/workflows/${profile.workflow}`, created_at: finished, updated_at: finished,
repository: { full_name: 'fx/fx' }, head_repository: { full_name: run.headRepo ?? 'fx/fx' },
};
const tree = (sha: string) => repo.git('rev-parse', `${sha}^{tree}`);
return {
...built,
repo: 'fx/fx',
deps: {
run: id => { if (id !== api.id) throw new Error('HTTP 404'); return api; },
runsForHead: () => [api],
jobs: () => profile.jobs.map(name => ({ name, conclusion: 'success', completed_at: finished })),
artifact: () => ({ run_id: api.id, run_attempt: 1, event: 'pull_request', sha: merge, head_sha: run.head, tree: tree(run.head), workflow: profile.workflow, os: ['Linux', 'macOS', 'Windows'], files: run.files, tests: run.files.length * 20 }),
commitTree: sha => (sha === merge ? run.mergeTree ?? tree(run.head) : tree(sha)),
},
};
}
async function gatedFile(repo: FixtureRepo) {
const loaded = buildContext({ repoRoot: repo.work, base: 'main', policyFrom: 'base', allowRepoCommands: true, env: { ...process.env, ...repo.env }, progress: () => {}, cores: 4, testBackend: 'local' });
if (!('ctx' in loaded)) throw new Error('fixture context refused');
const file = await runPregate(loaded.ctx, { stage: 'tests' });
const gate = file.requires_remote?.find(r => r.lane === LANE);
if (!gate) throw new Error('no remote gate obligation');
gate.tests = [...(gate.tests ?? []), PAID];
repo.git('config', 'remote.origin.url', 'https://github.com/fx/fx.git');
return { file, selected: gate.tests.filter(f => f !== PAID) };
}
describe.skipIf(process.platform === 'win32')('clear --run with the Free Tests profile (GST-1024)', () => {
const RUN = 'https://github.com/fx/fx/actions/runs/41484634592';
const msg = (d: ReturnType<typeof clearDecision>) => ('refused' in d ? `${d.refused}: ${d.message}` : `cleared ${d.receipt}`);
test('a green Free Tests run at the pregate tree clears the gate; the selected paid file the gate skips is counted, not required', async () => {
const repo = fixture({ profiles: [PROFILE], inventory: INVENTORY, skipped: SKIPPED });
const head = repo.git('rev-parse', 'HEAD');
const { file, selected } = await gatedFile(repo);
const files = [...new Set([...selected, 'test/tool-cli.test.js', 'test/other.test.js'])];
const d = clearDecision(file, LANE, { runs: [RUN], evidence: gateCheck(repo, { head, files }) }, repo.work);
expect(msg(d)).toStartWith(`cleared remote-gate tree=${file.tree.slice(0, 12)} tests=${files.length * 20} skipped=1 runs=41484634592/1 profiles=${PROFILE} heads=${head.slice(0, 12)} verified=`);
}, 120_000);
test('refusals: no skipped command, a paid-only selection, the wrong tree, the wrong workflow, a failed or cancelled run, a fork run, a merge behind its base, an uncovered inventory file, a skipped list that names an inventory file', async () => {
const repo = fixture({ profiles: [PROFILE], inventory: INVENTORY, skipped: SKIPPED });
const older = repo.git('rev-parse', 'HEAD~1');
const head = repo.git('rev-parse', 'HEAD');
const { file, selected } = await gatedFile(repo);
const files = [...new Set([...selected, 'test/tool-cli.test.js', 'test/other.test.js'])];
const clear = (run: Run, over: Partial<RunCheck> = {}) => msg(clearDecision(file, LANE, { runs: [RUN], evidence: { ...gateCheck(repo, run), ...over } }, repo.work));
expect(clear({ head, files }, { gateRuns: { profiles: [PROFILE], inventory: INVENTORY } })).toMatch(new RegExp(`reason=inventory_uncovered — 1 of the \\d+ test file\\(s\\) ${LANE} must cover appear in no run's tree.json \\(${PAID}\\)`));
expect(clear({ head: older, files })).toMatch(/^PREGATE_RECEIPT_INVALID: REUSE: refused reason=tree_not_stamp_only — .*lib\/util\.js/);
expect(clear({ head, files, path: '.github/workflows/windows-free-tests.yml' })).toContain('reason=workflow_mismatch run 41484634592 attempt 1 ran .github/workflows/windows-free-tests.yml, not .github/workflows/free-tests.yml');
expect(clear({ head, files, conclusion: 'failure' })).toContain('reason=run_not_success run 41484634592 attempt 1 concluded failure');
expect(clear({ head, files, conclusion: 'cancelled' })).toContain('reason=run_not_success run 41484634592 attempt 1 concluded cancelled');
expect(clear({ head, files, headRepo: 'attacker/fx' })).toContain('reason=fork_run run 41484634592 attempt 1 ran for attacker/fx, not fx/fx');
expect(clear({ head, files, mergeTree: repo.git('rev-parse', `${older}^{tree}`) })).toContain('reason=tree_mismatch');
expect(clear({ head, files: files.filter(f => f !== 'test/other.test.js') })).toContain("reason=inventory_uncovered — 1 of the");
expect(clear({ head, files: files.filter(f => f !== 'test/other.test.js') }, { gateRuns: { profiles: [PROFILE], inventory: INVENTORY, skipped: `${SKIPPED}; echo test/other.test.js` } })).toContain('(test/other.test.js)');
expect(clear({ head, files }, { gateRuns: { profiles: [PROFILE], inventory: INVENTORY, skipped: 'exit 3' } })).toContain('reason=skipped_failed');
const paidOnly = structuredClone(file);
paidOnly.requires_remote!.find(r => r.lane === LANE)!.tests = [PAID];
expect(msg(clearDecision(paidOnly, LANE, { runs: [RUN], evidence: gateCheck(repo, { head, files }) }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=paid_only — the remote gate skips every selected test file (1), so no selected test ran; like the gate's own tests=0 report, that never clears; ${LANE} stays uncleared`);
}, 120_000);
test('remote_gate_runs.skipped must be one command line, and an unknown key names it as valid', () => {
const profiles = { p: { workflow: 'w.yml', events: ['push'], jobs: ['j'], artifact: 'a' } };
for (const skipped of [3, '', ' ', 'a\nb', 'x'.repeat(513)]) {
expect(runProfileErrors(profiles, { profiles: ['p'], inventory: 'echo t', skipped }).map(e => e.path)).toEqual(['remote_gate_runs.skipped']);
}
expect(runProfileErrors(profiles, { profiles: ['p'], inventory: 'echo t', skipped: 'echo s' })).toEqual([]);
expect(runProfileErrors(profiles, { profiles: ['p'], inventory: 'echo t', skip: 'echo s' })).toEqual([{ path: 'remote_gate_runs.skip', message: 'unknown key; valid keys: profiles, inventory, skipped' }]);
});
});
+59 -1
View File
@@ -14,7 +14,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { changedLiterals, constantsIn, consumersOf } from '../lib/pregate/checks/literals';
import { changedLiterals, constantsIn, consumersOf, type LiteralWaiver } from '../lib/pregate/checks/literals';
import { parseSummary, planRuns } from '../lib/pregate/checks/lanes';
import { DEFAULT_CONFIG, validateConfig } from '../lib/pregate/config';
import { isDocsEmptyLane } from '../lib/pregate/docs-lane';
@@ -298,6 +298,51 @@ describe('literals (pure)', () => {
expect(consumersOf(root, Object.keys(files), { name: 'attempts', old: '1', new: '3', file: 'lanes/review-noloss/judge-run.ts' })).toEqual(['tests/judge-run.test.ts:3']);
fs.rmSync(root, { recursive: true, force: true });
});
// Value: protects=a test that keeps an old value on purpose (the refused input after a minimum-version bump) can waive that one line
// with a `pregate-literal: NAME=value reason` comment, while every other old-value line, a marker for another constant or value,
// a marker with no reason (a bare `*/`, `**/` or `* */` closer is not a reason) and marker text inside a string still count, and a line marked
// for one constant can still be waived for another by the comment line above; fails_when=the waiver widens past its line or a stray
// marker clears a real consumer (#3490, gbrain#7690); why_new=the only escape was warn: ["literals"] for every constant;
// seam=none; no_claim=the policy file (no key exists; see the config test)
test('consumersOf: a pregate-literal marker waives only its own line, for its own constant and old value, with a reason (#3490)', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-literals-waiver-'));
const nl = String.fromCharCode(10);
const files: Record<string, string> = {
'test/runtime.test.ts': [
"import { MIN_RUNTIME, checkRuntime } from '../src/runtime';",
"expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MIN_RUNTIME='1.4.0' refused input below the minimum",
"// pregate-literal: MIN_RUNTIME=1.4.0 the oldest runtime we still name in the error",
"expect(message('1.4.0')).toContain('upgrade');",
"expect(MIN_RUNTIME).toBe('1.4.0');",
''].join(nl),
'test/other-name.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MAX_RUNTIME='1.4.0' wrong constant", ''].join(nl),
'test/other-value.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MIN_RUNTIME='1.3.9' wrong value", ''].join(nl),
'test/no-reason.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MIN_RUNTIME='1.4.0'", ''].join(nl),
'test/in-string.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "const src = \"expect(v('1.4.0')); // pregate-literal: MIN_RUNTIME='1.4.0' looks waived\"; check('1.4.0');", ''].join(nl),
'test/two-above.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "// pregate-literal: MIN_RUNTIME='1.4.0' too far above", '', "expect(checkRuntime('1.4.0')).toBe('refused');", ''].join(nl),
'test/all-waived.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); # pregate-literal: MIN_RUNTIME='1.4.0' refused input", ''].join(nl),
'test/block-no-reason.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' */", ''].join(nl),
'test/block-stars.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' **/", ''].join(nl),
'test/block-star-space.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' * */", ''].join(nl),
'test/block-stars-reason.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' refused input **/", ''].join(nl),
'test/two-constants.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "// pregate-literal: MIN_RUNTIME='1.4.0' lower bound of the refused range", "expect(inRange('1.4.0', '2.0.0')).toBe(false); /* pregate-literal: MAX_RUNTIME='2.0.0' upper bound */", ''].join(nl),
};
for (const [rel, text] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), text); }
const change = { name: 'MIN_RUNTIME', old: "'1.4.0'", new: "'1.4.4'", file: 'src/runtime.ts' };
const waived: LiteralWaiver[] = [];
const base = (rel: string) => rel === 'test/runtime.test.ts' ? files[rel]!.split(nl).slice(0, 2).join(nl) : null;
expect(consumersOf(root, Object.keys(files), change, base, waived)).toEqual([
'test/runtime.test.ts:5', 'test/other-name.test.ts:2', 'test/other-value.test.ts:2', 'test/no-reason.test.ts:2', 'test/in-string.test.ts:2', 'test/two-above.test.ts:4', 'test/block-no-reason.test.ts:2', 'test/block-stars.test.ts:2', 'test/block-star-space.test.ts:2',
]);
expect(waived).toEqual([
{ at: 'test/runtime.test.ts:2', reason: 'refused input below the minimum', added: false },
{ at: 'test/runtime.test.ts:4', reason: 'the oldest runtime we still name in the error', added: true },
{ at: 'test/all-waived.test.ts:2', reason: 'refused input', added: true },
{ at: 'test/block-stars-reason.test.ts:2', reason: 'refused input', added: true },
{ at: 'test/two-constants.test.ts:3', reason: 'lower bound of the refused range', added: true },
]);
fs.rmSync(root, { recursive: true, force: true });
});
test('consumersOf: a history row is skipped only when this branch added the new-value row to the same table; other old-value lines still count (GST-344)', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-literals-history-'));
const nl = String.fromCharCode(10);
@@ -500,6 +545,19 @@ describe('gstack-pregate (bin)', () => {
expect(r.err).toContain('(PREGATE_LITERALS)');
});
// Value: protects=end to end, a marked refused-input line passes literals and the run names the waiver, flagging one this branch
// added; a branch policy cannot add a literals waiver; fails_when=the check ignores markers, hides a waiver, or the policy grows a
// literals key (#3490); why_new=no waiver existed; seam=none; no_claim=gstack-gate-diff tagging
test('#3490: a pregate-literal marker passes literals for its line and the run lists the waiver as added on this branch', () => {
const repo = fixture();
commit(repo, { 'test/nightly/retries.test.js': "const test = require('node:test');\nconst assert = require('node:assert');\nconst { MAX_RETRIES, retry } = require('../../lib/retry.js');\n// pregate-literal: MAX_RETRIES=3 the old cap is the input the guard must refuse\ntest('refuses 3', () => assert.throws(() => retry(() => 0, { MAX_RETRIES: 3 })));\n" });
commit(repo, { 'lib/retry.js': "exports.MAX_RETRIES = 4;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\n", 'test/retry.test.js': "const test = require('node:test');\nconst assert = require('node:assert');\nconst { MAX_RETRIES } = require('../lib/retry.js');\ntest('retries four times', () => assert.strictEqual(MAX_RETRIES, 4));\n" });
const r = pregate(repo, ['--stage', 'preflight', '--only', 'literals']);
expect(r.status).toBe(EXIT.ok);
expect(r.out).toContain('literals: waived: test/nightly/retries.test.js:5: MAX_RETRIES 3 (the old cap is the input the guard must refuse; marker added on this branch)');
expect(validateConfig({ ...BASE_PREGATE, literals: { allow: [{ name: 'MAX_RETRIES', value: '3' }] } }).map(e => e.path)).toEqual(['literals']);
});
test('GST-344: a history table the branch extends passes literals through the merge-base read; one it does not extend fails', () => {
const history = (...ns: number[]) => ["const { MAX_RETRIES } = require('../lib/retry.js');", 'const versions = [', ...ns.map(n => ` [${n}, 'digest-${n}'],`), '];', ''].join('\n');
const repo = fixture({ baseFiles: { 'test/history.test.js': history(2, 3) } });
+91
View File
@@ -0,0 +1,91 @@
import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { collect, fixableRaises, parseFindings, verify } from '../lib/review-ledger';
const ROOT = path.resolve(import.meta.dir, '..');
const NATIVE = fs.readFileSync(path.join(ROOT, 'test/fixtures/review-ledger/gsta-141-native.md'), 'utf8').split('\n').filter(l => /^\d+\. /.test(l)).join('\n\n');
const plan = (cls: string, reasons: string) => [`REVIEW_CLASS: ${cls} (x)`, `REVIEW_DEPTH: full runtime_lines=9 rules=r reasons=${reasons} codes=none`];
const CODE = plan('code', 'runtime=9');
describe('native FIXABLE by review class (GST-987)', () => {
// Value: protects=outside the gate class with no risk reason a native finding's own Severity field decides, so gstack-evals #141's
// eval-driver findings (FIXABLE. Severity: Medium/Low. Blocks: no) collect as Medium and Low, while gate, risk and unreadable plans
// keep normalizer v2; fails_when=FIXABLE raises a fielded finding on a code-class plan, or a gate, risk, error or missing plan stops
// raising it; why_new=GEVA-2 reported #141 collecting both as CRITICAL; seam=fixableRaises, parseFindings and collect --manifest;
// no_claim=which class a diff belongs to (lib/review-depth.ts decides)
test('#141 native #4 and #6 collect at their own Severity on a code-class plan', () => {
expect(parseFindings('adversarial-native', NATIVE, fixableRaises(CODE)).map(f => f.label)).toEqual(['Medium', 'Low']);
expect(parseFindings('adversarial-native', NATIVE).map(f => f.label)).toEqual(['CRITICAL', 'CRITICAL']);
});
test('gate class, any risk reason, a depth error or an unreadable plan keeps normalizer v2', () => {
for (const lines of [plan('gate', 'gate-class,runtime=9'), plan('code', 'risk:money,runtime=9'), plan('code', 'risk:data,runtime=9'), plan('code', 'error:DIFF,runtime=9'), plan('code', 'runtime=9,history:fixed-blocking'), plan('code', 'runtime=9,floor:full'), plan('code', ''), plan('unknown', 'runtime=9'), [CODE[0]!], [], undefined])
expect(fixableRaises(lines)).toBe(true);
expect(fixableRaises(CODE)).toBe(false);
expect(fixableRaises(plan('docs', 'runtime=0'))).toBe(false);
});
test('on a code-class plan FIXABLE without a Severity field still collects CRITICAL, and Blocks: yes still raises', () => {
expect(parseFindings('adversarial-native', '1. Race in a.ts:3. FIXABLE', false).map(f => f.label)).toEqual(['CRITICAL']);
expect(parseFindings('adversarial-native', 'FIXABLE: race in a.ts:3\nSeverity: Medium\nBlocks: no', false).map(f => f.label)).toEqual(['Medium']);
expect(parseFindings('adversarial-native', 'FIXABLE: race in a.ts:3\nSeverity: Medium\nBlocks: yes', false).map(f => f.label)).toEqual(['CRITICAL']);
expect(parseFindings('adversarial-native', '1. Race in a.ts:3. Severity: FIXABLE', false).map(f => f.label)).toEqual(['CRITICAL']);
expect(parseFindings('adversarial-native', '1. FIXABLE: race in store.ts:40. Severity: High', false).map(f => f.label)).toEqual(['CRITICAL']);
expect(parseFindings('adversarial-native', '1. FIXABLE: race in store.ts:40\nSeverity: High\nBlocks: no', false).map(f => f.label)).toEqual(['High']);
expect(parseFindings('adversarial-native', '## FIXABLE\n1. Leak in a.ts:3. Severity: Low. Blocks: no\n2. Race in b.ts:4\n3. Drift in c.ts:5. Severity: Low', false).map(f => f.label)).toEqual(['Low', 'CRITICAL', 'CRITICAL']);
expect(parseFindings('adversarial-native', '**FIXABLE**\n- Leak in a.ts:3\n Severity: Medium', false).map(f => f.label)).toEqual(['CRITICAL']);
expect(parseFindings('adversarial-native', 'FIXABLE: race in a.ts:3. Severity: Low. Blocks: no-op path only', false).map(f => f.label)).toEqual(['CRITICAL']);
});
// Value: protects=a paragraph under a numbered finding heading joins that heading's finding, so the reader's later Severity and Blocks
// fields attach to it instead of splitting it into a CRITICAL heading plus a body item (gstack-evals #149); fails_when=any of the three
// shapes collects 4 findings or the heading alone keeps CRITICAL on a code-class plan; why_new=#149 split native findings in both
// classes; seam=parseFindings; no_claim=how unnumbered headings group
test.each([
['### 1. FIXABLE: race in a.ts:3\n\nThe body explains it.\n\nSeverity: Medium, Blocks: no\n\n### 2. FIXABLE: drift in b.ts:9\n\nMore body.\n\nSeverity: Low, Blocks: no'],
['**1. Race in a.ts:3** FIXABLE\n\nBody.\n\nSeverity: Medium, Blocks: no\n\n**2. Drift in b.ts:9** FIXABLE\n\nBody.\n\nSeverity: Low, Blocks: no'],
['### 1. Race in a.ts:3 — FIXABLE\n\nBody.\n\n**Severity:** Medium\n**Blocks:** no\n\n### 2. Drift in b.ts:9 — FIXABLE\n\nBody.\n\n**Severity:** Low\n**Blocks:** no'],
])('a numbered finding heading keeps its body paragraph and fields (#149) %#', (output) => {
expect(parseFindings('adversarial-native', output, false).map(f => f.label)).toEqual(['Medium', 'Low']);
expect(parseFindings('adversarial-native', output, true).map(f => f.label)).toEqual(['CRITICAL', 'CRITICAL']);
});
test('a labeled or located paragraph after a numbered heading stays its own finding (#149 joins only plain body text)', () => {
for (const raises of [true, false]) {
expect(parseFindings('outside-voice', '### 1. P2 race in a.ts:3\n\nP0: also data loss in b.ts:9\n\nSeverity: Medium', raises).map(f => f.label)).toEqual(['P2', 'P0']);
expect(parseFindings('outside-voice', '### 1. P2 race in a.ts:3\n\nBody.\n\nP1: second issue in b.ts:9', raises).map(f => f.label)).toEqual(['P2', 'P1']);
}
});
test('collect reads the class from --manifest and defaults to the gate class without one', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gst987-'));
const out = path.join(dir, 'native.txt');
fs.writeFileSync(out, NATIVE + '\n');
const manifest = path.join(dir, 'pass-manifest.json');
const ledger = path.join(dir, 'ledger.jsonl');
fs.writeFileSync(manifest, JSON.stringify({ version: 1, ledger, plan_lines: CODE, readers: [] }));
const foreign = path.join(dir, 'foreign.json');
fs.writeFileSync(foreign, JSON.stringify({ version: 1, ledger: path.join(dir, 'other.jsonl'), plan_lines: CODE, readers: [] }));
let n = 0;
const labels = (extra: string[]) => {
fs.rmSync(ledger, { force: true });
const merged = path.join(dir, `${n++}.merged.jsonl`);
const r = spawnSync(path.join(ROOT, 'bin/gstack-review-dispatch'), ['collect', '--ledger', ledger, '--role', 'adversarial-native', '--output', out, '--merged', merged, ...extra], { cwd: ROOT, encoding: 'utf8', timeout: 60_000 });
expect(r.stderr).toBe('');
return fs.readFileSync(merged, 'utf8').trim().split('\n').map(l => JSON.parse(l).label);
};
expect(labels(['--manifest', manifest])).toEqual(['Medium', 'Low']);
expect(fs.readFileSync(ledger, 'utf8')).toContain('"fixable_raises":false');
expect(labels([])).toEqual(['CRITICAL', 'CRITICAL']);
expect(fs.readFileSync(ledger, 'utf8')).not.toContain('fixable_raises');
expect(labels(['--manifest', foreign])).toEqual(['CRITICAL', 'CRITICAL']);
fs.rmSync(dir, { recursive: true, force: true });
});
test('verify re-derives a code-class collect with the class it was collected under', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gst987v-'));
const out = path.join(dir, 'native.txt');
fs.writeFileSync(out, NATIVE + '\n');
const c = collect(path.join(dir, 'l.jsonl'), 'adversarial-native', NATIVE + '\n', path.join(dir, 'm.jsonl'), out, undefined, undefined, false);
expect(c.lines.map(l => l.label)).toEqual(['Medium', 'Low']);
expect(JSON.stringify(verify(path.join(dir, 'l.jsonl')))).not.toContain('merged_line_differs');
fs.rmSync(dir, { recursive: true, force: true });
});
});
+7 -4
View File
@@ -11,6 +11,7 @@ const FIELD = new RegExp(`(?:(?:^|\\| )(?:[-*+>]\\s+|\\d+[.)]\\s+)?\\**\\s*Sever
const LEAD = new RegExp(`(?:^|\\| )[^\\p{L}\\p{N}]*(?:\\d+[.)]\\s*[^\\p{L}\\p{N}]*)?(?:\\w[\\w./-]*:\\d+(?:[-–:,]\\s*\\d+)*\\S*\\s+[^\\p{L}\\p{N}]*)?(?:${LABELS})(?![a-z])`, 'u');
const HEADING = new RegExp(`^(?:#{1,6}\\s*|\\*\\*)(?:${LABELS})\\b`, 'm');
const LABEL = new RegExp(`${LABELS}`, 'gi');
const LEAD_NO_FIXABLE = new RegExp(LEAD.source.replace('|FIXABLE|INVESTIGATE', ''), 'u');
describe('parseFindings is never weaker than main (GST-976 invariant)', () => {
// Value: protects=no input the suite pins, no reader fixture and no Severity field shape collects below the label main (82e9d7ec)
@@ -18,8 +19,10 @@ describe('parseFindings is never weaker than main (GST-976 invariant)', () => {
// leading label, a label heading, Blocks: yes or the native voice's FIXABLE raises it; fails_when=a field chain (High/Critical) keeps
// its weaker label (b3ddd6c) or a field whose clause runs on stops counting and a body P0 raises the finding (f3810d4);
// why_new=three GST-976 review rounds each found a shape the row tables missed; seam=parseFindings; no_claim=that main's label was
// right (it is the floor), or coverage of stored private reader outputs (checked outside the repo)
test('every pinned input, reader fixture and generated field shape', () => {
// right (it is the floor), or coverage of stored private reader outputs (checked outside the repo). GST-987: the gate class (and every
// risk class) keeps this rule; outside it the native voice's FIXABLE beside Blocks: no no longer counts as a raise, so such a fielded
// finding collects at its field unless a lead, a label heading or Blocks: yes raises it
test.each([['gate', true], ['code', false]] as const)('every pinned input, reader fixture and generated field shape (%s class)', (_cls, raises) => {
const { entries, shapes } = JSON.parse(fs.readFileSync(path.join(ROOT, 'test/fixtures/review-ledger/main-labels.json'), 'utf8'));
const { bodies, fields: tails, joins, readers, generated } = shapes;
for (const [b, f, j, r, main] of generated) entries.push({ reader: readers[r], output: bodies[b] + joins[j] + tails[f], main });
@@ -27,14 +30,14 @@ describe('parseFindings is never weaker than main (GST-976 invariant)', () => {
let findings = 0;
for (const e of entries as Array<{ reader: string; output?: string; fixture?: string; main: string[] }>) {
const output = e.output ?? fs.readFileSync(path.join(ROOT, 'test/fixtures', e.fixture!), 'utf8');
const got = parseFindings(e.reader, output);
const got = parseFindings(e.reader, output, raises);
if (!e.main.length) continue;
if (got.length !== e.main.length) { bad.push(`count ${got.length}/${e.main.length} ${JSON.stringify(output.slice(0, 80))}`); continue; }
got.forEach((f, i) => {
findings++;
const fields = [...f.raw.matchAll(FIELD)].map(m => (m[1] ?? m[2])!);
const single = fields.length === 1 && fields[0]!.match(LABEL)!.length === 1 ? fields[0]!.match(LABEL)![0] : undefined;
const raised = /Blocks:?\**:?\s*\**\s*yes/i.test(f.raw) || (e.reader.startsWith('adversarial-native') && /FIXABLE/.test(f.raw)) || LEAD.test(f.raw) || HEADING.test(output);
const raised = /Blocks:?\**:?\s*\**\s*yes/i.test(f.raw) || (e.reader.startsWith('adversarial-native') && /FIXABLE/.test(f.raw) && (raises || !/Blocks:?\**:?\s*\**\s*no\b/i.test(f.raw))) || (raises ? LEAD : LEAD_NO_FIXABLE).test(f.raw) || HEADING.test(output);
const ok = single === undefined ? (rank(f.label) ?? 0) >= (rank(e.main[i]) ?? 0)
: (rank(f.label) ?? 0) >= (rank(single) ?? 0) && (raised || (rank(f.label) ?? 0) === (rank(single) ?? 0));
if (!ok) bad.push(`${e.reader} main=${e.main[i]} got=${f.label} field=${single ?? '-'} ${JSON.stringify(f.raw.slice(0, 100))}`);
+14 -15
View File
@@ -65,6 +65,7 @@ const NO_LOSS_REPLAYS = {
'R51': "R51 risk-tier routing no-loss re-score (GSTA-167): the saved panel findings (349 records over 24 of the 26 no-loss units) re-scored against the branch's plan lose no blocker whose only finders are roles it drops, and every gate, security, user-data and money fixture keeps main's reviewers plus the full panel (test/review-targeted.test.ts, test/review-risk-routing.test.ts); reviewer text is unchanged",
'GST-508+R51': "wave review-53cfb5bfe011 merge of #3744 into main: lib/review-depth.ts keeps both admitted changes, the GST-508 no-loss replay of review_exclude_paths (Arm V 33 of 41 blockers vs R 33 of 41, p_NI 0.0279; GStack Minder drive handoffs/GSTA-202/noloss-results.md) and the R51 risk-tier re-score with GST-850's dispatch-superset no-loss check: excluded paths sit beside the added and removed line maps, and the exclusion filter keeps the publishes-docs rule",
'GST-508+GST-464': "wave review-53cfb5bfe011 merge of #3744 into main: ship/sections/review-army.md and adversarial.md carry the GST-508 no-loss-replayed exclude pathspec placeholders inside the brief bodies (handoffs/GSTA-202/noloss-results.md) and the GST-464 lead-instruction edits outside the cut spans, which its brief-identity no-loss check admitted",
'GST-986': "GST-986 brief-identity no-loss check (the brief-body test below): every reviewer brief lib/review-brief.ts cuts from ship/sections/adversarial.md is byte-identical to the bodies at the GST-257 pin; only the lead's Step 11 instruction naming the targeted record's path changed, outside the cut spans",
'GST-911-redos': "GST-911 ReDoS fix verdict-identity no-loss check (wave review-e7b551ebd865): SVG_REFUSED's url() entry, rewritten to run in linear time, refuses exactly the strings #3838's pattern refused (test/review-risk-redos.test.ts: 60,000 seeded strings and the hand-picked edges, and every SVG blob in the repository's history, with zero differences); SVG_MARKUP now refuses a DOCTYPE naming any document but W3C's SVG DTD (test/review-planner-misreads.test.ts), which only keeps more SVGs code and so drops no reviewer; GST-911's no-loss replay holds unchanged; reviewer text is unchanged",
'GST-911': "GST-911 no-loss replay (GSTA-249/13): the planner over the last 100 first-parent main commits to f5cb6ec1e, origin/main's code against this branch, gate scan fixed at 0, changes no commit's depth, class, risk classes, targeted classes or SCOPE_AUTH; the three misreads (a drawing-only SVG, a session-named data file, a bare base) are proven on fixtures (test/review-planner-misreads.test.ts), where every SVG that can run script stays code with the security class; reviewer text is unchanged",
'GST-508+R51+GST-911': "wave review-53cfb5bfe011 merge of main 3d99c2249 (#3848) into #3852: lib/review-depth.ts keeps GST-508's review_exclude_paths (no-loss replay, GStack Minder drive handoffs/GSTA-202/noloss-results.md), R51 with GST-850's dispatch-superset no-loss check, and GST-911's SVG drawings with GST-911-redos (no-loss replay: 0 of 100 main commits changed, reports/GSTA-249-911-decisions.tsv; test/review-planner-misreads.test.ts, test/review-risk-redos.test.ts); lib/review-risk.ts is main's GST-911 bytes. Merged-head checks (handoffs/GSTA-202/merge-3d99c22-checks.md): Arm P on unit 3593 differs only in the exclude pathspec and the disclosure line, the same brief diff as before the merge, and GST-911 panel unit f5cb6ec1e plans, dispatches and renders byte-identical briefs under 3d99c2249 and the merged head",
@@ -98,10 +99,10 @@ const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
};
/**
* The six reviewer brief bodies lib/review-brief.ts cuts out of the two ship sections, pinned at
* the bytes replays L and GST-257 admitted, except fixtures, redTeam and native, which carry the exclude
* pathspec placeholders the GST-508 replay admitted (byte-identical to #3744 at 995e442). A section repin under
* GST-464 is valid only while every body still hashes here: the section may change outside the cut spans, never inside them.
* The reviewer brief bodies lib/review-brief.ts cuts out of the ship sections, at the bytes replays L and GST-257
* admitted, except fixtures, redTeam and native, which carry the exclude pathspec placeholders the GST-508 replay
* admitted (byte-identical to #3744 at 995e442). A section repin under GST-464 or GST-986 is valid only while every
* body still hashes here: the section may change outside the cut spans, never inside them.
*/
const BRIEF_BODIES: Record<string, string> = {
specialist: '1ebe47ddb3291a9e4fe90b5a260eb57e328144959549bc95c119b68d23652cb5',
@@ -111,22 +112,20 @@ const BRIEF_BODIES: Record<string, string> = {
native: 'a7cc08fb31b6ca65932a316d589f0b09677426d193d63f88ec74fee6d93ce053',
outside: 'bd96406da28b03742384560425a333b6d6d7ed2be95bd2026c90e0039632cee2',
};
const bodyHashes = (root: string) => Object.fromEntries(Object.entries(sectionTexts(root)).map(([k, v]) => [k, createHash('sha256').update(v).digest('hex')]));
describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () => {
// Value: protects=the GST-464 repin of ship/sections/review-army.md and adversarial.md admits only lead-instruction
// edits: every brief a reviewer reads renders byte-identical to the no-loss-tested bodies; fails_when=an edit inside a
// cut span changes a brief while its section pin is moved; seam=sectionTexts, the function every render uses;
// no_claim=the checklists and the targeted brief, pinned by their own file hashes above
// Value: protects=the GST-986 repin of ship/sections/adversarial.md admits only a lead-instruction edit: every brief a reviewer
// reads renders byte-identical to the no-loss-tested bodies; fails_when=an edit inside a cut span changes a brief while the
// section pin moves; seam=sectionTexts, the function every render uses; no_claim=the checklists (pinned by file hash below)
test('every reviewer brief cut from the ship sections is byte-identical to its pinned body', () => {
expect(bodyHashes(ROOT)).toEqual(BRIEF_BODIES);
const hashes = (root: string) => Object.fromEntries(Object.entries(sectionTexts(root)).map(([k, v]) => [k, createHash('sha256').update(v).digest('hex')]));
expect(hashes(ROOT)).toEqual(BRIEF_BODIES);
const copy = fs.mkdtempSync(path.join(os.tmpdir(), 'brief-bodies-'));
try {
fs.mkdirSync(path.join(copy, 'ship', 'sections'), { recursive: true });
for (const f of ['review-army.md', 'adversarial.md']) fs.copyFileSync(path.join(ROOT, 'ship', 'sections', f), path.join(copy, 'ship', 'sections', f));
const army = path.join(copy, 'ship', 'sections', 'review-army.md');
fs.writeFileSync(army, fs.readFileSync(army, 'utf8').replace('You are a specialist code reviewer.', 'You are a specialist code reviewer. Skim.'));
expect(bodyHashes(copy).specialist).not.toBe(BRIEF_BODIES.specialist);
fs.cpSync(path.join(ROOT, 'ship', 'sections'), path.join(copy, 'ship', 'sections'), { recursive: true });
const adversarial = path.join(copy, 'ship', 'sections', 'adversarial.md');
fs.writeFileSync(adversarial, fs.readFileSync(adversarial, 'utf8').replace('Recommendation:', 'Recommendation (short):'));
expect(hashes(copy)).not.toEqual(BRIEF_BODIES);
} finally {
fs.rmSync(copy, { recursive: true, force: true });
}
+52
View File
@@ -247,3 +247,55 @@ describe('GST-951: the receipt counts every blocker the readers found', () => {
expect(both.err).toContain('(REVIEW_SCOPE_INPUT;');
});
});
describe('GST-986 item 5: fix-check passes and their findings count from the ledgers', () => {
// #3862 (reconstruction from its PR body; GST-963, pin 0ead1ce50): round 1's red-team CRITICAL sat in pass 1's row unclassified,
// fixed in be9480829; the first granted fix-check found a CRITICAL, fixed in 0ead1ce50; the second fix-check was clean; the
// targeted reader then raised 2 informational findings. Only pass 1 had a review-log row, so the receipt's tool counters read
// blocking_found=0 fixed=0 fix_checks=0 and the lane hand-set blocking_found=2 fixed=2 fix_checks=2 (its third, the docs
// re-check, ran outside the pass program). The outside voice's findings, which GStack Minder ruled below the floor, are left out.
// Value: protects=a fix-check that ran through gstack-review-dispatch counts as a round, and a blocker a later fix-check
// followed counts as fixed, without a review-log row; fails_when=summary counts fix_checks only from rows, or reports such
// blockers as unrecorded; seam=gstack-review-scope summary --run; no_claim=noted (a floor ruling lives only in rows)
test('#3862: two fix-check ledgers count as fix_checks=2, and the blockers they followed count as fixed', () => {
const repo = branchWith([review(0, 1, [{ fingerprint: 'lib/tree-receipt.ts:120:logic', raised_by: 'red-team' }])]);
const run = path.join(repo.root, 'run');
const step = (dir: string, readers: Array<[string, string]>) => { pass(path.join(run, dir), readers); Bun.sleepSync(5); };
step('p1', [['specialist-batched', CLEAN], ['red-team', '[CRITICAL] lib/tree-receipt.ts:120 the tree receipt floors the branch\'s own stamp row at the gated VERSION']]);
step('fc1', [['fix-check', '[CRITICAL] lib/ship-queue-train-stamp.ts:88 the train stamp and verify checks pass no baseRef']]);
step('fc2', [['fix-check', CLEAN]]);
step('s11', [['targeted', '- [INFORMATIONAL] lib/stamp-only.ts:14 a comment names the old bound\n- [INFORMATIONAL] lib/stamp-only.ts:30 the side argument has no default test']]);
const s = loop(summary(repo, '--run', run));
expect(s).toMatchObject({ passes: 1, blocking_found: 2, fixed: 2, fix_checks: 2, rounds: 3, ledgers_read: 4 });
expect(s.unrecorded).toBeUndefined();
});
// Value: protects=a fix-check with both a row and a ledger counts once, and once every fix-check has its row the rows decide
// dispositions (a blocker no row disposes stays unrecorded); fails_when=the ledger count adds to the row count, or a logged
// fix-check turns undisposed blockers fixed; seam=gstack-review-scope summary --run
test('a fix-check counted by row and ledger counts once; with its row logged, undisposed blockers stay unrecorded', () => {
const repo = branchWith([
review(0, 1, [{ fingerprint: 'lib/a.ts:5:logic', severity: 'CRITICAL', blocking: true, action: 'deferred', raised_by: 'red-team' }]),
fixCheck(30, []),
]);
const run = path.join(repo.root, 'run');
const step = (dir: string, readers: Array<[string, string]>) => { pass(path.join(run, dir), readers); Bun.sleepSync(5); };
step('p1', [['red-team', '[CRITICAL] lib/a.ts:5 a deferred defect outside the diff\n[CRITICAL] lib/b.ts:9 a fixed defect']]);
step('fc1', [['fix-check', '[CRITICAL] lib/c.ts:3 the fix broke the caller']]);
const s = loop(summary(repo, '--run', run));
expect(s).toMatchObject({ passes: 1, blocking_found: 3, deferred: 1, fixed: 0, fix_checks: 1, rounds: 2, unrecorded: 2 });
});
// Value: protects=a blocker the last fix-check raises again is not fixed: only a fix-check after its last occurrence follows it
// (round 2 outside voice on this PR: the re-raised blocker read blocking_found=1 fixed=1 with nothing unrecorded);
// fails_when=an earlier occurrence's follow-up marks it fixed; seam=gstack-review-scope summary --run
test('a blocker the last fix-check raises again stays unrecorded', () => {
const repo = branchWith([review(0, 1, [{ fingerprint: 'lib/a.ts:10:logic', raised_by: 'red-team' }])]);
const run = path.join(repo.root, 'run');
const step = (dir: string, readers: Array<[string, string]>) => { pass(path.join(run, dir), readers); Bun.sleepSync(5); };
step('p1', [['red-team', '[CRITICAL] lib/a.ts:10 the cap is never enforced']]);
step('fc1', [['fix-check', '[CRITICAL] lib/a.ts:10 the cap is still never enforced']]);
const s = loop(summary(repo, '--run', run));
expect(s).toMatchObject({ passes: 1, blocking_found: 1, fixed: 0, fix_checks: 1, unrecorded: 1 });
});
});
+295
View File
@@ -0,0 +1,295 @@
/**
* GST-986 (GST-966 part 2): the targeted reader's record from verify to the receipt.
* verify records what the reader did and the commit it read; write binds that record only to a receipt whose product patch
* is the one the reader read; write and carry refuse a receipt the queue's targeted verdict would refuse; /ship's receipt
* writes pass the record. Cases come from gstack-evals#140, C5-36/C6-36 item 5 and GSTA-234's outside-voice fixture on #3859.
*/
import { afterEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { checkoutTree } from '../lib/review-brief';
import { TARGETED_RULES, targetedBriefSha } from '../lib/review-targeted';
import { extractReceipt, placeReceiptFirst } from '../lib/ship-receipt';
import { receiptDepthAtHead } from '../lib/ship-receipt-depth';
import { cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
setDefaultTimeout(180_000);
const ROOT = path.resolve(import.meta.dir, '..');
const repos: FixtureRepo[] = [];
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
function moneyBranch(): { repo: FixtureRepo; base: string; head: string } {
const repo = makeFixtureRepo({ prefix: 'gst986-' });
repos.push(repo);
repo.git('checkout', '-q', '-b', 'feat');
fs.mkdirSync(path.join(repo.work, 'src'), { recursive: true });
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 5;\n');
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'charge');
return { repo, base: repo.git('merge-base', 'origin/main', 'HEAD'), head: repo.git('rev-parse', 'HEAD') };
}
const commit = (repo: FixtureRepo, file: string, text: string, msg: string) => {
fs.mkdirSync(path.dirname(path.join(repo.work, file)), { recursive: true });
fs.writeFileSync(path.join(repo.work, file), text);
repo.git('add', '-A'); repo.git('commit', '-q', '-m', msg);
return repo.git('rev-parse', 'HEAD');
};
/** One targeted read through the real dispatch steps; `reply` is the reader's raw output after its REVIEWED_TREE line. */
function targetedRead(repo: FixtureRepo, base: string, dir: string, reply: string | null, sent = true, classes = 'money'): string {
const out = path.join(repo.root, dir);
const ledger = path.join(out, 'review-ledger.jsonl');
const render = runBin(repo, 'gstack-review-dispatch', ['render', '--role', 'targeted', '--classes', classes, '--diff-base', base, '--out', out]);
expect(render.status, render.err).toBe(0);
const file = /file=(\S+)/.exec(render.out)![1]!;
if (sent) expect(runBin(repo, 'gstack-review-dispatch', ['sent', '--ledger', ledger, '--role', 'targeted', '--sent', file]).status).toBe(0);
if (reply === null) return ledger;
const output = path.join(out, 'targeted.out');
fs.writeFileSync(output, `REVIEWED_TREE: ${checkoutTree(repo.work)}\n${reply}\n`);
const merged = path.join(out, 'merged.jsonl');
expect(runBin(repo, 'gstack-review-dispatch', ['collect', '--ledger', ledger, '--role', 'targeted', '--output', output, '--merged', merged]).status).toBe(0);
const judged = path.join(out, 'judged.jsonl');
fs.writeFileSync(judged, fs.readFileSync(merged, 'utf8').split('\n').filter(Boolean).map(l => JSON.stringify({ id: JSON.parse(l).id, severity: JSON.parse(l).label }) + '\n').join(''));
expect(runBin(repo, 'gstack-review-dispatch', ['judge', '--ledger', ledger, '--merged', merged, '--judged', judged]).status).toBe(0);
return ledger;
}
const verify = (repo: FixtureRepo, ledger: string, record?: string) =>
runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger, ...(record ? ['--targeted-out', record] : [])]);
const recordLine = (out: string) => out.split('\n').find(l => l.startsWith('TARGETED_RECORD:')) ?? '';
const finding = (line: number, summary: string) => JSON.stringify({ severity: 'INFORMATIONAL', confidence: 7, path: 'src/charge.ts', line, category: 'money', summary, fingerprint: `src/charge.ts:${line}:money`, specialist: 'targeted' });
const write = (repo: FixtureRepo, args: string[]) => runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', ...args, '--json']);
describe('verify records the targeted reader\'s outcome and the commit it read (item 1)', () => {
// Value: protects=a targeted reader that completed reads as done, with its findings counted, however the head moves after it
// (gstack-evals#140, C5-36 and C6-36 p2: Step 11 fixes moved the head and the clean or 2-finding read printed status=failed
// findings=0); a reader that did not complete stays failed; fails_when=verify fails a completed read because HEAD moved, zeroes
// its findings, or marks a read with no matching dispatch or output done; seam=gstack-review-dispatch verify --targeted-out;
// no_claim=binding the record to the receipt (write, below)
test('a clean read and a 2-finding read stay done after a later fix commit; an unsent or uncollected read is failed', () => {
const { repo, base, head } = moneyBranch();
const clean = targetedRead(repo, base, 'clean', 'NO FINDINGS');
const found = targetedRead(repo, base, 'found', `${finding(1, 'the cap is never enforced')}\n${finding(1, 'the cap has no unit test')}`);
const unsent = targetedRead(repo, base, 'unsent', 'NO FINDINGS', false);
const silent = targetedRead(repo, base, 'silent', null);
commit(repo, 'src/charge.ts', 'export const spend_cap_usd = 5; // enforced in run()\n', 'Step 11 fix after the reader');
const record = path.join(repo.root, 'targeted.json');
const v = verify(repo, clean, record);
expect(recordLine(v.out)).toStartWith(`TARGETED_RECORD: classes=money status=done findings=0 brief=${targetedBriefSha().slice(0, 12)} rules=${TARGETED_RULES}`);
expect(JSON.parse(fs.readFileSync(record, 'utf8'))).toEqual({ classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), status: 'done', findings: 0, raised_by: 'targeted', head });
expect(recordLine(verify(repo, found).out)).toStartWith('TARGETED_RECORD: classes=money status=done findings=2 ');
expect(recordLine(verify(repo, unsent).out)).toStartWith('TARGETED_RECORD: classes=money status=failed ');
expect(recordLine(verify(repo, silent).out)).toStartWith('TARGETED_RECORD: classes=money status=failed findings=0 ');
});
// Value: protects=a read rendered on uncommitted tracked edits never reads as done (nothing proves which patch it read);
// fails_when=the item 1 repair accepts it; seam=gstack-review-dispatch render + verify
test('a read rendered on uncommitted tracked edits is failed', () => {
const { repo, base } = moneyBranch();
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 6;\n');
const dirty = targetedRead(repo, base, 'dirty', 'NO FINDINGS');
repo.git('checkout', '--', 'src/charge.ts');
expect(recordLine(verify(repo, dirty).out)).toStartWith('TARGETED_RECORD: classes=money status=failed ');
});
// Value: protects=a run whose ledger holds no targeted render leaves no record for Step 19 to pass, so an earlier run's
// .gstack/tmp/targeted.json never reaches write (pass 1 Red Team note on this PR); fails_when=verify leaves the stale file;
// seam=gstack-ship-receipt write's input via gstack-review-dispatch verify --targeted-out
test('verify with no targeted render removes a stale --targeted-out file', () => {
const { repo, base } = moneyBranch();
const out = path.join(repo.root, 'plain');
const render = runBin(repo, 'gstack-review-dispatch', ['render', '--role', 'specialist', '--name', 'testing', '--diff-base', base, '--out', out]);
expect(render.status, render.err).toBe(0);
const record = path.join(repo.root, 'targeted.json');
fs.writeFileSync(record, '{"stale":true}\n');
const v = verify(repo, path.join(out, 'review-ledger.jsonl'), record);
expect(recordLine(v.out)).toBe('');
expect(fs.existsSync(record)).toBe(false);
});
// Value: protects=a verify that stops early (an empty ledger: no brief records) still removes the stale record (round 2 Red
// Team and adversarial on this PR); fails_when=the early return leaves it; seam=gstack-review-dispatch verify --targeted-out
test('verify on a ledger with no brief records removes a stale --targeted-out file too', () => {
const { repo } = moneyBranch();
const ledger = path.join(repo.root, 'empty-ledger.jsonl');
fs.writeFileSync(ledger, '');
const record = path.join(repo.root, 'targeted.json');
fs.writeFileSync(record, '{"stale":true}\n');
expect(verify(repo, ledger, record).status).toBe(EXIT.refused);
expect(fs.existsSync(record)).toBe(false);
});
});
describe('write binds a targeted record only to the product patch the reader read (item 2)', () => {
// Value: protects=a record verified at H1 never vouches for H2's new product code (GSTA-234's outside-voice CRITICAL on #3859:
// write rebound the H1 record to H2's product patch-id and ready printed verdict=ok); a later commit that leaves the product
// patch alone (a test, the version stamp) still binds; fails_when=write accepts the stale record, rejects the unchanged
// product, or accepts a record that names no commit; seam=gstack-ship-receipt write --review-targeted
test('GSTA-234\'s fixture: an auth commit after verify is refused; a test-only commit binds', () => {
const { repo, base, head } = moneyBranch();
const record = path.join(repo.root, 'targeted.json');
expect(verify(repo, targetedRead(repo, base, 'h1', 'NO FINDINGS'), record).status).toBe(0);
commit(repo, 'test/charge.test.ts', 'test("cap", () => {});\n', 'test only');
const testOnly = write(repo, ['--review-targeted', `@${record}`]);
expect(testOnly.status, testOnly.err).toBe(EXIT.ok);
expect(JSON.parse(testOnly.out).review_targeted).toMatchObject({ classes: ['money'], status: 'done' });
commit(repo, 'src/session.ts', 'export function checkSession(token: string) { return token.length > 0; }\n', 'auth change after verify');
const body = path.join(repo.root, 'body.md');
const stale = write(repo, ['--review-targeted', `@${record}`, '--body', body]);
expect(stale.status).toBe(EXIT.fail);
expect(stale.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
expect(stale.err).toContain('(RECEIPT_INVALID');
expect(fs.existsSync(body)).toBe(false);
const { head: _drop, ...headless } = JSON.parse(fs.readFileSync(record, 'utf8'));
const old = write(repo, ['--review-targeted', JSON.stringify(headless)]);
expect(old.status).toBe(EXIT.fail);
expect(old.err).toContain('review_targeted.head');
});
// Value: protects=a record binds only to the product patch the reader read, so a docs or TODOS.md commit after the read (which
// changes the product patch) is refused like a money edit; binding docs-only commits is planned under GST-1031 with a full
// review; fails_when=write binds a record whose read product patch is not the receipt's; seam=gstack-ship-receipt write --review-targeted
test('a docs and TODOS.md commit after the read is refused; a money edit after it is refused', () => {
const { repo, base, head } = moneyBranch();
const record = path.join(repo.root, 'targeted.json');
expect(verify(repo, targetedRead(repo, base, 'h1', 'NO FINDINGS'), record).status).toBe(0);
commit(repo, 'docs/charges.md', '# Charges\n\nThe cap is enforced per run.\n', 'Step 14.5 docs');
commit(repo, 'TODOS.md', '# TODOS\n\n- [x] enforce the cap\n', 'Step 14 TODOS');
const docs = write(repo, ['--review-targeted', `@${record}`]);
expect(docs.status).toBe(EXIT.fail);
expect(docs.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
commit(repo, 'src/charge.ts', 'export const spend_cap_usd = 50;\n', 'money edit after verify');
const stale = write(repo, ['--review-targeted', `@${record}`]);
expect(stale.status).toBe(EXIT.fail);
expect(stale.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
});
// Value: protects=a code edit after the read that drops a cap's enforcing line carries no money token, so the classifier tags
// nothing, but the product patch the reader read is gone and the record must not bind (targeted gate reader on this PR at
// 071532752, CRITICAL lib/ship-receipt-depth.ts:153); fails_when=write binds a record whose read product patch is not the
// receipt's; seam=gstack-ship-receipt write --review-targeted
test('a code edit after the read that removes an enforcing line with no money token is refused', () => {
const { repo, base } = moneyBranch();
commit(repo, 'src/runner.ts', 'export function run(spent: number, limit: number) {\n if (spent > limit) throw new Error("over");\n}\n', 'enforce the limit');
const head = repo.git('rev-parse', 'HEAD');
const record = path.join(repo.root, 'targeted.json');
expect(verify(repo, targetedRead(repo, base, 'h1', 'NO FINDINGS'), record).status).toBe(0);
expect(write(repo, ['--review-targeted', `@${record}`]).status).toBe(EXIT.ok);
commit(repo, 'src/runner.ts', 'export function run(spent: number, limit: number) {\n}\n', 'drop the enforcing line');
const dropped = write(repo, ['--review-targeted', `@${record}`]);
expect(dropped.status).toBe(EXIT.fail);
expect(dropped.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
});
// Value: protects=a test edit after the read that skips a case is gate-class work the reader never saw, so the record does not
// bind though the product patch (which leaves tests out) is unchanged (round 2 outside voice and adversarial on this PR);
// a record whose read commit is not an ancestor of the head is refused; fails_when=write binds either;
// seam=gstack-ship-receipt write --review-targeted
test('a test skipped after the read is refused though the product patch is unchanged; a rewritten read is refused', () => {
const { repo, base } = moneyBranch();
const cases = (cap: string) => `import { test, expect } from "bun:test";\ntest.skip("unit", () => { expect(1).toBe(1); });\n${cap}("cap", () => { expect(5).toBe(5); });\n`;
commit(repo, 'test/charge.test.ts', cases('test'), 'cap test, unit case skipped');
const head = repo.git('rev-parse', 'HEAD');
const record = path.join(repo.root, 'targeted.json');
expect(verify(repo, targetedRead(repo, base, 'h1', 'NO FINDINGS', true, 'money,gate'), record).status).toBe(0);
expect(write(repo, ['--review-targeted', `@${record}`]).status).toBe(EXIT.ok);
commit(repo, 'test/charge.test.ts', cases('test.skip'), 'skip the cap test');
const skipped = write(repo, ['--review-targeted', `@${record}`]);
expect(skipped.status).toBe(EXIT.fail);
expect(skipped.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
repo.git('reset', '-q', '--hard', base);
commit(repo, 'src/charge.ts', 'export const spend_cap_usd = 75;\n', 'rewritten money change');
const rewritten = write(repo, ['--review-targeted', `@${record}`]);
expect(rewritten.status).toBe(EXIT.fail);
expect(rewritten.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
});
});
describe('write and carry refuse a receipt the queue\'s targeted verdict refuses (item 4, #3854)', () => {
// Value: protects=/ship never publishes a receipt whose head needs a targeted class it did not cover (gstack-evals#140 wrote
// gstack_version with receipt=legacy); a head that needs none writes as before; fails_when=write records no or a failed pass
// on a money head, or refuses a head without classes; seam=gstack-ship-receipt write + receiptTargetedVerdict
test('write: missing and failed records are refused on a money head; a covered or classless head writes', () => {
const { repo, base } = moneyBranch();
const missing = write(repo, []);
expect(missing.status).toBe(EXIT.fail);
expect(missing.err).toContain('REVIEW_TARGETED: required=money receipt=legacy verdict=missing');
expect(missing.err).toMatch(/requires the targeted reader on money, but the receipt records no targeted pass.*\(RECEIPT_INVALID/);
const record = path.join(repo.root, 'targeted.json');
verify(repo, targetedRead(repo, base, 'h1', 'NO FINDINGS'), record);
const failed = write(repo, ['--review-targeted', JSON.stringify({ ...JSON.parse(fs.readFileSync(record, 'utf8')), status: 'failed' })]);
expect(failed.status).toBe(EXIT.fail);
expect(failed.err).toContain('targeted pass failed');
expect(write(repo, ['--review-targeted', `@${record}`]).status).toBe(EXIT.ok);
const plain = makeFixtureRepo({ prefix: 'gst986-plain-' });
repos.push(plain);
plain.git('checkout', '-q', '-b', 'feat');
commit(plain, 'docs/notes.md', '# notes\n', 'docs');
const none = write(plain, []);
expect(none.status, none.err).toBe(EXIT.ok);
expect(JSON.parse(none.out)).not.toHaveProperty('review_targeted');
});
// Value: protects=carry never rebinds a receipt the queue would refuse on targeted coverage (#3854: carry checked depth only);
// a legacy receipt and a covered one carry as before; fails_when=carry prints CARRY: carried for an uncovered money head, or
// refuses a covered or legacy one; seam=gstack-ship-receipt carry
test('carry: an uncovered versioned receipt is refused; a covered or legacy one carries', () => {
const { repo, head } = moneyBranch();
const saved = { HOME: process.env.HOME, GSTACK_HOME: process.env.GSTACK_HOME, GSTACK_STATE_DIR: process.env.GSTACK_STATE_DIR };
Object.assign(process.env, { HOME: repo.home, GSTACK_HOME: repo.home, GSTACK_STATE_DIR: repo.home });
let depth: ReturnType<typeof receiptDepthAtHead>;
try { depth = receiptDepthAtHead(repo.work, 'main', path.join(ROOT, 'bin'), head); } finally {
for (const [k, v] of Object.entries(saved)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; }
}
const versioned = { schema_version: 1, pr: 5, head, base: 'main', gstack_version: '1.92.75.0', review_depth: depth };
const covered = { ...versioned, review_targeted: { classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), product_patch_id: 'p', status: 'done', findings: 0, raised_by: 'targeted' } };
commit(repo, 'test/charge.test.ts', 'test("cap", () => {});\n', 'test only');
let n = 0;
const carry = (receipt: object) => {
const body = path.join(repo.root, `body-${++n}.md`);
fs.writeFileSync(body, placeReceiptFirst('## Summary\n\nProse.\n', receipt as any));
return { ...runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body], { GSTACK_STATE_ROOT: path.join(repo.root, 'state') }), body };
};
const uncovered = carry(versioned);
expect(uncovered.status).toBe(EXIT.refused);
expect(uncovered.err).toMatch(/requires the targeted reader on money, but the receipt records no targeted pass.*\(RECEIPT_CARRY_REFUSED/);
expect(uncovered.err).toContain('the queue refuses it too (#3859)');
expect(extractReceipt(fs.readFileSync(uncovered.body, 'utf8')).receipt!.head).toBe(head);
const ok = carry(covered);
expect(ok.status, ok.err).toBe(EXIT.ok);
expect(ok.out).toContain('CARRY: carried from');
const legacy = carry({ schema_version: 1, pr: 5, head, base: 'main' });
expect(legacy.status, legacy.err).toBe(EXIT.ok);
});
});
describe('/ship\'s receipt writes pass the targeted record (item 3, #3853)', () => {
// Value: protects=the record Step 11's verify writes reaches every receipt write /ship runs (Step 19, the handoff, the driver);
// fails_when=a write command omits --review-targeted or reads another path than verify writes; seam=the generated sections'
// command text expanded by bash; no_claim=the agent following it (prose)
test('verify writes .gstack/tmp/targeted.json and the Step 19, handoff and driver writes read it', () => {
const section = (name: string) => fs.readFileSync(path.join(ROOT, 'ship', 'sections', `${name}.md`), 'utf8');
const recordPath = /verify --targeted-out (\S+?)`/.exec(section('adversarial'))?.[1];
expect(recordPath).toBe('.gstack/tmp/targeted.json');
const writeLine = section('pr-body').split('\n').find(l => l.includes('gstack-ship-receipt write --pr'))!;
const optional = /\$\(\[ -s (\S+) \] && echo (--review-targeted @\S+)\)/.exec(writeLine);
expect(optional?.slice(1)).toEqual([recordPath!, `--review-targeted @${recordPath}`]);
expect(section('handoff')).toContain(`When Step 11 saved \`${recordPath}\`, add \`--review-targeted @${recordPath}\` to that write.`);
expect(fs.readFileSync(path.join(ROOT, 'ship', 'DRIVER.md'), 'utf8')).toContain('[--review-targeted @<f>] --body <body>');
const { repo } = moneyBranch();
const expand = () => spawnSync('bash', ['-c', `printf '%s\\n' ${optional![0]}`], { cwd: repo.work, encoding: 'utf8', timeout: 10_000 }).stdout.split('\n').filter(Boolean);
expect(expand()).toEqual([]);
fs.mkdirSync(path.join(repo.work, '.gstack', 'tmp'), { recursive: true });
fs.writeFileSync(path.join(repo.work, recordPath!), '{}\n');
expect(expand()).toEqual(['--review-targeted', `@${recordPath}`]);
});
});
+13 -10
View File
@@ -298,14 +298,14 @@ describe('the targeted role end to end on a fixture repository', () => {
expect(verify.status, verify.out).toBe(0);
expect(verify.out).toContain(`TARGETED_RECORD: classes=money status=done findings=1 brief=${targetedBriefSha().slice(0, 12)} rules=${TARGETED_RULES}`);
expect(verify.out).toContain('VERIFY: clean');
expect(JSON.parse(fs.readFileSync(record, 'utf8')).head).toBe(head);
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 50;\n');
repo.git('commit', '-qam', 'later product change the reader never read');
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain('TARGETED_RECORD: classes=money status=failed findings=0');
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain(`TARGETED_RECORD: classes=money status=done findings=1 brief=${targetedBriefSha().slice(0, 12)} rules=${TARGETED_RULES} read=${head.slice(0, 12)}`);
const later = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', '--review-targeted', `@${record}`, '--json']);
expect(later.status).not.toBe(0);
expect(later.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
repo.git('reset', '-q', '--hard', head);
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 7;\n');
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain('TARGETED_RECORD: classes=money status=failed findings=0');
repo.git('checkout', '--', 'src/charge.ts');
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain('TARGETED_RECORD: classes=money status=done findings=1');
const body = path.join(repo.root, 'body.md');
const write = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', '--review-targeted', `@${record}`, '--body', body, '--json']);
@@ -316,8 +316,8 @@ describe('the targeted role end to end on a fixture repository', () => {
expect(rebound.status).not.toBe(0);
expect(rebound.err).toContain("write binds the pass to the receipt's product_patch_id");
const plain = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', '--json']);
expect(plain.status, plain.err).toBe(0);
expect(JSON.parse(plain.out)).not.toHaveProperty('review_targeted');
expect(plain.status).not.toBe(0);
expect(plain.err).toContain('REVIEW_TARGETED: required=money receipt=legacy verdict=missing');
const slug = runBin(repo, 'gstack-slug', ['--get', 'SLUG']).out.trim();
const logDir = path.join(repo.home, 'projects', slug);
@@ -352,7 +352,7 @@ describe('the /ship commands carry the targeted record to the receipt (GST-464)'
const optional = /\$\(\[ -s (\S+) \] && echo (--review-targeted @\S+)\)/.exec(writeLine);
expect(optional?.slice(1)).toEqual([recordPath!, `--review-targeted @${recordPath}`]);
expect(section('handoff')).toContain(`When Step 11 saved \`${recordPath}\`, add \`--review-targeted @${recordPath}\` to that write.`);
expect(fs.readFileSync(path.join(ROOT, 'ship', 'DRIVER.md'), 'utf8')).toContain(`[--review-targeted @${recordPath}]`);
expect(fs.readFileSync(path.join(ROOT, 'ship', 'DRIVER.md'), 'utf8')).toContain('[--review-targeted @<f>] --body <body>');
const entry = /Every entry also runs\n`([^`]+)`/.exec(section('review-army'))?.[1];
expect(entry).toContain(`rm -f ${recordPath}`);
@@ -390,8 +390,11 @@ describe('the /ship commands carry the targeted record to the receipt (GST-464)'
describe('the receipt record and the queue line', () => {
const record = { classes: ['gate', 'money'], rules: TARGETED_RULES, brief_sha: 'a'.repeat(64), status: 'done', findings: 0, raised_by: 'targeted' };
test('parseReviewTargeted binds the receipt\'s product patch, orders the classes and refuses a malformed record', () => {
expect(parseReviewTargeted(record, 'p1')).toEqual({ ok: true, targeted: { ...record, classes: ['money', 'gate'], product_patch_id: 'p1' } as any });
for (const bad of [{ ...record, classes: ['cost'] }, { ...record, status: 'skipped' }, { ...record, findings: -1 }, { ...record, brief_sha: 'abc' }, { ...record, extra: 1 }, null]) expect(parseReviewTargeted(bad, 'p1').ok).toBe(false);
const read = { ...record, head: 'c'.repeat(40) };
const patchOf = (head: string) => (head === read.head ? 'p1' : undefined);
expect(parseReviewTargeted(read, 'p1', patchOf)).toEqual({ ok: true, targeted: { ...record, classes: ['money', 'gate'], product_patch_id: 'p1' } as any });
for (const bad of [{ ...read, classes: ['cost'] }, { ...read, status: 'skipped' }, { ...read, findings: -1 }, { ...read, brief_sha: 'abc' }, { ...read, extra: 1 }, record, { ...read, head: 'abc' }, null]) expect(parseReviewTargeted(bad, 'p1', patchOf).ok).toBe(false);
expect(parseReviewTargeted(read, 'p2', patchOf)).toMatchObject({ ok: false, message: expect.stringContaining(`the targeted reader read ${'c'.repeat(12)}, whose product patch p1 is not this receipt's p2`) });
});
// Value: protects=the queue refuses an uncovered required class (GST-966); fails_when=the verdict misreads a legacy, failed or partial
// receipt as covered, or a receipt without classes as required; seam=receiptTargetedVerdict (what ready, train build and land refuse on)
+1 -1
View File
@@ -93,7 +93,7 @@ function harness(options: { drainOnClose?: boolean; refuse?: boolean } = {}) {
expect(name).toBe('shared-libs-review-revalidation'); expect(timeout).toBe(CAPTURE_LONG_MS); registered = callback;
},
createSharedLibsFixture: (label: string) => ({ root: `/virtual/${label}`, repo: '/virtual', env: {} }),
seedReviewSources() {}, fixtureWrite() {}, installNormalizingFilter() {}, seedSkippedAdvisory: async () => ({}),
seedReviewSources() {}, fixtureWrite() {}, installNormalizingFilter() {}, seedSkippedAdvisory: async () => ({ evidence_paths: [] }),
fixtureWorkingTree: () => 'same', fixtureGit() {}, reviewLifecycleInstructions: () => '',
seedPathReviewPrerequisites: () => ({ input: '' }), checkPathReviewPrerequisites: () => ({ settled: true }),
reviewRevalidationPrompt: (f: any) => f.root,
@@ -98,7 +98,7 @@ describe('native executable shared-code checker evidence', () => {
const start = scenario.indexOf(marker) + marker.length;
const body = scenario.slice(start, scenario.indexOf('\n });', start));
const verify = new Function('deps', 'result', new Bun.Transpiler({ loader: 'ts' }).transformSync(`const {
change, questions, expect, toolCommandTrace, reviewRecords, createHash, path, f, current,
change, questions, expect, toolCommandTrace, reviewRecords, createHash, path, f, current, sources,
fixtureGit, fixtureWorkingTree, hasTrustedReviewStartRead, hasTrustedSharedLibsCheck, SHARED_LIBS_ROOT,
resumed, prerequisites, checkPathReviewPrerequisites, hasPathReviewPrerequisiteReceipt
} = deps; ${body}`));
@@ -106,7 +106,7 @@ describe('native executable shared-code checker evidence', () => {
.map((event: any) => ({ tool: event.message.content[0].name, input: event.message.content[0].input })) };
let checks = 0;
verify({ change, questions: run.questions, expect, toolCommandTrace, reviewRecords, createHash, path,
f: run.f, current: run.current, fixtureGit, fixtureWorkingTree, hasTrustedReviewStartRead, SHARED_LIBS_ROOT,
f: run.f, current: run.current, sources: run.expected.sources, fixtureGit, fixtureWorkingTree, hasTrustedReviewStartRead, SHARED_LIBS_ROOT,
resumed: run.resumed, prerequisites: run.prerequisites, checkPathReviewPrerequisites, hasPathReviewPrerequisiteReceipt,
hasTrustedSharedLibsCheck: (events: unknown[], expected: any) => {
checks++;
@@ -144,7 +144,7 @@ describe('native executable shared-code checker evidence', () => {
const start = scenario.indexOf(marker) + marker.length;
const body = scenario.slice(start, scenario.indexOf('\n });', start));
const verify = new Function('deps', 'result', new Bun.Transpiler({ loader: 'ts' }).transformSync(`const {
change, questions, expect, toolCommandTrace, reviewRecords, createHash, path, f, current,
change, questions, expect, toolCommandTrace, reviewRecords, createHash, path, f, current, sources,
fixtureGit, fixtureWorkingTree, hasTrustedReviewStartRead, hasTrustedSharedLibsCheck, SHARED_LIBS_ROOT,
resumed, prerequisites, checkPathReviewPrerequisites, hasPathReviewPrerequisiteReceipt
} = deps; ${body}`));
@@ -153,7 +153,7 @@ describe('native executable shared-code checker evidence', () => {
.map((event: any) => ({ tool: event.message.content[0].name, input: event.message.content[0].input })) };
let checks = 0;
const deps = { change, questions: change === 'unchanged' ? [] : [{}], expect, toolCommandTrace,
reviewRecords, createHash, path, f: run.f, current: run.current, fixtureGit, fixtureWorkingTree,
reviewRecords, createHash, path, f: run.f, current: run.current, sources: run.expected.sources, fixtureGit, fixtureWorkingTree,
hasTrustedReviewStartRead, SHARED_LIBS_ROOT, resumed: run.resumed, prerequisites: run.prerequisites,
checkPathReviewPrerequisites, hasPathReviewPrerequisiteReceipt, hasTrustedSharedLibsCheck: (events: unknown[], expected: any) => {
checks++;
@@ -182,6 +182,43 @@ describe('native executable shared-code checker evidence', () => {
expect(() => verify({ ...deps, reviewRecords: () => [] }, native)).toThrow();
});
test('an errored batched caller read counts only when its output holds the whole source, and the registered filtered callback passes the sources (red 40737741454)', () => {
const run = replay('filtered');
const [file, next] = run.current.evidence_paths;
expect(call(run, 2).input.command).toBe(`cat ${shellQuote(file)}`);
const whole = result(run, 2).content;
call(run, 2).input.command = `cat ${file}; echo ===ROUTE; cat ${next}; cat missing.ts`;
Object.assign(result(run, 2), { is_error: true, content: `Exit code 1\n${whole}\n===ROUTE\ncat: missing.ts: No such file or directory` });
expect(hasTrustedSharedLibsCheck(run.events, run.expected)).toBe(true);
expect(hasTrustedSharedLibsCheck(run.events, { ...run.expected, sources: undefined })).toBe(false);
const truncated = structuredClone(run);
result(truncated, 2).content = `Exit code 1\n${whole.slice(0, Math.floor(whole.length / 2))}`;
expect(hasTrustedSharedLibsCheck(truncated.events, truncated.expected)).toBe(false);
const single = structuredClone(run);
call(single, 2).input.command = `cat ${file}`;
expect(hasTrustedSharedLibsCheck(single.events, single.expected)).toBe(false);
const scenario = source.slice(source.indexOf("test('shared-libs-review-revalidation'"));
const marker = '}, result => {';
const start = scenario.indexOf(marker) + marker.length;
const body = scenario.slice(start, scenario.indexOf('\n });', start));
const verify = new Function('deps', 'result', new Bun.Transpiler({ loader: 'ts' }).transformSync(`const {
change, questions, expect, toolCommandTrace, reviewRecords, createHash, path, f, current, sources,
fixtureGit, fixtureWorkingTree, hasTrustedReviewStartRead, hasTrustedSharedLibsCheck, SHARED_LIBS_ROOT,
resumed, prerequisites, checkPathReviewPrerequisites, hasPathReviewPrerequisiteReceipt
} = deps; ${body}`));
const native = { events: run.events, toolCalls: run.events.filter((event: any) => event.type === 'assistant')
.map((event: any) => ({ tool: event.message.content[0].name, input: event.message.content[0].input })) };
let passed: unknown[] = [];
verify({ change: 'filtered', questions: [{}], expect, toolCommandTrace, reviewRecords, createHash, path, f: run.f,
current: run.current, sources: run.expected.sources, fixtureGit, fixtureWorkingTree, hasTrustedReviewStartRead,
SHARED_LIBS_ROOT, resumed: run.resumed, prerequisites: run.prerequisites, checkPathReviewPrerequisites,
hasPathReviewPrerequisiteReceipt, hasTrustedSharedLibsCheck: (events: unknown[], expected: any) => {
passed = Object.keys(expected.sources ?? {});
return hasTrustedSharedLibsCheck(events, expected);
} }, native);
expect(passed.sort()).toEqual([...run.current.evidence_paths].sort());
});
test.each(pathKinds)('the actual %s callback consumes false proof and enforces final excluded coverage', async kind => {
const run = replay(`path-${kind}`);
run.kind = kind;
@@ -185,6 +185,22 @@ describe('ship/SKILL.md — Plan Completion gate invariants', () => {
expect(entry).not.toMatch(/Step 15\.[012]/);
});
test('one owner per release commit and a defined result for a version Step 12 does not write', () => {
// Value: protects=the ship workflow judge's 6/6 cited conflict (Step 15 committed release metadata by hand while Step 16 finish also commits it) and its 3/6 undefined stamp-at-merge/kept result; fails_when=Step 15 regains a manual release commit or Step 12 drops the skipped/kept route; why_new=no pin covered commit ownership; seam=none; no_claim=judge scores
const entry = fs.readFileSync(path.join(SHIP_DIR, 'SKILL.md'), 'utf8');
const commit = entry.slice(entry.indexOf('## Step 15:'), entry.indexOf('## Step 16:'));
expect(commit).not.toContain('git commit');
expect(commit.replace(/\s+/g, ' ')).toMatch(/TODOS\.md and the digest uncommitted: Step 16's `finish` commits them last/);
const finish = entry.slice(entry.indexOf('## Step 16:'), entry.indexOf('### 1. Finish writers'));
expect(finish).toContain("makes Step 15's item-3 commit");
const version = entry.slice(entry.indexOf('## Step 12:'), entry.indexOf('1. **Classify state**')).replace(/\s+/g, ' ');
expect(version).not.toContain('replaces items 1–4');
expect(version).toContain('`VERSION: skipped`');
expect(version).toContain('`VERSION: <v> kept (…; no write)` ends Step 12 with `NEW_VERSION` unset');
expect(version).toContain('ALREADY_BUMPED derives it in item 1');
expect(version).toContain("Steps 18 and 20 take NO_VERSION's branch");
});
test('a rejected push stops publication and routes changed content back through verification', () => {
const entry = fs.readFileSync(path.join(SHIP_DIR, 'SKILL.md'), 'utf8');
const push = entry.slice(entry.indexOf('## Step 17:'), entry.indexOf('## Step 20:'));
+5 -3
View File
@@ -19,6 +19,8 @@ afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
const receiptIn = (body: string) => extractReceipt(fs.readFileSync(body, 'utf8')).receipt!;
const writeReceipt = (repo: FixtureRepo, body: string, extra: string[] = []) =>
runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '7', '--base', 'main', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body, ...extra]);
/** A done targeted record read at HEAD on every class: since GST-986, write refuses a money, gate, auth or secret head without one. */
const targetedAtHead = (repo: FixtureRepo) => ['--review-targeted', JSON.stringify({ classes: ['money', 'gate', 'auth', 'secret'], rules: 'r', brief_sha: 'a'.repeat(64), status: 'done', findings: 0, raised_by: 'targeted', head: repo.git('rev-parse', 'HEAD') })];
const lines = (n: number) => Array.from({ length: n }, (_, i) => `exports.f${i} = () => ${i};`).join('\n') + '\n';
const rewrite = (body: string, edit: (r: Record<string, any>) => void) => {
const text = fs.readFileSync(body, 'utf8');
@@ -62,11 +64,11 @@ describe('the queue refuses a receipt below the depth its head needs', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const head = commit(repo, { 'lib/billing.js': lines(80) });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
expect(writeReceipt(repo, body, targetedAtHead(repo)).status).toBe(EXIT.ok);
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'full', runtime_lines: 80, risk: ['money'] });
const base = repo.git('rev-parse', 'origin/main').slice(0, 12);
const bind = (text: string) => bindShippedReceipt(repo.work, 'origin/main', text, '7', [head]);
const untargeted = bind(fs.readFileSync(body, 'utf8'));
const untargeted = bind(rewrite(body, r => { delete r.review_targeted; }));
expect(!untargeted.ok && untargeted.code).toBe('QUEUE_RECEIPT_TARGETED_MISSING');
expect(!untargeted.ok && untargeted.depthLine).toContain('REVIEW_DEPTH: receipt=full required=full');
const good = bind(rewrite(body, r => { r.review_targeted = { classes: ['money'], rules: 'r', brief_sha: 'a'.repeat(64), product_patch_id: r.product_patch_id, status: 'done', findings: 0, raised_by: 'targeted' }; }));
@@ -103,7 +105,7 @@ describe('the queue refuses a receipt below the depth its head needs', () => {
const repo = makeFixtureRepo(); repos.push(repo);
const head = commit(repo, { 'lib/auth.js': lines(3) });
const body = path.join(repo.root, 'body.md');
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
expect(writeReceipt(repo, body, targetedAtHead(repo)).status).toBe(EXIT.ok);
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'full', class: 'gate' });
const bind = (text: string) => bindShippedReceipt(repo.work, 'origin/main', text, '7', [head]);
const light = bind(rewrite(body, r => { r.review_depth.depth = 'light'; }));
+51
View File
@@ -142,6 +142,57 @@ describe('the Review-loop exception line (GST-977, gstack-evals #139)', () => {
});
});
describe('the Documentation exception line (GST-983)', () => {
const excepted = {
...receipt,
gate: { status: 'green', run: '983', unit: { pass: 1181, skip: 70 } },
docs: { status: 'blocked', scope: 'docs/', exception: { accepted_by: 'Garry', date: '2026-11-02', via: 'GSTA-36', reason: 'sections 1 and 2 skip the audit' } },
};
const line = 'Documentation exception: accepted by Garry 2026-11-02 via GSTA-36: sections 1 and 2 skip the audit';
// Value: protects=a body that carries the docs exception line write requires can also pass --check-prose; fails_when=the
// owner's reason is read as counter claims ("2 skip" against 70); seam=lib/ship-receipt-prose.ts checkReceiptProse
test('the exact line the receipt\'s docs exception requires is not prose; the rest of the body still is', () => {
expect(checkReceiptProse(excepted, `## Documentation\n\n${line}\n\nGate: 70 skip.\n`)).toEqual({ checked: 1, mismatches: [] });
expect(checkReceiptProse(excepted, ` ${line}\r\nGate: 69 skip.`).mismatches).toEqual([{ field: 'gate.unit.skip', prose: 69, receipt: 70 }]);
});
test('the same words anywhere else, a line for a different exception, or a receipt with no exception stay prose', () => {
const twoSkip = [{ field: 'gate.unit.skip', prose: 2, receipt: 70 }];
expect(checkReceiptProse(excepted, `Note: ${line}`).mismatches).toEqual(twoSkip);
expect(checkReceiptProse(excepted, line.replace('GSTA-36', 'GSTA-37')).mismatches).toEqual(twoSkip);
expect(checkReceiptProse({ ...excepted, docs: { status: 'current', scope: 'docs/' } }, line).mismatches).toEqual(twoSkip);
});
});
describe('code fences are not prose (GST-984, gstack-evals #142)', () => {
const lanes = { ...receipt, gate: { status: 'green', run: '142', passed: 1181, failed: 0, unit: { pass: 1181, fail: 0 } } };
const evidence = '```text\n- // one none trial per band task (the first 35 fail) and one told trial per pilot task.\nr3-drop, r3, r3-sol: 113 passed, 0 failed with the real list\nunit: 113 pass, 0 fail\n```\n';
// Value: protects=pasted test output and quoted code in a fence never count as this PR's counters; fails_when=the
// fenced "113 passed" or "35 fail" is read against the receipt; seam=lib/ship-receipt-prose.ts checkReceiptProse
test('numbers inside a closed fence are skipped; counts outside it are still checked', () => {
expect(checkReceiptProse(lanes, `Gate: 1181 passed.\n\n${evidence}\nUnit: 1181 pass.\n`)).toEqual({ checked: 2, mismatches: [] });
expect(checkReceiptProse(lanes, `${evidence}\nUnit: 113 pass.\n`).mismatches).toEqual([{ field: 'gate.unit.pass', prose: 113, receipt: 1181 }]);
expect(checkReceiptProse(lanes, evidence.replace(/```/g, '~~~~').replace(/~~~~\n$/, '~~~~~\n'))).toEqual({ checked: 0, mismatches: [] });
});
test('a fence closes only on its own run, an unclosed fence stays prose, and the fence still separates the text around it', () => {
const short = '````\n113 pass\n```\nstill fenced 113 pass\n````\n';
expect(checkReceiptProse(lanes, short)).toEqual({ checked: 0, mismatches: [] });
expect(checkReceiptProse(lanes, 'Intro\n```text\n113 pass\n').mismatches).toEqual([{ field: 'gate.unit.pass', prose: 113, receipt: 1181 }]);
expect(checkReceiptProse(lanes, 'Shipped 3\n```\nx\n```\npass rates are below.\n')).toEqual({ checked: 0, mismatches: [] });
});
// Value: protects=a line that only looks like a fence opener, or a shorter fence inside an unclosed one, cannot hide a
// claim GitHub shows as prose; fails_when=either shape skips the "113 pass" after it; seam=lib/ship-receipt-prose.ts checkReceiptProse
test('inline triple backticks open no fence, and nothing after an unclosed fence is skipped', () => {
const wrong = [{ field: 'gate.unit.pass', prose: 113, receipt: 1181 }];
expect(checkReceiptProse(lanes, '```npm test``` ran.\nUnit: 113 pass.\n\n```text\nlog\n```\n').mismatches).toEqual(wrong);
expect(checkReceiptProse(lanes, '````\n```\nUnit: 113 pass.\n```\n').mismatches).toEqual(wrong);
});
});
describe('gstack-ship-receipt read --check-prose', () => {
const run = (body: string) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'receipt-prose-'));
@@ -0,0 +1,4 @@
{
"bytes": 4,
"why": "Steps 12 and 15 name who makes the release commit and what a skipped or kept version leaves for Steps 13, 18 and 20 (ship workflow judge flake, GSTA-253)"
}
@@ -0,0 +1,4 @@
{
"bytes": 232,
"why": "GST-986: Step 11 verify writes the targeted record to .gstack/tmp/targeted.json (adversarial.md lead instruction); merged over GST-967"
}
+6 -1
View File
@@ -24,7 +24,7 @@ const ROOT = path.resolve(import.meta.dir, '..');
const FIXTURE = path.join(ROOT, 'test', 'fixtures', 'plans', 'ui-heavy-feature.md');
const designFocusBoundary = (fp: AskUserQuestionFingerprint): boolean =>
fp.nativeCall?.answered === true && !fp.nativeCall.failed && fp.nativeCall.questions.some(({ question, options }) => {
fp.nativeCall?.answered === true && !fp.nativeCall.failed && fp.nativeCall.questions.some(({ header, question, options }) => {
const text = question.trim().replace(/^D\d+(?:\.\d+)?\s*[—–:-]\s*/i, '');
// A target menu can mention a design system without reviewing this plan.
// The source Step 0D question, or a native title (first line) that offers all
@@ -46,7 +46,12 @@ const designFocusBoundary = (fp: AskUserQuestionFingerprint): boolean =>
// or focus on a few?") and lettered its options ("A) Review all 7 areas"). Either place
// carries the offer; issue-numbered options are a finding, never Step 0D. Runs 41178131219
// and 41186376825 lettered the leading option itself ("A) All 7 passes (recommended)").
// Every captured Step 0D (ten calls, runs 39199692993 to 41414111284) set the native header chip
// to "Focus", and no other captured question did; run 41414111284's label put the count second
// ("Full review, all 7 (recommended)"). With that header, the rating and an all-7 option suffice.
const lead = text.split(/\r?\n|\s+(?=Project\/branch\/task:)/, 1)[0]!;
if (/^\s*focus\s*$/i.test(header ?? '') && rated(text) && options.length >= 2 && !issueLabeled(options)
&& options.some(({ label }) => /\ball (?:7|seven)\b/i.test(label))) return true;
return /^I(?:['’]ve| have) rated this plan (?:10(?:\.0+)?|[0-9](?:\.\d+)?)\/10 on design completeness\.[\s\S]*\bWant me to focus on specific areas instead(?: of all (?:7|seven))?\?/i.test(text)
|| (/\ball (?:7|seven) (?:design )?(?:dimensions|passes)\b/i.test(title) && /\bfocus\b/i.test(title)
&& (/\bdesign\b/i.test(title) || rated(text.slice(title.length))) && /\?\s*$/.test(title))
+2
View File
@@ -215,6 +215,7 @@ describeE2E('Shared-code safety and review lifecycle (gate)', () => {
const instructions = reviewLifecycleInstructions(f);
const input = path.join(f.root, 'current-advisory.jsonl');
const { action: _priorAction, ...current } = prior;
const sources = Object.fromEntries(current.evidence_paths.map((file: string) => [file, fs.readFileSync(path.join(f.repo, file), 'utf8')]));
fs.writeFileSync(input, JSON.stringify({ ...current, specialist: 'maintainability' }) + '\n');
const resumed = seedPathReviewPrerequisites(f);
const prerequisites = checkPathReviewPrerequisites(f, resumed.input);
@@ -248,6 +249,7 @@ describeE2E('Shared-code safety and review lifecycle (gate)', () => {
coveredPaths: change === 'unchanged' ? current.evidence_paths
: last.findings.find((finding: any) => finding.advisory && finding.action === 'skipped'
&& finding.fingerprint === current.fingerprint)?.snapshot_covered_paths,
sources,
})).toBe(true);
} else if (change === 'unchanged' || change === 'filtered') {
// A true hash comparison cannot substitute for the trusted capture and path checks.