# 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.