* 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>
108 lines
3.8 KiB
TypeScript
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();
|
|
}
|
|
});
|