# Row 267 (C2-15, #1772): hybrid universes, exclusions, pins and the crossover. Build notes Morgana, 2026-10-25. Brief: `docs/plans/2026-10-20_case2-rows.md` "C2-15 hybrid universes: exclusions, pins and the end-to-end crossover" (blob 85e6fe2e). Reviewer Filbert, gate owner Sage. Claimed at rev 4087. Branch `morgana/row267` from refactor 78b69e1e6. Design 1.4 is `agents/gosalyn/work/case2/DESIGN.md`. Status: build committed under sage-4389 and sage-4407. Mos approved the survey.test.mjs:75 change (mos-1667, read against 5b2da2203). Nothing is run. No runs before Sage's CLEAR. ## 1. What the code does today (78b69e1e6) - `mapping.mjs:119-134` checks only the shape of a constraint. `exclude` is any folder path, and `pin.entity` is any folder path with a scalar value. Nothing checks either against the sources. - `query.mjs:52-57` `sees()` reads `exclude` as a path prefix only (`inside(c.exclude, file.path)`). The type form `second/Characters` (`mapping.test.mjs:203`) passes the validator but hides nothing, because no file path starts with `second/`. - `reader.mjs:206-210` gives R6 only for a canon link into a universe that is neither the file's own nor one of its sources. A hybrid canon file that links into an excluded source path gets no problem. - `guard.mjs:91-97` lets any mix of one hybrid and its sources pass. It doesn't check exclusions. - `problems.mjs` has R1-R7 and no R8. ## 2. Design ### 2.1 Validator (`mapping.mjs`) The validator runs after every universe is read, alongside the existing sources check at `:141-146`, because it needs each source's books, playground and supporting lists. For each constraint of a hybrid: - **Type form:** the first segment is one of the hybrid's `sources`, and the rest is exactly one key of `types`. - **Path form:** the path equals, or lies inside, a book folder of one of the sources. - Both forms fit: refuse `library-mapping-invalid` ("... reads as both a source/type and a source path"). - Neither form fits: refuse `library-mapping-invalid` ("... lies inside no book of the hybrid's sources"). This covers a path in the hybrid's own books, in a non-source universe, or unmapped. - **Path form under a playground:** a path equal to or inside one of a source's playground prefixes refuses ("... lies under a playground of universes.X, which the hybrid never reads"). "Under" means the path equals the prefix without its trailing `/`, or starts with the prefix. - **`pin.entity`:** the same path-form rules apply, with no type form. An entity is one file. The stored shape stays `{ exclude }` / `{ pin }`. I don't add a `form` field. `library_universe` returns `constraints` as they are now, and the index cache digest doesn't change for existing mappings. `excluded()` works out the form again each time. It's pure and cheap. ### 2.2 `excluded(mapping, hybrid, path)` (`scope.mjs`) This returns true when any `exclude` of universe `hybrid` covers `path`: - **Type form `S/T`:** `path` starts with `${book}/${T}/` for some book of source S. - **Path form P:** `path === P` or `path` starts with `${P}/`. It returns false when `hybrid` isn't a hybrid. It reads only the path (D2). It has three callers: - `query.mjs` `sees()` replaces its own `inside(c.exclude, ...)` reading with this helper. - `reader.mjs` uses it for the excluded-link R6. - `guard.mjs` uses it for the excluded cite. `query-fixture.mjs` `EXCLUSION` (`Book One/Research`) is path form and still fits, so the existing C2-05a query tests keep their constants. ### 2.3 Reader: excluded-link R6 and R8 - **Excluded-link R6:** in the canon branch at `:206`, a hybrid file's link to a target where `excluded(mapping, file.universe, to.path)` is true gives `problem('R6', file.path, link.line)`, with the default message. A custom message isn't possible: `mirror.mjs:196-201` `validProblem` accepts a sync-state problem only when `message === RULES[code]`, so a new message would make every later sync state read refuse `library-state-invalid`. The link stays in `file.links`, as the other R-rules leave it. `sees()` already drops it from the hybrid's `links_out`. A link that is both foreign and excluded can't occur, because an excluded path always lies in a source, so each link gets at most one R6. - **R8:** a new `RULES.R8` entry reads "pinned value changed". The check runs once per commit, after the link pass, for each pin of each hybrid: - **Entity missing** from `byPath`: R8 at `entity`, with no line. This covers a file that is absent, R1, R2 or R7. - **Entity present:** R8 at `entity`, on the `field` key's line where there is one, unless the parsed front matter holds `field` as a scalar (string, number or boolean) and `String(parsed) === String(value)`. A list, null, a missing key and unparsed (R3) front matter all count as changed. Under a plain `String()` comparison, `['grey']` would equal `'grey'`, so this rule closes that hole. - **Hybrid versions:** each canon file of the hybrid whose `type` (from `classify`) equals the entity's type, and whose name matches the entity's, is a version of it. Names are the trimmed front matter `name` on both sides. When either file has no text `name`, the file names (stems) compare instead. A version that holds `field` with another value gets R8 at its own path, on that key's line. A version without `field` gets nothing, and neither does any other file that only links to the entity. - The entity's type and name come from `classify(mapping, entity)` and the entity's parsed file. When the entity is missing, its stem stands in for the name. - An entity outside any type folder (a book root file, a manuscript file, or a folder that isn't a type) has type null or `manuscript`. Its versions are then the hybrid canon files with that same type and a matching name or stem. The brief doesn't cover this case, and no special rule is added for it (Filbert N2). - R8 entries are deduplicated by path and line, so two pins or two hybrids on one missing or changed entity give one entry (Filbert N1). Survey counts and the sync state count each path and line once. ### 2.4 Guard (`guard.mjs`) After the existing span check at `:91-97`, the guard looks for a hybrid among the cited universes. If there is one, and some cite lies in one of its sources while `excluded(mapping, hybrid, c.path)` is true, the guard refuses `citation-cross-universe` ("citation N (path) is excluded from hybrid H"). A reply that cites only source paths, excluded ones included, has no hybrid among its universes, so it's unchanged. ### 2.5 Setup sheet `docs/case2/SETUP-SHEET.md` gets one section after "Supporting folders", headed "Making a crossover (hybrid) universe". It covers `kind`, `name`, `sources`, `books`, `playground`, `constraints` (`exclude` in both forms, `pin`) and `notes`, and says that the continuity role reads and cites the notes. It also says that Jason edits this entry by hand and agents can't change it (C2-04 rule M9), as the sheet already says for the rest of the entry. ## 3. Tests and mutants New files and cases, all on scratch repos under TMPDIR (never /tmp): - **`packages/library/tests/hybrid.test.mjs` (new):** - A business-file-shaped mapping (fixture `mapping.json` plus a `cross` hybrid) on a repo in her layout's shape. - `find` and `search` in `cross` return its own files plus the sources' canon, each carrying its source universe. They never return a source playground or an excluded path, and this runs once for each exclude form. - `get` keeps `links_out` into the sources and drops excluded targets. - A source universe's lookups never return a `cross` file. - `universeInfo` returns the sources, constraints and notes. - `excluded()` gets unit cases for both forms and for a non-hybrid. - **`mapping.test.mjs`:** one case per new refusal: both forms fit, neither fits, the hybrid's own book, an exclude under a source playground, a pin under a source playground, and a `pin.entity` that doesn't end in `.md` (Q5). `second/Characters` at `:203` still checks. - **`reader.test.mjs`:** - R8 cases: entity missing, two pins on a missing entity give one entry (N1), entity changed, a same-name hybrid version with no link, and a list value. - Non-R8 cases: a hybrid place file with `age: 300` that links the entity, and a different hybrid character that links it. - The excluded-link R6, once for each exclude form. - **`guard.test.mjs`:** a hybrid cite with source canon passes, and a hybrid cite beside an excluded path refuses in both forms. A cite of only an excluded source path passes as that universe's item. - **`packages/bus/tests/citation-guard.test.mjs`:** through `citationWorld` with `mapChange` adding the hybrid, the excluded-beside-hybrid send refuses and the `messages` count stays the same. - **Sync state after an excluded-link R6 (sage-4389 finding a):** one case that syncs a commit holding a hybrid file's excluded link, then reads the sync state back with `readSyncState` and finds the R6 with the default message. This shows `validProblem` (`mirror.mjs:196-201`) still accepts the state. - **`packages/bus/tests/library-lookups.test.mjs`:** one broker case, where `library.find` and `library.universe` in the hybrid return what the library tests above assert. - **C2-04 M9 rule (agents can't write `projects`), not a mutant:** a case in `packages/business/tests/library-projects-protected.test.mjs` where an organization write that adds a new hybrid universe refuses `organization-projects-protected`. This is outside the brief's "tests in library and bus" list. Sage ruled it in (Q1), so the business suite is on the run list. - **`docs/case2/SURVEY-RUN.md:98`** names the problems line as `R1 .., ..., R8 ..`. No test reads that file. - **One existing assertion changes:** `survey.test.mjs:75` gains `, R8 0`. The before and after lines are in `assertion-diff.md`, which goes to Mos before the commit (mos-1272). Mutants. Each runs only its own named tests, and the tree is restored after each one. | # | Mutation | Killed by | |---|---|---| | X1 | `excluded()` ignores the type form | hybrid.test "C2-15 excluded(): ..." and "C2-15 type form: find and search ..." | | X2 | `excluded()` ignores the path form | hybrid.test "C2-15 excluded(): ..." and "C2-15 path form: find and search ..." | | X3 | `sees()` keeps its old `inside()` reading | hybrid.test "C2-15 type form: find and search ..." | | X4 | the validator accepts "both forms fit" | mapping.test "C2-15: each exclude and pin is checked against the hybrid's sources" | | X5 | the validator skips the playground check | mapping.test "C2-15: each exclude and pin is checked against the hybrid's sources" | | X6 | R8 compares with plain `String()` (list equals scalar) | reader.test "C2-15 R8: the pinned entity reads clean ..." (list case) | | X7 | a version is matched by link instead of name | reader.test "C2-15 R8: a hybrid version of the entity ..." | | X8 | the reader drops the excluded-link R6 | reader.test "C2-15 R6: a hybrid file that links into an excluded source path ..."; sync.test "C2-15 sage-4389 (a) ..." also fails | | X9 | the guard skips the excluded check | guard.test "C2-15 a hybrid cite beside an excluded source path refuses citation-cross-universe, both forms" (the mutant run); bus citation-guard "C2-15 excluded cite beside a hybrid refuses and inserts nothing" also fails | ## 4. Rulings (sage-4389) - **Q1 yes.** The C2-04 M9 case is new and goes only in `packages/business/tests/library-projects-protected.test.mjs`. No existing assertion there changes. The business suite is on the run list. - **Q2 dropped.** The condition was a file:line showing that the hybrid's index, reader and guard never read a source's supporting prefix. Two of the three do read it: - Index: `query.mjs:55` `sees()` returns false for a source file whose scope isn't `canon`, so the hybrid's lookups never show a source's supporting file. This one holds. - Reader: `reader.mjs:206-209` checks R5 for playground targets only, and R6 skips any target in a source universe. A hybrid canon file that links into a source's supporting file gets no problem and keeps the link (`:211`). - Guard: `guard.mjs:91-97` checks the cited universes and never their scope. A reply that cites a hybrid file beside a source's supporting file passes, and so does one beside a source's playground file (`guard.mjs:87-90` only stops a playground cite marked `as: 'canon'`). Sage's ranges in sage-4399 (`reader.mjs:206-211`, `guard.mjs:87-97`) cover the same lines. Sage files a DEFERRED entry for the guard and reader gap at landing. So the validator does not refuse an exclude under a source's supporting prefix. The guard observation is outside this row's guard scope ("the excluded-path cite only"); I report it to Sage as a possible follow-up and don't change it here. - **Q3:** R8 reaches the survey and the sync state as described. `survey.test.mjs:75` goes through `assertion-diff.md`. Finding (a) adds the sync-state readback test in ยง3. `packages/library/README.md` lists R1-R7 in its problems table, so it gains an R8 row; no test reads that file. - **Q4 as written:** versions count only in the hybrid's canon. - **Q5 yes:** a `pin.entity` that doesn't end in `.md` refuses `library-mapping-invalid`, with its own test. - **Finding (a):** R6 keeps the default message, with the readback test above. ## 5. Gate Filbert approves the manifest. The library and bus suites, plus business if Q1 stands, run green on Sage's gate along with every `scripts/test-*.sh`. Hash pins on the changed files are grepped before in-review. ## 6. R2 (reviewer finding F1, written 2026-11-02T19:27:10Z) **Finding.** The GPT reviewer's F1 (batch1-b-gpt.md, P2, blocks approval): `search()` in `packages/library/src/query.mjs` built its hits without `universe`, in both shapes (the normal hit and the `excerpt_too_long` hit). The row brief (`docs/plans/2026-10-20_case2-rows.md`, C2-15 Tests paragraph) says `library_find` and `library_search` in the hybrid return its own files and its sources' canon, each with its source universe. `find` already returned `universe`; `search` did not, and the tests reduced search hits to paths, so they passed without checking it. The broker forwards the query result unchanged, so the broker reply lacked it too. F2 (the hybrid's source exception ignoring source scope in the reader and guard) is out of scope for this round; the lead files it as a follow-up. **Fix.** `visible()` filters `index.files` entries, and `find` returns `file.universe` from those same entries, so `file.universe` in `search()` is the file's source universe (`index-build.mjs:31`). Both hit shapes now carry `universe: file.universe`, placed after `scope` as in `find`. Nothing else in `query.mjs` changed. The reply-cap check in `search()` measures the hit with `universe` included, so a line within about 20 bytes of the cap now comes back marked `excerpt_too_long` where it used to return its excerpt. No test sits on that edge. **Pins found by the step 4 grep.** `query.test.mjs:178` deep-equals the exact `excerpt_too_long` hit for `harbor`; it gains `universe: 'harbor'`. No other test pins the hit shape. `packages/library/README.md` does not list search hit fields, so it is unchanged. The harness tool text for `library_search` (`packages/harness/src/tools.mjs:245`) names no hit fields, so it is unchanged, and no harness test reads search hits. No 64-hex pin names `query.mjs`, `hybrid.test.mjs`, `library-lookups.test.mjs` or `query.test.mjs`. **Tests.** - `hybrid.test.mjs`, both exclude forms: the search hits keep the existing path checks and now also assert that the hybrid's own hit (`Crossover/Places/Bridge.md`) is `cross`, a source-canon hit is its source (`harbor` for Ferry Logs in the type form, `second` for Ilse in the path form), and every hit equals the owner of its path. The helper `owner()` is shared with the existing find check. - `hybrid.test.mjs`, new: an oversized line in a source's canon, searched through the hybrid, comes back as the `excerpt_too_long` hit carrying `universe: 'harbor'`. No test reached that branch with a hybrid before. - `library-lookups.test.mjs`, the C2-15 broker case: `library.search` in the hybrid returns `[path, universe]` pairs for `Elsewhere` (harbor's Tomas, the hybrid's chapter) and `ferry` (harbor's two files), with the excluded second-world file absent. **RED.** The new assertions failed on the unfixed code: 4 of 21 tests failed (the broker case, both forms of the hybrid case, the oversized case), all on a missing `universe`. **Mutants.** Removing `universe` from the normal hit only: 3 fail (`library-lookups.test.mjs:267`, `hybrid.test.mjs:71` in both forms). Removing it from the `excerpt_too_long` hit only: 2 fail (`hybrid.test.mjs:92`, `query.test.mjs:178`). Both restored byte for byte.