Files

17 KiB

Row 216 build notes (C2-05a, lookup index and queries, #1717)

Phase A only: code, tests and this file. No test, suite or mutant has been run. node --check passed on every changed .mjs.

  • Branch: researcher/row216
  • Base: d372102882d6858fa77af4aee453f866c886ad5c (origin/refactor)
  • Design: agents/gosalyn/work/case2/DESIGN.md on origin/gosalyn/case2-design, section 3 (table at lines 625-631, bullets 633-641), 1.2-1.4, B12, D2
  • Brief: docs/plans/2026-10-20_case2-rows.md, C2-05a at lines 320-357

Files

Path Status
packages/library/src/index-build.mjs new
packages/library/src/query.mjs new
packages/library/src/index.mjs edit: 2 export lines added
packages/library/tests/query-fixture.mjs new, test helper
packages/library/tests/query.test.mjs new
packages/library/tests/index-build.test.mjs new
agents/researcher/work/queue-216/BUILD.md new (this file)

No existing test or source file changed beyond index.mjs.

Design rows and where each is tested

Design row Code Tests (query.test.mjs unless noted)
library_find find find is bound to its universe; every result carries its scope; long list pages; large items page under the cap
library_get get hybrid gets a source file; get lists visible links both ways; get pages its lines
library_search search search returns scoped line hits; item that cannot fit alone is refused
library_manuscript manuscript one chapter at most 400 lines; refused without chapter; hybrid reads a source chapter
library_universe universeInfo universe info reports a hybrid's sources, constraints, notes; unknown universe refused
Hybrid semantics (1.4) sees a hybrid sees its sources canon minus exclusions, no playground
Unmapped never appears index build + sees unmapped files never appear (query.test); index holds mapped files only (index-build.test)
Per-commit cache (3) indexFor same commit, path, mapping return one index; different path, mapping or commit get their own; eight-entry bound; failed read not kept; unchecked mapping refused (index-build.test)
Rebuild time (3, C2-05a gate) indexFor 3,000-file rebuild, time reported with t.diagnostic, no threshold asserted
REPLY_CAP parity QUERY_REPLY_CAP equals REPLY_CAP from harness tools.mjs

Choices where the design table is silent or differs

These are departures or gaps. Each is built as stated, so Sage can rule before Phase B. A ruling that changes one of them is a small edit.

  1. Paging arguments (gap a). The table has no offset. find and search take an optional offset (a position in the filtered list, default 0). A reply carries next: the position of the next item, or null.
  2. library_get has no universe (gap b). Built with a required universe. Without it the reply could not be isolated. A missing file and a file the universe cannot see both return null, so a caller cannot tell them apart. get also takes an optional from line.
  3. get over the cap (gap c). Lines page by from with next. links_out and links_in come back whole on every page. If the frontmatter and links alone exceed the cap, the call is refused with query-item-too-large. A hub note with many backlinks may be refused. This needs a ruling: link paging, or accept the refusal.
  4. manuscript without a chapter (gap d). Refused with query-chapter-required. The design names no list form. A chapter name that matches two visible files is refused with query-chapter-ambiguous.
  5. search excerpt (gap e). The excerpt is the whole line, not a truncation. A line that alone exceeds the cap is refused. Truncating it would cut an item.
  6. exclude form (gap f). checkMapping accepts only a folder path for exclude (folder()), so an exclusion is a path prefix. There is no type or source form to build. Confirm.
  7. Pin constraints. pin does not hide any file. It is reported by universeInfo only. Confirm.
  8. Playground through get. scope applies to lists. get by an explicit path returns a playground file of the caller's own universe. Confirm.
  9. Links. A link into a file the universe cannot see is dropped from links_out and links_in, in line with N1. Links from a visible file that point into another universe are kept in the index (the reader keeps them under R6), and the query layer drops them for universes that cannot see them.
  10. find matching. Case-insensitive substring over the front matter name and the file name. An empty query matches every file in scope.
  11. Cache key. repo, commit and a sha256 of the canonical mapping. Keying by commit alone would return the wrong scopes after a mapping change. The cache holds eight entries, least recently used out. Failed reads are not kept.
  12. universeInfo. Reads only the checked mapping and the served commit. It builds no index.
  13. Reply commit. Every reply carries the index commit, which is the served commit passed in.

Provisional refusal codes (pending Sage approval)

The brief says new refusal codes need Sage's approval first. These are provisional and must not be treated as settled:

  • query-argument-invalid
  • query-universe-unknown
  • query-chapter-required
  • query-chapter-ambiguous
  • query-item-too-large

Reused, existing codes: library-mapping-invalid, plus the reader's library-git, library-commit through indexFor.

Grep results (grep only, not a test run)

  • Test references to the changed paths: packages/library/tests/*.mjs import ../src/index.mjs and ../src/mapping.mjs. packages/bus/tests/library.test.mjs and library-sync.test.mjs import the library index as a namespace. Neither enumerates its exports, so the two new export lines break no assertion.
  • packages/cli/tests/stack-copy.test.mjs:52 checks that packages/library/src/index.mjs exists, and it still does.
  • 64-hex pins: none in packages/library/tests, none on index.mjs, query.mjs or index-build.mjs.
  • scripts/test-*.sh: no reference to the new modules.

Flag for Sage

  • packages/library/tests/helpers.mjs repo() builds its fixtures under tmpdir(), which is /tmp. The launcher rules forbid that. The new tests do not use it; they use tests/query-fixture.mjs, which builds under /mnt/storage/scratch/tmp. The existing helper should be fixed in a separate row, not here.

Phase B commands (not run)

Run only after Sage sends "slot clear". Set the scratch TMPDIR first, per the launcher rule.

cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-aa3cc98787bc5152c/packages/library
export TMPDIR=/mnt/storage/scratch/tmp
node --test tests/query.test.mjs tests/index-build.test.mjs
node --test tests/*.test.mjs

Also run packages/bus/tests/library.test.mjs and library-sync.test.mjs, since they import the index.

Status

Not yet verified. Nothing has been tested and nothing is committed. The working tree holds the Phase A changes. AGENTS.md invariant 8 allows a commit only after the applicable suites are green, so the commit waits for Phase B. The dispatch asked for a local commit in Phase A; Sage to confirm that the commit waits for Phase B.

Round 2 (2026-10-22, sage-2708, sage-2716): rulings applied, rebased onto aca16404c

Added after the text above; nothing above is edited. "Nothing is committed", "Provisional refusal codes", query-chapter-required and the tmpdir flag above were true when written and are superseded here.

  • Branch researcher/row216-r2, one local commit on origin/refactor aca16404c. Row 268 (C2-01c) is in that base. Row 253 has not landed; the work rebases onto 253's tip before any gate. Not pushed, not run.
  • Rulings: agents/sage/work/queue-216/RULINGS.md at da9555ca2, rulings 1-13 and the tmpdir flag.
  • Drift check, d37210288..aca16404c against my paths: the library files that changed are mapping.mjs, scope.mjs, README.md and the mapping and scope tests. None is a file I add or edit. query.mjs reads the checked mapping through them, so the whole library package runs in phase B.

What changed from phase A

Ruling Change in query.mjs Test
3 get over the cap links_out and links_in page together with links_from; every reply carries links_next, next and the front matter. A page with lines left carries at least one line. Links take at most half the cap while lines remain. query-item-too-large is left for front matter alone or one line alone (or one link alone) hub note with 900 backlinks pages and adds up; front matter alone and a line alone refused; bad links_from
4 manuscript no chapter chapter optional: the first visible chapter in path order. Every reply carries next_chapter. query-chapter-required removed. Ambiguity still refuses first chapter and next_chapter; ambiguous name refused
5 search long line hit comes back {path, line, excerpt: null, excerpt_too_long: true, scope}, other hits still return marked hit and unmarked hits in one reply
6 exclude path-prefix reading only. The test names EXCLUSION and EXCLUDED_PATHS in query-fixture.mjs, so C2-15 (row 267) swaps two constants exclusion hides from the hybrid and not from the source
8 hybrid get unchanged code; test added hybrid gets a source's playground file as null; the source gets it with scope: "playground"
PRD 1.14 (row 268) universeInfo reports kind from the mapping (draft included) four tests below

PRD 1.14 tests, one each: a lookup naming a book leaves book: null files out (find and search); library_manuscript never reads a book: null file; a draft universe has no canon, so its lookups return files only with scope: "playground"; library_universe on a draft returns kind, name and prefixes. The levels fixture is WITH_LEVELS in query.test.mjs: harbor gets Notes/Harbor/ as a universe-level playground prefix, and idea is a draft over Drafts/New World/.

query-fixture.mjs builds under tmpdir(), as helpers.mjs does (ruling "tmpdir flag"). The earlier flag about helpers.mjs building under /tmp is closed by that ruling: the gate sets TMPDIR.

Refusal codes now: query-argument-invalid, query-universe-unknown, query-chapter-ambiguous, query-item-too-large. All four approved in the rulings.

Grep results (grep only, not a test run)

  • git grep over packages/*/tests/* and scripts/* for query.mjs and index-build: no hit outside packages/library/tests/query*.test.mjs and index-build.test.mjs.
  • 64-hex pins naming a library path: none. Positive control: the same pathspec finds the sha256 lines in packages/cli/tests/case1-hostcheck.test.mjs and the packages/library/src/index.mjs existence check at packages/cli/tests/stack-copy.test.mjs:52.
  • Tests that read packages/library/src as a directory: packages/cli/tests/stack-release-fixture.mjs:309 copies packages/library/src whole into a release fixture. It carries no file list, so two new files break no assertion. stack-release.test.mjs is not in the gate unless Sage wants it.
  • Tests that import the library index as a namespace: packages/bus/tests/library.test.mjs and library-sync.test.mjs. Neither enumerates exports, so the two export lines add no break. Both run whole in phase B.

Run limits (fixed before any run)

Every node file runs as its own setsid -w timeout -k 10 LIMIT node --test FILE; nothing runs unbounded.

  • Node test files: LIMIT 900 s. The library test files have no recorded time. 900 s is above the longest recorded whole-package run I know of (cli, 591 s, row 281 BUILD.md "Runs" step 3), so it holds the 3x rule for any single file. The first run records each file's time, and a file past 300 s gets a limit re-derived from it before the next run. query.test.mjs builds a 900-file repo and a 3,000-file index rebuild is in index-build.test.mjs; both are the files most likely to need it.
  • No shell script is in this row's gate, so no script limit applies.

Any timeout goes to Sage, then Mos, unplaced (mos-1275).

Phase B (not run; Sage's CLEAR first, then a rebase onto 253's tip)

export TMPDIR=/mnt/storage/scratch/tmp
cd <worktree>/packages/library
for f in tests/*.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
cd ../bus
for f in tests/library.test.mjs tests/library-sync.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
cd ../cli
setsid -w timeout -k 10 900 node --test tests/stack-copy.test.mjs

Mutants I propose (my list, not read off the brief; each is meant to be killed by the named test, and none has been thrown; the kill claims are unverified until CLEAR):

  • M-a: sees() returns true for a file of another universe: killed by "find is bound to its universe".
  • M-b: the playground filter dropped: killed by "playground appears only when asked for".
  • M-c: the hybrid reads a source's playground: killed by "a hybrid gets a source playground file as null".
  • M-d: links_next always null: killed by the hub paging test.
  • M-e: the book filter lets book: null through: killed by the PRD 1.14 book test.

Status

Built, not run. node --check has not been run on this round's files (no node before the CLEAR); the phase A pass above covered the earlier text only. The commit is local and waits for no gate under Sage's sage-2716 instruction, which allows git and docs work only; push and claim wait for Sage's go.

Round 3: rebase onto 45dca69f6, drift check, hash-pin grep (2026-10-23T18:53:43Z, sage-3576)

Appended; nothing above is edited. Git only; nothing run, nothing pushed. Branch researcher/row216-r4 holds the one row 216 commit on 45dca69f6 (origin/refactor, 10-23). Earlier base: 48e450ce8 (row 253).

  • Rebase: clean. git range-diff 48e450ce8..038089e5f 45dca69f6..researcher/row216-r4 shows the commit as =.
  • Landed in 48e450ce8..45dca69f6: 1175 files. Path overlap with this row's 7 paths: none. packages/library: no landed file. packages/harness/src/tools.mjs (imported by query-fixture.mjs or the tests): not landed.
  • Hash-pin grep (the item 216 owed): 64-hex in packages/library: none. Positive control: 364 files in packages/*/tests and scripts match 64-hex. Tests and scripts that name library files by path: bus/tests/library.test.mjs and library-sync.test.mjs and bus/src/runtime.mjs import library/src/index.mjs; business/src/business.mjs and its test import library/src/mapping.mjs (unchanged by this row); cli/tests/stack-copy.test.mjs, stack-release-fixture.mjs:310 and stack-release.test.mjs:1991 copy packages/library/src as a tree; scripts/stack-copy.sh:37 copies it; scripts/mosaic:41 runs survey.mjs. None pins a hash of index.mjs, index-build.mjs or query.mjs.
  • bus/src/runtime.mjs landed changes after 48e450ce8 and uses libraryModule by named members (ensureMirror, SLOT, readLibraryFile, libraryStatus, sync, LibraryError, SYNC_INTERVAL_MS) only; the landed diff touches none of them, and this row's two new index.mjs exports (query and index build) are not iterated by any caller.
  • Gate additions for phase B: run bus library.test.mjs and library-sync.test.mjs whole (they import index.mjs), and the cli stack-copy and stack-release tests that copy packages/library/src.

Round 4: phase B on 4e5d28ab2, two test fixes for r5 (2026-10-23T20:20:51Z, sage-3702, mos-1528)

Appended; nothing above is edited.

Phase B receipts on 4e5d28ab2 (researcher-r216-kit/out-b1): library whole 114 pass, 2 fail; bus library.test and library-sync.test 12 pass; cli 266 pass. The 3,000-file rebuild took 3807 ms (the test, with fixture setup, 4070 ms; t.diagnostic in index-build.test.mjs). No row 216 mutant was required or thrown in that round.

The 2 library reds are test faults, not product faults. Both are placed as first-run reds in files this row adds (mos-1272); Mos ruled both fixes in mos-1528 and Sage relayed it in sage-3712.

  • R4: packages/library/tests/index-build.test.mjs, test "a different repo path, mapping or commit gets its own index". Old: writeFileSync(join(repo, 'Book One/Places/New.md'), …) with no Book One/Places folder in FILES, so the write failed with ENOENT. New: one mkdirSync(join(repo, 'Book One/Places'), { recursive: true }) before the write (import line gains mkdirSync). No assertion changes.
  • R5: packages/library/tests/query.test.mjs, test "a hybrid sees its sources canon, minus exclusions, and no playground". The assertion sorts the actual paths and compared them to an unsorted literal, so the literal had the wrong order (Characters before Chapters; Second World Characters before Chapters). New: only the expected literal is reordered into sorted order. The .sort() on the actual side stays, and the assertion still checks order. The set of seven paths is unchanged.
  • Product code: no edit.

Reruns owed after the r5 commit (run on Sage's CLEAR, after the 283 gate): index-build.test.mjs whole, query.test.mjs whole, library whole, and the mutants those two files kill (M-a to M-e above). Results are appended below as a new entry.

Run limit note: stack-release.test.mjs ran 644 s in the cli run. A per-file limit of 3 x 644 = 1932 s covers it. Sage's gates run cli as one whole-package node --test under 2100 s.

Carry notes from Filbert's static check (doc only, no change in r5): N1 and N2 stay recorded as he gave them. The After list in this file still names row 268; row 268 landed at c27207f6e.