Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d9ee12c3c |
@@ -0,0 +1,80 @@
|
||||
# Row 268 build notes (Gizmoduck)
|
||||
|
||||
Case 2 C2-01c, playground levels. Gitea #1773, Vikunja 464. Brief: docs/plans/2026-10-20_case2-rows.md,
|
||||
"C2-01c playground levels: universe-level prefixes and draft universes" (blob a6ac627c8). The ruling is
|
||||
agents/gosalyn/work/case2/DESIGN.md "Ruling after r11: playgrounds at three levels" (:49-88 at
|
||||
db1e7732a), and it wins where the two differ. Reviewer Webby, gate owner Sage. Branch gizmoduck/row268
|
||||
from refactor db1e7732a. Line numbers are on this branch unless a commit is named.
|
||||
|
||||
## mapping.mjs
|
||||
|
||||
- `kind` (:71). An entry with a `kind` key must say `"hybrid"` or `"draft"`. Before, a plain universe
|
||||
with `kind` refused as an unknown key, and `kind: "draft"` did the same.
|
||||
- A draft (:73-86) refuses `books` with its own message before the key check, so the cause reads
|
||||
plainly. It takes `kind`, `name` and `playground`, needs one prefix or more, and comes back as
|
||||
`{ kind: 'draft', name, books: [], playground }`, frozen with the rest.
|
||||
- The prefix level check moved out of the per-entry loop to :127-140, after every entry is read,
|
||||
because it needs every entry's books. It runs after the book-holds-book, prefix-holds-prefix and
|
||||
listed-twice checks, so an overlap still reports as before. For each prefix and each book folder:
|
||||
- equal to any book folder: `library-mapping-invalid` (brief, What ships, "or equal to a book folder");
|
||||
- inside or holding another entry's book: `library-playground-other-universe`, and the message says
|
||||
which, with the book and its entry;
|
||||
- holding its own entry's book: `library-mapping-invalid`.
|
||||
A prefix inside its own book is book level, and one that touches no book is universe level. A draft
|
||||
has no books, so its prefixes are either outside every book or refused as another entry's.
|
||||
- `sources` (:117-120) now refuses any entry with a `kind`, so a draft can't be a source either. The
|
||||
message changed from "not a non-hybrid universe" to "not a plain universe". mapping.test.mjs:61 on
|
||||
db1e7732a pinned the old words, and that line moves with it.
|
||||
- `library-playground-outside-book` is gone from packages and docs/case2.
|
||||
|
||||
## scope.mjs
|
||||
|
||||
`classify` (:12-24) now runs two passes. The first finds a prefix in any entry, and the file is
|
||||
`playground` with that entry's id and `book` set to the entry's book folder that holds the prefix, or
|
||||
`null`. The second finds a book folder, and the file is `canon`. `place` (:26-32) works out `type` and
|
||||
`folder` from the first folder under the prefix or the book, as before. The validator keeps prefixes
|
||||
from overlapping, so the first pass finds at most one.
|
||||
|
||||
## Readers on this base
|
||||
|
||||
- reader.mjs:182 and :194 spread `classify`'s answer into each file record, so a universe-level or
|
||||
draft file carries `book: null`. Nothing in the reader reads `book`.
|
||||
- reader.mjs:206-209. R5 fires for a canon file linking into any playground. R6 fires when the target's
|
||||
universe differs and isn't a source. A draft can't be a source, so a link into one gives R5 and R6.
|
||||
A link into the file's own universe-level prefix gives R5 only. No code change was needed. The new
|
||||
test covers both.
|
||||
- survey.mjs:66 and mirror.mjs:246 read only `scope`. mirror.mjs:251 calls `readLibrary` and checks
|
||||
only `path`.
|
||||
- No other package calls this `classify` or reads `universes` (git grep over packages/*/src).
|
||||
- C2-05a (row 216) and C2-06 (row 217) have no candidate branch on origin at the time of writing. I
|
||||
check again before in-review.
|
||||
|
||||
## Tests
|
||||
|
||||
- mapping.test.mjs: the old outside-book test splits into the other-universe refusals (inside another
|
||||
entry's book, holding one, a draft prefix inside a book), the mapping-invalid ones (equal to a book,
|
||||
own or another's, and holding its own), universe level in a plain universe and in a hybrid with
|
||||
`books: []`, and the draft shape (frozen with `books: []`, with `books`, with none or empty
|
||||
prefixes, an extra key, and five bad `kind` values). The overlap test gains a cross-entry hold and a
|
||||
cross-entry duplicate. The hybrid test gains a draft source. The caller-supplied `fail` test now
|
||||
uses `Second World/Playground/` and sees the new code, since `Elsewhere/` is valid now.
|
||||
- scope.test.mjs, new: one case per level (book, universe in a plain universe, universe in a hybrid,
|
||||
draft), `.canvas` under each new prefix staying unmapped, prefixes first inside a book that holds a
|
||||
prefix, and the reader case for R5 and R6, with `book: null` on the read records.
|
||||
- business.test.mjs: `Elsewhere/Playground/` now loads, and `Second World/Playground/` refuses with
|
||||
`library-playground-other-universe` through `validateBusinessDocument`.
|
||||
|
||||
## Docs
|
||||
|
||||
- packages/library/README.md: the universe keys, a line on the three levels, the refusal table and
|
||||
the Scope section.
|
||||
- docs/case2/SETUP-SHEET.md "Your playground": one line per level and the draft entry.
|
||||
- docs/case2/SURVEY-RUN.md:114: the new code and message.
|
||||
|
||||
## Hash pins
|
||||
|
||||
`git grep -E '[0-9a-f]{64}'` over packages/*/tests finds no pin on any changed file.
|
||||
|
||||
## Runs
|
||||
|
||||
None yet. Every run waits for Sage's CLEAR.
|
||||
@@ -22,9 +22,15 @@ edits it by hand, and the agents can't change it. He will ask you to confirm it
|
||||
|
||||
## Your playground
|
||||
|
||||
A folder named in the `playground` list of that entry is your playground. Each playground folder sits
|
||||
inside one book folder, so one per book works well, for example `<book>/playground/`. You choose the
|
||||
names and tell Jason. Until you have one, every note in a book folder is canon.
|
||||
A folder named in the `playground` list of that entry is your playground. A playground folder can sit
|
||||
at one of three levels:
|
||||
- Book level: inside one book folder, for ideas about that book, for example `<book>/playground/`.
|
||||
- Universe level: outside every book folder, for ideas about a universe that aren't tied to one book.
|
||||
- A new universe: a draft universe entry with a name and its own playground folders and no books, for a
|
||||
world you are only starting. When it is real, Jason turns it into a plain universe with books and you
|
||||
move the notes in Obsidian.
|
||||
|
||||
You choose the names and tell Jason. Until you have one, every note in a book folder is canon.
|
||||
|
||||
What the playground means for the agents:
|
||||
- They search canon unless they ask for the playground on purpose.
|
||||
|
||||
@@ -111,7 +111,7 @@ repo, so a bad mapping gives exit 3 even when the repo path is wrong too.
|
||||
| 4 | `library survey: usage: mosaic library survey <repo> [--mapping <file>]` | no repo, two repos, an unknown flag, `--mapping` without a file or twice | Fix the command |
|
||||
| 3 | `library survey: library-mapping-invalid: <file> is not a readable JSON file` | missing, unreadable by `mosaic`, or not JSON | Fix the file or its mode |
|
||||
| 3 | `library survey: library-mapping-invalid: <file> <reason>` | the entry breaks the schema. The reason names the key | Give it to Jason for the draft |
|
||||
| 3 | `library survey: library-playground-outside-book: <file> universes.<u>.playground "<prefix>" is not inside one of its books` | a playground prefix outside its universe's books | Give it to Jason for the draft |
|
||||
| 3 | `library survey: library-playground-other-universe: <file> universes.<u>.playground "<prefix>" lies inside book folder "<book>" of universes.<v>`, or `holds book folder` | a playground prefix inside, or holding, another universe's book folder | Give it to Jason for the draft |
|
||||
| 2 | `library survey: library-git: <repo> is not a directory` | wrong path | Check `<repo>` |
|
||||
| 2 | `library survey: library-git: repository has no readable HEAD commit (project-git)` | not a repo, a folder inside one, another owner, no commits, a `HEAD` with no commit, or no `git` on `PATH` | Check each. If `<repo>` is the bus mirror, stop and report to Sage |
|
||||
| 2 | `library survey: library-git: <other reason>`, such as `tree listing refused (project-git)` | git failed or ran past 100 seconds | Report to Sage |
|
||||
|
||||
@@ -21,8 +21,12 @@ test('C2-04: library mappings load without roots, remain frozen, and retain mapp
|
||||
assert.ok(Object.isFrozen(loaded.projects.books.universes.harbor.books));
|
||||
const only = structuredClone(doc); delete only.projects.stack;
|
||||
assert.deepEqual(Object.keys(validateBusinessDocument(only, file, { rolesDir: REPO_ROLES }).projects), ['books']);
|
||||
// C2-01c: a prefix outside every book is a universe-level playground; one inside another
|
||||
// universe's book refuses.
|
||||
doc.projects.books.universes.harbor.playground = ['Elsewhere/Playground/'];
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-playground-outside-book/);
|
||||
assert.deepEqual(validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }).projects.books.universes.harbor.playground, ['Elsewhere/Playground/']);
|
||||
doc.projects.books.universes.harbor.playground = ['Second World/Playground/'];
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-playground-other-universe/);
|
||||
doc.projects.books = { ...mappingEntry(), root: '/forbidden' };
|
||||
refuses(() => validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES }), /library-mapping-invalid/);
|
||||
});
|
||||
|
||||
@@ -34,22 +34,30 @@ deep-frozen copy. `where` names the entry in messages. `fail(code, message)` def
|
||||
- `types` maps a folder name to `character`, `place`, `research`, `tracker` or `summary`.
|
||||
`manuscript` lists folder names and shares none with `types`.
|
||||
- A universe has `name`, `books` and `playground`. A hybrid adds `kind: "hybrid"`, two or more
|
||||
non-hybrid `sources`, `constraints` (each one `{ "exclude": ... }` or
|
||||
`{ "pin": { "entity", "field", "value" } }`) and `notes`.
|
||||
plain-universe `sources`, `constraints` (each one `{ "exclude": ... }` or
|
||||
`{ "pin": { "entity", "field", "value" } }`) and `notes`, and may have `books: []`. A draft
|
||||
universe has only `kind: "draft"`, `name` and one or more `playground` prefixes, and comes back as
|
||||
`{ kind: "draft", name, books: [], playground }`. `kind` is absent, `"hybrid"` or `"draft"`.
|
||||
- A playground prefix ends in `/` and has one of three levels (C2-01c). Book level lies inside one of
|
||||
its own entry's book folders. Universe level, in a plain universe or a hybrid, lies outside every
|
||||
book folder. A draft's prefixes all lie outside every book folder. No prefix holds another or is
|
||||
listed twice, across every entry.
|
||||
|
||||
Refusals:
|
||||
|
||||
| Code | When |
|
||||
|---|---|
|
||||
| `library-playground-outside-book` | a playground prefix is not strictly inside a book of its own universe |
|
||||
| `library-mapping-invalid` | any other shape fault: unknown key, bad remote or branch, a book folder in two universes, a book folder that holds another, a playground prefix that holds another, an unknown hybrid source |
|
||||
| `library-playground-other-universe` | a playground prefix lies inside, or holds, a book folder of another entry |
|
||||
| `library-mapping-invalid` | any other shape fault: unknown key, bad remote or branch, an unknown `kind`, a book folder in two universes, a book folder that holds another, a playground prefix that holds another or is listed twice, a prefix equal to a book folder or holding its own entry's book, a draft with `books` or with no prefix, a hybrid source that is unknown, a hybrid or a draft |
|
||||
|
||||
## Scope
|
||||
|
||||
`classify(mapping, path)` decides scope from the path alone. Only paths ending in lowercase `.md` are
|
||||
mapped. A path under a playground prefix is `playground`. Any other path under a book folder is
|
||||
`canon`. Everything else is `unmapped`. The type is the first folder after the book, or after the
|
||||
playground prefix: a `types` name, `manuscript`, or `null`.
|
||||
mapped. It checks prefixes first. A path under any entry's playground prefix is `playground`, with that
|
||||
entry as `universe` and the book folder holding the prefix as `book`, or `null` at universe level and
|
||||
in a draft. Any other path under a book folder is `canon`. Everything else is `unmapped`. The type is
|
||||
the first folder after the playground prefix, or after the book: a `types` name, `manuscript`, or
|
||||
`null`.
|
||||
|
||||
## Reader
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// The library project entry of a business file (design 1.1): her folders mapped onto universes,
|
||||
// books, type folders, manuscript folders and playground prefixes. A pure validator. Business file
|
||||
// books, type folders, manuscript folders and playground prefixes at three levels (ruling after r11,
|
||||
// C2-01c): inside one of the entry's books, outside every book, or in a draft universe. A pure validator. Business file
|
||||
// loading wires it in C2-04, which passes its own `fail` so the refusal reads as a business file
|
||||
// problem. Agents can't write this entry (M9, C2-04).
|
||||
import { raise } from './problems.mjs';
|
||||
@@ -10,6 +11,7 @@ const BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
||||
// C2-03a (sage-2021): the bot account sync answers git's Username prompt with. Row 199's NAME.
|
||||
const USER = /^[A-Za-z0-9_.-]{1,100}$/;
|
||||
const CONSTRAINT_KEYS = ['exclude', 'pin'];
|
||||
const KINDS = ['hybrid', 'draft'];
|
||||
|
||||
// A folder segment from her vault: spaces and capitals are fine, separators, control characters
|
||||
// and dot names are not.
|
||||
@@ -61,29 +63,29 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
|
||||
object(entry.universes, 'universes');
|
||||
if (!Object.keys(entry.universes).length) bad('universes must name at least one universe');
|
||||
const universes = {}, bookOwner = new Map(), prefixes = [];
|
||||
const universes = {}, bookOwner = new Map(), prefixes = [], prefixOwner = new Map();
|
||||
for (const [id, universe] of Object.entries(entry.universes)) {
|
||||
const name = `universes.${id}`;
|
||||
if (!ID.test(id)) bad(`universe id ${JSON.stringify(id)} is not a plain id`);
|
||||
object(universe, name);
|
||||
const hybrid = universe.kind === 'hybrid';
|
||||
only(universe, hybrid ? ['kind', 'name', 'sources', 'books', 'playground', 'constraints', 'notes'] : ['name', 'books', 'playground'], name);
|
||||
if (Object.hasOwn(universe, 'kind') && !KINDS.includes(universe.kind)) bad(`${name}.kind must be "hybrid" or "draft"`);
|
||||
const hybrid = universe.kind === 'hybrid', draft = universe.kind === 'draft';
|
||||
if (draft && Object.hasOwn(universe, 'books')) bad(`${name} is a draft and takes no books`);
|
||||
only(universe, hybrid ? ['kind', 'name', 'sources', 'books', 'playground', 'constraints', 'notes']
|
||||
: draft ? ['kind', 'name', 'playground'] : ['name', 'books', 'playground'], name);
|
||||
if (typeof universe.name !== 'string' || !universe.name.trim() || universe.name.length > 128 || /[\u0000-\u001f]/.test(universe.name))
|
||||
bad(`${name}.name must be a short display name`);
|
||||
const books = list(universe.books, `${name}.books`, folder, 'a folder path');
|
||||
if (!hybrid && !books.length) bad(`${name}.books must name at least one book folder`);
|
||||
const books = draft ? [] : list(universe.books, `${name}.books`, folder, 'a folder path');
|
||||
if (!hybrid && !draft && !books.length) bad(`${name}.books must name at least one book folder`);
|
||||
for (const book of books) {
|
||||
if (bookOwner.has(book)) bad(`book folder ${JSON.stringify(book)} is in two universes`);
|
||||
bookOwner.set(book, id);
|
||||
}
|
||||
const playground = list(universe.playground ?? [], `${name}.playground`,
|
||||
value => typeof value === 'string' && value.endsWith('/') && folder(value.slice(0, -1)), 'a folder path ending in /');
|
||||
for (const prefix of playground) {
|
||||
// Every playground file must have a universe and a book (1.1, B1).
|
||||
if (!books.some(book => holds(book, prefix) && prefix !== `${book}/`))
|
||||
fail('library-playground-outside-book', `${where} ${name}.playground ${JSON.stringify(prefix)} is not inside one of its books`);
|
||||
prefixes.push(prefix);
|
||||
}
|
||||
if (draft && !playground.length) bad(`${name}.playground must name at least one prefix`);
|
||||
for (const prefix of playground) { prefixes.push(prefix); prefixOwner.set(prefix, id); }
|
||||
if (draft) { universes[id] = { kind: 'draft', name: universe.name, books, playground }; continue; }
|
||||
const out = { name: universe.name, books, playground };
|
||||
if (hybrid) {
|
||||
out.kind = 'hybrid';
|
||||
@@ -114,14 +116,28 @@ export function checkMapping(entry, where = 'library', { fail = raise } = {}) {
|
||||
}
|
||||
for (const [id, universe] of Object.entries(universes)) {
|
||||
for (const source of universe.sources ?? []) {
|
||||
if (!Object.hasOwn(universes, source) || universes[source].kind === 'hybrid')
|
||||
bad(`universes.${id}.sources names ${JSON.stringify(source)}, which is not a non-hybrid universe`);
|
||||
if (!Object.hasOwn(universes, source) || Object.hasOwn(universes[source], 'kind'))
|
||||
bad(`universes.${id}.sources names ${JSON.stringify(source)}, which is not a plain universe`);
|
||||
}
|
||||
}
|
||||
const books = [...bookOwner.keys()];
|
||||
for (const a of books) for (const b of books) if (a !== b && holds(a, b)) bad(`book folder ${JSON.stringify(a)} holds book folder ${JSON.stringify(b)}`);
|
||||
for (const a of prefixes) for (const b of prefixes) if (a !== b && holds(a, b)) bad(`playground ${JSON.stringify(a)} holds playground ${JSON.stringify(b)}`);
|
||||
if (new Set(prefixes).size !== prefixes.length) bad('a playground prefix is listed twice');
|
||||
// A prefix's level comes from where it sits (ruling after r11). Inside one of its own entry's books
|
||||
// is book level, outside every book is universe level, and a draft's prefixes are all outside. Scope
|
||||
// checks prefixes first (scope.mjs), so a prefix equal to or holding a book would turn that book
|
||||
// into playground.
|
||||
for (const [prefix, id] of prefixOwner) {
|
||||
for (const [book, owner] of bookOwner) {
|
||||
const at = `universes.${id}.playground ${JSON.stringify(prefix)}`;
|
||||
if (prefix === `${book}/`) bad(`${at} is the whole book folder ${JSON.stringify(book)}`);
|
||||
const inside = holds(book, prefix), holding = holds(prefix, book);
|
||||
if ((inside || holding) && owner !== id)
|
||||
fail('library-playground-other-universe', `${where} ${at} ${inside ? 'lies inside' : 'holds'} book folder ${JSON.stringify(book)} of universes.${owner}`);
|
||||
if (holding) bad(`${at} holds its own book folder ${JSON.stringify(book)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return deepFreeze({
|
||||
kind: 'library', remote: entry.remote, branch: entry.branch,
|
||||
|
||||
@@ -3,19 +3,30 @@
|
||||
const inside = (folder, path) => path.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
|
||||
|
||||
// { scope: 'playground' | 'canon', universe, book, type, folder } or { scope: 'unmapped' }.
|
||||
// `type` is the mapped type of the first folder under the book (canon) or under the playground
|
||||
// prefix (playground), 'manuscript' for a manuscript folder, else null. `folder` is that folder name.
|
||||
// Prefixes come first (ruling after r11, C2-01c): a `.md` file under any entry's playground prefix is
|
||||
// playground, with that entry as `universe` and the book folder that holds the prefix as `book`, or
|
||||
// null at universe level and in a draft. Otherwise a `.md` file in a book folder is canon. `type` is
|
||||
// the mapped type of the first folder under the prefix (playground) or under the book (canon),
|
||||
// 'manuscript' for a manuscript folder, else null. `folder` is that folder name. checkMapping keeps
|
||||
// prefixes from overlapping, so at most one matches.
|
||||
export function classify(mapping, path) {
|
||||
if (typeof path !== 'string' || !path.endsWith('.md')) return { scope: 'unmapped' };
|
||||
for (const [universe, entry] of Object.entries(mapping.universes)) {
|
||||
const book = entry.books.find(b => inside(b, path));
|
||||
if (!book) continue;
|
||||
const entries = Object.entries(mapping.universes);
|
||||
for (const [universe, entry] of entries) {
|
||||
const prefix = entry.playground.find(p => inside(p, path));
|
||||
const rest = path.slice((prefix ?? `${book}/`).length).split('/');
|
||||
const folder = rest.length > 1 ? rest[0] : null;
|
||||
const type = folder === null ? null : Object.hasOwn(mapping.types, folder) ? mapping.types[folder]
|
||||
: mapping.manuscript.includes(folder) ? 'manuscript' : null;
|
||||
return { scope: prefix ? 'playground' : 'canon', universe, book, type, folder };
|
||||
if (prefix) return place(mapping, path, prefix, { scope: 'playground', universe, book: entry.books.find(b => inside(b, prefix)) ?? null });
|
||||
}
|
||||
for (const [universe, entry] of entries) {
|
||||
const book = entry.books.find(b => inside(b, path));
|
||||
if (book) return place(mapping, path, `${book}/`, { scope: 'canon', universe, book });
|
||||
}
|
||||
return { scope: 'unmapped' };
|
||||
}
|
||||
|
||||
function place(mapping, path, under, where) {
|
||||
const rest = path.slice(under.length).split('/');
|
||||
const folder = rest.length > 1 ? rest[0] : null;
|
||||
const type = folder === null ? null : Object.hasOwn(mapping.types, folder) ? mapping.types[folder]
|
||||
: mapping.manuscript.includes(folder) ? 'manuscript' : null;
|
||||
return { ...where, type, folder };
|
||||
}
|
||||
|
||||
@@ -13,16 +13,57 @@ test('the fixture mapping checks and comes back frozen', () => {
|
||||
assert.throws(() => { m.types.Extra = 'place'; });
|
||||
});
|
||||
|
||||
test('a playground prefix outside its universe books refuses library-playground-outside-book', () => {
|
||||
refuses(e => { e.universes.harbor.playground = ['Second World/Playground/']; return e; }, 'library-playground-outside-book');
|
||||
refuses(e => { e.universes.harbor.playground = ['Elsewhere/']; return e; }, 'library-playground-outside-book');
|
||||
// The whole book as its playground would leave no canon: not inside the book.
|
||||
refuses(e => { e.universes.harbor.playground = ['Book One/']; return e; }, 'library-playground-outside-book');
|
||||
const draft = (playground = ['Drafts/New World/']) => ({ kind: 'draft', name: 'New World', playground });
|
||||
|
||||
test('C2-01c: a prefix inside or holding another entry\'s book refuses library-playground-other-universe', () => {
|
||||
refuses(e => { e.universes.harbor.playground = ['Second World/Playground/']; return e; },
|
||||
'library-playground-other-universe', /universes.harbor.playground "Second World\/Playground\/" lies inside book folder "Second World" of universes.second/);
|
||||
refuses(e => { e.universes.second.books = ['Worlds/Second']; e.universes.harbor.playground = ['Worlds/']; return e; },
|
||||
'library-playground-other-universe', /"Worlds\/" holds book folder "Worlds\/Second" of universes.second/);
|
||||
// A draft has no books, so a draft prefix inside a book is always inside another entry's.
|
||||
refuses(e => { e.universes.idea = draft(['Book One/New World/']); return e; },
|
||||
'library-playground-other-universe', /universes.idea.playground "Book One\/New World\/" lies inside book folder "Book One" of universes.harbor/);
|
||||
});
|
||||
|
||||
test('C2-01c: a prefix equal to a book folder, or holding its own, refuses library-mapping-invalid', () => {
|
||||
// The whole book as its playground would leave no canon.
|
||||
refuses(e => { e.universes.harbor.playground = ['Book One/']; return e; }, 'library-mapping-invalid', /is the whole book folder "Book One"/);
|
||||
refuses(e => { e.universes.harbor.playground = ['Second World/']; return e; }, 'library-mapping-invalid', /is the whole book folder "Second World"/);
|
||||
refuses(e => { e.universes.harbor.books = ['Shelf/Book One']; e.universes.harbor.playground = ['Shelf/']; return e; },
|
||||
'library-mapping-invalid', /"Shelf\/" holds its own book folder "Shelf\/Book One"/);
|
||||
});
|
||||
|
||||
test('C2-01c: universe-level prefixes lie outside every book, in a plain universe and in a hybrid with no books', () => {
|
||||
const m = mapping(e => {
|
||||
e.universes.harbor.playground = ['Book One/Playground/', 'Harbor Ideas/'];
|
||||
e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: [], playground: ['Crossover Ideas/'] };
|
||||
return e;
|
||||
});
|
||||
assert.deepEqual(m.universes.harbor.playground, ['Book One/Playground/', 'Harbor Ideas/']);
|
||||
assert.deepEqual([m.universes.cross.books, m.universes.cross.playground], [[], ['Crossover Ideas/']]);
|
||||
});
|
||||
|
||||
test('C2-01c: a draft takes kind, name and playground only, and comes back frozen with books []', () => {
|
||||
const m = mapping(e => { e.universes.idea = draft(); return e; });
|
||||
assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'] });
|
||||
assert.ok(Object.isFrozen(m.universes.idea) && Object.isFrozen(m.universes.idea.books) && Object.isFrozen(m.universes.idea.playground));
|
||||
refuses(e => { e.universes.idea = { ...draft(), books: ['New World'] }; return e; }, 'library-mapping-invalid', /universes.idea is a draft and takes no books/);
|
||||
refuses(e => { e.universes.idea = { ...draft(), books: [] }; return e; }, 'library-mapping-invalid', /takes no books/);
|
||||
refuses(e => { e.universes.idea = draft([]); return e; }, 'library-mapping-invalid', /universes.idea.playground must name at least one prefix/);
|
||||
refuses(e => { e.universes.idea = { kind: 'draft', name: 'New World' }; return e; }, 'library-mapping-invalid', /at least one prefix/);
|
||||
refuses(e => { e.universes.idea = { ...draft(), notes: 'x' }; return e; }, 'library-mapping-invalid', /unknown key "notes"/);
|
||||
for (const kind of ['library', 'Draft', 'plain', null, 1])
|
||||
refuses(e => { e.universes.harbor.kind = kind; return e; }, 'library-mapping-invalid', /universes.harbor.kind must be "hybrid" or "draft"/);
|
||||
});
|
||||
|
||||
test('no playground prefix holds another, and no book folder holds another', () => {
|
||||
refuses(e => { e.universes.harbor.playground = ['Book One/Playground/', 'Book One/Playground/Deep/']; return e; },
|
||||
'library-mapping-invalid', /holds playground/);
|
||||
// The rule covers every level and every entry (C2-01c).
|
||||
refuses(e => { e.universes.harbor.playground = ['Ideas/']; e.universes.idea = draft(['Ideas/New World/']); return e; },
|
||||
'library-mapping-invalid', /playground "Ideas\/" holds playground "Ideas\/New World\/"/);
|
||||
refuses(e => { e.universes.harbor.playground = ['Ideas/']; e.universes.idea = draft(['Ideas/']); return e; },
|
||||
'library-mapping-invalid', /listed twice/);
|
||||
refuses(e => { e.universes.second.books = ['Second World', 'Book One/Part Two']; return e; }, 'library-mapping-invalid', /holds book folder/);
|
||||
refuses(e => { e.universes.second.books = ['Book One']; return e; }, 'library-mapping-invalid', /in two universes/);
|
||||
});
|
||||
@@ -58,7 +99,10 @@ test('a hybrid universe is a shape the query code can take (1.4), with sources t
|
||||
notes: 'Set after book one.' }; return e; };
|
||||
const m = checkMapping(hybrid(mappingEntry()));
|
||||
assert.deepEqual(m.universes.cross.sources, ['harbor', 'second']);
|
||||
refuses(e => { hybrid(e); e.universes.cross.sources = ['harbor', 'nowhere']; return e; }, 'library-mapping-invalid', /non-hybrid universe/);
|
||||
refuses(e => { hybrid(e); e.universes.cross.sources = ['harbor', 'nowhere']; return e; }, 'library-mapping-invalid', /not a plain universe/);
|
||||
// A draft has no canon to draw on (C2-01c).
|
||||
refuses(e => { hybrid(e); e.universes.idea = draft(); e.universes.cross.sources = ['harbor', 'idea']; return e; },
|
||||
'library-mapping-invalid', /sources names "idea", which is not a plain universe/);
|
||||
refuses(e => { hybrid(e); e.universes.cross.sources = ['harbor']; return e; }, 'library-mapping-invalid', /two or more/);
|
||||
refuses(e => { hybrid(e); e.universes.cross.constraints = [{ exclude: 'x', pin: {} }]; return e; }, 'library-mapping-invalid', /exactly one/);
|
||||
});
|
||||
@@ -67,9 +111,9 @@ test('a caller-supplied fail receives the code, as business file loading will pa
|
||||
const seen = [];
|
||||
const fail = (code, message) => { seen.push(code); throw new Error(message); };
|
||||
const entry = mappingEntry();
|
||||
entry.universes.harbor.playground = ['Elsewhere/'];
|
||||
entry.universes.harbor.playground = ['Second World/Playground/'];
|
||||
assert.throws(() => checkMapping(entry, 'businesses/acme.json projects.books', { fail }), /businesses\/acme.json projects.books/);
|
||||
assert.deepEqual(seen, ['library-playground-outside-book']);
|
||||
assert.deepEqual(seen, ['library-playground-other-universe']);
|
||||
});
|
||||
|
||||
test('scope comes from the path alone (design 1.2)', () => {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { classify, readLibrary } from '../src/index.mjs';
|
||||
import { BASE, mapping, repo } from './helpers.mjs';
|
||||
|
||||
// Playgrounds at three levels (ruling after r11, C2-01c). The fixture's harbor keeps its book-level
|
||||
// prefix and gains a universe-level one. A hybrid with no books has a universe-level prefix, and a
|
||||
// draft has its own.
|
||||
const levels = () => mapping(e => {
|
||||
e.universes.harbor.playground = ['Book One/Playground/', 'Harbor Ideas/'];
|
||||
e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: [], playground: ['Crossover Ideas/'] };
|
||||
e.universes.idea = { kind: 'draft', name: 'New World', playground: ['Drafts/New World/'] };
|
||||
return e;
|
||||
});
|
||||
|
||||
test('C2-01c: each level gives its universe, and its book or null', () => {
|
||||
const m = levels();
|
||||
assert.deepEqual(classify(m, 'Book One/Playground/Characters/New.md'),
|
||||
{ scope: 'playground', universe: 'harbor', book: 'Book One', type: 'character', folder: 'Characters' });
|
||||
assert.deepEqual(classify(m, 'Harbor Ideas/Places/Island.md'),
|
||||
{ scope: 'playground', universe: 'harbor', book: null, type: 'place', folder: 'Places' });
|
||||
assert.deepEqual(classify(m, 'Harbor Ideas/Loose.md'), { scope: 'playground', universe: 'harbor', book: null, type: null, folder: null });
|
||||
assert.deepEqual(classify(m, 'Crossover Ideas/Chapters/Meeting.md'),
|
||||
{ scope: 'playground', universe: 'cross', book: null, type: 'manuscript', folder: 'Chapters' });
|
||||
assert.deepEqual(classify(m, 'Drafts/New World/Characters/Ana.md'),
|
||||
{ scope: 'playground', universe: 'idea', book: null, type: 'character', folder: 'Characters' });
|
||||
for (const path of ['Harbor Ideas/Board.canvas', 'Crossover Ideas/Sketch.canvas', 'Drafts/New World/Map.canvas',
|
||||
'Drafts/Other.md', 'Drafts/New World.md', 'Harbor Ideasx/a.md'])
|
||||
assert.deepEqual(classify(m, path), { scope: 'unmapped' }, path);
|
||||
});
|
||||
|
||||
test('C2-01c: prefixes come first: in a book that holds a prefix, a file outside it is canon and one under it playground', () => {
|
||||
const m = levels();
|
||||
assert.deepEqual(classify(m, 'Book One/Places/Dock.md'), { scope: 'canon', universe: 'harbor', book: 'Book One', type: 'place', folder: 'Places' });
|
||||
assert.deepEqual(classify(m, 'Book One/Playground.md'), { scope: 'canon', universe: 'harbor', book: 'Book One', type: null, folder: null });
|
||||
assert.deepEqual(classify(m, 'Book One/Playground/What If.md'),
|
||||
{ scope: 'playground', universe: 'harbor', book: 'Book One', type: null, folder: null });
|
||||
assert.deepEqual(classify(m, 'Second World/Chapters/01.md'), { scope: 'canon', universe: 'second', book: 'Second World', type: 'manuscript', folder: 'Chapters' });
|
||||
});
|
||||
|
||||
test('C2-01c: a canon file linking into a draft gives R6, and into its own universe-level prefix gives R5', async t => {
|
||||
const r = repo(t, { ...BASE, 'Harbor Ideas/Island.md': 'An island.\n', 'Drafts/New World/Ana.md': 'Ana.\n',
|
||||
'Book One/Places/Dock.md': 'See [[Island]].\n', 'Book One/Places/Pier.md': 'Like [[Ana]].\n' });
|
||||
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: levels() });
|
||||
const codes = path => result.problems.filter(p => p.path === path).map(p => p.code).sort();
|
||||
const file = path => result.files.find(f => f.path === path);
|
||||
assert.deepEqual(codes('Book One/Places/Dock.md'), ['R5']);
|
||||
// Any playground gives R5. A draft is never a source, so it gives R6 as well.
|
||||
assert.deepEqual(codes('Book One/Places/Pier.md'), ['R5', 'R6']);
|
||||
assert.deepEqual(file('Book One/Places/Pier.md').links.map(l => l.path), ['Drafts/New World/Ana.md']);
|
||||
for (const [path, universe] of [['Harbor Ideas/Island.md', 'harbor'], ['Drafts/New World/Ana.md', 'idea']]) {
|
||||
assert.deepEqual([file(path).scope, file(path).universe, file(path).book], ['playground', universe, null], path);
|
||||
assert.deepEqual(codes(path), [], path);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user