fix(library): row 267 R2 search hits carry their source universe (#1772)

(cherry picked from commit 3006729160)
This commit is contained in:
jason.woltje committed 2026-11-02 14:40:12 -06:00
1 parent ddf20567ee
commit cab3ef3e8f
7 files changed
+76 -8

No files matched your search

+1
View File
@@ -5851,3 +5851,4 @@ Q284 / #470 source-only security fix 2026-10-29T03:33:00.640624+00:00: pin worke
2026-11-02T19:10:25Z | Claude Sonnet 5.5 implementation sub-agent (lead T3 c5c1e8ba) | row 334 (#1832) R2 on lead/r334-land at 2b3e3ec83 | F1: the test "row334 no-proxy renders stay byte-identical to the pre-row renderer" took its baseline from `git show 78b69e1e6:scripts/case2-unit.sh`, which still requires `--send`; #385 removed `--send` from the library flags, so the merged tree failed `1 !== 0` ("case2-unit: serve flag --send is required"). The baseline is now the frozen fixture `packages/cli/tests/fixtures/case2-unit-pre-row334-renderer.sh.txt`, the bytes of `418f00bce:scripts/case2-unit.sh` (git blob d669e8cbd), and the test checks the fixture blob id before it runs. The fixture ends in `.txt` and is mode 0644, so no `*.sh` listing picks it up; stack-copy and stack-release already exclude `packages/*/tests`, and COPY_PATHS does not name it. F3: the re-install refusal in `scripts/case2-unit.sh` now also names `--proxy`; the pinned regex at case2-unit.test.mjs:593 follows it, and one new test covers adding `--proxy` to a no-proxy install (refuses, bytes unchanged, no systemctl call). F4: the "both units" comments (header, re-install comment, and the WantedBy comment near line 467) now name the bus and Console units, and the proxy unit under `--proxy`. No product logic changed. RED on 2b3e3ec83: the one test fails `1 !== 0`. GREEN: that test passes; case2-unit 83/83 (82 s), case2-install 28/28 (11 s), each run once; the three row253 C4 clock tests passed on the first run. Hash pins: the grep over `packages/*/tests/*` and `scripts/test-*.sh` found one pin in a file that names case2-unit.sh (case1-accept.test.mjs:802, which hashes case1-accept.sh); none on a changed file, and case1-accept.test.mjs was not run. F2 (stack-upgrade.sh and `--proxy`) is not touched. One local commit, no push.
2026-11-02T19:04:01Z Lead sub-agent (Claude Sonnet 5.5) ROW 173 R3 #1671 (Seen-intent test fix on lead/landing-batch1, base 09a847eda): BEFORE `MOSAIC_REQUIRE_EDITOR_BUILD=1 node --test packages/webui/tests/board-held.test.mjs` gave 4 tests, 3 pass, 1 fail ("a Seen or Unsee click that waited out a Board read keeps its meaning"), timing out at board-held.test.mjs:207 on the sessions table and held rows before any click. CAUSE: the row 173 test called setup(false), which starts the page with Show machine sessions off, so table.sessions never renders; its sibling at :168 uses setup(true). Not base drift: the same single test fails identically on a detached checkout of dewey/row173-r2 tip 6d15eb5fc20a (based on 78b69e1e6). The test had never run (BUILD.md: "no runs yet"). FIX: board-held.test.mjs:198 setup(false) to setup(true), one token; app.js unchanged. AFTER the test passes (1/1); whole board-held file 4 tests, 4 pass, 0 fail; whole webui package `node --test tests/*.test.mjs` 491 tests, 491 pass, 0 fail, 0 skipped. Teeth check: with app.js reverted to the pre-row-173 behavior (no intent check, seen: !r.seen) the test fails at the announce wait; app.js restored byte for byte. Hash-pin grep (packages/*/tests/*, 33 hex lines, positive control account-migration pin found): no pin names board-held.test.mjs or app.js. Comparison worktree and links removed. Not run: scripts/test-*.sh (lead gate). Corrects the r2 BUILD.md claim of no runs; no earlier entry edited.
2026-11-02T19:27:10Z Claude Sonnet 5.5 (child of lead c5c1e8ba) row 267 (C2-15, #1772) R2, F1 search hits carry their source universe: BEFORE `search()` in packages/library/src/query.mjs returned hits without `universe` in both shapes (normal and excerpt_too_long); find already carried it; hybrid.test.mjs reduced search hits to paths and passed. AFTER both shapes carry `universe: file.universe` after `scope`, as find does; hybrid.test.mjs asserts the source universe under both exclude forms (own file and a source's canon) plus a new oversized-line case through the hybrid; the broker C2-15 case asserts it on library.search; query.test.mjs:178 shape pin gains universe. RED 4 of 21 failed on the missing field. GREEN the two files 21/21, library 182/182, business 284/284, bus 1351/1351 (see correction). Mutants: universe off the normal hit killed by 3 tests, off the excerpt_too_long hit killed by 2. F2 (hybrid source exception ignores source scope in reader and guard) not touched, lead files it. Correction recorded honestly: the first bus run in a checkout without node_modules failed 3 tests in model-hire-ownership.test.mjs, also failing on base 09a847eda, and pass with node_modules linked; a second bus run with the link failed 1 test in model-final-completion.test.mjs that passed 3 of 3 alone on this tree and on base; a rerun was 1351/1351. No push.
+42
View File
@@ -185,3 +185,45 @@ Mutants. Each runs only its own named tests, and the tree is restored after each
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.
+1
View File
@@ -1901,3 +1901,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-26 — rockerduck — row334 Mos1961 targeted serial slot — integrated 09a847eda, whole files 81+28 pass, two mutants killed/restored 2 pass; old RED preserved, test roots removed, SLOTBACK; no push/live action/review dispatch.
2026-11-02T19:10:25Z | Claude Sonnet 5.5 implementation sub-agent (lead T3 c5c1e8ba) | row 334 (#1832) R2 on lead/r334-land at 2b3e3ec83 | F1 fixed: the byte-identity test now uses a frozen fixture of 418f00bce:scripts/case2-unit.sh (blob d669e8cbd) instead of 78b69e1e6. F3: the re-install refusal names `--proxy`, with one new test. F4: "both units" comments corrected. RED 1 test fails `1 !== 0`, then GREEN: case2-unit 83/83, case2-install 28/28. One local commit; no push, PR, queue, Vikunja, Gitea or live action.
2026-11-02 | Lead sub-agent (Claude Sonnet 5.5) | row173 R3 (#1671) Seen-intent test fix | Cause: test called setup(false) (machine sessions off) and waited for table.sessions; sibling uses setup(true); not base drift (tip 6d15eb5fc20a fails the same). Fixed board-held.test.mjs:198 only; app.js unchanged. Test 1/1, board-held 4/4, whole webui 491/491; mutant (old intent behavior) fails the test. Local commit on lead/landing-batch1, no push.
2026-11-02 | Claude Sonnet 5.5 (child of lead c5c1e8ba) | row 267 R2 (C2-15, #1772), reviewer finding F1 | search hits now carry universe in both shapes; tests assert it under both exclude forms, in the oversized-line case and through the broker; query.test.mjs:178 pin updated; RED 4 failed, GREEN library 182, business 284, bus 1351, two mutants killed. F2 untouched. Committed locally on t3/row267-r2-fix, not pushed.
+7 -1
View File
@@ -252,7 +252,7 @@ test('C2-05c broker forwards supporting scope and returns the served blob withou
await assert.rejects(f.call('library.manuscript', { universe: 'harbor', book: 'Book One', scope: 'supporting' }), { code: 'query-argument-invalid' });
});
test('C2-15 broker: library.find and library.universe in a hybrid see its own files and source canon, minus the type-form exclude', async t => {
test('C2-15 broker: library.find, library.search and library.universe in a hybrid see its own files and source canon, minus the type-form exclude', async t => {
const projects = { books: mappingEntry() };
projects.books.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'],
constraints: [{ exclude: 'second/Characters' }], notes: 'Fixture crossover.' };
@@ -262,6 +262,12 @@ test('C2-15 broker: library.find and library.universe in a hybrid see its own fi
['Book One/Chapters/01.md', 'harbor'], ['Book One/Characters/Tomas.md', 'harbor'], ['Crossover/Chapters/01.md', 'cross']]);
assert.deepEqual((await f.call('library.find', { universe: 'cross', query: '', scope: 'playground' })).results, []);
assert.ok(!(await f.call('library.find', { universe: 'harbor', query: '' })).results.some(x => x.path.startsWith('Crossover/')));
// Search hits name their source universe too: a source's canon and the hybrid's own file, and nothing excluded.
const pairs = reply => reply.results.map(x => [x.path, x.universe]);
assert.deepEqual(pairs(await f.call('library.search', { universe: 'cross', text: 'Elsewhere' })), [
['Book One/Characters/Tomas.md', 'harbor'], ['Crossover/Chapters/01.md', 'cross']]);
assert.deepEqual(pairs(await f.call('library.search', { universe: 'cross', text: 'ferry' })), [
['Book One/Chapters/01.md', 'harbor'], ['Book One/Characters/Tomas.md', 'harbor']]);
const universe = await f.call('library.universe', { universe: 'cross' });
assert.deepEqual([universe.kind, universe.sources, universe.constraints, universe.notes],
['hybrid', ['harbor', 'second'], [{ exclude: 'second/Characters' }], 'Fixture crossover.']);
+3 -2
View File
@@ -177,6 +177,7 @@ export async function get(source, { universe: name, path, scope, from, links_fro
}
// search: lines whose text holds `text`, case-insensitive. The excerpt is the whole line. Up to 50 per page.
// Each hit names its source universe, as find does: a hybrid's hits come from its own files and its sources' canon.
export async function search(source, { universe: name, text, book, scope, offset } = {}) {
const universe = universeArg(source.mapping, name);
const needle = textArg(text, 'text').toLowerCase();
@@ -188,10 +189,10 @@ export async function search(source, { universe: name, text, book, scope, offset
for (const file of files) {
for (const [line, body] of file.lines) {
if (!body.toLowerCase().includes(needle)) continue;
const hit = { path: file.path, line, excerpt: body, scope: file.scope };
const hit = { path: file.path, line, excerpt: body, scope: file.scope, universe: file.universe };
// The excerpt is the whole line. A line too long to fit alone is marked, not cut and not refused.
hits.push(bytes({ commit: index.commit, results: [hit], next: Number.MAX_SAFE_INTEGER }) > QUERY_REPLY_CAP
? { path: file.path, line, excerpt: null, excerpt_too_long: true, scope: file.scope } : hit);
? { path: file.path, line, excerpt: null, excerpt_too_long: true, scope: file.scope, universe: file.universe } : hit);
}
}
return fitPage(hits, {
+21 -4
View File
@@ -4,7 +4,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { excluded } from '../src/scope.mjs';
import { find, get, search, universeInfo } from '../src/query.mjs';
import { find, get, search, universeInfo, QUERY_REPLY_CAP } from '../src/query.mjs';
import { mapping, MARKER } from './helpers.mjs';
import { scratchRepo, note } from './query-fixture.mjs';
@@ -32,6 +32,8 @@ const withCross = exclude => mapping(e => {
return e;
});
const src = (ref, map) => ({ repo: ref.repo, commit: ref.commit, mapping: map });
// The source universe of a fixture path: the hybrid's own book, harbor's book, or second's.
const owner = path => path.startsWith('Crossover/') ? 'cross' : path.startsWith('Book One/') ? 'harbor' : 'second';
const ALL_CANON = ['Book One/Characters/Mara Vell.md', 'Book One/Research/Ferry Logs.md', 'Crossover/Chapters/01.md',
'Crossover/Places/Bridge.md', 'Second World/Characters/Ilse.md', 'Second World/Places/Gate.md'];
@@ -56,12 +58,19 @@ for (const [form, { exclude, hidden }] of Object.entries(FORMS)) {
const s = src(scratchRepo(t, FILES), withCross(exclude));
const found = (await find(s, { universe: 'cross', query: '' })).results;
assert.deepEqual(found.map(r => r.path), ALL_CANON.filter(p => p !== hidden));
for (const r of found) assert.equal(r.universe, r.path.startsWith('Crossover/') ? 'cross' : r.path.startsWith('Book One/') ? 'harbor' : 'second');
for (const r of found) assert.equal(r.universe, owner(r.path));
assert.deepEqual((await find(s, { universe: 'cross', query: '', scope: 'playground' })).results, []);
const hits = (await search(s, { universe: 'cross', text: MARKER })).results.map(r => r.path);
const searched = (await search(s, { universe: 'cross', text: MARKER })).results;
const hits = searched.map(r => r.path);
assert.ok(!hits.includes(hidden) && !hits.includes('Book One/Playground/What If.md'));
assert.ok(hits.includes('Crossover/Places/Bridge.md'));
assert.ok(hits.includes(form === 'type' ? 'Book One/Research/Ferry Logs.md' : 'Second World/Characters/Ilse.md'));
const canonHit = form === 'type' ? 'Book One/Research/Ferry Logs.md' : 'Second World/Characters/Ilse.md';
assert.ok(hits.includes(canonHit));
// Each search hit names its source universe, as find does: the hybrid's own file, a source's canon, every other hit.
const universeOf = path => searched.find(r => r.path === path).universe;
assert.equal(universeOf('Crossover/Places/Bridge.md'), 'cross');
assert.equal(universeOf(canonHit), form === 'type' ? 'harbor' : 'second');
for (const r of searched) assert.equal(r.universe, owner(r.path), r.path);
assert.equal(await get(s, { universe: 'cross', path: hidden }), null);
});
@@ -75,6 +84,14 @@ for (const [form, { exclude, hidden }] of Object.entries(FORMS)) {
});
}
test('C2-15 a search line too long to fit alone, found through the hybrid, still names its source universe', async t => {
const huge = 'y'.repeat(QUERY_REPLY_CAP + 1000);
const files = { ...FILES, 'Book One/Places/Wall.md': note('place', 'Wall', `${huge} ${MARKER}`) };
const s = src(scratchRepo(t, files), withCross('second/Characters'));
const reply = await search(s, { universe: 'cross', text: 'yyyyyyyy' });
assert.deepEqual(reply.results, [{ path: 'Book One/Places/Wall.md', line: 5, excerpt: null, excerpt_too_long: true, scope: 'canon', universe: 'harbor' }]);
});
test('C2-15 a source universe never sees a hybrid file', async t => {
const s = src(scratchRepo(t, FILES), withCross('Book One/Research'));
for (const universe of ['harbor', 'second']) {
+1 -1
View File
@@ -175,7 +175,7 @@ test('a search line too long to fit alone comes back marked, with no excerpt, an
const huge = 'y'.repeat(QUERY_REPLY_CAP + 1000);
const s = src(scratchRepo(t, { ...FILES, 'Book One/Places/Wall.md': note('place', 'Wall', `${huge} ${MARKER}`) }));
const reply = await search(s, { universe: 'harbor', text: 'yyyyyyyy' });
assert.deepEqual(reply.results, [{ path: 'Book One/Places/Wall.md', line: 5, excerpt: null, excerpt_too_long: true, scope: 'canon' }]);
assert.deepEqual(reply.results, [{ path: 'Book One/Places/Wall.md', line: 5, excerpt: null, excerpt_too_long: true, scope: 'canon', universe: 'harbor' }]);
const mixed = await search(s, { universe: 'harbor', text: MARKER.toLowerCase() });
assert.ok(mixed.results.some(r => r.excerpt_too_long === true));
assert.ok(mixed.results.some(r => typeof r.excerpt === 'string' && !('excerpt_too_long' in r)));