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.
- package/dist/core/StageTransitionValidator.d.ts.map +1 -1
- package/dist/core/StageTransitionValidator.js +16 -1
- package/dist/core/StageTransitionValidator.js.map +1 -1
- package/dist/monitoring/guardPostureView.d.ts +4 -0
- package/dist/monitoring/guardPostureView.d.ts.map +1 -1
- package/dist/monitoring/guardPostureView.js +11 -1
- package/dist/monitoring/guardPostureView.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +25 -7
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.975.md +21 -0
- package/upgrades/1.3.976.md +60 -0
- package/upgrades/eli16/guard-uninspectable-summary.md +35 -0
- package/upgrades/side-effects/guard-uninspectable-summary.md +137 -0
- package/upgrades/side-effects/honest-merge-base-verification.md +132 -0
|
@@ -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.
|