Files
rea/tests/boundary/guidanceSearch.test.ts
Kaoru0822andClaude Opus 5.5 41e7333cb5 feat(guidance): distinguish focused matches from broad parent sections (#8305)
* feat(guidance): distinguish focused matches from broad parent sections

Guidance search treated a parent section, including every descendant, as one
match. Terms scattered across unrelated subsections made a 49K-character root
the only result for "switch provider annotations", and every ancestor of a
focused hit was listed alongside it.

The section parser now records each section's own text (heading and body
before its first subsection) and its parent. Search keeps the same admission
rule but classifies each match by match_scope: "section" when every term is
in the section's own text, "subsections" when the terms are only spread
across subsections. Ancestors whose terms all sit inside one subsection are
no longer listed, section matches rank first, and subsections matches carry
own_text_terms plus bounded greedy pointers to the subsections holding the
remaining terms. Every match reports content_length so callers can judge a
read before making it. Read still returns the complete section.

Own text is tokenized once per request instead of each expanded subtree.

Fixes #8290

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(guidance): require the scattered-match checks to run

Count the subsections matches the real-bundle test inspects and fail when
none were checked, and document that a parent whose own text holds every
term stays listed beside its matching subsection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:11:05 -07:00

108 lines
3.8 KiB
TypeScript

import { expect, it } from "vitest";
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import {
readGuidanceSection,
searchGuidance,
} from "../../src/application/GuidanceAccess.js";
it("finds and retrieves the selected section from the generated package bundle", async () => {
const resources = new ArtifactResourceScope();
try {
const result = await searchGuidance(
{ query: "exact JavaScript module" },
resources,
);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
status: "matches",
source: "installed_rea_guidance",
package: expect.stringMatching(/^rea-agents@/u),
});
if (result.value.status !== "matches")
throw new Error("Expected guidance matches");
const match = result.value.matches.find(
({ heading }) => heading === "Read a module without repeating analysis",
);
expect(match).toBeDefined();
expect(result.value.matches[0]?.heading).toBe(
"Read a module without repeating analysis",
);
expect(match).toMatchObject({
guide: "references/javascript-applications.md",
matched_terms: expect.arrayContaining(["exact", "javascript", "module"]),
context: expect.stringContaining("javascript-module"),
});
if (match === undefined) throw new Error("Expected selected guidance");
const retrieved = await readGuidanceSection(
{ locator: match.locator },
resources,
);
if (!retrieved.ok) throw retrieved.error;
expect(retrieved.value).toMatchObject({
status: "section",
source: "installed_rea_guidance",
guide: match.guide,
heading: match.heading,
locator: match.locator,
content: expect.stringContaining("node_id"),
});
} finally {
await resources.close();
}
});
it("separates focused installed guidance from terms spread across a broad parent", async () => {
const resources = new ArtifactResourceScope();
try {
const focused = await searchGuidance(
{ query: "Ghidra annotations" },
resources,
);
if (!focused.ok) throw focused.error;
if (focused.value.status !== "matches")
throw new Error("Expected guidance matches");
expect(focused.value.matches[0]).toMatchObject({
heading: "Function annotations in Ghidra",
match_scope: "section",
});
const terms = ["switch", "provider", "annotations"];
const scattered = await searchGuidance(
{ query: terms.join(" "), limit: 20 },
resources,
);
if (!scattered.ok) throw scattered.error;
if (scattered.value.status !== "matches")
throw new Error("Expected guidance matches");
let checkedScattered = 0;
for (const match of scattered.value.matches) {
const read = await readGuidanceSection(
{ locator: match.locator },
resources,
);
if (!read.ok || read.value.status !== "section")
throw new Error("Expected a readable section");
expect(read.value.content).toHaveLength(match.content_length);
if (match.match_scope === "section") continue;
checkedScattered += 1;
const pointers = match.subsections ?? [];
expect(pointers.length).toBeGreaterThan(0);
for (const pointer of pointers)
expect(
terms.every((term) => pointer.matched_terms.includes(term)),
).toBe(false);
// Every term is either in the parent's own text or behind a pointer.
const located = new Set([
...(match.own_text_terms ?? []),
...pointers.flatMap((pointer) => pointer.matched_terms),
]);
expect(terms.filter((term) => !located.has(term))).toEqual([]);
}
// These terms sit in different subsections of the installed guides.
expect(checkedScattered).toBeGreaterThan(0);
} finally {
await resources.close();
}
});