instar 1.3.982 → 1.3.984
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/CredentialManualLevers.js +0 -0
- package/dist/core/CredentialManualLevers.js.map +1 -1
- package/dist/core/DeliverMessageHandler.js +0 -0
- package/dist/core/DeliverMessageHandler.js.map +1 -1
- package/dist/core/PeerEndpointResolver.d.ts +0 -0
- package/dist/core/PeerEndpointResolver.d.ts.map +1 -1
- package/dist/core/PeerEndpointResolver.js +0 -0
- package/dist/core/PeerEndpointResolver.js.map +1 -1
- package/dist/core/ProjectRoundExecution.d.ts +45 -2
- package/dist/core/ProjectRoundExecution.d.ts.map +1 -1
- package/dist/core/ProjectRoundExecution.js +56 -7
- package/dist/core/ProjectRoundExecution.js.map +1 -1
- package/dist/core/QueueDrainLoop.js +0 -0
- package/dist/core/QueueDrainLoop.js.map +1 -1
- package/dist/core/ScopeAccretionSweep.js +1 -1
- package/dist/core/ScopeAccretionSweep.js.map +1 -1
- package/dist/core/SessionOwnership.js +1 -1
- package/dist/core/SessionOwnership.js.map +1 -1
- package/dist/core/StageTransitionValidator.d.ts +26 -0
- package/dist/core/StageTransitionValidator.d.ts.map +1 -1
- package/dist/core/StageTransitionValidator.js +11 -1
- package/dist/core/StageTransitionValidator.js.map +1 -1
- package/dist/core/StandardsEnforcementAuditor.js +1 -1
- package/dist/core/StandardsEnforcementAuditor.js.map +1 -1
- package/dist/core/StoreSnapshot.js +2 -2
- package/dist/core/StoreSnapshot.js.map +1 -1
- package/dist/core/UnionReader.js +0 -0
- package/dist/core/UnionReader.js.map +1 -1
- package/dist/core/benchmarkDivergenceCore.js +1 -1
- package/dist/core/benchmarkDivergenceCore.js.map +1 -1
- package/dist/core/cartographerSummary.js +0 -0
- package/dist/core/cartographerSummary.js.map +1 -1
- package/dist/core/routingPriceAuthority.d.ts +0 -0
- package/dist/core/routingPriceAuthority.d.ts.map +1 -1
- package/dist/core/routingPriceAuthority.js +0 -0
- package/dist/core/routingPriceAuthority.js.map +1 -1
- package/dist/messaging/relayContentDedup.js +0 -0
- package/dist/messaging/relayContentDedup.js.map +1 -1
- package/dist/monitoring/BenchmarkDivergenceAnalyzer.js +10 -10
- package/dist/monitoring/BenchmarkDivergenceAnalyzer.js.map +1 -1
- package/dist/monitoring/ExternalHogArmMarker.js +0 -0
- package/dist/monitoring/ExternalHogArmMarker.js.map +1 -1
- package/dist/monitoring/ExternalHogClassifier.js +0 -0
- package/dist/monitoring/ExternalHogClassifier.js.map +1 -1
- package/dist/monitoring/ExternalHogSampler.js +0 -0
- package/dist/monitoring/ExternalHogSampler.js.map +1 -1
- package/dist/monitoring/FeatureMetricsLedger.js +1 -1
- package/dist/monitoring/FeatureMetricsLedger.js.map +1 -1
- package/dist/monitoring/GreenPrAutoMerger.js +1 -1
- package/dist/monitoring/GreenPrAutoMerger.js.map +1 -1
- package/dist/monitoring/PermissionPromptAutoResolver.js +1 -1
- package/dist/monitoring/PermissionPromptAutoResolver.js.map +1 -1
- package/dist/monitoring/ProviderCostReportStore.js +0 -0
- package/dist/monitoring/ProviderCostReportStore.js.map +1 -1
- package/dist/monitoring/blockerSettleAuthority.js +0 -0
- package/dist/monitoring/blockerSettleAuthority.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +55 -4
- 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.982.md +17 -2
- package/upgrades/1.3.983.md +108 -0
- package/upgrades/1.3.984.md +116 -0
- package/upgrades/side-effects/behavioural-promise-unverifiable.md +17 -2
- package/upgrades/side-effects/merged-record-carries-its-evidence.md +230 -0
- package/upgrades/side-effects/raw-nul-bytes-hide-source-from-grep.md +199 -0
|
@@ -100,8 +100,23 @@ explicit in code: absence of a verdict is reported as absence, never as either o
|
|
|
100
100
|
between them causes no change in expiry behaviour — checked before editing.
|
|
101
101
|
- **PromiseBeacon / overdue surfacing** operate on open commitments; `pending` keeps them
|
|
102
102
|
visible, which is the stated intent of the precedent ("should NAG, not vanish").
|
|
103
|
-
- No persistence change, no migration, no config key.
|
|
104
|
-
|
|
103
|
+
- No persistence change, no migration, no config key.
|
|
104
|
+
- ~~Existing rows re-evaluate to `pending` on the next sweep without a data rewrite.~~
|
|
105
|
+
**CORRECTED 2026-07-26 — this was FALSE and is struck rather than deleted.** The
|
|
106
|
+
behavioural branch returns BEFORE the `switch` in `verifyOne`, so `mutateSync` never runs
|
|
107
|
+
and a pre-existing row is never touched. **The change is FORWARD-ONLY.**
|
|
108
|
+
Verified on the live store after deploy: 98 behavioural rows still read **74 `verified` /
|
|
109
|
+
24 `violated`**, unchanged. What the change DID do, measured over two reads 75s apart:
|
|
110
|
+
tick accumulation stopped dead (one row frozen at 163,135 where it had been climbing every
|
|
111
|
+
minute). What it did NOT do: clear the 98 stale verdicts — so the false comfort this change
|
|
112
|
+
set out to remove is still displayed for every row that predates it, and `getHealth`, which
|
|
113
|
+
now counts actual `verified` rows, will report those 74 stale stamps as verified.
|
|
114
|
+
Remedy is an open operator decision, not a reflex: either a migration resetting behavioural
|
|
115
|
+
rows to `pending` (a data rewrite this very section claimed was unnecessary) or a read
|
|
116
|
+
surface that refuses a verdict for a never-verifiable row whatever is stored.
|
|
117
|
+
**Found by reading the live surface for the exact case the change fixed** — no test caught
|
|
118
|
+
it, and the suite was green. An artifact that over-claims is the same defect class as the
|
|
119
|
+
bug it documents, which is why the wrong sentence stays visible above.
|
|
105
120
|
|
|
106
121
|
## 6. External surfaces
|
|
107
122
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Side-Effects Review — a merged record that could not name what merged it
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `merged-record-carries-its-evidence`
|
|
4
|
+
**Date:** `2026-07-26`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `author-applied lenses — see Phase 5 (reduced independence, disclosed)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`/projects/:id/advance` validated a submitted artifact for `building → merged` — PR
|
|
11
|
+
state `MERGED`, a format-valid merge sha, that sha reachable from canonical main, CI
|
|
12
|
+
rollup green — and then wrote **only** `pipelineStage`. The validated evidence built
|
|
13
|
+
the validation context and was discarded. Measured on the live store: both Tier-1
|
|
14
|
+
children of `convergence-towards-coherence` read `pipelineStage: "merged"` with no
|
|
15
|
+
`prNumber`, no `mergeCommitOid`, no `ciCheckedAt`.
|
|
16
|
+
|
|
17
|
+
The root cause was a **type**. `StageTransitionResult`'s success case was `{ ok: true }`
|
|
18
|
+
— one bit — while every refusal carried `reason` + `code`. The validator explained
|
|
19
|
+
itself when it refused and said nothing when it approved, so the caller had nothing to
|
|
20
|
+
persist. The fix widens the success case to carry a `MergedEvidence` record and
|
|
21
|
+
persists it in the same `update()` as the stage.
|
|
22
|
+
|
|
23
|
+
**The consequential half:** two merged-state reconcilers select on `mergeCommitOid` —
|
|
24
|
+
`GET /projects/:id`'s lazy reconciler (documented "may mutate", runs on every read) and
|
|
25
|
+
`verifyMergedItemsViaGit`. With the field never written, the candidate set was always
|
|
26
|
+
empty, `verifyMergedItemsViaGit` was never called, and both reported nothing. A
|
|
27
|
+
regression detector that scans nothing is indistinguishable from one that finds no
|
|
28
|
+
regressions.
|
|
29
|
+
|
|
30
|
+
Because that path had never executed, three defects sat in it unexercised — all three
|
|
31
|
+
already fixed in the advance path a few hundred lines away: (1) `SafeGitExecutor.run`
|
|
32
|
+
without `sourceTreeReadOk`, so SourceTreeGuard refuses the read against an instar
|
|
33
|
+
source tree (the #1641 defect); (2) a hardcoded `origin/main`, which on a dev-agent
|
|
34
|
+
home is the agent's FORK; (3) `catch {}` → not verified → caller marks `regressed`,
|
|
35
|
+
i.e. a refusal rendered as "it was reverted". Writing the evidence is what ARMS that
|
|
36
|
+
path, so the two halves ship together.
|
|
37
|
+
|
|
38
|
+
## Decision-point inventory
|
|
39
|
+
|
|
40
|
+
| point | classification | note |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| success-case evidence on `StageTransitionResult` | `invariant` | Pure data carriage. Adds no branch and cannot change a verdict — the same four checks decide `ok`. |
|
|
43
|
+
| evidence persistence in `/advance` | `invariant` | Conditional on `result.evidence` presence only. Same single write as the stage, so no window exists where `merged` has no evidence. |
|
|
44
|
+
| `verifyMergedItemsViaGit` three-state outcome | `invariant` | Deterministic on git's documented exit status: 1 ⇒ regressed, everything else ⇒ unverifiable. No judgment. |
|
|
45
|
+
| reconciler unverifiable branch | `invariant` | Deterministic on set membership. Strictly *removes* an authority — the old code demoted on absence-from-`verified`. |
|
|
46
|
+
|
|
47
|
+
## 1. Over-block
|
|
48
|
+
|
|
49
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
50
|
+
|
|
51
|
+
Nothing new is rejected. `/advance` accepts exactly what it accepted before — the four
|
|
52
|
+
checks and their refusal codes are untouched; only the success return widened. No
|
|
53
|
+
artifact field became required.
|
|
54
|
+
|
|
55
|
+
The reconciler now demotes **strictly fewer** items: previously anything absent from
|
|
56
|
+
`verified` became `regressed`; now only git exit 1 does. So the change cannot introduce
|
|
57
|
+
a false demotion, only remove them.
|
|
58
|
+
|
|
59
|
+
Real cost, stated plainly: an item that genuinely regressed but whose check cannot be
|
|
60
|
+
run stays at `merged` rather than being demoted. That is a deliberate trade — the old
|
|
61
|
+
behaviour "caught" it only by demoting everything it could not verify, which is not
|
|
62
|
+
detection. The item keeps taking the `ciCheckedAt` backoff, so it is re-asked, and its
|
|
63
|
+
unverifiable reason is returned to the caller rather than swallowed.
|
|
64
|
+
|
|
65
|
+
## 2. Under-block
|
|
66
|
+
|
|
67
|
+
**What failure modes does this still miss?**
|
|
68
|
+
|
|
69
|
+
- **No new regression detection is added.** This makes the existing reconcilers
|
|
70
|
+
*possible* (they now have evidence to select on) and *safe* (they can no longer guess).
|
|
71
|
+
Whether they catch a real revert is unproven until one occurs — recorded rather than
|
|
72
|
+
implied.
|
|
73
|
+
- **Items merged BEFORE this change carry no evidence**, so they remain invisible to the
|
|
74
|
+
reconcilers. They surface as `unverifiable` with the reason "no mergeCommitOid
|
|
75
|
+
recorded on the item" rather than silently dropping out of the candidate set. No
|
|
76
|
+
backfill is attempted: reconstructing which PR merged a historical item would mean
|
|
77
|
+
guessing, and a fabricated evidence row is worse than an absent one.
|
|
78
|
+
- The `GET /projects/:id` reconciler still selects only `pipelineStage === 'building'`
|
|
79
|
+
children, so an item that went straight to `merged` via `/advance` is not revalidated
|
|
80
|
+
by it. `verifyMergedItemsViaGit` is reachable for any child id the caller passes.
|
|
81
|
+
Widening that selection is a behavioural change to which items get demoted and is NOT
|
|
82
|
+
attempted here.
|
|
83
|
+
- `defaultVerifyMergedItems` remains a deliberate no-op stub for the injected
|
|
84
|
+
`verifyMergedItems` seam (returns an empty Set). Its own comment says production
|
|
85
|
+
callers should pass a real verifier; the one production caller does.
|
|
86
|
+
|
|
87
|
+
## 3. Level-of-abstraction fit
|
|
88
|
+
|
|
89
|
+
**Is this at the right layer?**
|
|
90
|
+
|
|
91
|
+
Yes. The evidence originates in the validator (the only place that proves it), is
|
|
92
|
+
carried on the validator's own result type, and is persisted by the route that owns the
|
|
93
|
+
write. No new store, no new field on the wire, no new endpoint — `prNumber`,
|
|
94
|
+
`mergeCommitOid` and `ciCheckedAt` are pre-existing declared `Initiative` fields, and
|
|
95
|
+
`mergeCommitOid`'s own comment already said "recorded at building → merged". The fix
|
|
96
|
+
makes the comment true rather than inventing a mechanism.
|
|
97
|
+
|
|
98
|
+
## 4. Signal vs authority compliance
|
|
99
|
+
|
|
100
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
101
|
+
|
|
102
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
103
|
+
|
|
104
|
+
It **reduces** brittle authority. The reconciler holds real authority (it demotes items
|
|
105
|
+
to `regressed` and clears a round's future `autoAdvanceAt`), and it was exercising that
|
|
106
|
+
authority on the brittle ground of "absent from a set that could be empty for any
|
|
107
|
+
reason, including a permission refusal". Demotion now requires git's single documented
|
|
108
|
+
negative exit status. Nothing in this change blocks, delays, or alters a message or an
|
|
109
|
+
action.
|
|
110
|
+
|
|
111
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
112
|
+
|
|
113
|
+
No judgment point is introduced. Every new branch is a deterministic comparison (a
|
|
114
|
+
`status === 1` test, a set-membership test, a presence test on `result.evidence`). The
|
|
115
|
+
change *retires* a pseudo-judgment — treating an unanswerable question as a negative
|
|
116
|
+
answer — and makes the asymmetry explicit in the type: `unverifiable` is a first-class
|
|
117
|
+
outcome carrying its reason, never folded into either verdict.
|
|
118
|
+
|
|
119
|
+
## 5. Interactions
|
|
120
|
+
|
|
121
|
+
- **`GET /projects/:id` lazy reconciler** — the one production consumer. Now receives a
|
|
122
|
+
three-state result and a resolved canonical-main ref. An unverifiable candidate takes
|
|
123
|
+
the `ciCheckedAt` backoff and keeps its stage; only `regressed` membership triggers a
|
|
124
|
+
demotion and the round's `autoAdvanceAt` clearing (that downstream logic is unchanged).
|
|
125
|
+
- **`verifyMergedItemsViaGit` signature** — return type widened
|
|
126
|
+
(`Set<string>` → `MergedVerificationResult`) and a fourth optional parameter added.
|
|
127
|
+
Grepped all callers: exactly one production callsite (`routes.ts`), updated. The
|
|
128
|
+
injected `verifyMergedItems` seam on `RunRoundInput` is a DIFFERENT type
|
|
129
|
+
(`Set<string>`) and is untouched, so `ProjectRoundExecution`'s own callers and its 8
|
|
130
|
+
unit tests are unaffected — verified by running them.
|
|
131
|
+
- **`StageTransitionResult`** — the widened success case is an optional property, so
|
|
132
|
+
every existing `if (!result.ok)` caller and every `expect(r.ok).toBe(true)` assertion
|
|
133
|
+
keeps working. Confirmed: 48 pre-existing validator tests pass unchanged.
|
|
134
|
+
- No persistence migration: the three fields already exist in the `Initiative` type,
|
|
135
|
+
the create/update allowlists, and the JSON store.
|
|
136
|
+
|
|
137
|
+
## 6. External surfaces
|
|
138
|
+
|
|
139
|
+
`POST /projects/:id/advance`'s 200 body gains `item.prNumber`, `item.mergeCommitOid`,
|
|
140
|
+
`item.ciCheckedAt` and an `evidence` object on a merged transition. Additive only — no
|
|
141
|
+
field removed, no shape changed, no status code changed. `GET /projects/:id` children
|
|
142
|
+
now carry those fields once written. Refusal responses are byte-identical.
|
|
143
|
+
|
|
144
|
+
## 6b. Operator-surface quality
|
|
145
|
+
|
|
146
|
+
The operator previously read `merged` with no way to ask "merged by what?" — and got a
|
|
147
|
+
silent all-clear from a regression detector that had never examined anything. They now
|
|
148
|
+
see the PR, the merge commit and the check time on the record itself, and an
|
|
149
|
+
unverifiable item names its reason instead of being demoted. Items merged before this
|
|
150
|
+
change honestly report "no mergeCommitOid recorded" rather than appearing clean.
|
|
151
|
+
|
|
152
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
153
|
+
|
|
154
|
+
**Posture: `unified` by construction — no new state.** No new field, file, store, or
|
|
155
|
+
surface is introduced; three already-declared fields on an existing per-machine store
|
|
156
|
+
start being written. The evidence is a pure function of a PR's GitHub state plus the
|
|
157
|
+
target repo's git history, both of which are machine-independent facts. The
|
|
158
|
+
canonical-main ref is resolved per-machine from that machine's remotes, which is
|
|
159
|
+
correct: it names the same upstream commit graph whatever the local remote is called —
|
|
160
|
+
and resolving it (rather than assuming `origin/main`) is precisely what makes the check
|
|
161
|
+
give the same answer on a fork-origin home as on a canonical one. No replication path
|
|
162
|
+
is required and no `machine-local-justification` marker is needed.
|
|
163
|
+
|
|
164
|
+
## 8. Rollback cost
|
|
165
|
+
|
|
166
|
+
Three source files, one commit, no migration and no persisted-state change. Reverting
|
|
167
|
+
restores a `merged` record that cannot name its evidence and two reconcilers that
|
|
168
|
+
select nothing — and re-arms the "could not check ⇒ regressed" mistranslation the
|
|
169
|
+
moment anything writes `mergeCommitOid`. Written-but-unread evidence fields are inert,
|
|
170
|
+
so a revert leaves no corrupt data behind.
|
|
171
|
+
|
|
172
|
+
## Phase 5 — Second-pass review (independent reviewer subagent)
|
|
173
|
+
|
|
174
|
+
**Disclosure, per Truthful Provenance:** no independent reviewer subagent was spawned —
|
|
175
|
+
a standing instruction in this session prohibits it unless the operator requests it.
|
|
176
|
+
The review lenses were applied by the author. That is **reduced independence**, recorded
|
|
177
|
+
as such rather than presented as a concurring second pass.
|
|
178
|
+
|
|
179
|
+
What author-applied review caught and changed:
|
|
180
|
+
|
|
181
|
+
1. **The first instinct was to persist the evidence and stop** — a two-line change that
|
|
182
|
+
would have armed a code path carrying three unexercised defects, turning silent
|
|
183
|
+
blindness into confident false regressions. The consumer had to be read before the
|
|
184
|
+
producer was fixed. This is the same "fix one site, leave its twin" mistake flagged
|
|
185
|
+
on #1647 and again on #1650 earlier the same night; third occurrence, caught before
|
|
186
|
+
shipping this time.
|
|
187
|
+
2. **The hardcoded `origin/main` would have mis-fired specifically on this machine.**
|
|
188
|
+
The advance path had already resolved it via `resolveCanonicalMainRef` for exactly
|
|
189
|
+
this reason. Passing the resolved ref through was not optional — without it a
|
|
190
|
+
dev-agent home demotes every healthy item.
|
|
191
|
+
3. **The unverifiable branch initially bumped nothing**, which would have re-asked an
|
|
192
|
+
unanswerable question on every read (a hot loop). It now takes the `ciCheckedAt`
|
|
193
|
+
backoff while explicitly leaving `pipelineStage` alone.
|
|
194
|
+
4. **Consumers were grepped, not assumed** — establishing that the widened return type
|
|
195
|
+
has exactly one production caller and that the similarly-named injected
|
|
196
|
+
`verifyMergedItems` seam is a distinct type that must NOT change.
|
|
197
|
+
5. **One new test passed for the wrong reason and was fixed:** a fixed-size text window
|
|
198
|
+
ran past the branch it meant to inspect and matched the next block's
|
|
199
|
+
`pipelineStage`. Now bounded at the branch's own `continue`. A test measuring
|
|
200
|
+
adjacent text is the same defect class as the subject of this change.
|
|
201
|
+
|
|
202
|
+
## Phase 6 — What CI refused, and why the fix went in the prose
|
|
203
|
+
|
|
204
|
+
The first push was refused by the empty-catch ratchet
|
|
205
|
+
(`tests/unit/no-empty-catch-blocks.test.ts`): count 1 against a baseline of 0. The
|
|
206
|
+
offending occurrence was at `ProjectRoundExecution.ts:506` — **inside this change's own
|
|
207
|
+
doc comment**, on the line quoting the forbidden bodyless-catch shape while explaining
|
|
208
|
+
that it had been removed. The lint scans `src/` as text and does not strip comments, so
|
|
209
|
+
it cannot distinguish an example from an instance.
|
|
210
|
+
|
|
211
|
+
Noted rather than glossed, because it is the same failure mode this change is about,
|
|
212
|
+
one layer out: a checker built to catch "errors turning into nothing" was tripped by a
|
|
213
|
+
sentence *about* errors turning into nothing. It is also the third occurrence of that
|
|
214
|
+
shape in a single session (twice in the outbound-message checker, once here), and the
|
|
215
|
+
sibling test `tests/unit/projects-advance-mergebase-wiring.test.ts` already documents
|
|
216
|
+
the identical trap for its own assertions — it strips comments before asserting,
|
|
217
|
+
precisely because an earlier version of it matched the helper's own explanatory prose.
|
|
218
|
+
|
|
219
|
+
**The prose was reworded; the lint was NOT touched.** Weakening a safety check to
|
|
220
|
+
unblock one's own change is the wrong instinct even when the check is imprecise, and
|
|
221
|
+
this lint's stated history (a bare catch in a 5-second loop causing a real cost
|
|
222
|
+
incident) earns it the benefit of the doubt. Making the lint comment-aware is a
|
|
223
|
+
legitimate improvement, but it belongs in its own change with its own review — not
|
|
224
|
+
smuggled in as a side-effect of needing this PR to go green.
|
|
225
|
+
|
|
226
|
+
**Re-verification scope, corrected.** The refusal proved my local scope was wrong: I
|
|
227
|
+
had run the tests covering the changed module, but this failure came from a test that
|
|
228
|
+
scans the whole source tree and whose subject my change never touched. The right scope
|
|
229
|
+
for a source change is therefore *every tree-scanning test*, located by grep rather
|
|
230
|
+
than guessed: 99 files, 8,370 tests, exit 0, `tsc --noEmit` clean.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Side-Effects Review — a raw NUL byte made 30 source files invisible to grep
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `raw-nul-bytes-hide-source-from-grep`
|
|
4
|
+
**Date:** `2026-07-26`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `author-applied lenses — see Phase 5 (reduced independence, disclosed)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`grep` classifies a file as binary if it contains a NUL (0x00) byte, and on a binary
|
|
11
|
+
file it emits **nothing** — not a match, not a `Binary file X matches` line, not even a
|
|
12
|
+
`0` under `-c`. Thirty tracked text files each contained one or more raw NUL bytes, so
|
|
13
|
+
every grep-based audit over `src/` silently skipped them and the empty result read as
|
|
14
|
+
"absent".
|
|
15
|
+
|
|
16
|
+
The bytes were never corruption. Each was a deliberate composite-key or hash separator
|
|
17
|
+
written as a literal byte instead of the six-character escape:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const key = `${row.model}<literal 0x00>${row.framework}`; // now: \u0000
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The delimiter choice is correct — NUL cannot occur inside a model or framework name, so
|
|
24
|
+
it is collision-proof. Writing it raw is the whole defect.
|
|
25
|
+
|
|
26
|
+
**Measured:** 30 tracked files (22 under `src/`, 7 tests, 1 spec doc). Affected modules
|
|
27
|
+
include `blockerSettleAuthority.ts` (the true-blocker settle gate), `SessionOwnership.ts`,
|
|
28
|
+
`GreenPrAutoMerger.ts`, `PermissionPromptAutoResolver.ts` (an always-on safety floor),
|
|
29
|
+
all three `ExternalHog*` modules, and `StandardsEnforcementAuditor.ts` — the module that
|
|
30
|
+
audits whether standards carry structural guards was itself invisible to the standard
|
|
31
|
+
search instrument.
|
|
32
|
+
|
|
33
|
+
**Second consequence, worse in kind:** git applies the same rule but only sniffs the
|
|
34
|
+
first 8000 bytes. For the **11 files** whose NUL fell inside that window, `git diff`
|
|
35
|
+
rendered `Bin 5407 -> 5412 bytes` instead of a line diff — so pull requests touching
|
|
36
|
+
safety-critical authority code were reviewed **without the reviewer being shown the
|
|
37
|
+
changed lines**. For the other 19, git saw text while grep did not: the two instruments
|
|
38
|
+
disagreed, which is precisely why this survived so long.
|
|
39
|
+
|
|
40
|
+
Every raw byte is replaced with `\u0000` (identical runtime string; valid and unambiguous
|
|
41
|
+
in string, template and regex contexts — unlike `\0`, a legacy octal escape when followed
|
|
42
|
+
by a digit), and a new ratchet fails the build if one ever returns.
|
|
43
|
+
|
|
44
|
+
## Decision-point inventory
|
|
45
|
+
|
|
46
|
+
| point | classification | note |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `containsRawNul(file)` | `invariant` | A byte is present or it is not. No judgment, no model, no competing signals. |
|
|
49
|
+
| lint scope (extension allowlist) | `invariant` | Deterministic set of text-by-definition extensions. Binary fixtures are out of scope by construction, not by heuristic. |
|
|
50
|
+
| control bytes other than 0x00 | `invariant` (deliberate exclusion) | Empirically verified not to cause grep-skip; see §2. |
|
|
51
|
+
|
|
52
|
+
## 1. Over-block
|
|
53
|
+
|
|
54
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
55
|
+
|
|
56
|
+
The lint rejects a raw 0x00 byte in a file with a text extension. There is **no
|
|
57
|
+
legitimate case it blocks**, because there is no case that needs one: any runtime NUL a
|
|
58
|
+
program wants is expressible as an escape, with an identical result. The lint therefore
|
|
59
|
+
carries no exemption list — an exemption would only ever be used to reinstate the defect.
|
|
60
|
+
|
|
61
|
+
The nearest real cost: a genuinely binary fixture that someone gives a `.json`/`.md`
|
|
62
|
+
extension would be refused. That is a naming error the lint is right to surface, and no
|
|
63
|
+
such file exists in the tree today (verified by the sweep).
|
|
64
|
+
|
|
65
|
+
## 2. Under-block
|
|
66
|
+
|
|
67
|
+
**What failure modes does this still miss?**
|
|
68
|
+
|
|
69
|
+
- **Other raw control bytes remain.** ESC (0x1b), BEL (0x07), 0x1f and 0x7f appear in a
|
|
70
|
+
handful of hostile-input test fixtures (`RevertDetector.ts`, `liveOutputStream.ts`,
|
|
71
|
+
`process-health-render.test.ts`, and others). These are **deliberately not covered**:
|
|
72
|
+
I verified empirically that a file containing ESC/BEL is still `ASCII text` to `file`
|
|
73
|
+
and still fully searchable by `grep` — only 0x00 triggers the skip. Escaping them would
|
|
74
|
+
be cosmetic. A lint should enforce exactly the failure it is named for. Recorded rather
|
|
75
|
+
than silently widened. (A separate concern does exist — a raw ESC in a file that gets
|
|
76
|
+
`cat`'d is a terminal-escape vector — but that is a different defect with a different
|
|
77
|
+
argument, and folding it in here would smuggle it past review.)
|
|
78
|
+
- **Untracked and generated files are not scanned.** The lint walks `src`, `tests`,
|
|
79
|
+
`docs`, `scripts`, `.github`. Build output is excluded on purpose (it is regenerated
|
|
80
|
+
from the now-clean sources).
|
|
81
|
+
- **This fixes one silent instrument, not the class.** Other tools can also fail
|
|
82
|
+
closed-mouthed. Nothing here establishes that no others remain — see §5.
|
|
83
|
+
|
|
84
|
+
## 3. Level-of-abstraction fit
|
|
85
|
+
|
|
86
|
+
**Is this at the right layer?**
|
|
87
|
+
|
|
88
|
+
Yes, and at two layers deliberately:
|
|
89
|
+
|
|
90
|
+
1. **The data** — the bytes themselves are corrected at source, which is the only place
|
|
91
|
+
the fix can be complete. A tooling workaround (`grep -a` everywhere, a `.gitattributes`
|
|
92
|
+
override) would have left the files hostile to every *other* consumer, including
|
|
93
|
+
editors, reviewers, and any future agent's search.
|
|
94
|
+
2. **The guard** — a repo-level test, matching the existing tree-scanning ratchets
|
|
95
|
+
(`no-empty-catch-blocks`, `no-silent-fallbacks`). It reads bytes with `readFileSync`
|
|
96
|
+
rather than shelling out to a search tool, so the lint cannot be blinded by the very
|
|
97
|
+
defect it detects.
|
|
98
|
+
|
|
99
|
+
Notably a `.gitattributes` `*.ts text` entry was considered and **rejected**: it would
|
|
100
|
+
have fixed git's rendering while leaving `grep` — the instrument that actually failed —
|
|
101
|
+
just as blind, and would have converted a visible defect into a hidden one.
|
|
102
|
+
|
|
103
|
+
## 4. Signal vs authority compliance
|
|
104
|
+
|
|
105
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
106
|
+
|
|
107
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
108
|
+
|
|
109
|
+
It holds blocking authority (a failing test blocks a merge) on **maximally non-brittle**
|
|
110
|
+
logic: the presence of one specific byte in a file. There is no model, no heuristic, no
|
|
111
|
+
threshold, no parse. It cannot drift, cannot be rate-limited, and produces the same
|
|
112
|
+
verdict on every machine.
|
|
113
|
+
|
|
114
|
+
This is the correct place for hard authority — a deterministic, universally-checkable
|
|
115
|
+
fact — and precisely the shape the standard reserves it for.
|
|
116
|
+
|
|
117
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
118
|
+
|
|
119
|
+
No judgment point is introduced. Every decision in the lint is a byte comparison or a
|
|
120
|
+
set membership test. Nothing consults an LLM; nothing weighs competing signals.
|
|
121
|
+
|
|
122
|
+
## 5. Interactions
|
|
123
|
+
|
|
124
|
+
- **Runtime behaviour is unchanged.** `\u0000` denotes exactly the byte it replaces. The
|
|
125
|
+
lint asserts this explicitly (`JSON.parse` of the escape text `=== String.fromCharCode(0)`)
|
|
126
|
+
rather than leaving it as a claim in prose.
|
|
127
|
+
- **Persisted data is unaffected.** Several sites feed these separators into hashes
|
|
128
|
+
(`relayContentDedup`, `blockerSettleAuthority`, `UnionReader`, `ExternalHogArmMarker`).
|
|
129
|
+
Because the string is identical, every hash is identical — **no migration, no cache
|
|
130
|
+
invalidation, no re-keying**. This was the primary risk and it is checked, not assumed.
|
|
131
|
+
- **`git diff` becomes readable** for the 11 previously-binary files. A one-time
|
|
132
|
+
consequence: this PR shows them as `Bin` on the *old* side.
|
|
133
|
+
- **The empty-catch ratchet** scans `src/` as text via `readFileSync`, so it was NOT
|
|
134
|
+
blinded — verified before assuming a shared failure. Grep-based *ad hoc* audits were the
|
|
135
|
+
affected consumers, not the file-reading lints.
|
|
136
|
+
- **This session's own reasoning was affected.** A search for a retraction mechanism
|
|
137
|
+
across `src/` returned one unrelated subsystem, and I was one step from recording
|
|
138
|
+
"no such path exists" as a finding. That search had skipped 22 source files. The
|
|
139
|
+
correction is recorded in the run log rather than quietly dropped: **the finding I was
|
|
140
|
+
about to publish was manufactured by the defect this PR fixes.**
|
|
141
|
+
|
|
142
|
+
## 6. External surfaces
|
|
143
|
+
|
|
144
|
+
None. No route, no config key, no persisted state, no user-visible behaviour. This is a
|
|
145
|
+
source-text and build-time change only.
|
|
146
|
+
|
|
147
|
+
## 6b. Operator-surface quality
|
|
148
|
+
|
|
149
|
+
Nothing reaches the operator. The one surface is the failing-test message, which names
|
|
150
|
+
each offending file, states the consequence in plain terms ("INVISIBLE to grep — every
|
|
151
|
+
grep-based audit silently skips them and reports 'not found'"), and gives the exact fix.
|
|
152
|
+
A lint that only says "assertion failed" would have re-created the original problem at
|
|
153
|
+
the level of the guard.
|
|
154
|
+
|
|
155
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
156
|
+
|
|
157
|
+
**Posture: `unified` by construction — no state at all.** This change introduces no
|
|
158
|
+
feature, no field, no file, no surface. It edits source text and adds a build-time check
|
|
159
|
+
that runs identically wherever the repo is checked out. No replication path is required
|
|
160
|
+
and no `machine-local-justification` marker applies, because there is nothing machine-local
|
|
161
|
+
to justify.
|
|
162
|
+
|
|
163
|
+
## 8. Rollback cost
|
|
164
|
+
|
|
165
|
+
Trivial and total: one commit, 30 files, 33 single-line substitutions plus one new test
|
|
166
|
+
file. Reverting restores the raw bytes — and with them the grep-invisibility of 22 source
|
|
167
|
+
modules and the binary diffs on 11. There is no data to migrate back and no state to
|
|
168
|
+
unwind.
|
|
169
|
+
|
|
170
|
+
## Phase 5 — Second-pass review (independent reviewer subagent)
|
|
171
|
+
|
|
172
|
+
**Disclosure, per Truthful Provenance:** no independent reviewer subagent was spawned — a
|
|
173
|
+
standing instruction in this session prohibits it unless the operator requests it. The
|
|
174
|
+
review lenses were applied by the author. That is **reduced independence**, recorded as
|
|
175
|
+
such rather than presented as a concurring second pass.
|
|
176
|
+
|
|
177
|
+
What author-applied review caught and changed:
|
|
178
|
+
|
|
179
|
+
1. **The first draft of this very lint shipped five raw NUL bytes and hid itself from
|
|
180
|
+
grep.** I typed the escape inline; it was normalised into actual NUL bytes on write.
|
|
181
|
+
Caught only because I byte-checked the file I had just written instead of trusting it.
|
|
182
|
+
The lint now constructs both the raw byte and the escape *text* from char codes and
|
|
183
|
+
never types the escape inline — and the authoring hazard is documented in its header,
|
|
184
|
+
because the next author will hit exactly the same trap.
|
|
185
|
+
2. **The scope was nearly widened to all control bytes** for tidiness. I tested the
|
|
186
|
+
assumption instead and found ESC/BEL do not cause the skip — so widening would have
|
|
187
|
+
added churn and noise while claiming safety it does not provide. Narrowed back, with
|
|
188
|
+
the empirical result recorded in §2.
|
|
189
|
+
3. **A "scan is not silently empty" assertion was added.** Without it, a scan that walked
|
|
190
|
+
the wrong roots would find zero offenders and pass forever — the same absence-reads-as-
|
|
191
|
+
presence failure, reincarnated inside its own guard. The lint now asserts it examined
|
|
192
|
+
>500 files.
|
|
193
|
+
4. **The hash-stability question was checked, not assumed.** Four sites feed these
|
|
194
|
+
separators into digests; had the substitution altered the string, persisted dedup keys
|
|
195
|
+
and arm markers would have silently changed meaning. Verified identical before
|
|
196
|
+
committing.
|
|
197
|
+
5. **`.gitattributes` was rejected as the fix** (see §3) because it would have repaired
|
|
198
|
+
the *visible* symptom (git diffs) while leaving the *silent* one (grep) intact —
|
|
199
|
+
trading a detectable failure for an undetectable one.
|