From f8b9c2f73ea89ca69c1f04e4f6e942f595231fbc Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 22 Oct 2026 21:47:46 -0500 Subject: [PATCH] row 292 (#1797) packet: rebase onto b23783f39 with the 0585b3213 resolution (row 203's console and federation text kept word for word, Gander's text for any other key), range-diff, the three patch-ids, C1/N3, mutants M1-M9 (unrun) and the run plan (sage-3146, sage-3148) --- agents/gizmoduck/work/queue-292/BUILD.md | 205 +++++++++++++++++++++ agents/gizmoduck/work/queue-292/mutants.py | 50 +++++ 2 files changed, 255 insertions(+) create mode 100644 agents/gizmoduck/work/queue-292/BUILD.md create mode 100644 agents/gizmoduck/work/queue-292/mutants.py diff --git a/agents/gizmoduck/work/queue-292/BUILD.md b/agents/gizmoduck/work/queue-292/BUILD.md new file mode 100644 index 000000000..0a3138f15 --- /dev/null +++ b/agents/gizmoduck/work/queue-292/BUILD.md @@ -0,0 +1,205 @@ +# Row 292 (#1797): config_checks runs the target loader on any config + +Builder Gizmoduck (sage-3146, mos-1387), reviewer Duckworth, gate owner Sage. Brief +docs/plans/2026-10-22_config-checks-any-key-brief.md. Gander built round 1 on 01983cd3f +(gander/config-checks-any-key, packet agents/gander/work/config-checks-any-key/, Duckworth static +approve duckworth-0229 with C1-C2 and N1-N4). Gander doesn't build 292; questions about round 1 go +to Gander. Nothing in this packet has run. No test, mutant or repo node ran, with one exception +recorded under "Disclosure". + +## Rebase (sage-3146 item 1, sage-3148) + +Branch gizmoduck/row292, base b23783f39 (the row 270+271 landing 7328da8ca plus its docs commit; +row 203 landed earlier). `git rebase --onto b23783f39 01983cd3f d4c796e44`. + +| commit | rebased | patch-id (stable) before | after | +|---|---|---|---| +| 0585b3213 fix(release) | 3b17c7550 | a922252a85b1317e09a625193172167b7ea39edb | 15f1a38e3c1e6c72c38b77a460b6cd4ce60c6dd4 | +| e7371025f docs (plan, packet) | a54baff21 | 8e9b78ac185a49548e94e76c83f7fea1a982038a | 8e9b78ac185a49548e94e76c83f7fea1a982038a | +| d4c796e44 docs (approval) | 9c6e39b25 | fc50b630d432ff7134e3d6c711a054d15ff1f997 | fc50b630d432ff7134e3d6c711a054d15ff1f997 | + +e7371025f and d4c796e44 keep their patch-ids. 0585b3213 changes, by the conflict below. + +### The conflict and its resolution + +Only `scripts/stack-release.sh` conflicted, in `config_checks` (now :1076-1107). Row 203 +(284d55cf2) made the old probe exit 0 on a `console` or a `federation` key and the refusal read +"holds a console or federation key". Gander's commit replaced the probe and split the refusal into a +console text and a text for any other key. The test file and docs/TOOLS.md merged without conflict. + +Resolution, per sage-3146 item 1 and sage-3148: + +1. The probe picks `console` first, then `federation`, then the first key outside the oldest set + `configVersion, environment, dataRoot, execution`. Without the federation step, a config that + holds a later key before `federation` would get Gander's text although the loader refuses + federation. +2. With no target loader, a console or federation key gets 203's reason "it has no + scripts/mosaic-config.mjs", as at b23783f39. Any other later key gets Gander's "to validate + configuration key: ". +3. The refusal for a console or federation key is 203's line, byte for byte as 284d55cf2 and + b23783f39 have it (compared by script, not by eye). Gander's text, "is refused by $TO's loader + ($first); remove the refused key first (restore the config.json backup from the window's step 1), + ...", applies only when the key is neither console nor federation. In both, `$first` is the + loader's first stderr line, which names the refused key (`unsupported configuration key: ""`). +4. The comment names 203's protection. docs/TOOLS.md gains one sentence: a console or federation key + keeps row 203's message. + +Gander's console-only text "holds a console key" (round 1 at 01983cd3f, before 203) is gone: 203's +wording covers console as well. Duckworth's N1 (console plus another refused key keeps the console +text) holds, now for federation too. + +Row 203's tests: every line of stack-release.test.mjs up to :2600 at b23783f39 is unchanged. The +rebased file only appends. KEY_MSG (:1471), the item 3 console cases (:1575-1580), the row 203 +federation test (:1585-1595) and :1607 are byte-identical to the base. No 203 test or assertion +changed, so nothing goes to Mos under mos-1272 for it. + +### Range-diff + +`git range-diff 01983cd3f..d4c796e44 b23783f39..9c6e39b25`: + +``` +1: 0585b3213 ! 1: 3b17c7550 fix(release): validate non-console config keys with target loader + @@ docs/TOOLS.md: onto the pointer's copy from the target, past any same-copy re-pi + +key. Every existing parseable config is checked; a missing file passes. A target + +without a loader accepts only the oldest top-level keys: `configVersion`, + +`environment`, `dataRoot`, and `execution`; any later key refuses. The refusal + -+names the rejected key and says to restore the config.json backup first. So a + ++names the rejected key and says to restore the config.json backup first. A + ++`console` or `federation` key keeps row 203's message, which says the config + ++holds a console or federation key. So a + window re-pins to row 192 or later before it writes the key, and a rollback past + row 192 removes the key first. + + @@ scripts/stack-release.sh: slot_problem() { + -# target's own scripts/mosaic-config.mjs validate; its output, the resolved config, is not kept. + +# Row 200 item 3 and #1797: every existing parseable config must be one the + +# target loader reads. Older copies without a loader accept only the four keys + -+# in c3d29e796's first loader. Resolved config stdout is never kept. + ++# in c3d29e796's first loader. A console or federation key (row 203) keeps its own refusal text. + ++# Resolved config stdout is never kept. + config_checks() { + - local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first + -- # 0: a console key; 1: no key, or no file; 2: the file doesn't parse, so whether it holds the key + +- # 0: a console or federation key (row 203: a pre-203 loader refuses federation too); 1: neither, or no file; 2: the file doesn't parse, so whether it holds the key + - # is unknown, and the run refuses rather than pass it (webby-0028 N2). load_config already exits on + - # such a file before anything is read or stopped, so this is reached only if the file changes after it. + + local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first extra + + # 1: missing file; 2: unreadable JSON. This also fences a change after load_config. + ++ # extra: "console" or "federation" first (row 203: a pre-203 loader refuses federation too), + ++ # then the first key outside the oldest set. + local key=0 + - node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)} + --process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' -- "$cfg" 2>/dev/null || key=$? + +-process.exit(d!==null&&typeof d==="object"&&(Object.hasOwn(d,"console")||Object.hasOwn(d,"federation"))?0:1)' -- "$cfg" 2>/dev/null || key=$? + + extra="$(node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)} + +const oldest=["configVersion","environment","dataRoot","execution"]; + +if(d===null||typeof d!=="object"||Array.isArray(d)){process.stdout.write(JSON.stringify("[configuration shape]"));process.exit(0)} + -+const k=Object.hasOwn(d,"console")?"console":Object.keys(d).find(k=>!oldest.includes(k));if(k!==undefined)process.stdout.write(JSON.stringify(k))' -- "$cfg" 2>/dev/null)" || key=$? + ++const k=Object.hasOwn(d,"console")?"console":Object.hasOwn(d,"federation")?"federation":Object.keys(d).find(k=>!oldest.includes(k));if(k!==undefined)process.stdout.write(JSON.stringify(k))' -- "$cfg" 2>/dev/null)" || key=$? + [ "$key" != 1 ] || return 0 + if [ "$key" != 0 ]; then + check_refuse config-console-key "$cfg cannot be read as JSON, so whether $TO's loader reads it is unknown; fix the config first, then run the $OP again" + @@ scripts/stack-release.sh: slot_problem() { + - if [ ! -f "$loader" ]; then out="it has no scripts/mosaic-config.mjs" + + if [ ! -f "$loader" ]; then + + [ -n "$extra" ] || return 0 + -+ if [ "$extra" = '"console"' ]; then out="it has no scripts/mosaic-config.mjs" + ++ if [ "$extra" = '"console"' ] || [ "$extra" = '"federation"' ]; then out="it has no scripts/mosaic-config.mjs" + + else out="it has no scripts/mosaic-config.mjs to validate configuration key: $extra"; fi + elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0 + fi + first="${out%%$'\n'*}" + -- check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again" + -+ if [ "$extra" = '"console"' ]; then + -+ check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again" + +- check_refuse config-console-key "$cfg holds a console or federation key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again" + ++ if [ "$extra" = '"console"' ] || [ "$extra" = '"federation"' ]; then + ++ check_refuse config-console-key "$cfg holds a console or federation key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again" + + else + + check_refuse config-console-key "$cfg is refused by $TO's loader ($first); remove the refused key first (restore the config.json backup from the window's step 1), then run the $OP again" + + fi +2: e7371025f = 2: a54baff21 docs(release): record any-key config plan and static review packet +3: d4c796e44 = 3: 9c6e39b25 docs(release): record static approval and post-compaction review conditions +``` + +## C1 and N3 (sage-3146 item 3, duckworth-0229 C1) + +Commit 4fa7d6834, test file only. + +- LOADER_OLD (stack-release-fixture.mjs:301) is today's loader with a shorter first list, so it + still reads `compactionReadBytes` and `compactionStreams` through the `.concat`. It is no pre-270 + witness. The new stand-in `PRE270` is C1's copy of today's loader with the + `.concat(["compactionReadBytes", "compactionStreams"])` taken out. That leaves + `["configVersion", "environment", "dataRoot", "execution", "console", "federation"]`, the list + the loader had at bce491715^ (01b57f48b), before bce491715 added the first key. The stand-in + asserts the replace changed something, so a reworded loader fails loudly rather than passing a + no-op stand-in. The fixture module isn't edited (not an owned file). +- New test "config-any-key: a pre-270 target refuses each row 270 key on a full activate and + rollback, naming the key". It seeds on B, activates C1 and installs PRE270 as B's loader. For + `compactionReadBytes` 1048576 and `compactionStreams` 1024 in turn, it first runs C1's own loader + `validate` on the config (exit 0, so the refusal is the target's). Then it asserts the exact + refusal for a full `activate B` and for `rollback`, using `refused()`: exit 3, the exact stderr, + no file changed, no start or stop and one refused log line. The text is ANY_CONFIG_MSG with + `mosaic-config: unsupported configuration key: ""`. Afterwards it restores the loader and + config, and the rollback to B passes. +- N3: the missing-loader test sets `compactionStreams` to 1024, not `['messages']`. +- The missing-loader test also checks 203's no-loader text word for word for `console` and for + `federation`, each placed after `compactionStreams` in the file. This covers resolution items 1 + and 2, which no 203 test reaches: 203's tests all use a target with a loader. + +## Files (manifest) + +Paths changed from b23783f39 to the candidate, with sha256 at the candidate: + +| path | b23783f39 sha256 | candidate sha256 | +|---|---|---| +| scripts/stack-release.sh | d3f52f72f2ea345118d30dc34f045f2b5b6abce41697e62f6de07d7a2f25748c | 8f535d53896fefe44525dea39169ac8df29f3d7845f83552a34ec1cd707595bb | +| packages/cli/tests/stack-release.test.mjs | 45faa5d236d4cfb5cf3c026d64d039a5f29ce8868efeed4e9428cf85a5f0067f | 5a228aee7c3811f906513809f8a78a1fe4f3a26057089ae6c87918f432a5ba65 | +| docs/TOOLS.md | 423babffc7be40a2d578ef00e97cdc49d887762e416fc20726d328d96b2a9533 | 611aeb79d603dd1af1917dcbb3db81b51ac0ae30c5b4991ea72e18bc45ee59de | + +The rest of the diff is packets: Gander's agents/gander/work/config-checks-any-key/ (eight files) +and this one (BUILD.md, mutants.py). The manifest with every path and sha256 goes to Duckworth with +the round key. + +## Mutants + +agents/gizmoduck/work/queue-292/mutants.py. Each anchor appears exactly once, and each mutated +script passes `bash -n` (checked with python3 and bash only, no node). The kill lines below come +from reading the tests. None has run. + +| id | edit | should die in | +|---|---|---| +| M1 | restore the console-only skip (brief's required mutant) | config-any-key non-console refusal; the pre-270 test; row 203 federation | +| M2 | ignore a failed target validate | config-any-key non-console refusal (exit 0, not 3) | +| M3 | no-loader target passes any later key | config-any-key missing-loader | +| M4 | malformed JSON passes as absent | config-any-key malformed JSON | +| M5 | skip the loader on an oldest-key config | config-any-key accepting loaders (no witness line) | +| M6 | probe drops the federation preference | missing-loader federation case (gets the later-key text) | +| M7 | probe drops the console preference | missing-loader console case | +| M8 | final refusal treats only console as 203's | row 203 federation test; missing-loader federation case | +| M9 | no-loader branch treats only console as 203's | missing-loader federation case | + +M1-M5 are Gander's edits (changes.json) on the resolved function. M6-M9 test the resolution. + +## Run plan (on Sage's CLEAR only, C2) + +Gate per the brief. stack-release.test.mjs whole, the cli suite whole and the ten +`scripts/test-*.sh`, then M1-M9, each on stack-release.test.mjs whole. Every mutant gets +`git checkout`, sha256 equal to the candidate and a clean tree before the next. TMPDIR from +`mktemp -d /mnt/storage/scratch/tmp/gXXX`, pipefail, tee, `timeout -k 10` everywhere. + +Limits from the run 5 receipts (gizmo-271-out/r4, 01:49Z 10-23): stack-release.test.mjs whole took +682 s (duration_ms 681648), the cli suite took 624 s, and the longest shell suite took 44 s. At 3x: +2100 s for stack-release.test.mjs, alone or as a mutant run, 2100 s for the cli suite and 900 s per +shell suite. Expect about 12 + 11 + 3 minutes for the base and about 9 x 12 minutes for the mutants, +so roughly 2.2 hours in all. Sage may prefer a shorter mutant run; this packet doesn't assume one. + +C2 (duckworth-0229, sage-3146 item 4): every B, C1 and C release now validates its copied real +loader. Any red in an old cli test is a suspect from this change. It stops the run and goes to Sage +and Mos under mos-1272, unplaced. + +## Disclosure + +During the hold (sage-3146 item 6, no repo node) I ran `node --check +packages/cli/tests/stack-release.test.mjs` once, between the rebase finish (02:43:51Z, reflog) and +the first C1 commit (02:45:41Z). It is a parse-only syntax check that lasted a few milliseconds. It printed nothing. No other node, test, queue write or https push/fetch has run. diff --git a/agents/gizmoduck/work/queue-292/mutants.py b/agents/gizmoduck/work/queue-292/mutants.py new file mode 100644 index 000000000..76a5d37fd --- /dev/null +++ b/agents/gizmoduck/work/queue-292/mutants.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# Row 292 mutants. M1-M5 are Gander's (agents/gander/work/config-checks-any-key/changes.json), with +# the same edits on the resolved config_checks; M1 restores the console-only skip (the brief's +# required mutant). M6-M9 are the resolution's: row 203's console and federation text. `apply ` +# edits the worktree and prints the touched path; each edit asserts its anchor appears exactly once. +# `check` prints each anchor's count and kill. K names the test that should fail (checked by reading, +# not run). A restore is `git checkout -- `, then sha256 against the blob in BUILD.md. +import sys +WT = "/mnt/storage/scratch/gizmo-292/" +S = "scripts/stack-release.sh" +T = "packages/cli/tests/stack-release.test.mjs" +M = { + "M1": (S, ' [ "$key" != 1 ] || return 0\n if [ "$key" != 0 ]; then', + ' [ "$key" != 1 ] || return 0\n [ "$extra" = \'"console"\' ] || return 0\n if [ "$key" != 0 ]; then'), + "M2": (S, 'elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0', + 'elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)" || true; then return 0'), + "M3": (S, ' [ -n "$extra" ] || return 0', ' return 0 # drop missing-loader later-key refusal'), + "M4": (S, ' if [ "$key" != 0 ]; then\n check_refuse config-console-key', + ' if [ "$key" = 99 ]; then\n check_refuse config-console-key'), + "M5": (S, ' if [ ! -f "$loader" ]; then', ' [ -n "$extra" ] || return 0\n if [ ! -f "$loader" ]; then'), + "M6": (S, 'Object.hasOwn(d,"federation")?"federation":', ''), + "M7": (S, 'const k=Object.hasOwn(d,"console")?"console":', 'const k='), + "M8": (S, ' if [ "$extra" = \'"console"\' ] || [ "$extra" = \'"federation"\' ]; then\n check_refuse', + ' if [ "$extra" = \'"console"\' ]; then\n check_refuse'), + "M9": (S, '[ "$extra" = \'"federation"\' ]; then out="it has no scripts/mosaic-config.mjs"', + '[ "$extra" = \'"federation-off"\' ]; then out="it has no scripts/mosaic-config.mjs"'), +} +K = { + "M1": "config-any-key: a non-console target refusal stops activate and rollback before any stop; also the pre-270 test and row 203's federation test", + "M2": "config-any-key: a non-console target refusal stops activate and rollback before any stop (exit 0, not 3)", + "M3": "config-any-key: missing-loader copies accept only the four oldest keys (compactionReadBytes passes)", + "M4": "config-any-key: missing config passes but malformed JSON refuses without calling the loader", + "M5": "config-any-key: accepting loaders run validate with the selected config (no witness line on an oldest-key config)", + "M6": "config-any-key: missing-loader copies ... (federation after compactionStreams gets the later-key text)", + "M7": "config-any-key: missing-loader copies ... (console after compactionStreams gets the later-key text)", + "M8": "row 203: a config whose federation key the target loader refuses ... (Gander's text, not KEY_MSG); also the missing-loader federation case", + "M9": "config-any-key: missing-loader copies ... (federation gets 'to validate configuration key')", +} +cmd = sys.argv[1] +if cmd == "list": + print(" ".join(M)); sys.exit(0) +if cmd == "check": + for k, (f, a, b) in M.items(): + print(k, f, open(WT + f).read().count(a), "kill:", K[k]) + sys.exit(0) +f, a, b = M[sys.argv[2]] +s = open(WT + f).read() +assert s.count(a) == 1, (sys.argv[2], s.count(a)) +open(WT + f, "w").write(s.replace(a, b)) +print(f)