REVIEWED_TREE: 958620236acdf2d3263674529149d88ba0e6dae0 /workspace/gstack

Range: `ef64250..HEAD` (`60f8c46`). I read all the non-fixture source in full: `lib/ship-patch.ts`, `lib/ship-release.ts`, `lib/ship-policy*.ts`, `lib/review-depth.ts`, `bin/gstack-diff-scope`, the docs, the changelog fragment and `ship/SKILL.md.tmpl`. I also read what that code calls: `headPolicyDiff`, `parseConfig`, `classifyPath` and `gstack-docs-candidate snapshot`. **Fixtures and tests were reviewed in summary mode only.** That covers the 4 host goldens (one line each, for the "one reader (no round)" wording), the review-pin repin, the `GST-1276` replay pin, the size ledger, small edits to `ship-control-flow` and `ship-rereview-triggers`, and the new `test/ship-bounded-route.test.ts`. I ran that test file locally and got 12 pass, 0 fail. None of the five findings blocks.

**1. A changed expected value in a comparison-style check gets the scoped reader instead of Steps 5–11.5 (FIXABLE).** `literalOnly` only treats a line as a check if it names a call in `CHECK_CALL` (`expect`, `assert*`, `test` and so on). A check written as a plain comparison names none of them. I reproduced this in a scratch repo with `gateEditedPaths`, changing `'old'` to `'new'` in each file:
- `if got != "old" {` in a Go test was not gated, so it goes to the scoped reader.
- `if (got !== 'old') throw new Error('bad');` in a TS test was not gated either.
- `expect(f()).toBe('old')` was gated, which is correct.

Before this diff, all three went to behavior. Both the changelog and `docs/review-policy.md` say "A changed expected value … in the assertion's own statement … still stops", so the docs are wrong for these cases, and no test covers them. Fix: refuse a pair whose statement holds a comparison operator (`==`, `!=`, `===`, `!==`, `<`, `>`) or a `throw`, `t.Error*` or `t.Fatal*`, and add those cases to the test. Severity: Medium. Blocks: no, because a reader still reads the hunk.

**2. The audit-prose check doesn't show the audit made the edit (FIXABLE).** `boundedPaths` accepts a post-review edit to `docs/*-policy.md`, `docs/{captain,unattended,pregate}.md`, `AGENTS.md` or `CLAUDE.md` when its blob equals `--docs-post`'s `hashes[p]`. The problem is in `gstack-docs-candidate snapshot`: it hashes `tracked`, which includes every committed, staged, unstaged and untracked path on the branch, not just docs the audit touched. So any edit of up to 20 lines made after review and before the post-audit snapshot passes. That includes an edit to `docs/review-policy.md`, the file that defines the review loop. The docs only rule out edits made after the snapshot. Fix: also require the path among the docs the Step 14.5 audit record (`.gstack/tmp/docs-audit.json`) says it edited, and require its reviewed-tree blob to equal the pre-audit one. Severity: Medium. Blocks: no.

**3. A literal-only swap can change what a test runs (INVESTIGATE).** A plain quoted string can be a binary path, a module path or an env value. In the same scratch repo, `const BIN = 'bin/gstack-real';` changed to `'/bin/true'` was not gated, so the test now runs a no-op and still passes. Separately, `inTemplate` counts backticks across all earlier lines, including ones inside comments and strings, so one odd backtick flips which lines count as inside a template literal. The scoped reader's brief does ask about bypassed tests, which limits the risk, but the route is wider than "a fixture value the secrets scan flagged". Severity: Low. Blocks: no.

**4. Bookkeeping globs can name build and dependency inputs (FIXABLE).** `bookkeepingGlobError` only checks for a `.tsv`, `.csv` or `.txt` ending and refuses `.gstack/` and `.github/`. That lets through `requirements.txt`, `CMakeLists.txt` and `**/*.txt`, so a 20-line dependency or build change would take one reader. That contradicts the "never code" message. The key is read from the base only, so it takes an owner misconfiguration. Fix: refuse known manifest and build names (or the `DATA_CONFIG` set) both at validation and in `boundedPaths`. Severity: Low. Blocks: no.

**5. A lanes edit is a gate path and still takes one reader (INVESTIGATE).** `.gstack/pregate.json` matches `GATE_PATH` (`pregate[^/]*\.json`), and the lanes kind never checks `isGatePath`. Only bookkeeping does. That is consistent with pregate itself accepting lanes the head adds through `headPolicy`. But it departs from "gate paths get the most review", and the changelog only promises that gate-path prose never qualifies. A new broad glob mapped to a cheap test is caught only by the reader's prompt. This is worth one explicit line in `docs/review-policy.md`. Severity: Low. Blocks: no.

Recommendation: Fix the comparison-style hole in `literalOnly` (`CHECK_CALL` in `lib/ship-patch.ts`) before merge because a changed expected value in a Go `if got != "…"` or JS `if (…) throw` check now routes to one scoped reader instead of Steps 5–11.5, contrary to the documented contract, though nothing here blocks.
