Compare commits

...
2 changed files with 193 additions and 0 deletions

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
# 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: draft design. Nothing is built or 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.
### 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 only she edits this entry (C2-04 rule M9).
## 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, and a pin under a source playground. `second/Characters` at
`:203` still checks.
- **`reader.test.mjs`:**
- R8 cases: entity missing, 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.
- **`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 (§4 Q1).
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 lookups, type form |
| X2 | `excluded()` ignores the path form | hybrid.test lookups, path form |
| X3 | `sees()` keeps its old `inside()` reading | hybrid.test type form |
| X4 | the validator accepts "both forms fit" | mapping.test |
| X5 | the validator skips the playground check | mapping.test |
| X6 | R8 compares with plain `String()` (list equals scalar) | reader.test list value |
| X7 | a version is matched by link instead of name | reader.test non-R8 cases |
| X8 | the reader drops the excluded-link R6 | reader.test |
| 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. Open questions for Sage and Filbert
- **Q1 (manifest):** the C2-04 M9 test lives in `packages/business/tests`, which the brief's file list leaves
out. I recommend adding it there, beside the existing C2-04 M9 cases, and putting the business suite on the
gate. The alternative is a bus-level `organization.set` test, which needs more setup and tests the
same writer.
- **Q2 (supporting):** the brief refuses a path under a source's playground. A source's supporting
prefix is never read by the hybrid either (`sees()` passes canon only). I recommend refusing that too,
under the same REQ-LIB-3 reason. That widens the brief by one rule, so I need a yes.
- **Q3 (R8 reaches other readers):** adding `RULES.R8` goes beyond the reader:
- `survey.mjs:70` counts problems per `RULES` key, so the survey line gains `R8 0`.
`survey.test.mjs:75` pins that line and has to change. It's in the library tests, so it's in scope.
- `mirror.mjs` `validProblem` accepts R8 through `RULES`, so a sync state written after this row
carries R8 problems.
- A copy running older code would refuse such a state. That only matters if a re-pin goes backwards.
- I'll grep `docs/case2/SURVEY-RUN.md` and the Console for R-code lists before in-review.
- **Q4 (version scope):** I count only the hybrid's canon files as versions. Playground is "what if" and
is exempt, as it is from R5 and R6. A hybrid with `books: []` therefore never gets a version R8. It
still gets the entity R8.
- **Q5 (`pin.entity` not `.md`):** a non-`.md` entity is never indexed, so R8 would report it missing at
every commit. I recommend refusing it in the validator. The brief leaves existence to the reader, but
this is a shape check, not an existence check.
## 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.
@@ -0,0 +1,22 @@
# Row 267: one existing assertion changes (mos-1272)
File: `packages/library/tests/survey.test.mjs:75`, in the test that checks the survey's last three lines
with `--mapping`.
Before:
```
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0']);
```
After:
```
'problems R1 0, R2 3, R3 0, R4 0, R5 0, R6 0, R7 0, R8 0']);
```
Why: R8 ("pinned value changed") is a new rule in `problems.mjs` `RULES`, and the survey line lists a
count for every `RULES` key (`survey.mjs:70`), so it gains `R8 0`. The fixture has no hybrid and no pin,
so the count is 0 and no other count moves.
No other existing assertion in the row changes.