instar 1.3.974 → 1.3.976

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ # Side-Effects Review — honest merge-base verification (a refusal is not a fact)
2
+
3
+ ## Summary of the change
4
+
5
+ Two defects in the projects `/advance` merge-base check, found 2026-07-25 while recording PR #1641
6
+ as merged (project `convergence-towards-coherence`, Tier 1 item 1).
7
+
8
+ **A. The read never declared itself a read.** `routes.ts`'s injected `gitMergeBaseIsAncestor`
9
+ called `SafeGitExecutor.readSync(['merge-base','--is-ancestor',…])` without
10
+ `sourceTreeReadOk: true`. A project's `targetRepoPath` IS an instar source tree on any dogfooding
11
+ agent, so `runSourceTreeChecks` refused the query every time with `SourceTreeGuardError`. Note
12
+ `merge-base` is ALREADY in `SOURCE_TREE_READ_TIER_VERBS` — the permission for exactly this read
13
+ existed and was never requested. Fix: pass the flag.
14
+
15
+ **B. (The load-bearing half.) The helper translated every failure into a factual negative.** The
16
+ old body was `catch { return false }` with a comment asserting that a non-zero exit "IS the
17
+ negative answer, not a degradation". That is true only for exit status 1. A guard refusal, a
18
+ missing binary, a bad revision (128) or a timeout all returned the same `false`, and the validator
19
+ turned it into `MERGE_COMMIT_UNREACHABLE` — "this merge is not on main" — about a merge that
20
+ demonstrably was. Fix: only `status === 1` returns false; every other failure is rethrown, and
21
+ `StageTransitionValidator` catches it and returns a NEW distinct code `MERGE_BASE_UNVERIFIABLE`
22
+ with the underlying cause in the reason.
23
+
24
+ Reproduced under the server's exact PATH/cwd BEFORE fixing, all four boundary cases:
25
+
26
+ | input | result | old code said |
27
+ |---|---|---|
28
+ | real merge SHA, no flag | `SourceTreeGuardError`, `status: undefined` | "not reachable" (false) |
29
+ | real merge SHA, with flag | returns → ancestor TRUE | — |
30
+ | real commit not on main | `status: 1` | "not reachable" (correct) |
31
+ | nonexistent SHA | `status: 128` | "not reachable" (wrong — unanswerable ≠ negative) |
32
+
33
+ ## Decision-point inventory
34
+
35
+ - `gitMergeBaseIsAncestor` — a helper feeding a GATE (the `building → merged` transition). Its
36
+ verdict decides whether a project item may be recorded merged. This change does not widen what
37
+ passes: a genuine non-ancestor still fails. It narrows what is *called* a non-ancestor.
38
+ - `validateStageTransition` `to === 'merged'` — the gate itself. Gains one new refusal code; loses
39
+ no existing refusal. `MERGE_COMMIT_UNREACHABLE` still fires for real non-ancestors
40
+ (asserted in a dedicated test so the boundary is not blurred in the other direction).
41
+ - No other decision point, hook, reaper, sentinel, scheduler or migration path is touched.
42
+
43
+ ## 1. Over-block
44
+
45
+ The gate becomes *more* likely to refuse in one respect: a case that previously produced
46
+ `MERGE_COMMIT_UNREACHABLE` may now produce `MERGE_BASE_UNVERIFIABLE`. Both are refusals, so nothing
47
+ that used to be blocked is now allowed — the transition is equally gated, with an honest reason.
48
+
49
+ The new `throw` path is bounded: it is caught inside `validateStageTransition` and converted to a
50
+ verdict. It cannot escape to a 500. (Asserted by the validator unit test, which passes a throwing
51
+ helper and expects `ok: false` with the new code, not an exception.)
52
+
53
+ ## 2. Under-block
54
+
55
+ **Nothing is loosened.** `status === 1` — git's documented "not an ancestor" — still returns false
56
+ and still yields `MERGE_COMMIT_UNREACHABLE`. The one behaviour change in the permissive direction
57
+ is that a read the guard was refusing now succeeds; that read is already sanctioned for source
58
+ trees by `SOURCE_TREE_READ_TIER_VERBS`, so this asks for a permission the design already grants
59
+ rather than creating one.
60
+
61
+ `sourceTreeReadOk: true` is scoped to this single call. It does not relax SourceTreeGuard for any
62
+ other operation, and it cannot: the flag is per-invocation and only bypasses the check for verbs in
63
+ the read-tier set.
64
+
65
+ ## 3. Blast radius
66
+
67
+ `src/server/routes.ts` (one helper), `src/core/StageTransitionValidator.ts` (one call wrapped, one
68
+ new code). Every consumer of the merged-transition verdict was checked:
69
+
70
+ - `grep -rn "MERGE_COMMIT_UNREACHABLE"` — the route surfaces `result.code` verbatim in a 409 body;
71
+ no consumer switches on the code value, so a new code string breaks no branch.
72
+ - `tests/unit/StageTransitionValidator.test.ts` — 41 tests, all pass, including the pre-existing
73
+ `MERGE_COMMIT_UNREACHABLE` case (deliberately kept and re-asserted).
74
+ - `tests/integration/projects-api.test.ts` — mocks the helper with plain booleans; unaffected,
75
+ because the boolean contract is unchanged for helpers that do not throw.
76
+
77
+ ## 4. Rollback plan
78
+
79
+ Single-commit revert; no state, no config key, no migration, no persisted artifact. Reverting
80
+ restores the previous (wrong) behaviour immediately with nothing to clean up.
81
+
82
+ ## 5. Test coverage (all tiers that apply)
83
+
84
+ - **Unit — behaviour** (`tests/unit/StageTransitionValidator.test.ts`, +2): a throwing helper
85
+ yields `MERGE_BASE_UNVERIFIABLE` with a `could not verify` reason carrying the cause; and — the
86
+ other side of the boundary — a helper returning `false` still yields
87
+ `MERGE_COMMIT_UNREACHABLE`. Both directions, because making refusals honest must not stop the
88
+ check refusing real negatives.
89
+ - **Unit — wiring** (`tests/unit/projects-advance-mergebase-wiring.test.ts`, new): static
90
+ introspection asserting the callsite carries `sourceTreeReadOk: true`, the helper tests
91
+ `status === 1` explicitly and rethrows otherwise, the bare `catch { return false }` shape cannot
92
+ reappear, and the validator produces the new code from a `catch`. Static rather than mocked
93
+ **because mocking SafeGitExecutor is exactly how the existing `/advance` tests masked this gap**
94
+ — a runtime mock never reaches the guard.
95
+ - **No new integration/e2e tier**: the route change is a two-line option + error-classification
96
+ fix with no new endpoint or response shape; the existing `projects-api` integration suite covers
97
+ the route, and the real end-to-end proof is the live advance succeeding after release (a
98
+ condition of autonomous run `run-ms13zzrz-78576404`).
99
+
100
+ ## 6. The bug CLASS, measured rather than guessed
101
+
102
+ **This is the second instance of the same bug.** In May the identical missing declaration made
103
+ failure-learning's git reads silently fail on dogfooding agents while `/failures/*` reported
104
+ health. It was fixed, and `tests/unit/failure-learning-source-tree-readok-wiring.test.ts` was
105
+ written to stop it recurring — **scoped to that subsystem's five files and its
106
+ `failure-learning:*` operations only.** So the class survived, moved one subsystem over, and
107
+ reproduced.
108
+
109
+ Exposure, measured (not estimated): **34 of 46** `SafeGitExecutor.readSync` callsites in `src/` do
110
+ not pass `sourceTreeReadOk: true`. Many are legitimate — `WorktreeManager` on a fresh clone,
111
+ `nuke.ts` on a shadow install, `commands/init.ts` on a user's own repo — where the target is not
112
+ the instar source tree and the guard should not be bypassed.
113
+
114
+ **Deliberately NOT swept in this change.** A blind flag-everything pass would touch 34 callsites
115
+ across subsystems whose target paths I have not verified, and would relax a safety guard on
116
+ evidence I do not have. That is precisely the confident over-reach this project exists to remove.
117
+ The honest form is a per-callsite audit answering "can this path ever be the instar source tree?",
118
+ which is real work with a known size.
119
+
120
+ Proposed follow-up, with the size stated so it cannot be mistaken for a quick fix: generalise the
121
+ May ratchet from one subsystem to a repo-wide default-plus-declared-exception check (each callsite
122
+ either carries the flag or an explicit `// source-tree-n/a: <reason>` marker), landing after the
123
+ 34-callsite audit. <!-- tracked: CMT-1035 -->
124
+
125
+ ## 7. One thing found while writing the test, recorded rather than tidied away
126
+
127
+ The assertion forbidding the old `catch { return false }` shape initially FAILED — because it
128
+ matched the helper's own comment, which quotes that shape while explaining its removal. A
129
+ text-matching check fooled by prose describing the thing it forbids. Fixed by stripping comments
130
+ before matching, and worth naming: our constitution says an LLM gate must not string-match, and
131
+ this is the static-check equivalent of the same trap. Cheap at test-writing time; the same mistake
132
+ inside a live gate is how you get a check that fires on nothing.