Files

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

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