Files

12 KiB

Row 283 (#1788) C2-01d design, draft 2 (Honker)

Brief: docs/plans/2026-10-20_case2-rows.md "C2-01d" at refactor 94c33a43c. Sources: the design at agents/gosalyn/work/case2/DESIGN.md b68d79663, the ruling after r11 (:207), the M12 paragraph (:300), 1.1 (:755) and 1.2 (:817). Base 94c33a43c. Reviewer Webby, gate owner Sage. Nothing is built yet. The build waits for 282 to land (sage-2906).

1. Mapping (packages/library/src/mapping.mjs)

supporting:

  • Every universe kind takes an optional supporting list: plain ['name','books','playground','supporting'], hybrid adds it to its seven keys, and a draft gets ['kind','name','playground','supporting'].
  • Each value is checked as a playground prefix is: a string ending in / whose body passes folder().
  • The output carries supporting on every universe, [] when absent, the way a draft carries books: []. classify then needs no ?? []. The business writer writes the raw document (organization.mjs:195), not this output, so no business file changes shape.
  • The prefix rules, across every entry (ruling after r11):
    • A supporting prefix holding another supporting prefix, or one listed twice, refuses library-mapping-invalid.
    • A prefix equal to one of its own entry's book folders, or holding one, refuses library-mapping-invalid.
    • A prefix inside or holding another entry's book folder refuses library-supporting-other-universe.
    • Level comes from where the prefix sits, as for the playground: inside one of its own entry's books is book level, and outside every book is universe level. A draft's prefixes are all universe level.
  • Between the two lists (my choice, Q2):
    • A supporting prefix may equal a playground prefix of the same entry. That folder is writable.
    • A playground prefix may hold a supporting prefix of the same entry. The inner folder is supporting and the rest stays playground.
    • A supporting prefix that holds a playground prefix refuses library-mapping-invalid. classify checks supporting first, so that playground prefix could never match.
    • A supporting prefix equal to, holding or inside a playground prefix of another entry refuses library-mapping-invalid. Otherwise one path would have two universes.
  • The book loop at mapping.mjs:128-140 becomes a helper that both lists call with their own code and label. The playground's messages and codes stay byte-equal, so no existing test text moves.

editor:

  • The entry takes an optional editor. When present, it is an object with exactly the keys name and email (via only()). Each value is a string that is non-empty after trim and has no control character ([\u0000-\u001f\u007f-\u009f]), < or > (webby-0169 R1: git drops them from an ident and commits under a different name, so C2-02 would never see a failure). Anything else refuses library-mapping-invalid.
  • The output carries editor: { name, email } only when present, as user and vaultPrefix do. A save with no editor refusing is C2-02's job.

2. Scope (scope.mjs)

  • classify gets a supporting loop before the playground loop (design 1.2). It returns place(...) with { scope: 'supporting', universe, book }. The book is the book folder holding the prefix, or null at universe level and in a draft. The type and folder come from the first folder under the prefix, as for the playground.
  • The .md check stays first, so a .canvas or .base under a supporting prefix stays unmapped.
  • The comment above classify names the three prefix scopes and the order.

3. supportingPath (new supporting.mjs)

supportingPath(mapping, universe, path, tree) returns path or throws LibraryError('library-write-story-path', ...). It is pure and reads no git.

  • tree is the array listTree returns (reader.mjs:26, { mode, type, oid, size, path }), so the C2-02 broker passes its listing as it is (Q3). A non-array tree throws a TypeError, because that is a caller bug, not a path class.
  • The checks run in this order, each with its own message naming the class:
    1. The path must be a string of at most 1,024 UTF-8 bytes (Buffer.byteLength).
    2. It must have no control character, using the same control class as editor (not < or >).
    3. It must have no backslash.
    4. It must not be absolute (a leading /).
    5. It must have no empty, . or .. segment. That also catches // and a trailing /.
    6. classify(mapping, path) must give scope supporting and that same universe. This one check covers an unknown universe, an unmapped path, a .canvas, .base or attachment, a canon or story path, a playground path that isn't also supporting, and another universe's prefix. The scope rule then has one source, and M1 below hits both.
    7. Every proper prefix of the path (a, a/b, ...) and the path itself must not be in tree with mode 120000 (symlink) or 160000 (gitlink).
    8. No prefix of the path, and not the path itself, may equal a tree path or a tree directory (derived from the entries' paths) after normalize('NFC').toLowerCase() on both sides, unless it equals that tree path exactly. NFC as well as case, because APFS treats NFC and NFD forms of a name as one file (webby-0169 N1). Directories are checked too, because the folders under a supporting prefix are hers to name. A path in a tree that holds both a.md and A.md refuses, which matches R7.
  • The refusal messages quote the path with JSON, except for the control-character class, whose message leaves the path out.
  • The delete rule ("A delete also needs the path to exist at the tip as a 100644 blob", M12) isn't in the brief's supportingPath list. It stays with C2-02's delete (Q4). The C2-02 brief doesn't name it (only design :308), so BUILD.md carries it to C2-02 under Handoffs (webby-0169 Q4).
  • index.mjs exports supportingPath.

4. README (packages/library/README.md)

  • Mapping: supporting and its rules, the cross-list rules, and editor.
  • Refusals: a library-supporting-other-universe row, and the new cases in the library-mapping-invalid row.
  • Scope: supporting first, then playground, then canon.
  • A new section, "Agent write paths", for supportingPath and its classes.

5. Callers of classify outside files owned (Q1 for Sage)

  • survey.mjs:65 builds { canon: 0, playground: 0, unmapped: 0 } and increments scopes[classify(...).scope]. A supporting file adds supporting: NaN, and formatSurvey (:92) never prints it. I propose a departure: add supporting: 0 and print it, so the line becomes scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6. That re-pins survey.test:73 and adds a count test. Without it, the survey reports garbage once she marks a folder. The departure covers two files, packages/library/src/survey.mjs and packages/library/tests/survey.test.mjs.
  • No change in the rest, each for a reason:
    • reader.mjs:183 skips only unmapped, so supporting files are indexed like playground files.
    • R5 (:207) flags only canon to playground, as design 1.3 R5 says. A canon link to a supporting file raises nothing.
    • render.mjs:23 gives supporting a null badge. The supporting badge belongs to C2-10b (design :1415).
    • mirror.mjs:246 refuses only unmapped. Reading supporting files by scope is C2-05c's.
    • No bus or webui source filters on scope (git grep at 94c33a43c).
  • M9 already protects the whole projects key from agent edits (library-projects-protected.test). No test is added there.

6. Tests

  • mapping.test:
    • T1: supporting at book level, at universe level, in a hybrid and in a draft. The output carries it, with [] when absent. The draft deepEqual at :48 gains supporting: [].
    • T2: one test per prefix rule.
      • These refuse library-mapping-invalid: no trailing /, a bad folder, one holding another, one listed twice, one equal to its own book, one holding its own book, one holding a playground prefix, and one overlapping another entry's playground.
      • These refuse library-supporting-other-universe: one inside another universe's book, and one holding another universe's book.
      • An equal same-entry playground passes.
    • T3: editor passes, and absent passes. It refuses library-mapping-invalid for a non-object, an array, an extra key, a missing key, an empty or blank value, a non-string, and a control character, < or > in either value.
  • scope.test, T4:
    • Supporting at book level, universe level and in a draft each give their universe and book.
    • A folder in both lists is supporting, and a playground-only folder is playground.
    • A supporting .canvas or .base is unmapped.
  • supporting.test (new):
    • T5 passes at book level, universe level, in a draft, in a both-lists folder, and at exactly 1,024 bytes.
    • T6 has one refusal per class against a fixture tree with a symlink, a gitlink and a case variant. The classes are:
      • unmapped, .canvas, .base, an attachment, canon, playground-only, another universe's supporting prefix, and an unknown universe;
      • absolute, .., ., //, a trailing /, a backslash, a C0 control and DEL;
      • a symlink folder, a gitlink folder, and a symlink at the path itself;
      • a case-variant file, a case-variant folder, and an NFD-form variant of an NFC tree name;
      • 1,025 bytes made with a multi-byte character, so bytes and characters differ.
    • Each asserts the code library-write-story-path.
  • business.test: T7 adds one assertion that validateBusinessDocument refuses library-supporting-other-universe.
  • survey.test (only if Q1 is approved): T8 counts one supporting file, and :73 is re-pinned.

7. Mutants (exact strings go in BUILD.md)

Mutant Change Killed by
M1 classify checks playground before supporting (the brief's mutant) T4 both-lists, T5 both-lists
M2 the other-universe fail for supporting is dropped T2
M3 the symlink and gitlink check is dropped T6
M4 the case check is dropped T6
M4b check 8 drops normalize('NFC') T6 NFD variant
M5 the byte length uses path.length T6 1,025 bytes
M6 the editor control-character check is dropped T3
M6b the editor < and > check is dropped T3
M7 supportingPath skips the universe match T6 other universe
M8 the supporting-holds-playground check is dropped T2

8. Gate

  • Webby approves the manifest.
  • The library and business suites and every scripts/test-*.sh are green on Sage's gate.
  • Timeouts (mos-1275): each package timeout -k 10 1800, each script timeout -k 10 300, each single file and mutant timeout -k 10 600. These come from my row 256 gate at 10:34Z.
  • Before in-review, grep packages/*/tests for 64-hex pins on the changed files and for the changed message text, then drift-check BASE..refactor.

9. Questions

  • Q1 (Sage): approve the survey.mjs and survey.test.mjs departure in section 5?
  • Q2 (Webby): the cross-list rules in section 1?
  • Q3 (Webby): tree as the listTree array?
  • Q4 (Webby): the delete's 100644 check stays with C2-02?
  • Q5 (Webby): editor refuses only blank values and control characters, as the brief says. < and > would break a git ident. I'd leave them to C2-02's commit step unless you want them refused here.

10. Draft 2 (webby-0169)

  • R1 folded: editor refuses < and > too, with T3 cases and mutant M6b.
  • Q2 accepted. BUILD.md records the tradeoff: refusing a supporting prefix that holds a playground prefix goes past the brief, but it is the fail-closed reading and uses an existing code. The cost is that she can't nest a playground under a supporting folder, though she can nest the other way.
  • Q3 accepted. Q4: carried to C2-02 under Handoffs in BUILD.md, and Sage was asked to add it to C2-02's brief.
  • N1 folded into check 8 (NFC and case), with an NFD case in T6 and mutant M4b.
  • N2: the survey.test:73 pin gains only supporting, and the count test is the only addition (mos-1330, its own commit).
  • N3: before the build, re-grep tests outside packages/library for a deepEqual of the mapping output on the build base.