Files

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 classify in 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.books with checkMapping(mappingEntry()). That is output against output, so supporting: [] 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 editor refusing 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-array tree is 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)

  1. The touched files whole, each under timeout -k 10 600: library mapping, scope, supporting and survey tests, and business.test.mjs. Counts go below.
  2. M1-M8 as above.
  3. The full gate, under mos-1275 timeouts: node --test packages/library/tests/ and node --test packages/business/tests/ each under timeout -k 10 1800, and every scripts/test-*.sh under timeout -k 10 300. pipefail and tee, with skips named (test-task Docker groups).
  4. 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 refuses library-write-story-path instead of throwing a TypeError. supporting.test's string case now covers 7, null, undefined, 1n and an array.
  • E2 open: editor takes 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". Refusing value.trim() !== value would 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 asserts git log shows 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, user must be a string matching USER (/^[A-Za-z0-9_.-]{1,100}$/, the entry user check), else library-mapping-invalid "editor.user must be a plain account name". The output carries user only when given. An absent editor.user refusing library-human-missing is 288's (bubba-0009).
  • README: the editor line names user and 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 editor or 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.mjs rc=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.