fix(library): row 267 R2 search hits carry their source universe (#1772)
(cherry picked from commit 3006729160)
This commit is contained in:
1 parent
ddf20567ee
commit
cab3ef3e8f
7 files changed
+76
-8
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.']);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
Reference in new issue
Block a user