muse-crew 0.7.18 → 0.7.20
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/docs/publish-verification.md +45 -22
- package/lib/AGENTS.md +4 -1
- package/lib/compose-evidence-caption.js +57 -14
- package/lib/verify-publish.js +51 -13
- package/package.json +1 -1
- package/workflows/bugfix.js +368 -397
- package/workflows/chore.js +370 -397
- package/workflows/standard.js +378 -404
|
@@ -42,18 +42,25 @@ The rule: **the builder's applied report is never a verification signal, in
|
|
|
42
42
|
either direction.** A matching report certifies nothing (it is derived from
|
|
43
43
|
the carried diff — circular by construction, canary run 8). A mismatching or
|
|
44
44
|
empty report blocks nothing (false-negative mode demonstrated by `23ca8f3f`).
|
|
45
|
-
The workflow
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
The workflow dropped the report entirely on 2026-09-16 (clean-room task
|
|
46
|
+
`e2a8d9f8`): the trigger's JSON closeout contract traveled over the
|
|
47
|
+
stochastic text channel and the runtime's JSON-candidate heuristic misfired
|
|
48
|
+
on its prose ("workflow agent output was not JSON"), parking a task whose
|
|
49
|
+
edit may have gone through. The trigger is now awaited-but-unconsumed — no schema,
|
|
50
|
+
no consumed return value — and there is no builder report to observe:
|
|
51
|
+
`applied_report` is `missing-report` on ledger lines for issued triggers
|
|
52
|
+
(pre-trigger parks and unattributed-unknown parks write null — no trigger
|
|
53
|
+
was observed, so there is nothing to report). The
|
|
54
|
+
parent ignores the (absent) report entirely when deciding whether to stamp.
|
|
49
55
|
|
|
50
56
|
The contract is split on purpose:
|
|
51
57
|
|
|
52
|
-
- **Workflow-owned:** carrying the merged diff to the builder,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
- **Workflow-owned:** carrying the merged diff to the builder, attributing
|
|
59
|
+
the edit itself (fire-and-forget trigger — no builder report — via
|
|
60
|
+
pre-trigger toolcheck, pre-trigger build-state baseline, and post-trigger
|
|
61
|
+
build-state diff), the build-completion poll, post-deploy cleanup,
|
|
62
|
+
recording the Publish session completed, and parking with `publish:
|
|
63
|
+
verification-requested <commit>` instead of stamping. The workflow does NOT trigger the read-back inspection — an
|
|
57
64
|
async inspection triggered from inside a workflow run delivers its result
|
|
58
65
|
to the root agent, never back into the run, so a workflow-side trigger is
|
|
59
66
|
an orphan the verifier cannot consume. The parent triggers the one
|
|
@@ -313,24 +320,40 @@ For a task parked with `publish: verification-requested <commit>`:
|
|
|
313
320
|
second scan sees the unexpired `publish: verification-claimed` note and
|
|
314
321
|
skips. The lease expiry bounds the damage if a claimer dies.
|
|
315
322
|
|
|
316
|
-
## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7)
|
|
323
|
+
## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; fire-and-forget 2026-09-16)
|
|
317
324
|
|
|
318
325
|
Attempt 7 parked at Publish with outcome `unknown`: the rebuild trigger's
|
|
319
326
|
child failed structured closeout and the in-flight-only build-state poll
|
|
320
327
|
could not see the completed build — even though the build HAD run (a fresh
|
|
321
|
-
platform audit directory existed).
|
|
322
|
-
|
|
323
|
-
|
|
328
|
+
platform audit directory existed). 2026-09-16 (clean-room task `e2a8d9f8`)
|
|
329
|
+
showed the failure is worse than a catchable throw: the runtime's
|
|
330
|
+
JSON-candidate heuristic rejects the trigger call itself ("workflow agent
|
|
331
|
+
output was not JSON") whenever the child returns prose, whether or not the
|
|
332
|
+
edit went through. The trigger is therefore fire-and-forget — no schema, no
|
|
333
|
+
consumed return value — and the workflow always attributes the edit itself.
|
|
334
|
+
Two mechanisms close the gap.
|
|
335
|
+
|
|
336
|
+
**1. Pre-trigger toolcheck + baseline.** Before the trigger, a tiny schema'd
|
|
337
|
+
child proves the artifact tool namespace is available (one bounded retry on
|
|
338
|
+
explicit negative evidence — the only safe retry on the publish path:
|
|
339
|
+
without the tools the edit provably did not go through) and captures a
|
|
340
|
+
pre-trigger build-state baseline. After the trigger, the workflow diffs the
|
|
341
|
+
post-trigger build state against the baseline: a build whose `agent_id` is
|
|
342
|
+
new relative to the baseline is this edit's receipt. The baseline build's
|
|
343
|
+
`agent_id` is never substituted — a build already in flight at baseline
|
|
344
|
+
predates the trigger and is never attributed to this edit.
|
|
345
|
+
|
|
346
|
+
**2. Workflow-side durable evidence.** Before the rebuild trigger, the
|
|
324
347
|
workflow snapshots the artifact's audit-directory listing
|
|
325
|
-
(`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate).
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`
|
|
332
|
-
confirmed via durable audit evidence
|
|
333
|
-
|
|
348
|
+
(`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). When
|
|
349
|
+
no in-flight receipt was observed, it re-lists and diffs: a timestamped
|
|
350
|
+
directory that appeared during the trigger window is positive evidence the
|
|
351
|
+
edit went through and the build completed. The fallback never re-issues the
|
|
352
|
+
edit, never stamps provenance, and only routes to the parent's independent
|
|
353
|
+
content read-back. No new directory still parks `unknown` fail-closed. The
|
|
354
|
+
ledger's `detail` line distinguishes the two confirmations: `… edit
|
|
355
|
+
confirmed via durable audit evidence (new audit dir …)` vs `… build receipt
|
|
356
|
+
captured by workflow-owned build-state observation (pre/post-trigger diff)`.
|
|
334
357
|
|
|
335
358
|
The fallback's known limitation: audit directories are not attributed to
|
|
336
359
|
tasks, so two concurrent publishes to the same artifact could cross-read.
|
package/lib/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
14
14
|
- `test-publish-skip.sh` — regression tests for the no-lock graceful publish skip (park 2026-09-11): `lock-status` returns exact `UNLOCKED`/`LOCKED by <holder> since <ts> (pid <pid>)`; a fake empty-diff integrate takes the MERGED_EMPTY branch and never takes the lock; the shipped step-5 block from publish-npm.sh (extracted by anchor) skips gracefully on UNLOCKED (no park, no `PUBLISH_COMPLETE`), follows the refresh path when a lock is held, and fails closed on a `lock-status` query failure; the gate sits inside the ALREADY_PUBLISHED=0 branch and mutation steps are gated on NO_LOCK_HELD=0
|
|
15
15
|
- `publish-npm.sh` — deterministic npm publish: lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
16
16
|
- `compose-evidence.py` — deterministic visual-evidence compositor (Pillow): pairs identical PNG stems from baseline/ and postchange/ dirs, emits `<stem>-sidebyside.png` and amplified-difference `<stem>-overlay.png` into composites/, prints `PAIR`/`SKIP` manifests. Byte-deterministic; nonzero exit on errors.
|
|
17
|
-
- `compose-evidence-caption.js` — deterministic QA-evidence caption composer (2026-09-14): given `--crew-home`, `--task-id`, and the resolved `--audit-dir` name, extracts the task title/description, the
|
|
17
|
+
- `compose-evidence-caption.js` — deterministic QA-evidence caption composer (2026-09-14): given `--crew-home`, `--task-id`, and the resolved `--audit-dir` name, extracts the task title/description, the QA verdict, the `publish: verified` commit (or the global provenance record when it belongs to this task), and the merge commit's subject line from the project repo — and prints the out-of-context transition Eric asked for (problem → solution → evidence handoff) to stdout. The caller appends the desktop/mobile attachment lines after this caption. Verdict source (2026-09-16): Hazel's `task-evidence/<taskId>/postchange/verdict.json` is authoritative, then the last line of the append-only `verdicts.jsonl` ledger, then the newest legacy `visual_verdict:` note (the parent visual-verdict protocol was retired 2026-09-15). Honest labels: "provenance not yet stamped" when no commit is known, "no QA verdict recorded" when none exists, and the visual-protocol-unavailable note only when no verdict evidence exists at all and the newest `baseline:` note is `baseline: none` (a `baseline: none` note means no baseline captures to compare against — it never overrules a real QA verdict). "Latest" is by event timestamp, sorted explicitly. Exits non-zero on missing input.
|
|
18
18
|
- `edit-image.py` — deterministic pixel-level evidence editing (2026-09-15, Pillow): `crop --region x,y,w,h` (pixel-exact, rejects out-of-bounds), `zoom --factor <f> [--center x,y]` (nearest-neighbor, returns to source frame size), `label --text <caption>` (caption bar), `nup --cols <n> --in <a.png> --in <b.png> [--labels "a|b"]` (side-by-side grid). Byte-deterministic PNG output; no network, no time, no randomness. Derivatives supplement raw frames — the source frame stays archived.
|
|
19
19
|
- `render-html.js` — deterministic HTML evidence composition (2026-09-15, headless Chromium): renders a local HTML layout to PNG (fixed viewport width, device scale 1, full-page screenshot). Hermetic: remote HTTP(S) assets are blocked and fail the render loudly; relative image paths resolve against the HTML file; local/system fonts only. Exit 3 reports NOT POSSIBLE when Chromium is unavailable. The composition layer above edit-image.py's pixel layer.
|
|
20
20
|
- `see-act.js` — single-step browser driver for experiential QA (2026-09-14): one browser action per invocation (`aria`, `shot`, `click`, `scroll`, `type`), one JSON line on stdout, exit 0/2/3 (3 = NOT POSSIBLE: missing playwright-core or Chromium). Spawns its own loopback forward proxy on an ephemeral port (Chromium blocks direct loopback). The QA work agent closes the OODA loop: run a step, read the screenshot/aria, decide the next. Determinism: no wall-clock reads, no randomness. `SEE_ACT_ARCHIVE_DIR=<phase-dir>` (2026-09-14): every screenshot is archived automatically as `001-shot-desktop.png`, `002-click-mobile.png`, ... (counter in `<dir>/.seq`); `--out` becomes optional; JSON carries `screenshot` + `archived`; explicit `--out` + archive copies into the archive; unusable dir is NOT POSSIBLE exit 3.
|
|
@@ -22,3 +22,6 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
22
22
|
- `write-ooda-verdict.js` — deterministic writer for the OODA terminal verdict (2026-09-14, append-only ledger 2026-09-15): `node write-ooda-verdict.js --dir <phase-dir> --attempt <id> --verdict <PASS|FAIL|NOT_POSSIBLE> --summary <text> --expected <text> --actual <text> --missing <json-array> [--reason <text>]` writes `<phase-dir>/verdict.json` (the latest verdict) and appends one JSON line to `<phase-dir>/verdicts.jsonl` — the append-only ledger: every attempt's verdict is preserved with a mechanical `seq`, never overwritten; corrupt or non-contiguous ledgers fail loudly. `--reason` is REQUIRED and must be non-empty for `FAIL` and `NOT_POSSIBLE` — a reason-less negative verdict fails with exit 2 before anything is written (2026-09-15). Exit 2 on bad input. See `docs/ooda-report.md`.
|
|
23
23
|
- `read-ooda-verdict.js` — deterministic cross-checker for the OODA terminal verdict (2026-09-15): `node read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>` reads `<dir>/verdict.json`, prints one JSON line to stdout, exits 0 with `{ok:true, verdict, reason, summary, expected, actual, attempt}` when the record agrees with the prose expectation and a FAIL carries a non-empty reason, or exits 2 with `{ok:false, code}` — `missing|corrupt|contradiction|no_reason`. No wall-clock reads, no randomness. The bugfix QA closeout runs it against the prose `VERDICT:` line before any rework routing: a failed cross-check records the phase as failed for retry, never routes to rework.
|
|
24
24
|
- `serve-artifact.js` — local server for a built TS space for experiential QA (2026-09-14): serves `<space-dir>/client/dist` statically and dispatches POST `*/actions` to the compiled server actions with a locally-built Ctx. Prints `READY port=<n>` then serves until killed. Read-only w.r.t. the space directory. Fidelity: the served client and action handlers are the artifact's own built code; the Ctx is locally built (privileged handlers run from the space's own `server/dist/privileged.js` when present; blobs are stored in a per-run temp dir and served back at `/__blobs/<key>`); environment is inherited from the caller. It is not the hosted runtime — tasks that cannot be judged under it must report `NOT POSSIBLE: <reason>`.
|
|
25
|
+
- `readback-disk.js` — deterministic publish content sensor (2026-09-16): reads the on-disk tree the artifact is built/served from and emits the machine-readable findings block (`FILE:`/`ADDED:`/`REMOVED:`/`END_FILE`) that `verify-publish.js` judges. The primary sensor — the LLM-inspector path (`build-readback-request.js`) is manual-fallback only since `artifact_inspect` was removed by the platform 2026-09-14.
|
|
26
|
+
- `build-readback-request.js` — builds the LLM-inspector read-back `verbatim_request` from the merge commit's diff (2026-09-14): carries the merged diff as the expected change and asks for an independent read of the artifact's actual source. Retained as the manual fallback; the deterministic `readback-disk.js` is the primary sensor.
|
|
27
|
+
- `verify-publish.js` — mechanical publish verification judge (2026-09-14/16): certifies the read-back findings block against `git diff` (strict `FILE:`/`ADDED:`/`REMOVED:`/`END_FILE` parsing, every added line PRESENT / every removed line ABSENT, HEAD==commit supersession check) and only then stamps provenance. Binary files, mode-only changes, and fully-colliding added hunks fail closed as `unverifiable-content` (2026-09-16, critic findings 1/5) — they can never vacuously stamp. Content-mismatch, unreadable-result, superseded, and stamp failures exit 1 with `publish: verification-failed` and no stamp. Collision exemption (2026-09-15): a removed line occurring in the old tree strictly more times than the diff removes is exempt from ABSENT (zero discriminating power); computed from `git show <base>:<path>`, never the working tree.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// Prints the caption to stdout. Exits non-zero on any missing input.
|
|
19
19
|
|
|
20
20
|
const { execFileSync } = require("node:child_process");
|
|
21
|
+
const fs = require("node:fs");
|
|
21
22
|
const path = require("node:path");
|
|
22
23
|
|
|
23
24
|
function usage(msg) {
|
|
@@ -83,19 +84,51 @@ try {
|
|
|
83
84
|
usage("cannot read task events: " + (e.message || e));
|
|
84
85
|
}
|
|
85
86
|
const notes = events.filter((e) => e.type === "note");
|
|
87
|
+
// Newest-first by timestamp. The API orders DESC, but the sort is explicit:
|
|
88
|
+
// array order is not a contract this script should depend on, and the old
|
|
89
|
+
// [...notes].reverse().find() pattern silently returned the OLDEST match.
|
|
90
|
+
const byNewest = (a, b) => {
|
|
91
|
+
const ta = a.timestamp || "", tb = b.timestamp || "";
|
|
92
|
+
return tb < ta ? -1 : tb > ta ? 1 : 0;
|
|
93
|
+
};
|
|
94
|
+
const notesNewest = [...notes].sort(byNewest);
|
|
86
95
|
|
|
87
|
-
// QA verdict:
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
// QA verdict: Hazel owns the verdict — <phase-dir>/verdict.json is
|
|
97
|
+
// authoritative (2026-09-15: the parent visual-verdict protocol was retired,
|
|
98
|
+
// so the workflows no longer emit `visual_verdict:` notes; the old note
|
|
99
|
+
// contract is legacy fallback only). Prefer verdict.json, then the last line
|
|
100
|
+
// of the append-only verdicts.jsonl ledger, then the newest legacy note.
|
|
101
|
+
let verdict = null; // { status, summary }
|
|
102
|
+
const qaDir = path.join(crewHome, "task-evidence", taskId, "postchange");
|
|
103
|
+
function readVerdictFile(p) {
|
|
104
|
+
try {
|
|
105
|
+
const v = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
106
|
+
if (v && v.verdict) return { status: String(v.verdict), summary: v.summary || "" };
|
|
107
|
+
} catch { /* fall through */ }
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
verdict = readVerdictFile(path.join(qaDir, "verdict.json"));
|
|
111
|
+
if (!verdict) {
|
|
112
|
+
// Append-only ledger: the last line is the latest attempt's verdict.
|
|
113
|
+
try {
|
|
114
|
+
const lines = fs.readFileSync(path.join(qaDir, "verdicts.jsonl"), "utf8").trim().split("\n");
|
|
115
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
116
|
+
if (last && last.verdict) verdict = { status: String(last.verdict), summary: last.summary || "" };
|
|
117
|
+
} catch { /* fall through to the legacy note contract */ }
|
|
118
|
+
}
|
|
119
|
+
if (!verdict) {
|
|
120
|
+
const verdictNote = notesNewest
|
|
121
|
+
.find((e) => /^visual_verdict:\s*(PASS|FAIL)/.test(e.message || ""));
|
|
122
|
+
if (verdictNote) {
|
|
123
|
+
const m = /^visual_verdict:\s*(PASS|FAIL)\s*[—–-]?\s*([\s\S]*)$/.exec(verdictNote.message || "");
|
|
124
|
+
verdict = { status: m[1], summary: (m[2] || "").trim() };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
94
127
|
|
|
95
|
-
// Provenance: this task's own `publish: verified` event first; the
|
|
96
|
-
// provenance record only when it belongs to this task.
|
|
128
|
+
// Provenance: this task's own newest `publish: verified` event first; the
|
|
129
|
+
// global provenance record only when it belongs to this task.
|
|
97
130
|
let commit = null;
|
|
98
|
-
const verifiedNote =
|
|
131
|
+
const verifiedNote = notesNewest
|
|
99
132
|
.find((e) => (e.message || "").includes("publish: verified"));
|
|
100
133
|
if (verifiedNote) {
|
|
101
134
|
const m = /\b([0-9a-f]{40})\b/.exec(verifiedNote.message);
|
|
@@ -122,9 +155,19 @@ if (commit) {
|
|
|
122
155
|
if (!solution) solution = task.title;
|
|
123
156
|
|
|
124
157
|
const problem = trimWords(task.description || task.title, 280);
|
|
125
|
-
// The
|
|
126
|
-
//
|
|
127
|
-
|
|
158
|
+
// The "visual protocol unavailable" honest label answers "did QA get to
|
|
159
|
+
// look?" — and the only honest signal for that is the ABSENCE of any QA
|
|
160
|
+
// verdict evidence. A `baseline: none` note means the Capture phase had no
|
|
161
|
+
// baseline screenshots to compare against; it does NOT mean QA couldn't
|
|
162
|
+
// inspect. Room #5 (2026-09-16) logged `baseline: none (visual protocol
|
|
163
|
+
// unavailable)` as its newest baseline note, yet Hazel still ran the full
|
|
164
|
+
// experiential see-act loop and stamped PASS in verdict.json — the old
|
|
165
|
+
// .some() check mislabeled that run. So: any verdict evidence (file,
|
|
166
|
+
// ledger, legacy note) suppresses the label; without it, the newest
|
|
167
|
+
// `baseline: none` note earns it.
|
|
168
|
+
const newestBaselineNote = notesNewest.find((e) => /^baseline:\s*/i.test(e.message || ""));
|
|
169
|
+
const visualUnavailable = !verdict && !!newestBaselineNote &&
|
|
170
|
+
/^baseline:\s*none/i.test(newestBaselineNote.message || "");
|
|
128
171
|
|
|
129
172
|
const lines = [];
|
|
130
173
|
lines.push("These screenshots are dropping in out of context — quick transition.");
|
|
@@ -134,7 +177,7 @@ lines.push("Problem: " + problem);
|
|
|
134
177
|
lines.push("Shipped: " + trimWords(solution, 160) +
|
|
135
178
|
(commit ? " (commit `" + commit.slice(0, 12) + "`, parent-verified)" : " (provenance not yet stamped)"));
|
|
136
179
|
lines.push("Evidence: desktop + mobile captures below, taken by the platform audit harness after deploy (`" + auditDir + "`)." +
|
|
137
|
-
(verdict ? " QA verdict: " + trimWords(verdict, 200) + "." : " No QA verdict recorded."));
|
|
180
|
+
(verdict ? " QA verdict: " + verdict.status + (verdict.summary ? " — " + trimWords(verdict.summary, 200) : "") + "." : " No QA verdict recorded."));
|
|
138
181
|
if (visualUnavailable) {
|
|
139
182
|
lines.push("Note: the visual protocol is unavailable — these are the post-deploy capture plus QA's mechanical checks, not a QA visual inspection.");
|
|
140
183
|
}
|
package/lib/verify-publish.js
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
// verify-publish.js — deterministic parent publish verifier.
|
|
3
3
|
//
|
|
4
4
|
// The parent never eyeballs an inspection report. This script takes the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// checks supersession via git, and only then stamps provenance, logs
|
|
11
|
-
//
|
|
5
|
+
// read-back inspection's result JSON and MECHANICALLY decides the verdict:
|
|
6
|
+
// it parses the machine-readable findings block, compares every
|
|
7
|
+
// added/removed diff line against the reported present/absent verdicts
|
|
8
|
+
// (with mechanically computed collision exemptions for lines that also
|
|
9
|
+
// occur in untouched code and therefore have zero discriminating power),
|
|
10
|
+
// checks supersession via git, and only then stamps provenance, logs the
|
|
11
|
+
// terminal event, and re-queues the task.
|
|
12
12
|
//
|
|
13
|
-
// The
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// The sensor is the deterministic lib/readback-disk.js (it reads the
|
|
14
|
+
// on-disk tree the artifact is built/served from); this code is the judge.
|
|
15
|
+
// The LLM inspector driven by lib/build-readback-request.js is retained
|
|
16
|
+
// only as the manual fallback for environments without disk access.
|
|
17
|
+
// Unparseable findings, content mismatches, unverifiable changes (binary
|
|
18
|
+
// files, mode-only changes, renames, fully-colliding hunks), supersession,
|
|
19
|
+
// and stamp failures all fail CLOSED with a terminal parent verdict —
|
|
20
|
+
// never a stamp.
|
|
16
21
|
//
|
|
17
22
|
// Usage:
|
|
18
23
|
// node verify-publish.js --crew-home <path> --task-id <uuid>
|
|
@@ -177,13 +182,15 @@ try {
|
|
|
177
182
|
} catch (e) {
|
|
178
183
|
terminal("read-back-unavailable", `git diff failed: ${e.message}`);
|
|
179
184
|
}
|
|
180
|
-
const expected = new Map(); // path -> { added: [], removed: [] }
|
|
185
|
+
const expected = new Map(); // path -> { added: [], removed: [], isBinary: bool }
|
|
181
186
|
let curFile = null;
|
|
182
187
|
for (const line of diff.split("\n")) {
|
|
183
188
|
if (line.startsWith("diff --git")) {
|
|
184
189
|
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
185
190
|
curFile = m ? m[2] : "unknown";
|
|
186
|
-
expected.set(curFile, { added: [], removed: [] });
|
|
191
|
+
expected.set(curFile, { added: [], removed: [], isBinary: false });
|
|
192
|
+
} else if (curFile && line.startsWith("Binary files ")) {
|
|
193
|
+
expected.get(curFile).isBinary = true;
|
|
187
194
|
} else if (curFile && line.startsWith("+") && !line.startsWith("+++")) {
|
|
188
195
|
expected.get(curFile).added.push(line.slice(1));
|
|
189
196
|
} else if (curFile && line.startsWith("-") && !line.startsWith("---")) {
|
|
@@ -257,11 +264,42 @@ let exempted = 0;
|
|
|
257
264
|
for (const [path, exp] of expected) {
|
|
258
265
|
const found = findings.get(path);
|
|
259
266
|
if (!found) terminal("content-mismatch", `no findings for changed file ${path}`);
|
|
267
|
+
// (2026-09-16, critic finding 1) Binary files, mode-only changes, and
|
|
268
|
+
// renames produce zero added/removed lines: the loops below would
|
|
269
|
+
// iterate over empty arrays and the stamp would issue with "all added
|
|
270
|
+
// lines PRESENT, all removed lines ABSENT" — vacuously true, content
|
|
271
|
+
// never read. The line-based judge cannot verify these changes, so they
|
|
272
|
+
// fail closed as unverifiable (never stamped); a human verifies.
|
|
273
|
+
if (exp.added.length === 0 && exp.removed.length === 0) {
|
|
274
|
+
terminal("unverifiable-content",
|
|
275
|
+
`${exp.isBinary ? "binary file" : "no content lines (mode-only change or rename)"} ${path}: ` +
|
|
276
|
+
"the diff carries no added/removed lines for this file, so the line-based read-back checked nothing — " +
|
|
277
|
+
"provenance NOT stamped; human verification needed");
|
|
278
|
+
}
|
|
279
|
+
// (2026-09-16, critic finding 5) Added-line collisions: an added line
|
|
280
|
+
// that already occurs verbatim in the old tree is reported PRESENT
|
|
281
|
+
// whether or not the new hunk actually landed — zero discriminating
|
|
282
|
+
// power (the sensor is membership-only, not count-sensitive). Exempt
|
|
283
|
+
// such lines from the pass criteria, symmetric to the removed-side
|
|
284
|
+
// exemption below — and require at least one discriminating added line
|
|
285
|
+
// per file, or the added-side check is vacuous (finding 1's class).
|
|
286
|
+
let discriminatingAdded = 0;
|
|
260
287
|
for (const line of exp.added) {
|
|
261
288
|
const v = found.added.get(line);
|
|
262
289
|
if (v === undefined) terminal("unreadable-result", `no ADDED finding for line in ${path}: ${line.slice(0, 60)}`);
|
|
290
|
+
if (oldCount(path, line) > 0) {
|
|
291
|
+
exempted += 1; // colliding pre-existing line: zero signal, cannot fail a good publish
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
discriminatingAdded += 1;
|
|
263
295
|
if (v !== "PRESENT") terminal("content-mismatch", `added line ABSENT in ${path}: ${line.slice(0, 80)}`);
|
|
264
296
|
}
|
|
297
|
+
if (exp.added.length > 0 && discriminatingAdded === 0) {
|
|
298
|
+
terminal("unverifiable-content",
|
|
299
|
+
`all ${exp.added.length} added line(s) in ${path} already occur in the old tree: ` +
|
|
300
|
+
"their PRESENT findings cannot tell \"hunk landed\" from \"hunk dropped\" — " +
|
|
301
|
+
"provenance NOT stamped; human verification needed");
|
|
302
|
+
}
|
|
265
303
|
// The diff may remove the same line more than once; the old tree must
|
|
266
304
|
// account for every removal before a line counts as a collision.
|
|
267
305
|
const removedBudget = new Map();
|
|
@@ -316,7 +354,7 @@ if (!p || p.source_commit !== commit || p.task_id !== taskId || p.crew_release !
|
|
|
316
354
|
// --- 8. Terminal verified event + re-queue -----------------------------------
|
|
317
355
|
const verifiedMsg =
|
|
318
356
|
`publish: verified ${commit} (${inspectionId}) — read-back: all added lines PRESENT, all removed lines ABSENT` +
|
|
319
|
-
(exempted > 0 ? ` (${exempted} colliding
|
|
357
|
+
(exempted > 0 ? ` (${exempted} colliding line${exempted === 1 ? "" : "s"} exempted as non-discriminating)` : "") +
|
|
320
358
|
`; ${buildNote}; no supersession (HEAD=${commit}).`;
|
|
321
359
|
api("log-event", { task_id: taskId, type: "note", message: verifiedMsg.slice(0, 1000) });
|
|
322
360
|
api("update-task", { id: taskId, state: "in_progress" });
|
package/package.json
CHANGED