Compare commits

...
Author SHA1 Message Date
jason.woltje e0c6142f3a queue: revs 3861-3885 (glomgold 309 and bradford 327 in-review moves and outcomes; sage review requests and resolve for 309 and 327; boyd 309 approve; poe and dewey 327 round 2 and 3 records; sage 327 note and done) 2026-10-24 02:23:20 -05:00
jason.woltje 42ba0f5bc0 docs: row 327 push guard landed at 68c08b4e7, gate green on its own install after canonical node_modules was found empty, canonical restored (mos-1575); session and build log entries; doc only 2026-10-24 02:19:07 -05:00
jason.woltje 68c08b4e76 docs: brief template, builders never run npm or remove files in the canonical checkout, and every gate runs its own npm ci in its worktree (mos-1575); doc only 2026-10-24 02:16:26 -05:00
jason.woltje 76f84465fd docs: size question records the collected notices for the 14 extra packages and the W3C form left for an owner decision (gyro-0019, gladstone-0027); doc only 2026-10-24 02:16:26 -05:00
jason.woltje 1bd214e382 Detect binaries despite git attributes and reject unconfirmed reviews (#1825) 2026-10-24 01:40:50 -05:00
jason.woltje 56c1845505 Fix push-check binary checks from repository subdirectories (#1825) 2026-10-24 01:40:50 -05:00
jason.woltje 72a3125a6e Add reviewed-binary push check and scratch regression suite (#1825) 2026-10-24 01:40:50 -05:00
jason.woltje 6a5e125113 docs: size question notes gyro/row310 is pushed; doc only 2026-10-24 01:39:05 -05:00
jason.woltje 529e121379 docs: size question adds the measured 209-plus-14 vendor option, 26.5 MB packed, builds offline with a cargo-pruned lock, four notice concerns (mos-1574, gyro-0016/0018, gladstone-0026); doc only 2026-10-24 01:38:59 -05:00
jason.woltje 9042754c47 docs: push-check brief amendment 1, binary detection adds a NUL scan of the committed blob so attributes can't hide a binary, and an unconfirmed queue tail fails closed (dewey-0955, row 327); doc only 2026-10-24 01:30:57 -05:00
jason.woltje c98cd41d8d docs: size question for Jason (Vikunja 618): W1 packs 17.5 MB, each W2 WASM rebuild packs 16.7 MB, offline release build 398 s, toolchain facts (mos-1572/1573); doc only 2026-10-24 01:20:38 -05:00
jason.woltje 94d9e90a6a queue: revs 3845-3859+ (sage W row re-pins 309-311, issues, owners, reviewers, briefed for 309/310; glomgold 309 and gyro 310 in-progress; sage add and brief row 327, the push check) 2026-10-24 00:59:16 -05:00
20 changed files with 2202 additions and 25 deletions

No files matched your search

+1
View File
@@ -5475,3 +5475,4 @@ remain; no live state, schema or bus policy change.
2026-10-24 | Goldie | Row265 / #1770 first slot STOP (sage-3932) | New parser14/14, rooms18/18 green; serialized bus stops at old project-runtime.test.mjs:14 import-closure pin, partial619 ok/1 not ok. New pure address.mjs import is absent from expected imports. Static trace also needs address.mjs in seat RUNNER_FILES. No old-test/product correction, no mutant/restored run, no green commit/push; old-pin and out-of-owned-files mount change sent to Sage for ruling. Audit prose missed the direct source pin despite raw grep hit; correction recorded in BUILD.md.
2026-10-24 | Goldie | Row265 / #1770 verified (mos-1567, sage-3942) | Approved correction adds solely address import pin + Row265 comment to project-runtime.test and one pure-source RUNNER_FILES mount; new block checks parser has no imports. Serialized fresh-short-TMP timeout-k10: unchanged sandbox10/10, parser15/15, rooms18/18, full bus957/957, twelve named refusal mutants killed (each Node1, driver0), restored parser15/15 + rooms18/18. All positives zero fail/skip/cancel; every mutation restored full broker SHA71b32ee4. Old first-red evidence retained, no other old-test change. Slot returned goldie-0034; Boyd packet explicitly includes mount-list review, candidate push next.
2026-10-24T07:18:53Z | Sage | Row 327 (#1825, Bradford, Poe and Dewey) LANDED 68c08b4e7. Before: refactor at 94d9e90a6; the push guard was a scratch script with no reviewed-binary check. After: scripts/push-check.sh and scripts/test-push-check.sh on refactor, 3 commits cherry-picked onto 6a5e12511 with blobs equal to the round 3 tip d858c9f8b, plus doc commits 76f84465f and 68c08b4e7. Binaries are found by numstat or a NUL in the first 8000 committed bytes, regardless of attributes; each needs its sha256 and path in an approved latest-round candidate; an unconfirmed queue tail exits 2. Reviews: Poe c31525 rev 3882, Dewey c31526 rev 3883. Gate: 1st run 06:57Z RED, not the candidate: canonical node_modules was empty from 05:45:15Z (sage-4023). Rerun with its own root npm ci GREEN 07:15:49Z: 17 package suites 0 failures (cli 986, plugins 119, webui 418), 11 scripts exit 0, push-check 6 mutants KILLED; skips are the queue canonical-root check and the row 244 live provider cases. The landed push-check.sh passed its own push (NO_BINARY, NO_ATTRIBUTION, PUSH_READY). mos-1575: gates install their own node_modules; canonical restored with npm ci (128 packages).
+162
View File
@@ -0,0 +1,162 @@
# Row 327 — reviewed binary push check (#1825)
2026-10-24 Bradford, branch bradford/row327, base b617b80f6.
## Plan and boundaries
Implement the scratch check's ordered tokens in scripts/push-check.sh, using an
embedded Node program for byte-safe git paths and SHA-256 blob hashing. Bash is
the public entry point; Node is already a repository requirement. Git commands
use explicit resolved object IDs, disable external diff/textconv, and inspect
committed bytes. No queue writes, hook changes, binary evidence, or live state.
Fetch is opt-in. Read reviewed rows with the queue package rows() API rooted in
the caller's repository; this preserves canonical/witness/replay checks.
Only the latest round counts; any changes receipt blocks that round. A matching
approval authorizes exactly the manifest path/hash, or a path changed from the
commit candidate's first parent with matching committed blob hash and mode.
Deleted binaries need no candidate. Multiple --row values form a union.
Add scratch-only integration coverage in scripts/test-push-check.sh. Reuse queue
test fixture helpers to bootstrap real scratch queues, then use queue verbs to
open rounds and record receipts. All git writes, bare remotes and queues remain
under TMPDIR. Test fail-closed ordering and five required mutants, generated as
text copies in scratch. No old tests are modified. Add the TOOLS entry.
Every run needs Sage CLEAR; run timeout -k 10. Record plain-text results here or
in this packet. After green checks, commit and push only bradford/row327 through
the repository helper, send Sage and Poe tip/base/path sha256 manifest, and move
327 in-review. Sage owns request posting and landing; Dewey reviews once there.
## Recovery and choices
Initial worktree queue next refused linked-worktree access as designed. Queue
claim is performed from the canonical checkout; production reads in the guard
retain the same refusal rather than invent a worktree override.
Requested Sage ruling on AGENTS log appends outside the brief's owned paths.
Sage sage-3989: lead handles BUILD-LOG and SESSIONS at landing. Session entry:
2026-10-24 Bradford — row 327 / #1825 — implementation and scratch verification
in this packet; outcome to be appended after acceptance.
Implementation note: commit candidates use an explicit first-parent diff (empty
tree for a root commit), with rename detection disabled, like verify-commit.
The changes-veto case has both an approval and a changes receipt; a veto mutant
would otherwise still fail for lack of approval and provide false confidence.
A receipt for another candidate cannot be produced through queue verbs: the
suite corrupts only a scratch queue and expects the production reader to refuse.
Diff color, external diff and textconv are disabled so scans inspect plain git
output. Paths are read from NUL-delimited records (including spaces/tabs).
## Verification (Sage CLEAR sage-3991)
First new-suite run red: my whitespace-path fixture attempted a manifest path
that the queue intentionally forbids. Corrected the fixture to a commit
candidate, which can contain that git path. Production queue code and old tests
remain unchanged. Raw first-run output is test-run-1.txt.
Second run green: PASS push-check 22 checks; KILLED skip-hash,
accept-changes, earlier-round, working-tree, no-row. test-run-2.txt holds the
plain-text receipt. bash -n both scripts and git diff --check passed. Runs used
TMPDIR recorded in tmpdir.txt and timeout -k 10 240; all generated binary bytes
were in disposable scratch repositories, never in this checkout or evidence.
The slot was used for only the authorized new suite and syntax/diff checks.
No claim of the lead's full scripts/test-*.sh landing gate is made here.
Final inspection after the first green run found a path-decoding concern: lossy
UTF-8 decoding can conflate distinct git filenames. Hardened NUL-delimited path
decoding to reject invalid UTF-8, and quote newline-containing paths in refusal
output to preserve one STOP line. Added scratch cases for both. Requested a new
CLEAR (bradford-0005) because the previous slot had already been released.
Final run (sage-3994 CLEAR) green on final code: PASS push-check 24 checks,
all five mutants KILLED again, bash -n both scripts and git diff --check passed.
Receipt: test-run-3.txt. The guard rejects non-UTF-8 git paths as environment
errors rather than comparing replacement characters; newline paths remain
checkable through commit candidates and refusals quote them on one line.
Acceptance for the builder stage: ordered tokens and stop behavior, explicit
base/fetch, exact blob manifest/commit checks, latest-round/veto checks, deletion,
log/FF/secrets/attribution refusals, and all required mutants are covered. Queue
reads use rows() with production validation; tests use only scratch queues.
Candidate-mismatched receipts are impossible through the queue API and are
covered by a corrupt scratch queue refusing its read. The lead's full-suite gate,
Poe first review, Dewey landing review, and adoption for Sage's next push remain
landing stages. No hooks, role policy, credentials or other seats' paths changed.
## Poe first review R1 (poe-0010) — changes
cceea407e has a blocking subdirectory bypass: ls-tree without --full-tree can
omit an existing binary outside the caller cwd, making it look deleted.
Accepted the finding. Git now anchors all invocations at the repository root,
ls-tree uses --full-tree, and diff/diff-tree use --no-relative. EXPECTED_LOG still
resolves from the original caller cwd. Added outside-cwd and nested-binary cases
under diff.relative=true: unreviewed stops, reviewed manifest/commit passes.
No old test changed. New run requires Sage CLEAR before execution.
R1 verification under sage-3999 CLEAR: test-run-4.txt records PASS 29 checks
and KILLED all five required mutants. Syntax and diff whitespace checks passed.
A second bounded run used the new suite against cceea407e's script copied under
TMPDIR: r1-regression-red.txt records the outside-cwd unreviewed binary reaching
NO_BINARY/PUSH_READY (actual exit 0, expected 1). Thus the new regression detects
the reviewed bug, while the revised guard refuses it. No code changed between
the green run and this old-candidate reproduction. All runs used timeout -k 10.
Holding claimant queue writes per sage-3999 until Boyd finishes row 309's record.
Last acknowledged row 327 state is in-progress (bradford-0010, revision 3868).
## Dewey landing review B1 (dewey-0956) — changes
Read the invited review body at /mnt/storage/scratch/tmp/dew327vWXloCT/body.md.
B1 identifies a brief-level gap: numstat follows candidate/host git attributes,
so *.bin diff makes a NUL-containing blob disappear from the binary-only set.
The production script indeed filters numstat before reading any blob, consistent
with that data flow. Proposed correction: enumerate every changed numstat path,
skip absent/non-blob entries at SHA, classify binary when numstat says binary OR
committed blob has NUL among first 8000 bytes. Read with cat-file, never worktree.
Test committed .gitattributes, info/attributes, and core.attributesFile overrides
in scratch; reject with no row, allow exact reviewed bytes; add NUL-scan mutant.
Sage owns the brief amendment and next CLEAR/queue sequencing.
N1 acknowledged: candidate-equality check is defense in depth; invalid receipt
cannot pass queue replay, so the corrupt-queue case does not isolate that check.
N2/N3 recorded as described in review, without optional secrets/reader changes.
N4 planned hardening: refuse unconfirmed durable tail notes from rows() with
exit 2, with a real scratch witness-prefix case rather than a test-only reader.
No test run or queue write started for B1/N4 before Sage CLEAR.
Sage sage-4013/4015 authorizes B1 and N4 within the brief; amendment committed on
refactor at 9042754c4 (read with git show, no branch change). Fix and tests written
before requesting CLEAR as directed. Tests cover all three attribute sources,
approved manifest success under each override, exact 7999/8000 offset boundary,
and a real prefix-witness rollback to yield an unconfirmed approval tail.
Added only the requested sixth mutant, skip-nul-scan; the original five remain.
No new production configuration knobs, no secrets-diff change, no queue writes.
Will demonstrate red against the prior candidate using the new tests, then green
against revised code, under the approved slot. Full landing gate remains Sage's.
After Dewey's round-2 CHANGES record (comment 31522, rev3876), Sage sage-4016
released only the claimant return to in-progress. Acknowledged:
`ok bradford-0017 rev 3877 row 327 in-review→in-progress`.
No other queue write before the next in-review candidate move.
Test slot remains pending Sage CLEAR after Gyro's build.
## Amendment 1 verification (sage-4017 CLEAR)
b1-regression-red.txt: new tests against 704910ca3 reproduce attributes-hidden
asset.bin reaching NO_BINARY/PUSH_READY with exit0 instead of exit1.
n4-regression-red.txt: revised code with only the N4 check removed lets the real
scratch unconfirmed approval tail reach BINARY_REVIEWED/PUSH_READY, exit0 rather
than exit2. These are expected negative controls; neither changed committed code.
Final full new-suite run is green: test-run-5.txt records PASS 38 checks and
KILLED skip-hash, accept-changes, earlier-round, working-tree, no-row, skip-nul-scan.
All runs used timeout -k 10 240 with the same scratch TMPDIR; no old tests were
changed or run. bash -n both scripts and git diff --check also passed. No source
changes followed this green run. Sage's full landing gate is still separate.
Scope in the final delta: classify all changed present blobs using numstat OR
first-8000-byte NUL; fail closed on queue unconfirmed-tail notes; regression cases
and requested sixth mutant; TOOLS entry and this plain-text packet. B1's brief
amendment is Sage's 9042754c4; this branch does not edit the brief or queue files.
@@ -0,0 +1,27 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: LOG_OK
FF_OK
NO_BINARY
SECRETS_CLEAN
NO_ATTRIBUTION
PUSH_READY ee394f31d1e52751716e2413049f41b2f3072e65..76acc948149e718091fcd3f3807257e752a6616e
0 !== 1
at Object.check (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:44:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:100:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,27 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: LOG_OK
FF_OK
BINARY_REVIEWED 1
SECRETS_CLEAN
NO_ATTRIBUTION
PUSH_READY c1a8a1e9b8c3988874a8f7a35afab7c06aea94a5..325cd3671a7bf70b9a3aee60f4446bc93594a12a
0 !== 2
at Object.check (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:44:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:121:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 2,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,27 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: LOG_OK
FF_OK
NO_BINARY
SECRETS_CLEAN
NO_ATTRIBUTION
PUSH_READY 896bdf0ef56596c67d2dc427275743c46026e832..ad9f9c86112fadafef4d4854f46bd9994fe2fe5d
0 !== 1
at Object.check (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:44:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:130:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 0,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,21 @@
node:internal/modules/run_main:107
triggerUncaughtException(
^
AssertionError [ERR_ASSERTION]: queue: candidate manifest path must be a repo-relative path: "space tab\tasset.bin"
2 !== 1
at Object.round (file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:61:12)
at file:///home/jwoltje/.t3/worktrees/mosaic-stack/bradford-row327/[eval1]:116:7
at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 2,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
Node.js v26.8.1
@@ -0,0 +1,6 @@
PASS push-check 22 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
@@ -0,0 +1,6 @@
PASS push-check 24 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
@@ -0,0 +1,6 @@
PASS push-check 29 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
@@ -0,0 +1,7 @@
PASS push-check 38 checks
KILLED skip-hash
KILLED accept-changes
KILLED earlier-round
KILLED working-tree
KILLED no-row
KILLED skip-nul-scan
@@ -0,0 +1 @@
/mnt/storage/scratch/tmp/YPrPwR
+140
View File
@@ -0,0 +1,140 @@
# Size question for Jason: W1 and W2 history cost (Vikunja 618)
Written 2026-10-24 by Sage for Mos's mos-1572 and mos-1573. Rows 309 (W1, Glomgold) and 310 (W2, Gyro)
keep building and reviewing on their seat branches. Neither lands on refactor until Jason answers.
Status: complete for Mos, 06:20Z 10-24, with the 209-plus-14 measurement added at 06:39Z (mos-1574). Every number below is measured. W2's figures come from Gyro's
scratch measurement build (gyro-0013, 06:18Z), recorded in `agents/gyro/work/queue-310/BUILD.md` section
"Full-lock measurement results (sage-3992)" on gyro/row310 (pushed, tip 1716f5a22 at 06:32Z).
## Today's repository
`git count-objects -vH` in the canonical checkout, 05:55Z 10-24: size-pack 107.30 MiB, 137,187 objects in
5 packs, 119.26 MiB loose. Every number below adds to that, permanently. Git history isn't pruned here
(invariant 8, no force-push).
## W1, the WordNet thesaurus data (row 309)
- Generated data files: 21,916,660 bytes of text (Glomgold, glomgold-0004).
- Mos's figure for the row's text is about 58 MB, which includes the source data it is generated from.
- Changed text: 58,476,260 bytes across 45 paths (WordNet 3.1 source, generated JSON, generator, tests,
evidence). No binary file.
- Packed size of the row: 17,480,317 bytes (about 16.7 MiB), from
`git rev-list --objects d16958bcf..b10711603 | git pack-objects --stdout | wc -c` on glomgold/row309
tip b1071160363d5d0204de96b06a356c9a04845287 (glomgold-0007, 05:56Z). That's about 16% on top of
today's 107.30 MiB pack.
- Rebuild cost: the data is regenerated only when the WordNet source or the generator changes, so most
W1 follow-up rows add little. A regeneration with a changed generator adds a new copy of each changed
file, compressed against the old one where git's delta search finds it.
## W2, the WordCraft source and WASM build (row 310)
- Imported source, as of gyro-0007 05:47Z: upstream WordCraft 582 files, 21,021,869 bytes (30 icon,
screenshot and macOS packaging files left out, no notice or license among them). Cargo vendor tree 209
packages, 10,499 files, 130,737,230 bytes. Total 151,759,099 bytes.
- Non-text input files: 18 upstream (4,853,036 bytes) and 74 vendor (4,168,676 bytes). Fonts, test blobs,
an MP4 and ICC profiles. No .a, .so, .dll, .wasm or .exe in the inputs. List:
`agents/gyro/work/queue-310/binary-inputs.json` on gyro/row310.
- The packaging is still being settled. The 209-package vendor tree doesn't resolve against WordCraft's
538-entry lock without build-copy manifest edits (target sections, dangling feature items, cfg
evaluation; rulings sage-3976 to sage-3984g). At 05:58Z those edits hit their stop line (gyro-0011).
Cargo refuses `glifo ^0.2.0`, locked at 0.2.0, which an optional normal dependency of the reviewed
vello_cpu 0.1.0 requires. Only glifo 0.3.0 is in the 209, and no cfg section controls that edge. The
edits so far cover 28 manifests, 121 target-dependency sections and 109 dangling feature items, all in
scratch.
- The full-lock alternative: WordCraft's lock has 538 third-party records, 534 from crates.io and 4 from
git. Louie's existing toolchain cache holds all 534 .crate archives, 85,234,501 bytes compressed
(`agents/gyro/work/queue-310/full-lock-cache-size.json`). The 209 reviewed packages are 130,737,230
bytes extracted. Extracted in scratch from that cache (gyro-0012, 06:06Z), the full lock is
614,049,226 bytes in 25,378 files, including generated checksum files. That's 329 packages beyond the
reviewed 209 (328 from crates.io, 1 from git), and about 4.7 times the 209-package tree. Every extra
package would also need a source and notice review against S0. My read: the full lock is the wrong
trade for a few hundred MB of permanent history, so the choice is between one more kind of build-copy
edit for the 209 (optional dependencies no wasm32 feature turns on) and keeping the vendor tree off
refactor altogether.
- (a) History per rebuild if the WASM stays committed: 16,720,159 bytes (about 15.9 MiB) packed. The
release build writes four files, 44,142,800 bytes raw: WASM 43,957,877, JS 164,941, HTML 1,604 and an
icon 18,378. Gyro wrote those four blobs into a scratch bare repository (git 2.55.0, `hash-object -w`,
then `pack-objects --stdout --threads 4`, no gc) and measured the pack. Receipts:
`measurement-output-pack.json` and `full-lock-release-measurement.json` in Gyro's packet. Each later
rebuild that changes the WASM adds roughly that much again, because compiled WASM deltas badly
against the previous build. Ten rebuilds would be about 160 MiB, more than today's whole pack.
- (b) Could the release build make the WASM itself, so only source is committed? Facts so far:
- `scripts/stack-copy.sh` builds nothing from source. It `git archive`s COPY_PATHS (:29-41, :72) and
runs `npm ci --no-audit --no-fund` in the copy (:74), which needs the npm registry. So a release copy
is already not offline today.
- The pinned toolchain is Rust/Cargo 1.99.1 with the wasm32-unknown-unknown target, Trunk 0.22.0,
wasm-bindgen 0.2.129 and wasm-opt 131 (W2 brief, S0). This host's PATH has Arch's rustc and cargo
1.98.0 (not the pin), no rustup, and no trunk, wasm-bindgen or wasm-opt. A release host would need
all five pinned and hash-checked. That's an install row of its own, and it touches infra on .53/.54,
which is Jason's.
- Build time: 172 to 393 s per release build in Louie's and S0's baseline runs. Gyro's offline
release build took 397.9 s from a fresh target directory at nice 19, 4 CPUs, 4 Cargo jobs and 4
Binaryen cores. A first attempt (388.8 s) started at nice 15, inherited from the harness, so it
doesn't count. That's about 6.6 minutes per activation on this host, if the release builds it.
- No network: the build ran in an isolated network namespace with no external interface, Cargo and
Trunk set offline and update checks off, and exit 0. Nothing was fetched or installed. It used the
full-lock vendor set extracted in scratch, not the 209-package tree, so it proves an offline build
from vendored source works, not that the 209 alone suffice.
- Gyro also saw that the resolver used 14 packages outside the compiled closure (14,523,104 bytes,
`measurement-resolution-extra-packages.json`). That suggests a vendor set near 209 plus those 14
might resolve without the manifest edits. Nobody has tried it. This timing isn't restricted-adapter
or CSP evidence (M1 to M14).
- Precedent: row 154's editor bundle commits the built `packages/webui/src/public/editor.js`
(758,420 bytes of text) next to its third_party source, and `editor-provenance.test.mjs` rebuilds it
with the pinned esbuild on the landing gate (`MOSAIC_REQUIRE_EDITOR_BUILD=1`). W2 committing its WASM
and proving a rebuild on the gate follows that pattern. Building in stack-release.sh would be new.
## The 209-plus-14 option, measured (mos-1574)
Mos asked for this option to be measured before Jason chooses. Everything ran in scratch and nothing is
on refactor. Gyro's packet: gyro/row310 tip 1716f5a22, `agents/gyro/work/queue-310/BUILD.md` section
"223-source measurement" (line 498).
- Size: the 223 packages are 11,201 files, 145,260,334 bytes raw, 26,538,894 bytes packed. The 14 extras
alone are 702 files, 14,523,104 bytes raw, 1,720,566 bytes packed. Compare the full lock at 614 MB.
- Resolution: the existing build-copy filter script, unchanged, gives 31 changed manifests. With it,
cargo resolves offline only after writing its own lock. Against WordCraft's pinned lock (558 entries,
sha256 06b745e4...) that lock removes 332 entries and adds or changes none (name, version, source,
checksum), so it's a strict subset. The pinned lock itself is untouched.
- Build: it builds. Gyro's offline release build with the pruned lock under --frozen/--locked exited 0
in 380.7 s (gyro-0018, 06:38Z), with the same settings as the full-lock run (nice 19, 4 jobs, isolated
netns, fresh directories, no retry used). S0's root closures matched before compiling: 214 normal and
224 normal plus build, including 15 local. Outputs: WASM 43,952,335 bytes, JS 164,941, HTML 1,604,
icon 18,378 (`measurement-223-build.json`). The WASM is 5,542 bytes smaller than the full-lock build's
and its sha256 differs (0322beb9... against c1c15050...). The two builds had different vendor sets, so
this proves nothing either way about reproducibility. A repeat build of one input set would.
- Source and notice review of the 14 (Gladstone, Codex, gladstone-0026, 06:37Z, not a verdict): all 14
archives match the pinned lock's checksums byte for byte, and the extract matches the archives except
three filtered Cargo.toml files. 10 pass. 4 have notice concerns to settle before adoption:
- cursor-icon 1.2.0 and winit 0.30.13 apply W3C license terms inside source files (src/lib.rs,
src/keyboard.rs), which the filename-based notice collector missed. They need adding to M13.
- dpi 0.1.2 is "Apache-2.0 AND MIT", not an expression on S0's list. Both texts must stay.
- spirv 0.4.0 declares Apache-2.0 but its archive has no license text, so the standard text has to be
supplied under M13.
- winit also carries four example PNGs (under 3 KB each) and a Web Worker path. Neither is in the
measured build's closure, but the binary inventory must list the PNGs and the Worker source isn't
cleared for runtime use.
- Notice follow-up (gyro-0019 at 9522d2356, Gladstone's recheck gladstone-0027, 06:44Z): the four gaps
are now collected as evidence. The W3C terms for cursor-icon and winit are in the notice bundle with
their source hashes, both dpi texts and the libm headers stay, spirv maps WordCraft's pinned
LICENSE-APACHE text explicitly, and winit's four PNGs are inventoried. Gladstone found no further
gaps. What stays open is policy: the W3C form isn't among S0's accepted licenses, and the PNGs and
the Worker source need explicit classification if adopted.
- What adopting it would mean: commit a cargo-pruned lock in place of WordCraft's (a packaging change
for S0's reviewer), the 14 packages' source, the collected notices, and an owner decision on the W3C
license form.
## What Jason is asked
1. Whether the roughly 260 MB of raw text and source (W1 plus W2's inputs) goes into permanent history,
given the packed numbers above. For W2 that now includes which vendor tree: the 209 reviewed
packages with build-copy manifest edits (blocked at glifo, would need a further kind of edit), the
209 plus the 14 the resolver needed (measured above: 145 MB raw, 26.5 MB packed, a pruned lock and
four notice fixes), or the full 538-record lock (614 MB extracted, 329 more packages to review). I
recommend 209 plus 14.
2. For W2's output, one of: commit the WASM and rebuild-check it on the landing gate (the row 154
pattern, adding (a) per rebuild), or build it during release (needs the toolchain on every release
host, adds the build time per activation, and stack-copy.sh changes).
3. Option C from sage-3978 (Git LFS or an off-tree artifact store) stays open if neither cost is
acceptable.
+1
View File
@@ -1625,3 +1625,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-24T04:29Z | Duckworth (Opus 5.5) | row 208 (193d) second review, guards only, on 4a4bef6c1 (sage-3934) | APPROVE (static), no bus change, every hidden control has a broker refusal, grant hint per-grant and self-only, N1 5 s poll (duckworth-0302) |
2026-10-24T04:27Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row265 / #1770 C2-09b rooms | Goldie owns build, Boyd reviews under sage-3915. New goldie/row265 worktree at ec86f2c84, claim rev3814; room verbs/parser/new tests prepared, no old tests changed. Sage-3932 slot: new parser14/14 and rooms18/18 passed; whole bus in progress before refusal mutants, stop rule active.
2026-10-24T04:35Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row265 / #1770 verified | First slot stopped at project-runtime import pin. mos-1567 narrowly authorizes pin and pure-source mount correction. sage-3942 rerun: sandbox10/10, parser15/15, rooms18/18, bus957/957, twelve refusal mutants killed/restored SHA71b32ee4, parser15/15 + rooms18/18 again. Slot returned goldie-0034; no further test runs. Boyd reviews RUNNER_FILES alongside rooms; own-branch push/claimant in-review next.
2026-10-24T07:18:53Z | Sage (1ef1e4f8) | Row 327 / #1825 push guard landed (mos-1572 option A, mos-1575) | Gate went red 06:57Z on an empty canonical node_modules (Glomgold's webui npm ci through a link to canonical, glomgold-0011); rerun with its own npm ci GREEN 07:15:49Z on 1bd214e38; pushed 68c08b4e7 07:18:03Z; #1825 closed c31530; canonical restored (128 packages); brief template rule 4db23a51f | landed
+25
View File
@@ -1365,3 +1365,28 @@ public sign-in smoke and a forged-loopback 401 check; rollback drops flags
before middleware. Same-uid callers can still read the file; separate service
uid is Jason's question on #385. The Authentik allowed-uid file remains an
identity input and is never reused for proof.
## `scripts/push-check.sh` — verify a prospective push
`scripts/push-check.sh SHA EXPECTED_LOG [--row N]... [--base-ref REF] [--fetch]`
checks the repository at the caller's cwd; it never pushes or writes the queue.
Git checks cover the full repository even from a subdirectory; EXPECTED_LOG is
resolved relative to the original caller. Non-UTF-8 git paths refuse with exit 2.
EXPECTED_LOG must contain exactly `git log --oneline BASE..SHA` (including its
newlines). BASE defaults to `refs/remotes/origin/refactor`; `--base-ref` overrides
it. Only `--fetch` fetches origin, before resolving BASE.
Success prints `LOG_OK`, `FF_OK`, `NO_BINARY` or `BINARY_REVIEWED n`,
`SECRETS_CLEAN`, `NO_ATTRIBUTION`, then `PUSH_READY BASE..SHA` as the last line.
A changed present blob counts as binary when numstat marks it binary or its
first 8000 committed bytes contain NUL, regardless of git attributes. Each
present binary needs its committed blob sha256 and exact path in an approved
latest-round candidate from a supplied row. A `changes` receipt blocks that round.
Commit candidates authorize only changed paths with matching bytes and mode.
Deleted binaries need no review. Queue reads retain canonical-checkout checks
and refuse unconfirmed durable tails with exit 2.
Exit 0 means ready; 1 means refusal (`STOP reason`, always the last line); 2 means
usage or environment error. A refusal never prints later success tokens.
`timeout -k 10 240 bash scripts/test-push-check.sh` exercises isolated scratch
repositories and the six required mutants; set TMPDIR to a scratch filesystem.
@@ -101,3 +101,13 @@ if git log --format=%B "$BASE..$SHA" | grep -iqE 'co-authored-by|generated with'
echo NO_ATTRIBUTION
echo "PUSH_READY $BASE..$SHA"
```
### Amendment 1 (Dewey's landing review, dewey-0955)
`git diff --numstat` follows git attributes, so a `diff` attribute (from the tree's `.gitattributes`,
`.git/info/attributes` or `core.attributesFile`) can make a binary blob count as text. Item 2 changes.
A path is binary when numstat marks it binary, or when it exists at SHA and its blob (`git cat-file
blob SHA:path`) has a NUL byte in its first 8000 bytes, git's own rule. Attributes can add binaries
but never remove them. Tests add scratch cases for each of the three attribute sources, each with no
`--row` and each expecting `STOP binary`. A sixth mutant drops the NUL scan. Also, when the queue read
returns code 0 with an unconfirmed-tail note, the check stops (fail closed), with a test.
+8
View File
@@ -66,6 +66,14 @@ that prove the claim. Or keep it off refactor, in the seat's scratch, and
cite its path and sha256 in BUILD.md (mos-1570, after row 265's 22 gzip
logs).
Builders never run npm or remove files in the canonical checkout
(`/mnt/storage/src/mosaic-stack`), only in their own worktree. Every gate,
the builder's and the lead's landing gate, runs its own
`npm ci --ignore-scripts --no-audit --no-fund` in its worktree root and in
`packages/webui`, and never links to canonical node_modules (mos-1575,
after canonical node_modules was found empty at 05:45:15Z 10-24 and row
327's gate went red on it).
### Handoffs
Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner
+4 -3
View File
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
<!-- mosaic-queue:begin -->
Generated from `docs/plans/queue.json` revision 3844 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
Generated from `docs/plans/queue.json` revision 3885 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|---|---|---|---|---|---|---|---|---|
@@ -345,8 +345,8 @@ Generated from `docs/plans/queue.json` revision 3844 by `queue render`. Do not e
| 306 | A revoked run's late bus call stops it instead of exiting 22 (295 N1) | morgana; reviewers dewey | #1813 | done | 272 done | Dewey approves the design, then the manifest; bus, harness and cli suites and every scripts/test-*.sh green on Sage's gate (sage) | `docs/plans/2026-10-23_gate-env-rows.md` § Brief: a revoked run's late bus call stops it instead of exiting 22 (295 N1) | Landed 2d0318a44 on 4874d1e83: 33d3844a0..683833205 replayed, 4 patch-ids equal (a1960f36 55fadbc3 af1a3d88 35cf81a3), range-diff all =. Dewey APPROVE c31206 rev 3596 on 683833205. access-revoke.test numstat 7 7 (mos-1463). Sage gate 16:33Z: every package suite 0 fail 0 skipped (bus 894, cli 942, plugins 119, webui 392, business 231, harness 151), every scripts/test-*.sh rc 0, M1-M3 each killed on every named file, restores equal. N1/N2 to DEFERRED next. |
| 307 | Row 270's pi compaction reading moves behind the harness kind (REQ-PLUG-2, Vikunja 598) | gizmoduck; reviewers duckworth | #1814 | done | — | Duckworth approves the design, then the manifest; bus, cli, plugins (0 failures), webui and business suites, every scripts/test-*.sh green and the mutants killed on Sage's gate (sage) | `docs/plans/2026-10-23_harness-kind-compactions.md` § Brief: row 270's pi compaction reading moves behind the harness kind (Vikunja 598) | LANDED d243f6b25: 6 row commits replayed onto 8dc132bec, every non-log patch-id equal, candidate paths = 6fdfe3790 (manifest 17c9f8d9). Duckworth APPROVE rev 3586 c31189 round 1, C2 met. Sage gate: bus, cli 0 skipped, plugins fully green incl harness.test.mjs:192, webui, business, seat, every scripts/test-*.sh; M1-M4 killed. P19 598 commented. DEFERRED: 307 N1 store-unreadable not sticky. N3 doc-only BUILD.md red list one short, next BUILD.md change. |
| 308 | PRD 1.17 Case 2 story files are DOCX (Q235 to Q237, Q239) | gosalyn; reviewers webby | #1815 | done | 291 done | Webby checks the PRD diff against DRAFT-d9.md at 4607e15b8 one instruction at a time and that nothing else changed. No new Jason question (Q239 approved the text). tasks whole, bus whole, cli stack-copy.test and every scripts/test-*.sh green on Sage's gate. Mos gets the landing commit (mos-1470). (sage) | `docs/plans/2026-10-23_prd-1-17-brief.md` § PRD 1.17 Case 2 story files are DOCX (Q235 to Q237, Q239) | Landed 30e6bb680: Gosalyn 45fe7b3ce replayed onto 7defb58e9, patch-id 4658c831 equal, range-diff =, four blobs equal. Webby r1 APPROVE c31224 rev 3606, d9 at 4607e15b8 applied word for word. Sage gate 17:19Z: every package suite 0 fail 0 skipped (tasks 100, bus 894, cli 942, plugins 119, webui 392), every scripts/test-*.sh rc 0. Mos gets the sha (mos-1470). 570/571 build rows next. |
| 309 | 571 W1: reviewed local thesaurus data (post-10-27) | unassigned | — | queued | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W1 reviewed local thesaurus data | — |
| 310 | 571 W2: WordCraft source import and reproducible restricted web build (post-10-27) | unassigned | — | queued | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W2 WordCraft source import and reproducible restricted web build | — |
| 309 | 571 W1: reviewed local thesaurus data (post-10-27) | glomgold; reviewers boyd, duckworth | #1823 | in-review, round 1, request posted | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W1 reviewed local thesaurus data | — |
| 310 | 571 W2: WordCraft source import and reproducible restricted web build (post-10-27) | gyro; reviewers filbert, gladstone | #1824 | in-progress | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W2 WordCraft source import and reproducible restricted web build | — |
| 311 | 571 W3a: bus pins and human-only editor settings (post-10-27) | unassigned | — | queued | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W3a bus pins and human-only editor settings | — |
| 312 | 571 W3b: human DOCX store and instant commits (post-10-27) | unassigned | — | queued | 311 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W3b human DOCX store and instant commits | — |
| 313 | 571 W6: owned browser bridge and editor-only route (post-10-27) | unassigned | — | queued | 309 done, 310 done, 312 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W6 owned browser bridge and editor-only route | — |
@@ -363,6 +363,7 @@ Generated from `docs/plans/queue.json` revision 3844 by `queue render`. Do not e
| 324 | Tracker optional in the business file (Q247) | goldie; reviewers duckworth | #1820 | done | — | New tests only. Absent tracker: loader and validate pass, the bus boots with no credential-file error and no @sync. Present tracker unchanged. A mutant that skips the presence check is killed. Then whole touched test files, then the full landing gate. (sage) | `docs/plans/2026-10-23_optional-tracker-brief.md` § Brief: tracker becomes optional in the business file | LANDED 7e59ba8b3 + evidence dbdfcfd52 (ff from 3569bf54f), pushed with tip dd6cb5872 00:06:58Z 10-24. Duckworth APPROVE c31470 rev 3770 (static, N1 doc only). Goldie: new 5/5 and 4/4, business.test 21/21, presence mutant killed. Sage full gate GREEN 00:05:49Z on 64ea54083: every package suite 0 fail 0 skip (business 254, cli 986, bus 924, plugins 119, webui 411), every scripts/test-*.sh rc 0. Live unchanged 45dca69f6. Run B N1 for ops-01 P2/P3 (mos-1558). |
| 325 | Case 1 business file: re-key project to stack to match .mosaic/project.json, plus a project-file test (#414 stage 2, ops-0288, mos-1563) | goldie; reviewers gladstone | #1821 | done | — | New test file and packages/business whole on CLEAR; one key mutant killed; Gladstone review; Sage full landing gate (mos-1455) (sage) | `docs/plans/2026-10-24_case1-project-key-brief.md` § Brief: re-key the Case 1 project to stack | LANDED c5f96836d (ff from 99930362e), tip 3d2569ce2. Gladstone approve c31484 rev 3788 on c5f96836d. Sage gate green 01:31Z on c5f96836d: all package suites 0 fail (business 264, cli 986), every scripts/test-*.sh rc 0. Project key is now stack, matching .mosaic/project.json; new case1-project-key test checks the real project file. Old-test line :70 under mos-1564. |
| 326 | Host run sheet: five operator gaps (B2-2 proof format, 323 N2, pre-install key check, failed-first-install retry, backup SHA256SUMS form) | goldie; reviewers gladstone | #1822 | done | — | Gladstone approves the manifest; every new claim cites its line; no test reads the sheet; Sage's full landing gate green (sage) | `docs/plans/2026-10-24_run-sheet-doc-brief.md` § Brief: five run-sheet gaps | LANDED 33f49f7d5 (cherry-pick of a1b13c60e, patch-id match, sheet e4c14516) in f58049de0 with row 208 on one full gate 04:36-04:51Z, all suites green. Gladstone c31489. #1822 closed c31507. B2-2 closes in DEFERRED. Bundle 9fc7754a. |
| 327 | Push check in the repository: scripts/push-check.sh passes a binary only when an approved review candidate lists its path and sha256, plus scripts/test-push-check.sh (#1825, mos-1572 A) | bradford; reviewers dewey, poe | #1825 | done | — | Poe gives the first Codex review, Dewey reviews the landing candidate; scripts/test-push-check.sh and every other scripts/test-*.sh green on Sage's gate; the mutants killed; Sage's next push runs through scripts/push-check.sh (sage) | `docs/plans/2026-10-24_push-guard-binary-manifest.md` § Brief: push-check.sh passes a binary only when the reviewed candidate lists its path and sha256 | LANDED 68c08b4e7 (pushed 07:18:03Z 10-24): 3 commits on 6a5e12511, blobs equal round 3 d858c9f8b; Poe c31525 rev 3882, Dewey c31526 rev 3883; gate GREEN 07:15:49Z with its own npm ci (first run red on empty canonical node_modules, sage-4023); 6 push-check mutants KILLED; next push 42ba0f5bc ran through scripts/push-check.sh --fetch, PUSH_READY; #1825 closed c31530. |
<!-- mosaic-queue:end -->
+1340 -22
View File
File diff suppressed because it is too large. Load diff
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# Check an explicit committed push range. Fetch is opt-in; never pushes.
command -v node >/dev/null 2>&1 || { echo 'STOP node unavailable'; exit 2; }
exec node --input-type=module - "$@" <<'JS'
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const stop = (reason, code = 1) => { console.log(`STOP ${reason}`); process.exit(code); };
let gitRoot = process.cwd();
const git = (...args) => {
const r = spawnSync('git', args, { cwd: gitRoot, maxBuffer: 256 * 1024 * 1024 });
if (r.error || r.status !== 0) throw new Error('git');
return r.stdout;
};
// Git paths must round-trip as UTF-8 before comparing them with queue paths.
const pathsText = bytes => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
const shownPath = path => /[\r\n]/.test(path) ? JSON.stringify(path) : path;
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
const text = (...args) => git(...args).toString('utf8').trimEnd();
const resolve = ref => text('rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`);
const argv = process.argv.slice(2);
if (argv.length < 2) stop('usage: push-check.sh SHA EXPECTED_LOG [--row N]... [--base-ref REF] [--fetch]', 2);
const [ref, expectedFile] = argv.splice(0, 2);
let baseRef = 'refs/remotes/origin/refactor', fetch = false;
const rowIds = new Set();
while (argv.length) {
const flag = argv.shift();
if (flag === '--fetch' && !fetch) fetch = true;
else if (flag === '--row' && /^[1-9][0-9]*$/.test(argv[0] ?? '') && Number.isSafeInteger(Number(argv[0]))) rowIds.add(Number(argv.shift()));
else if (flag === '--base-ref' && argv[0] && !argv[0].startsWith('-')) baseRef = argv.shift();
else stop('usage', 2);
}
let sha, base, expected;
try {
// Resolve the log against the original caller, then anchor every Git command
// at the repository root. Queue reads retain their production cwd checks.
expected = readFileSync(expectedFile);
gitRoot = text('rev-parse', '--show-toplevel');
sha = resolve(ref);
}
catch { stop('sha or expected log unavailable', 2); }
if (fetch) { try { git('fetch', '-q', 'origin'); } catch { stop('fetch', 2); } }
try { base = resolve(baseRef); } catch { stop('base ref unavailable', 2); }
try {
if (!git('log', '--oneline', `${base}..${sha}`).equals(expected)) stop('log');
console.log('LOG_OK');
try { git('merge-base', '--is-ancestor', base, sha); } catch { stop('ff'); }
console.log('FF_OK');
// --no-renames keeps -z numstat to exactly one path per record. Never parse
// quoted path output or consult working-tree bytes.
const changedPaths = pathsText(git('diff', '--no-relative', '--no-color', '--no-ext-diff', '--no-textconv', '--no-renames', '--numstat', '-z', base, sha))
.split('\0').filter(Boolean).map(record => {
const first = record.indexOf('\t'), second = record.indexOf('\t', first + 1);
return { path: record.slice(second + 1), binary: record.startsWith('-\t-\t') };
});
const tree = commit => {
const entries = new Map();
for (const entry of pathsText(git('ls-tree', '--full-tree', '-r', '-z', commit)).split('\0').filter(Boolean)) {
const tab = entry.indexOf('\t');
const [mode, type, oid] = entry.slice(0, tab).split(' ');
entries.set(entry.slice(tab + 1), { mode, type, oid });
}
return entries;
};
const have = tree(sha);
// Attributes can force a binary to appear as text in numstat. Inspect raw
// committed bytes as well, following Git's first-8000-byte NUL rule.
const present = changedPaths.filter(({ path, binary }) => {
const entry = have.get(path);
if (!entry || entry.type !== 'blob') return false;
return binary || git('cat-file', 'blob', entry.oid).subarray(0, 8000).includes(0);
}).map(({ path }) => path);
let approved = [];
if (present.length && rowIds.size) {
let rows;
try {
const root = text('rev-parse', '--show-toplevel');
const { rows: readRows } = await import(pathToFileURL(join(root, 'packages/queue/src/store.mjs')).href);
const result = readRows({ cwd: process.cwd(), env: process.env });
if (result.code !== 0) throw new Error('queue');
if (result.err.some(note => /unconfirmed tail|not confirmed durable/.test(note))) stop('queue unconfirmed tail', 2);
rows = result.rows;
for (const id of rowIds) if (!rows.some(r => r.id === id)) throw new Error('row');
} catch { stop('queue unavailable or invalid', 2); }
for (const row of rows.filter(r => rowIds.has(r.id))) {
const round = row.review?.rounds.at(-1);
if (!round || round.receipts?.some(r => r.verdict === 'changes')) continue;
if (!round.receipts?.some(r => r.verdict === 'approve' && r.candidate === round.candidate.digest)) continue;
approved.push(round.candidate);
}
}
const candidates = new Map();
for (const c of approved) {
if (c.kind !== 'commit') continue;
const candidateTree = tree(c.digest);
let parent;
try { parent = text('rev-parse', '--verify', `${c.digest}^1`); }
catch { parent = text('hash-object', '-t', 'tree', '/dev/null'); }
const changed = new Set(pathsText(git('diff-tree', '--no-relative', '-r', '--no-renames', '--name-only', '-z', parent, c.digest)).split('\0').filter(Boolean));
candidates.set(c.digest, { candidateTree, changed });
}
for (const path of present) {
const h = have.get(path);
const digest = hash(git('cat-file', 'blob', h.oid));
const matches = approved.some(c => {
if (c.kind === 'manifest') return c.text.split('\n').includes(`${digest} ${path}`);
const { candidateTree, changed } = candidates.get(c.digest);
const entry = candidateTree.get(path);
return changed.has(path) && entry?.type === 'blob' && entry.mode === h.mode && hash(git('cat-file', 'blob', entry.oid)) === digest;
});
if (!matches) stop(`binary ${shownPath(path)} ${digest} not in a reviewed candidate`);
}
console.log(present.length ? `BINARY_REVIEWED ${present.length}` : 'NO_BINARY');
const diff = git('diff', '--no-relative', '--no-color', '--no-ext-diff', '--no-textconv', base, sha).toString('utf8');
const secret = /BEGIN [A-Z ]*PRIVATE KEY|ghp_[A-Za-z0-9]{20}|sk-ant-[A-Za-z0-9-]{10}|xox[bp]-|AKIA[0-9A-Z]{16}|(token|secret|password)["' ]*[:=] *["'][A-Za-z0-9_\-]{24,}/i;
if (diff.split('\n').some(line => line.startsWith('+') && secret.test(line))) stop('secrets');
console.log('SECRETS_CLEAN');
if (/co-authored-by|generated with/i.test(text('log', '--format=%B', `${base}..${sha}`))) stop('attribution');
console.log('NO_ATTRIBUTION');
console.log(`PUSH_READY ${base}..${sha}`);
} catch { stop('git or environment error', 2); }
JS
+256
View File
@@ -0,0 +1,256 @@
#!/bin/bash
# All repositories, queue writes, remotes and mutants are disposable scratch.
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd)
exec node --input-type=module - "$ROOT" "$@" <<'JS'
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createHash } from 'node:crypto';
const root = process.argv[2];
const { scratchRepo, genesisCommitted, cli } = await import(pathToFileURL(join(root, 'packages/queue/tests/helpers.mjs')));
const target = process.argv[3] ?? join(root, 'scripts/push-check.sh');
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
const binary = Buffer.from([0, 1, 2, 3]);
const digest = sha256(binary);
const cleanups = [];
let count = 0;
function fixture() {
const repo = scratchRepo({ after: fn => cleanups.push(fn) });
genesisCommitted(repo);
const run = (args, by = 'darkwing', allow = false) => {
const r = cli(repo, args, { by });
assert.ok(r.code === 0 || allow, r.err);
return r;
};
run(['move', '6', 'blocked', '--reason', 'paused', '--op', 'block-six-0001']);
run(['move', '9', 'in-progress', '--op', 'start-nine-0001']);
run(['set', '9', 'reviewers', 'dewey,filbert', '--op', 'set-reviewers-0001'], 'sage');
// A real local bare remote and clone, with no network or origin access.
const bare = join(repo.base, 'remote.git');
repo.g('init', '--bare', '-q', bare);
repo.g('clone', '-q', bare, join(repo.base, 'clone'));
repo.g('remote', 'add', 'origin', bare);
const base = repo.g('rev-parse', 'HEAD').trim();
repo.g('update-ref', 'refs/remotes/origin/refactor', base);
const commit = (path, bytes, message = 'content') => {
if (bytes === null) repo.g('rm', '--', path);
else { writeFileSync(join(repo.root, path), bytes); repo.g('add', '--', path); }
repo.g('commit', '-q', '-m', message);
return repo.g('rev-parse', 'HEAD').trim();
};
const check = (sha, extra = [], expectedCode = 0, reason = null, script = target, cwd = repo.root) => {
const log = join(repo.base, 'expected.log');
writeFileSync(log, repo.g('log', '--oneline', `${base}..${sha}`));
const r = spawnSync('bash', [script, sha, log, ...extra], { cwd, env: repo.env, encoding: 'utf8' });
assert.equal(r.status, expectedCode, `${r.stdout}\n${r.stderr}`);
const lines = r.stdout.trim().split('\n');
if (expectedCode === 0) assert.match(lines.at(-1), /^PUSH_READY /);
else {
assert.equal(lines.filter(s => s.startsWith('STOP ')).length, 1, r.stdout);
assert.match(lines.at(-1), reason ?? /^STOP /);
assert.ok(!r.stdout.includes('PUSH_READY'), r.stdout);
}
count++;
return r.stdout;
};
let n = 0;
const round = (candidate, verdict = 'approve') => {
n++;
const r = run(['move', '9', 'in-review', '--candidate', candidate, '--op', `review-nine-000${n}`], 'darkwing', true);
// Missing dummy transport credentials fails the request, after opening a
// valid round. Recording a receipt is cooperative and needs no credential.
assert.equal(r.code, 1, r.out + r.err);
const row = JSON.parse(run(['show', '9']).out);
const c = row.review.rounds.at(-1).candidate.digest;
run(['review', 'record', '9', '--verdict', verdict, '--candidate', c, '--comment', String(n), '--op', `verdict-nine-00${n}`], 'filbert');
};
const manifest = (d = digest, path = 'asset.bin') => {
const file = join(repo.base, 'manifest.sha256');
writeFileSync(file, `${d} ${path}\n`);
return file;
};
return { repo, run, base, commit, check, round, manifest };
}
try {
{
const f = fixture();
const sha = f.commit('text.txt', 'hello\n');
assert.match(f.check(sha), /NO_BINARY\nSECRETS_CLEAN\nNO_ATTRIBUTION\nPUSH_READY /);
f.check(sha, ['--base-ref', f.base]);
f.check(sha, ['--bad-option'], 2, /^STOP usage/);
// Explicit fetch goes only to the scratch remote.
f.check(sha, ['--fetch']);
const wrong = join(f.repo.base, 'wrong.log'); writeFileSync(wrong, 'wrong\n');
const r = spawnSync('bash', [target, sha, wrong], { cwd: f.repo.root, env: f.repo.env, encoding: 'utf8' });
assert.equal(r.status, 1); assert.equal(r.stdout, 'STOP log\n'); count++;
f.repo.g('update-ref', 'refs/remotes/origin/refactor', sha);
const log = join(f.repo.base, 'empty.log'); writeFileSync(log, '');
const ff = spawnSync('bash', [target, f.base, log], { cwd: f.repo.root, env: f.repo.env, encoding: 'utf8' });
assert.equal(ff.status, 1); assert.equal(ff.stdout, 'LOG_OK\nSTOP ff\n'); count++;
}
// B1: attributes may add binary classifications, never hide NUL blobs.
for (const source of ['tree', 'info', 'config']) {
const f = fixture();
if (source === 'tree') f.commit('.gitattributes', '*.bin diff\n');
if (source === 'info') writeFileSync(join(f.repo.gitDir, 'info/attributes'), '*.bin diff\n');
if (source === 'config') {
const attributes = join(f.repo.base, 'attributes'); writeFileSync(attributes, '*.bin diff\n');
f.repo.g('config', 'core.attributesFile', attributes);
}
const sha = f.commit('asset.bin', binary);
f.check(sha, [], 1, /^STOP binary asset.bin /);
f.round(f.manifest());
assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/);
}
// The content rule includes byte 7999, and excludes byte 8000.
for (const offset of [7999, 8000]) {
const f = fixture(), bytes = Buffer.alloc(8001, 65); bytes[offset] = 0;
f.commit('.gitattributes', '*.bin diff\n');
const sha = f.commit('asset.bin', bytes);
if (offset === 7999) f.check(sha, [], 1, /^STOP binary asset.bin /);
else assert.match(f.check(sha), /NO_BINARY\n/);
}
// N4: an approval beyond the durable witness is visible, not authority.
{
const f = fixture(), sha = f.commit('asset.bin', binary);
const witness = join(f.repo.gitDir, 'mosaic-queue.head');
const durable = readFileSync(witness);
f.round(f.manifest());
writeFileSync(witness, durable);
const visible = f.run(['show', '9']);
assert.match(visible.err, /not confirmed durable/);
f.check(sha, ['--row', '9'], 2, /^STOP queue unconfirmed tail$/);
}
{
const f = fixture(), sha = f.commit('asset.bin', binary);
f.check(sha, [], 1, /^STOP binary asset.bin /);
f.round(f.manifest('0'.repeat(64)));
f.check(sha, ['--row', '9'], 1, /^STOP binary asset.bin /);
}
{
const f = fixture(), sha = f.commit('asset.bin', binary);
f.round(f.manifest());
writeFileSync(join(f.repo.root, 'asset.bin'), 'working tree differs');
assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/);
f.check(sha, [], 1, /^STOP binary asset.bin /);
const other = f.commit('asset.bin', Buffer.from([0, 8, 9]));
// The working tree is the approved version; the pushed blob isn't.
writeFileSync(join(f.repo.root, 'asset.bin'), binary);
f.check(other, ['--row', '9'], 1, /^STOP binary asset.bin /);
}
{
const f = fixture(), sha = f.commit('asset.bin', binary);
f.round(sha);
assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/);
const other = f.commit('asset.bin', Buffer.from([0, 7]));
f.check(other, ['--row', '9'], 1, /^STOP binary asset.bin /);
}
{
const f = fixture(), sha = f.commit('space tab\tasset.bin', binary);
f.round(sha);
assert.match(f.check(sha, ['--row', '9']), /BINARY_REVIEWED 1\n/);
}
{
const f = fixture(), sha = f.commit('line\nbreak.bin', binary);
f.check(sha, [], 1, /^STOP binary "line\\nbreak.bin" /);
writeFileSync(Buffer.concat([Buffer.from(f.repo.root + '/'), Buffer.from([255]), Buffer.from('.bin')]), binary);
f.repo.g('add', '-A'); f.repo.g('commit', '-q', '-m', 'invalid UTF-8 path');
f.check(f.repo.g('rev-parse', 'HEAD').trim(), [], 2, /^STOP git or environment error$/);
}
{
const f = fixture(), sha = f.commit('asset.bin', binary);
const cwd = join(f.repo.root, 'nested'); mkdirSync(cwd);
f.repo.g('config', 'diff.relative', 'true');
f.check(sha, [], 1, /^STOP binary asset.bin /, target, cwd);
f.round(f.manifest());
assert.match(f.check(sha, ['--row', '9'], 0, null, target, cwd), /BINARY_REVIEWED 1\n/);
const relativeLog = spawnSync('bash', [target, sha, '../../expected.log', '--row', '9'], { cwd, env: f.repo.env, encoding: 'utf8' });
assert.equal(relativeLog.status, 0, relativeLog.stdout + relativeLog.stderr);
assert.match(relativeLog.stdout.trim().split('\n').at(-1), /^PUSH_READY /); count++;
}
{
const f = fixture(), cwd = join(f.repo.root, 'nested'); mkdirSync(cwd);
const sha = f.commit('nested/asset.bin', binary);
f.repo.g('config', 'diff.relative', 'true');
f.check(sha, [], 1, /^STOP binary nested\/asset.bin /, target, cwd);
f.round(sha);
assert.match(f.check(sha, ['--row', '9'], 0, null, target, cwd), /BINARY_REVIEWED 1\n/);
}
{
const f = fixture();
f.commit('asset.bin', binary);
const unrelated = f.commit('text.txt', 'unrelated\n');
f.round(unrelated);
f.check(unrelated, ['--row', '9'], 1, /^STOP binary asset.bin /);
}
{
const f = fixture(), sha = f.commit('asset.bin', binary);
f.round(f.manifest(), 'changes');
const row = JSON.parse(f.run(['show', '9']).out);
f.run(['review', 'record', '9', '--verdict', 'approve', '--candidate', row.review.rounds.at(-1).candidate.digest, '--comment', '2', '--op', 'approve-dewey-0001'], 'dewey');
f.check(sha, ['--row', '9'], 1, /^STOP binary asset.bin /);
}
{
const f = fixture(), sha = f.commit('asset.bin', binary);
f.round(f.manifest());
f.run(['move', '9', 'in-progress', '--op', 'revise-nine-0001']);
f.round(f.manifest('0'.repeat(64)));
f.check(sha, ['--row', '9'], 1, /^STOP binary asset.bin /);
}
{
const f = fixture(), sha = f.commit('asset.bin', binary);
f.round(f.manifest());
// This shape cannot be recorded by the queue. Its read API must reject
// tampered receipts before the guard can treat them as authority.
const path = f.repo.queuePath;
const doc = JSON.parse(readFileSync(path));
doc.rows.find(r => r.id === 9).review.rounds.at(-1).receipts[0].candidate = '0'.repeat(64);
writeFileSync(path, JSON.stringify(doc));
f.check(sha, ['--row', '9'], 2, /^STOP queue unavailable or invalid$/);
}
{
const f = fixture(), added = f.commit('asset.bin', binary);
f.repo.g('update-ref', 'refs/remotes/origin/refactor', added);
const deleted = f.commit('asset.bin', null);
// Explicit base retains the check helper's original expected log range.
f.repo.g('update-ref', 'refs/remotes/origin/refactor', f.base);
assert.match(f.check(deleted, ['--base-ref', added], 1, /^STOP log$/), /STOP log/);
const log = join(f.repo.base, 'delete.log'); writeFileSync(log, f.repo.g('log', '--oneline', `${added}..${deleted}`));
const r = spawnSync('bash', [target, deleted, log, '--base-ref', added], { cwd: f.repo.root, env: f.repo.env, encoding: 'utf8' });
assert.equal(r.status, 0, r.stdout); assert.match(r.stdout, /NO_BINARY\n/); assert.match(r.stdout.trim().split('\n').at(-1), /^PUSH_READY /); count++;
}
for (const [message, content, reason, prefix] of [
['content', 'password="' + 'a'.repeat(30) + '"\n', 'secrets', 'LOG_OK\nFF_OK\nNO_BINARY\n'],
['Generated with example', 'hello\n', 'attribution', 'LOG_OK\nFF_OK\nNO_BINARY\nSECRETS_CLEAN\n'],
]) {
const f = fixture(), sha = f.commit('text.txt', content, message);
assert.equal(f.check(sha, [], 1), `${prefix}STOP ${reason}\n`);
}
console.log(`PASS push-check ${count} checks`);
if (!process.argv[3]) {
const dir = mkdtempSync(join(tmpdir(), 'push-check-mutants-'));
cleanups.push(() => rmSync(dir, { recursive: true, force: true }));
const source = readFileSync(target, 'utf8');
const mutants = [
['skip-hash', "c.text.split('\\n').includes(`${digest} ${path}`)", "c.text.split('\\n').some(line => line.endsWith(` ${path}`))"],
['accept-changes', "round.receipts?.some(r => r.verdict === 'changes')", 'false'],
['earlier-round', 'row.review?.rounds.at(-1)', 'row.review?.rounds.at(0)'],
['working-tree', "hash(git('cat-file', 'blob', h.oid))", 'hash(readFileSync(path))'],
['no-row', 'if (!matches) stop(', 'if (!matches && rowIds.size) stop('],
['skip-nul-scan', " || git('cat-file', 'blob', entry.oid).subarray(0, 8000).includes(0)", ''],
];
for (const [name, before, after] of mutants) {
assert.ok(source.includes(before), name);
const file = join(dir, `${name}.sh`); writeFileSync(file, source.replace(before, after));
const r = spawnSync('timeout', ['-k', '10', '120', 'bash', join(root, 'scripts/test-push-check.sh'), file], { encoding: 'utf8', maxBuffer: 4 << 20 });
assert.equal(r.status, 1, `${name} survived or infrastructure failed: ${r.status}\n${r.stdout}\n${r.stderr}`);
assert.match(r.stderr, /AssertionError/, r.stderr);
console.log(`KILLED ${name}`);
}
}
} finally { for (const fn of cleanups.reverse()) fn(); }
JS