18 KiB
Row 283 (#1788) C2-01d build notes (Honker)
Append-only. Design: DESIGN.md d2 16e809457, APPROVE webby-0172. Gate owner Sage, reviewer Webby.
2026-10-23 build on 45dca69f6 (sage-3577)
Branch honker/row283-build from origin/refactor 45dca69f6e. The old branch
honker/row283 (16e809457) held only the design, so its two commits were cherry-picked as 398314cf4
(d1) and 4f66dccde (d2). The DESIGN.md blob equals 16e809457's.
Commits:
331653b38: code, README and tests T1-T8.28918824c: the survey.test:73 re-pin, alone (mos-1330 item 4).- this file.
Nothing has run. No node, test or mutant ran during the build: Darkwing held the slot. Every count below is a plan until Sage's CLEAR.
Drift check (sage-3577)
git diff --stat 94c33a43c 45dca69f6 (design base to build base) over the paths the tests read or
hash-pin:
- No change in packages/library, packages/business/src/business.mjs or packages/business/tests/business.test.mjs.
- No test hash-pins a library file. bus library.test and library-sync.test import index.mjs, and stack-copy.test only checks that index.mjs exists.
- webui library-cli.test changed one line and doesn't read scope. The
classifyin webui reads.mjs is the business one, not the library's.
I redo the drift check on BASE..refactor before the in-review move.
N3 (webby-0169): deepEquals of the mapping output outside packages/library
git grep -n playground -- 'packages/*/tests/*' ':!packages/library/*' and the files naming
universes give two files:
- business.test.mjs:20 compares
validateBusinessDocument(...).projects.bookswithcheckMapping(mappingEntry()). That is output against output, sosupporting: []shows up on both sides. :27 compares only.playground. - library-projects-protected.test.mjs compares file bytes (:31, :32) and change paths (:58), never the mapping output.
No re-pin is needed outside library. Inside library, only the draft deepEqual at mapping.test:48 gains
supporting: [], as the design says.
Q1 departure (sage-2923) and the :73 re-pin (mos-1330 item 4)
survey.mjs:65 counts supporting: 0 and formatSurvey prints it. T8 is the only added test
(survey.test "C2-01d: with --mapping the survey counts supporting files").
mos-1330 item 4, packages/library/tests/survey.test.mjs:73, commit 28918824c:
- old:
'scopes canon 5, playground 1, unmapped 3; indexed 6', - new:
'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6',
N2: the pin gains only supporting 0. No other text on the line or in that test moved.
Q2 tradeoff (webby-0169)
A supporting prefix that holds a playground prefix refuses library-mapping-invalid. That goes past
the brief, but it is the fail-closed reading, and it uses an existing code: classify checks supporting
first, so that playground prefix could never match. The cost is that she can't nest a playground under
a supporting folder, though she can nest the other way (a playground holding a supporting folder).
Handoffs
- Q4 (webby-0169, sage-2930): the M12 delete rule ("A delete also needs the path to exist at the tip as a 100644 blob", case2 DESIGN.md :308) is not in supportingPath. It binds row 288 (C2-02) via Gitea #1793 comment 30738, with tests for a missing path and a non-100644 entry.
- A save with no
editorrefusing is C2-02's (DESIGN.md section 1).
Choices the design left open
- The book loop is now
againstBooks(owners, list, code)in mapping.mjs, called once per list. The playground's messages and codes are byte-equal to before. - A non-string path refuses
library-write-story-path("the path must be a string") as part of check 1. A non-arraytreeis a TypeError (DESIGN.md section 3). - Check 8 builds the tree's names as every path and every folder above it, keyed by
normalize('NFC').toLowerCase(). A name on the way refuses if its key holds any name other than itself, so a tree that already holds case twins refuses a write to either (R7).
Mutants (exact strings)
Each applies one replacement to the build tip. Run one at a time, and restore with git checkout --
on the one file before the next.
| Mutant | File | Replace | With | Killed by |
|---|---|---|---|---|
| M1 | src/scope.mjs | const prefix = entry.supporting.find(p => inside(p, path)); |
const prefix = entry.supporting.find(p => inside(p, path) && !entry.playground.some(q => inside(q, path))); |
scope.test "supporting comes before playground", supporting.test "passes at book level" (both-lists path) |
| M2 | src/mapping.mjs | if ((inside || holding) && owner !== id) |
if ((inside || holding) && owner !== id && list !== 'supporting') |
mapping.test "each supporting prefix rule", business.test C2-04 |
| M3 | src/supporting.mjs | for (const p of own) if (LINKS.has(modes.get(p))) |
for (const p of own) if (false && LINKS.has(modes.get(p))) |
supporting.test "symlink or gitlink" |
| M4 | src/supporting.mjs | const fold = name => name.normalize('NFC').toLowerCase(); |
const fold = name => name.normalize('NFC'); |
supporting.test "case or Unicode form" |
| M4b | src/supporting.mjs | const fold = name => name.normalize('NFC').toLowerCase(); |
const fold = name => name.toLowerCase(); |
supporting.test "case or Unicode form" (NFD case) |
| M5 | src/supporting.mjs | Buffer.byteLength(path, 'utf8') > MAX_BYTES |
path.length > MAX_BYTES |
supporting.test "1025 bytes" |
| M6 | src/mapping.mjs | const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/; |
const IDENT_BAD = /[<>]/; |
mapping.test "editor is optional" |
| M6b | src/mapping.mjs | const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/; |
const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f]/; |
mapping.test "editor is optional" |
| M7 | src/supporting.mjs | if (place.scope !== 'supporting' || place.universe !== universe) |
if (place.scope !== 'supporting') |
supporting.test "outside the universe's supporting folders" |
| M8 | src/mapping.mjs | if (sid === pid && s !== p && holds(s, p)) |
if (false && sid === pid && s !== p && holds(s, p)) |
mapping.test "each supporting prefix rule" (Workshop case) |
(The \| in the table is an escaped | in the source text.) Each mutant runs the one test file named,
plus business.test for M2, under timeout -k 10 600. A mutant passes the gate only when its run is red
on the named test and the restored file's sha256 equals the build tip's.
Gate plan (on Sage's CLEAR)
- The touched files whole, each under
timeout -k 10 600: library mapping, scope, supporting and survey tests, and business.test.mjs. Counts go below. - M1-M8 as above.
- The full gate, under mos-1275 timeouts:
node --test packages/library/tests/andnode --test packages/business/tests/each undertimeout -k 10 1800, and every scripts/test-*.sh undertimeout -k 10 300. pipefail and tee, with skips named (test-task Docker groups). - Before in-review: grep packages/*/tests for a 64-hex pin on any changed file and for the changed message text, then the drift check on BASE..refactor.
2026-10-23 Webby early look (webby-0227, static, not a verdict)
- E1: business.test.mjs is an existing test file with a 4-line addition after the playground refusal. The in-review request lists it next to survey.test:73.
- E3 fixed: supportingPath now runs the string check before
JSON.stringify, so a BigInt (or any non-string) path refuseslibrary-write-story-pathinstead of throwing a TypeError. supporting.test's string case now covers 7, null, undefined, 1n and an array. - E2 open:
editortakes surrounding whitespace, and git trims it at commit, so the saved ident and the commit ident can differ. DESIGN.md section 1 says "non-empty after trim". Refusingvalue.trim() !== valuewould change that approved rule, so it went to Sage instead of into the build.
2026-10-23 E2 departure (sage-3601)
A departure from DESIGN.md section 1 (which stays unedited). The design says each editor value is
"non-empty after trim". Sage ruled that surrounding whitespace refuses too, because git trims an
ident at commit, so a padded value would save one name and commit another. mapping.mjs now adds
if (value.trim() !== value) bad(editor.${key} must not start or end with whitespace) after the
IDENT_BAD check. trim() covers Unicode whitespace, so U+00A0 and U+3000 refuse as well. Inner
whitespace is kept ("Her Middle Name" passes).
T3 gains a case per key for leading, trailing and both-ends spaces, U+00A0 and U+3000, plus one inner whitespace pass.
M6 grows a third part:
| Mutant | File | Replace | With | Killed by |
|---|---|---|---|---|
| M6c | src/mapping.mjs | if (value.trim() !== value) bad( |
if (false && value.trim() !== value) bad( |
mapping.test "editor is optional" (padded cases) |
Fixture grep: git grep -nE "\beditor\b|\bauthor\b" -- 'packages/library/tests/*' 'packages/business/tests/*',
leaving out mapping.test.mjs, finds nothing. packages/business/tests has no fixtures directory. The
library fixture mapping.json has no editor. The positive control (vaultPrefix in mapping.json)
matches once. No fixture maps an editor or author with surrounding whitespace.
2026-10-23 E4 (webby-0230, sage-3606), same departure as E2
Claimed: queue move 283 in-progress, op honker-283-start, rev 3642, 19:10:49Z (briefed→in-progress).
Git's ident.c crud() strips any byte <= 0x20 and , : ; < > " \ ' from both ends of an ident. I
checked that with git 2.55.0: Jane, "Jane" 'Jane' Jane; Jane: Jane\ and ,Jane all
commit as Jane, while Jane. and Ja,ne are kept. mapping.mjs now takes the set from crud()
rather than from that list: IDENT_CRUD_END = /^[\u0000-\u0020,:;<>"\\']|[\u0000-\u0020,:;<>"\\']$/.
The E2 check became if (value.trim() !== value || IDENT_CRUD_END.test(value)), which refuses
library-mapping-invalid with "editor. must not start or end with whitespace or any of , : ; " ' ".
trim() still catches Unicode whitespace (U+00A0, U+3000), which crud() leaves alone.
Inner characters: IDENT_BAD (/[\u0000-\u001f\u007f-\u009f<>]/) already refuses <, >, newline,
tab and every other control anywhere in the value. T3 now pins that at the start, the end and the
middle. Inner , : ; " ' \ are kept, since git keeps them, and T3 pins that too.
Tests:
- T3 has one case per character in [space, U+00A0, U+3000,
,,:,;,",',\], at the start and at the end, for each key.<,>, newline and tab are pinned at the start, end and middle. Four accepted names have inner crud characters. - New round-trip test, "an accepted editor round-trips through a real git commit unchanged". It
commits in a scratch repo with editor
Doe, Jane Q./[email protected].as author and committer, with GIT_AUTHOR_* and GIT_COMMITTER_* stripped from the env, and assertsgit logshows exactly the saved values. I preflighted that commit with git 2.55.0 by hand: both values came back unchanged.
M6c now replaces the whole end check (this supersedes the earlier M6c row):
| Mutant | File | Replace | With | Killed by |
|---|---|---|---|---|
| M6c | src/mapping.mjs | if (value.trim() !== value || IDENT_CRUD_END.test(value)) |
if (false) |
mapping.test "editor is optional" (end cases) |
| M6d | src/mapping.mjs | if (value.trim() !== value || IDENT_CRUD_END.test(value)) |
if (value.trim() !== value) |
mapping.test "editor is optional" (, : ; " ' \ cases) |
2026-10-23 E5 (webby-0236, not blocking), fixed
The round-trip test stripped only GIT_AUTHOR_* and GIT_COMMITTER_*. With GIT_DIR, GIT_WORK_TREE or
GIT_INDEX_FILE set (inside a hook, say), GIT_DIR beats -C root and the commit would land in the
outer repo, and the test read global and system config. It now deletes GIT_DIR, GIT_WORK_TREE,
GIT_COMMON_DIR and GIT_INDEX_FILE (as queue/tests/helpers.mjs gitEnv does) and sets
GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1 (as ledger.test.mjs does). The -c user.*
ident still applies. Test only; no mutant changes.
2026-10-23 sage-2925: optional editor.user (sage-3649)
sage-2925 (recorded in ludwig/row286-r2 agents/ludwig/work/queue-286/BUILD.md, Q4) ruled that row 283
adds an optional editor.user, her Gitea login for the library-human slot, which row 288 (C2-02)
passes to humanCredential(context, {user}). It reached this row after the DESIGN.md d2 approval
(ludwig-0068, bubba-0009, sage-3649), so it departs from DESIGN.md section 1 ("exactly the keys
name and email"). DESIGN.md stays unedited.
- mapping.mjs:
only(entry.editor, ['name', 'email', 'user'], 'editor'). When present,usermust be a string matching USER (/^[A-Za-z0-9_.-]{1,100}$/, the entryusercheck), else library-mapping-invalid "editor.user must be a plain account name". The output carriesuseronly when given. An absenteditor.userrefusing library-human-missing is 288's (bubba-0009). - README: the
editorline namesuserand the end-character rule from E2/E4. - Tests: T3 (mapping.test "editor is optional") gains one accepted case (
her.login-1, kept in the output) and refused cases for '', an inner space, '@', 101 characters, 7 and null. They go in mapping.test with the other editor cases, not in the new supporting.test, which tests write paths. - No test outside mapping.test reads
editoror pins mapping.mjs or README (git grep).
| Mutant | File | Replace | With | Killed by |
|---|---|---|---|---|
| M6e | src/mapping.mjs | if (Object.hasOwn(entry.editor, 'user') && (typeof entry.editor.user !== 'string' || !USER.test(entry.editor.user))) |
if (false) |
mapping.test "editor is optional" (editor.user refusals) |
| M6f | src/mapping.mjs | ...(Object.hasOwn(entry.editor, 'user') ? { user: entry.editor.user } : {}) |
...({}) |
mapping.test "editor is optional" (her.login-1 kept) |
2026-10-23 T2 run (sage-3695 CLEAR, lane A, library and business only)
Node v26.8.1. refactor was e2b873390 (226 landed); git diff --stat 45dca69f6 e2b873390 -- packages/library packages/business is empty. TMPDIR was a mktemp -d under /mnt/storage/scratch/tmp, with pipefail.
Singles and mutants ran under timeout -k 10 600, suites under timeout -k 10 1800.
First-run red (mos-1272, placed): the first base run on 46bad7ea3 at 20:01:09Z was red in
supporting.test "malformed paths refuse by class, in order". 5 of 6 passed, with TypeError: Do not know how to serialize a BigInt. The cause was the test helper, not the product: refuses() labelled each
assertion with JSON.stringify(path), and the 1n case added for E3 can't be stringified. In c18ad3e12
the label uses util.inspect(path), a 2-line test-only change. The product's 1n refusal was never in
question: supportingPath type-checks before it stringifies (E3). The whole run below is on c18ad3e12.
Base, the touched files whole (20:01:43Z), all rc=0:
- library mapping.test 16/16, scope.test 6/6, supporting.test 6/6, survey.test 7/7;
- business business.test 21/21;
- 0 skipped, 0 cancelled, 0 todo.
Mutants (20:01:44Z). Each was applied by exact string with count 1, then restored with git checkout --.
Every restored sha256 equals HEAD's blob, and the tree was clean after.
| Mutant | Run | Result | Failing test |
|---|---|---|---|
| M1 | scope.test | rc=1, 5/6 | supporting comes before playground; a folder in both lists is supporting… |
| M1 | supporting.test | rc=1, 5/6 | a path passes at book level, universe level, in a draft and in a folder in both lists |
| M2 | mapping.test | rc=1, 15/16 | each supporting prefix rule refuses with its code |
| M2 | business.test | rc=1, 20/21 | C2-04: library mappings load without roots, remain frozen, and retain mapping refusals |
| M3 | supporting.test | rc=1, 5/6 | a symlink or gitlink on the way, or at the path itself, refuses |
| M4 | supporting.test | rc=1, 5/6 | a name that differs from a tree name only in case or Unicode form refuses… |
| M4b | supporting.test | rc=1, 5/6 | the same case/Unicode test |
| M5 | supporting.test | rc=1, 5/6 | a path of exactly 1024 bytes passes, and 1025 bytes… refuses |
| M6 | mapping.test | rc=1, 15/16 | editor is optional, kept when given, and refuses anything but a plain name and email |
| M6b | mapping.test | rc=1, 15/16 | the same editor test |
| M6c | mapping.test | rc=1, 15/16 | the same editor test |
| M6d | mapping.test | rc=1, 15/16 | the same editor test |
| M6e | mapping.test | rc=1, 15/16 | the same editor test |
| M6f | mapping.test | rc=1, 15/16 | the same editor test |
| M7 | supporting.test | rc=1, 5/6 | a path outside the universe's supporting folders refuses |
| M8 | mapping.test | rc=1, 15/16 | each supporting prefix rule refuses with its code |
14 of 14 mutants killed, each on its named test only.
Suites (20:01:47Z):
packages/library:node --test tests/*.test.mjsrc=0, 98/98, 0 skipped.packages/business:node --test tests/rc=0, 231/231, 0 skipped.
Not run here: every scripts/test-*.sh and the other packages. Those are Sage's 283 gate, after Webby's APPROVE.
2026-10-23 G1: mos-1528 OK for the mapping.test draft deepEqual (sage-3713)
This entry completes the N3 line above (BUILD.md:42, "only the draft deepEqual at mapping.test:48 gains
supporting: []"). BUILD.md is append-only, so :42 stays as written. Mos OK'd the change of that old
assertion in mos-1528. It is now at packages/library/tests/mapping.test.mjs:52, and commit 331653b38
changed it. Everything else in that commit's hunk is new test lines, not changed old ones.
- Old, mapping.test.mjs:48 at
45dca69f6:assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'] }); - New, mapping.test.mjs:52 at
b2a1f4092:assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'], supporting: [] });
The line gains only , supporting: []; nothing else in it moved. The business.test and scope.test hunks
add tests only (sage-3713: no OK needed). The other old-test change in this row is survey.test:73
(mos-1330 item 4, 28918824c). Doc only.