17 KiB
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-134checks only the shape of a constraint.excludeis any folder path, andpin.entityis any folder path with a scalar value. Nothing checks either against the sources.query.mjs:52-57sees()readsexcludeas a path prefix only (inside(c.exclude, file.path)). The type formsecond/Characters(mapping.test.mjs:203) passes the validator but hides nothing, because no file path starts withsecond/.reader.mjs:206-210gives 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-97lets any mix of one hybrid and its sources pass. It doesn't check exclusions.problems.mjshas 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 oftypes. - 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:pathstarts with${book}/${T}/for some book of source S. - Path form P:
path === Porpathstarts with${P}/.
It returns false when hybrid isn't a hybrid. It reads only the path (D2). It has three callers:
query.mjssees()replaces its owninside(c.exclude, ...)reading with this helper.reader.mjsuses it for the excluded-link R6.guard.mjsuses 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 whereexcluded(mapping, file.universe, to.path)is true givesproblem('R6', file.path, link.line), with the default message. A custom message isn't possible:mirror.mjs:196-201validProblemaccepts a sync-state problem only whenmessage === RULES[code], so a new message would make every later sync state read refuselibrary-state-invalid. The link stays infile.links, as the other R-rules leave it.sees()already drops it from the hybrid'slinks_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.R8entry 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 atentity, with no line. This covers a file that is absent, R1, R2 or R7. - Entity present: R8 at
entity, on thefieldkey's line where there is one, unless the parsed front matter holdsfieldas a scalar (string, number or boolean) andString(parsed) === String(value). A list, null, a missing key and unparsed (R3) front matter all count as changed. Under a plainString()comparison,['grey']would equal'grey', so this rule closes that hole. - Hybrid versions: each canon file of the hybrid whose
type(fromclassify) equals the entity's type, and whose name matches the entity's, is a version of it. Names are the trimmed front matternameon both sides. When either file has no textname, the file names (stems) compare instead. A version that holdsfieldwith another value gets R8 at its own path, on that key's line. A version withoutfieldgets nothing, and neither does any other file that only links to the entity.
- Entity missing from
- 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.jsonplus acrosshybrid) on a repo in her layout's shape. findandsearchincrossreturn 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.getkeepslinks_outinto the sources and drops excluded targets.- A source universe's lookups never return a
crossfile. universeInforeturns the sources, constraints and notes.excluded()gets unit cases for both forms and for a non-hybrid.
- A business-file-shaped mapping (fixture
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 apin.entitythat doesn't end in.md(Q5).second/Charactersat:203still 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: 300that 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: throughcitationWorldwithmapChangeadding the hybrid, the excluded-beside-hybrid send refuses and themessagescount 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
readSyncStateand finds the R6 with the default message. This showsvalidProblem(mirror.mjs:196-201) still accepts the state. packages/bus/tests/library-lookups.test.mjs: one broker case, wherelibrary.findandlibrary.universein the hybrid return what the library tests above assert.- C2-04 M9 rule (agents can't write
projects), not a mutant: a case inpackages/business/tests/library-projects-protected.test.mjswhere an organization write that adds a new hybrid universe refusesorganization-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:98names the problems line asR1 .., ..., R8 ... No test reads that file.- One existing assertion changes:
survey.test.mjs:75gains, R8 0. The before and after lines are inassertion-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:55sees()returns false for a source file whose scope isn'tcanon, so the hybrid's lookups never show a source's supporting file. This one holds. - Reader:
reader.mjs:206-209checks 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-97checks 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-90only stops a playground cite markedas: '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.
- Index:
- Q3: R8 reaches the survey and the sync state as described.
survey.test.mjs:75goes throughassertion-diff.md. Finding (a) adds the sync-state readback test in §3.packages/library/README.mdlists 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.entitythat doesn't end in.mdrefuseslibrary-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) iscross, a source-canon hit is its source (harborfor Ferry Logs in the type form,secondfor Ilse in the path form), and every hit equals the owner of its path. The helperowner()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 theexcerpt_too_longhit carryinguniverse: 'harbor'. No test reached that branch with a hybrid before.library-lookups.test.mjs, the C2-15 broker case:library.searchin the hybrid returns[path, universe]pairs forElsewhere(harbor's Tomas, the hybrid's chapter) andferry(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.