feat(guidance): search installed workflow sections (#6996)

* feat(guidance): search bundled REA workflow guidance

* fix(guidance): address review findings

* test(guidance): fix boundary and dispatcher coverage

* test(setup): use supported doctor fixture runtime

* fix(guidance): resolve CI conformance, coverage, and review findings

* fix(guidance): own complete requests through shutdown

* test(guidance): bound lifecycle gates and join failed work

---------

Co-authored-by: Deepak-Kambala <[email protected]>
Co-authored-by: gensokyo-dev <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-29 16:47:10 +08:00
1 parent 131fbe0754
commit c88e1b0b43
33 files changed
+2117 -41

No files matched your search

+6 -4
View File
@@ -440,10 +440,12 @@ result's `_meta`, whether the call succeeded or failed:
```
The marker is declared once per tool contract (`returnsTargetContent`) and is
on by default: only `export_evidence_bundle` and `record_unknown`, whose
results and errors contain only REA session state or caller-supplied values,
omit it. Tools whose results or errors can include provider, launcher, or
device output stay marked, including `binary_session` (provider failure
on by default: `export_evidence_bundle`, `record_unknown`, `search_guidance`,
and `read_guidance_section` omit it. The first two contain only REA session
state or caller-supplied values; the guidance tools return REA's own installed
documentation, not text from an analysis target. Tools whose results or errors
can include provider, launcher, or device output stay marked, including
`binary_session` (provider failure
reasons and availability diagnostics), `close_binary` (provider cleanup
diagnostics with captured launcher output), `inspect_adb_client` and
`inspect_apktool_client` (version output and stderr excerpts), and
+20
View File
@@ -18,6 +18,26 @@ non-string values are rejected with field paths. `action: "list"` accepts no
workflow identifier or arguments. The MCP adapter respects the same
`REA_MCP_TOOL_FAMILIES` scope as native prompts, including render requests.
To find a section in the installed guidance bundle by analyst question, use
`search_guidance` and pass a returned locator to `read_guidance_section`. The
CLI equivalent is `rea guidance --query "exact JavaScript module"`, followed
by `rea guidance --action read --locator LOCATOR`. Search is limited to the
packaged managed skill files, works without providers or a target, and returns
deterministically paged matches. Every non-generic query word must occur in the
section; generic-only queries return no matches. Results are documentation; locators
are bound to the installed bundle and report stale or unavailable guidance without
returning cached section content. A no-match result says nothing about analysis
capability availability.
Search returns at most 20 matches per page with 500-code-unit context excerpts.
Read retrieves the complete selected section. Admission permits 1 MiB per file,
4 MiB per bundle, 4,096 sections, and 25,165,824 expanded UTF-16 code units before
section text is materialized. Unreadable or oversized bundles return
`bundle_unavailable`, with no partial index or inferred absence. Cleanup failures
remain errors with their retained retry owner. The bundle and complete section
are bound by fixed-size hashes in locators, so a long heading does not make its own
locator unreadable.
## Prompt inventory
| Prompt | Purpose |
+5
View File
@@ -38,6 +38,11 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
// Test-only outputs are declared beside the test planner rather than becoming
// runtime or typecheck prerequisites. Add a consumer here when it reads one.
const artifactConsumers = {
"artifacts:skills": [
"tests/boundary/cli/guidanceCli.test.ts",
"tests/boundary/guidanceSearch.test.ts",
"tests/boundary/mcp/registerGuidanceTools.test.ts",
],
"artifacts:mcp-catalog": [
"tests/boundary/mcp/deferredRuntime.test.ts",
"tests/boundary/mcp/contractPresentation.test.ts",
+5 -5
View File
@@ -128,7 +128,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
);
const configurationError = cliConfigurationError(process.env);
let dispatchFailure;
let closeCliJavaScriptResources;
let closeCliResources;
// Incur can exit from the callback below before this function returns.
// That callback owns the status, including a closed-pipe exit of 141.
let exiting = false;
@@ -150,7 +150,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
writeStdout(versionOutput);
} else {
const cliRuntime = await import("../dist/cli-runtime/cli.js");
closeCliJavaScriptResources = cliRuntime.closeCliJavaScriptResources;
closeCliResources = cliRuntime.closeCliResources;
const { createCli } = cliRuntime;
const { createCliResultOutput } =
await import("../dist/cli-runtime/cliResultOutput.js");
@@ -176,10 +176,10 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
void (async () => {
let status = code;
try {
await closeCliJavaScriptResources();
await closeCliResources();
} catch (cleanup) {
process.stderr.write(
`Failed to close the JavaScript analysis worker: ${cleanupFailureText(cleanup)}\n`,
`Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`,
);
if (status === 0) status = 1;
}
@@ -225,7 +225,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
// Serve and output rendering can throw after a worker is parked. Stop it
// on that path too, and keep the dispatch failure when both fail.
try {
await closeCliJavaScriptResources?.();
await closeCliResources?.();
} catch (cleanup) {
// The exit callback reports this same shared failure and then exits.
if (!exiting) {
+4 -2
View File
@@ -61,8 +61,10 @@ try {
process.execPath,
resolve("scripts/run-turbo.mjs"),
"run",
...(plan.needsBuild ? ["artifacts:skills"] : []),
...plan.artifactTasks,
...new Set([
...(plan.needsBuild ? ["artifacts:skills"] : []),
...plan.artifactTasks,
]),
]);
if (prepared.code !== 0) process.exitCode = prepared.code;
}
@@ -0,0 +1,139 @@
import { expect, it } from "vitest";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { readGuidanceSectionInputSchema } from "../contracts/guidanceContracts.js";
import { MANAGED_SKILL_FILES } from "./SetupSkill.js";
import {
readGuidanceSectionWithReader,
searchGuidanceWithReader,
} from "./GuidanceAccess.js";
const reader = (text: string) => async (path: string) =>
path === MANAGED_SKILL_FILES[0] ? text : "# Other\n";
it("does not substitute a common domain word for an unknown query subject", async () => {
const resources = new ArtifactResourceScope();
expect(
await searchGuidanceWithReader(
{ query: "how do I inspect module frobnicate qzxv" },
reader("# Modules\nInspect a module with retained facts.\n"),
resources,
),
).toMatchObject({ ok: true, value: { status: "no_matches" } });
await resources.close();
});
it.each([
["## C#", "C#"],
["## C# ###", "C#"],
["## C\\#", "C\\#"],
[" ## C#", "C#"],
["## C# \t ### \t", "C#"],
["## ###", ""],
])("preserves the source title in %s", async (line, heading) => {
const resources = new ArtifactResourceScope();
const source = reader(`${line}\nneedle instructions\n`);
const result = await searchGuidanceWithReader(
{ query: "needle" },
source,
resources,
);
if (!result.ok || result.value.status !== "matches")
throw new Error("Expected matching documentation");
expect(result.value.matches[0]?.heading).toBe(heading);
const value = await readGuidanceSectionWithReader(
{ locator: result.value.matches[0]?.locator },
source,
resources,
);
expect(value).toMatchObject({
ok: true,
value: { status: "section", heading },
});
await resources.close();
});
it("issues readable locators independently of heading length", async () => {
const resources = new ArtifactResourceScope();
const heading = `needle ${"x".repeat(3000)}`;
const source = reader(`## ${heading}\nInstructions\n`);
const result = await searchGuidanceWithReader(
{ query: "needle" },
source,
resources,
);
if (!result.ok || result.value.status !== "matches")
throw new Error("Expected match");
const locator = result.value.matches[0]?.locator;
expect(readGuidanceSectionInputSchema.safeParse({ locator }).success).toBe(
true,
);
expect(
await readGuidanceSectionWithReader({ locator }, source, resources),
).toMatchObject({
ok: true,
value: { status: "section", heading },
});
await resources.close();
});
it.each([2048, 2049])(
"bounds the combined index across guides with %i sections each",
async (count) => {
const resources = new ArtifactResourceScope();
const result = await searchGuidanceWithReader(
{ query: "needle", limit: 1 },
async (path) =>
path === MANAGED_SKILL_FILES[0] || path === MANAGED_SKILL_FILES[1]
? "# needle\n".repeat(count)
: "",
resources,
);
expect(result).toMatchObject({
ok: true,
value: { status: count === 2048 ? "matches" : "bundle_unavailable" },
});
await resources.close();
},
);
it("preserves retry cleanup errors and their live owner in both operations", async () => {
const resources = new ArtifactResourceScope();
const source = reader("# needle\nInstructions\n");
const initial = await searchGuidanceWithReader(
{ query: "needle" },
source,
resources,
);
if (!initial.ok || initial.value.status !== "matches")
throw new Error("Expected match");
const locator = initial.value.matches[0]?.locator;
let blocked = true;
resources.retain({
kind: "lease",
resource: "guidance descriptor",
lease: {
release: async () => {
if (blocked) throw new Error("still owned");
},
},
});
const retryingReader = (path: string) => resources.run(() => source(path));
const errors = [];
for (const result of [
await searchGuidanceWithReader(
{ query: "needle" },
retryingReader,
resources,
),
await readGuidanceSectionWithReader({ locator }, retryingReader, resources),
]) {
if (result.ok) throw new Error("Cleanup failures must remain errors");
expect(result.error).toMatchObject({ cleanupIncomplete: true });
expect(result.error.cleanupResources).toContain("guidance descriptor");
errors.push(result.error);
}
blocked = false;
await resources.close();
expect(resources.hasPending).toBe(false);
for (const error of errors) expect(error.cleanupIncomplete).toBe(false);
});
@@ -0,0 +1,183 @@
import { expect, it } from "vitest";
import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import { RetainedRegularFileCleanupFailure } from "./RegularFileRead.js";
import { MANAGED_SKILL_FILES } from "./SetupSkill.js";
import {
readGuidanceSectionWithReader,
searchGuidanceWithReader,
} from "./GuidanceAccess.js";
const gate = () => {
let release = () => {};
const promise = new Promise<void>((resolve) => {
release = resolve;
});
return { promise, release };
};
const content = (path: string) =>
path === MANAGED_SKILL_FILES[0] ? "# needle\nInstructions\n" : "# Other\n";
it.each(["search", "read"] as const)(
"finishes every guide in an admitted %s while shutdown waits",
async (action) => {
const resources = new ArtifactResourceScope();
const reached = gate();
const resume = gate();
const paths: string[] = [];
const reader = (path: string, admitted = resources) =>
admitted.run(async () => {
paths.push(path);
if (path === MANAGED_SKILL_FILES[1]) {
reached.release();
await resume.promise;
}
return content(path);
});
const launched: Promise<unknown>[] = [];
try {
const initial = await searchGuidanceWithReader(
{ query: "needle" },
async (path) => content(path),
resources,
);
if (!initial.ok || initial.value.status !== "matches")
throw new Error("Expected match");
const operation =
action === "search"
? searchGuidanceWithReader({ query: "needle" }, reader, resources)
: readGuidanceSectionWithReader(
{ locator: initial.value.matches[0]?.locator },
reader,
resources,
);
launched.push(operation);
await awaitAcknowledgement(
reached.promise,
operation,
"second guidance read",
);
let closed = false;
const closing = resources.close().then(() => {
closed = true;
});
launched.push(closing);
await Promise.resolve();
expect(closed).toBe(false);
resume.release();
expect(await operation).toMatchObject({
ok: true,
value: { status: action === "search" ? "matches" : "section" },
});
await closing;
expect(paths).toEqual(MANAGED_SKILL_FILES);
expect(closed).toBe(true);
expect(resources.hasPending).toBe(false);
} finally {
resume.release();
await Promise.allSettled(launched);
await resources.close();
}
},
);
it("retains a failed child while another admitted request reaches later reads", async () => {
const resources = new ArtifactResourceScope();
const reached = gate();
const resume = gate();
const paths: string[] = [];
let blocked = true;
const failure = new RetainedRegularFileCleanupFailure(
new RegularFileCleanupFailure({
path: "owned guidance descriptor",
owner: {
close: async () => {
if (blocked) throw new Error("close denied");
},
} as never,
outcome: { kind: "failed", cause: new Error("read failed") },
cleanupCause: new Error("close denied"),
}),
);
const launched: Promise<unknown>[] = [];
try {
const active = searchGuidanceWithReader(
{ query: "needle" },
(path, child) =>
child.run(async () => {
paths.push(path);
if (path === MANAGED_SKILL_FILES[1]) {
reached.release();
await resume.promise;
}
return content(path);
}),
resources,
);
launched.push(active);
await awaitAcknowledgement(
reached.promise,
active,
"overlapping guidance read",
);
const failing = searchGuidanceWithReader(
{ query: "needle" },
async (_path, child) => {
child.retain(failure.cleanupOwner);
throw failure;
},
resources,
);
launched.push(failing);
const failed = await failing;
if (failed.ok) throw new Error("Expected cleanup failure");
expect(failed.error.cleanupResources).toEqual([
"owned guidance descriptor",
]);
expect(failed.error.cleanupIncomplete).toBe(true);
const closing = resources.close().then(
() => undefined,
(cause: unknown) => cause,
);
launched.push(closing);
resume.release();
expect(await active).toMatchObject({
ok: true,
value: { status: "matches" },
});
expect(paths).toEqual(MANAGED_SKILL_FILES);
expect(await closing).toMatchObject({
cleanup: { resources: ["owned guidance descriptor"] },
});
expect(resources.hasPending).toBe(true);
expect(failed.error.cleanupIncomplete).toBe(true);
blocked = false;
await resources.close();
expect(resources.hasPending).toBe(false);
expect(failed.error.cleanupIncomplete).toBe(false);
} finally {
resume.release();
blocked = false;
await Promise.allSettled(launched);
await resources.close();
}
});
it("refuses new work after shutdown without reporting a missing bundle", async () => {
const resources = new ArtifactResourceScope();
await resources.close();
let reads = 0;
await expect(
searchGuidanceWithReader(
{ query: "needle" },
async (path) => {
reads += 1;
return content(path);
},
resources,
),
).rejects.toMatchObject({ reason: "unavailable" });
expect(reads).toBe(0);
});
+320
View File
@@ -0,0 +1,320 @@
import { expect, it } from "vitest";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { MANAGED_SKILL_FILES } from "./SetupSkill.js";
import { searchGuidanceOutputSchema } from "../contracts/guidanceContracts.js";
import { RetainedRegularFileCleanupFailure } from "./RegularFileRead.js";
import {
readGuidanceSectionWithReader,
searchGuidanceWithReader,
} from "./GuidanceAccess.js";
const moduleGuide = `# JavaScript applications\n\n## Read a module without repeating analysis\n\nFor an exact JavaScript module (javascript-module), select its source path and module identity:\n\n\`\`\`json\n{"node_id":"module:main.js","kind":"javascript-module"}\n\`\`\`\n`;
const guideFixture = async (path: string): Promise<string> =>
path === "references/javascript-applications.md"
? moduleGuide
: `# ${path}\n\nGeneral module guidance for pagination.\n`;
const valueOf = async (
request: ReturnType<typeof searchGuidanceWithReader>,
) => {
const result = await request;
if (!result.ok) throw result.error;
return searchGuidanceOutputSchema.parse(result.value);
};
it("ranks a focused JavaScript module section and retrieves it with its locator", async () => {
const resources = new ArtifactResourceScope();
const result = await valueOf(
searchGuidanceWithReader(
{ query: "exact JavaScript module" },
guideFixture,
resources,
),
);
expect(result.status).toBe("matches");
if (result.status !== "matches") throw new Error("Expected guide matches");
expect(result.matches[0]).toMatchObject({
guide: "references/javascript-applications.md",
heading: "Read a module without repeating analysis",
matched_terms: ["exact", "javascript", "module"],
context: expect.stringContaining("javascript-module"),
});
const selected = result.matches[0];
if (selected === undefined) throw new Error("Expected selected guidance");
const retrieved = await readGuidanceSectionWithReader(
{ locator: selected.locator },
guideFixture,
resources,
);
if (!retrieved.ok) throw retrieved.error;
expect(retrieved.value).toMatchObject({
status: "section",
source: "installed_rea_guidance",
guide: selected.guide,
heading: selected.heading,
locator: selected.locator,
content: expect.stringContaining('"node_id":"module:main.js"'),
});
});
it("returns an honest no-match result and rejects empty queries and invalid bounds", async () => {
const resources = new ArtifactResourceScope();
expect(
await valueOf(
searchGuidanceWithReader(
{ query: "unlikely qzxv term" },
guideFixture,
resources,
),
),
).toMatchObject({
status: "no_matches",
total_matches: 0,
message: expect.stringContaining("does not indicate"),
});
expect(
await valueOf(
searchGuidanceWithReader(
{ query: "how do I frobnicate qzxv" },
guideFixture,
resources,
),
),
).toMatchObject({
status: "no_matches",
total_matches: 0,
});
expect(
await searchGuidanceWithReader({ query: " " }, guideFixture, resources),
).toMatchObject({ ok: false });
expect(
await searchGuidanceWithReader(
{ query: "x".repeat(2049) },
guideFixture,
resources,
),
).toMatchObject({ ok: false });
expect(
await searchGuidanceWithReader(
{ query: "module", limit: 21 },
guideFixture,
resources,
),
).toMatchObject({ ok: false });
});
it("does not treat Markdown headings inside fenced code as section boundaries", async () => {
const result = await valueOf(
searchGuidanceWithReader(
{ query: "phantom-heading-token" },
async (path) =>
`${await guideFixture(path)}\n\n\`\`\`markdown\n# phantom-heading-token\n\`\`\`\n`,
new ArtifactResourceScope(),
),
);
expect(result.status).toBe("matches");
if (result.status !== "matches") throw new Error("Expected guide matches");
expect(result.matches.map(({ heading }) => heading)).not.toContain(
"phantom-heading-token",
);
});
it("refuses an oversized guide bundle instead of indexing partial content", async () => {
const result = await searchGuidanceWithReader(
{ query: "module" },
async (path) =>
path === "SKILL.md" ? "x".repeat(4_194_305) : guideFixture(path),
new ArtifactResourceScope(),
);
expect(result).toMatchObject({
ok: true,
value: { status: "bundle_unavailable" },
});
});
it("paginates deterministically without losing, duplicating, or reordering matches", async () => {
const readGuide = guideFixture;
const first = await valueOf(
searchGuidanceWithReader(
{ query: "module", limit: 2 },
readGuide,
new ArtifactResourceScope(),
),
);
const repeat = await valueOf(
searchGuidanceWithReader(
{ query: "module", limit: 2 },
readGuide,
new ArtifactResourceScope(),
),
);
expect(first).toEqual(repeat);
if (first.status !== "matches") throw new Error("Expected guide matches");
expect(first.has_more).toBe(true);
const all = first.matches.map(({ locator }) => locator);
let cursor = first.next_cursor;
while (cursor !== undefined) {
const page = await valueOf(
searchGuidanceWithReader(
{ query: "module", limit: 2, cursor },
readGuide,
new ArtifactResourceScope(),
),
);
if (page.status !== "matches") throw new Error("Expected guide matches");
all.push(...page.matches.map(({ locator }) => locator));
cursor = page.next_cursor;
}
const complete = await valueOf(
searchGuidanceWithReader(
{ query: "module", limit: 20 },
readGuide,
new ArtifactResourceScope(),
),
);
if (complete.status !== "matches") throw new Error("Expected guide matches");
expect(new Set(all).size).toBe(all.length);
expect(all).toEqual(complete.matches.map(({ locator }) => locator));
});
it("rejects malformed locators and reports changed or missing installed guides", async () => {
const resources = new ArtifactResourceScope();
const search = await valueOf(
searchGuidanceWithReader(
{ query: "exact JavaScript module" },
guideFixture,
resources,
),
);
if (search.status !== "matches") throw new Error("Expected guide matches");
expect(
await readGuidanceSectionWithReader(
{ locator: "../../outside.md" },
guideFixture,
resources,
),
).toMatchObject({ ok: false });
const changedReader = async (path: string) =>
`${await guideFixture(path)}\nChanged after search.\n`;
const stale = await readGuidanceSectionWithReader(
{ locator: search.matches[0]?.locator },
changedReader,
resources,
);
expect(stale).toMatchObject({ ok: true, value: { status: "stale_locator" } });
const firstPage = await valueOf(
searchGuidanceWithReader(
{ query: "module", limit: 1 },
guideFixture,
resources,
),
);
if (firstPage.status !== "matches" || firstPage.next_cursor === undefined)
throw new Error("Expected a continuation token");
expect(
await searchGuidanceWithReader(
{ query: "module", limit: 1, cursor: firstPage.next_cursor },
changedReader,
resources,
),
).toMatchObject({ ok: true, value: { status: "stale_continuation" } });
expect(
await searchGuidanceWithReader(
{ query: "different query", cursor: firstPage.next_cursor },
guideFixture,
resources,
),
).toMatchObject({ ok: false });
expect(
await searchGuidanceWithReader(
{ query: "module", cursor: "garbage" },
guideFixture,
resources,
),
).toMatchObject({ ok: false });
const missingReader = async (): Promise<string> => {
throw Object.assign(new Error("missing"), { code: "ENOENT" });
};
expect(
await searchGuidanceWithReader(
{ query: "module" },
missingReader,
resources,
),
).toMatchObject({ ok: true, value: { status: "bundle_unavailable" } });
expect(
await readGuidanceSectionWithReader(
{ locator: search.matches[0]?.locator },
missingReader,
resources,
),
).toMatchObject({ ok: true, value: { status: "bundle_unavailable" } });
});
it("returns cleanup failures separately and retains their owner until retry succeeds", async () => {
const resources = new ArtifactResourceScope();
let failRelease = true;
const failure = new RegularFileCleanupFailure({
path: "guidance file handle",
owner: {
close: async () => {
if (failRelease) throw new Error("descriptor close failed");
},
} as never,
outcome: { kind: "failed", cause: new Error("guide read failed") },
cleanupCause: new Error("descriptor close failed"),
});
const retainedFailure = new RetainedRegularFileCleanupFailure(failure);
resources.retain(retainedFailure.cleanupOwner);
const result = await searchGuidanceWithReader(
{ query: "module" },
async () => {
throw retainedFailure;
},
resources,
);
if (result.ok) throw new Error("Expected a retained cleanup failure");
expect(result.error).toMatchObject({ cleanupIncomplete: true });
expect(result.error.cleanupResources).toEqual(["guidance file handle"]);
expect(resources.hasPending).toBe(true);
failRelease = false;
await resources.close();
expect(resources.hasPending).toBe(false);
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.cleanupResources).toEqual([]);
const freshResources = new ArtifactResourceScope();
freshResources.retain({
kind: "lease",
resource: "unrelated pending resource",
lease: { release: async () => undefined },
});
expect(
await searchGuidanceWithReader(
{ query: "module" },
async () => {
throw new Error("guide is unreadable");
},
freshResources,
),
).toMatchObject({
ok: true,
value: { status: "bundle_unavailable" },
});
await freshResources.close();
});
it("uses only the fixed managed guide inventory for injected reads", async () => {
const requested: string[] = [];
await searchGuidanceWithReader(
{ query: "module" },
async (path) => {
requested.push(path);
return guideFixture(path);
},
new ArtifactResourceScope(),
);
expect(requested).toEqual([...MANAGED_SKILL_FILES]);
});
+602
View File
@@ -0,0 +1,602 @@
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import type { z } from "zod";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { MANAGED_SKILL_FILES, readSkillFile } from "./SetupSkill.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import {
readGuidanceSectionInputSchema,
readGuidanceSectionOutputSchema,
type ReadGuidanceSectionOutput,
type SearchGuidanceOutput,
searchGuidanceInputSchema,
searchGuidanceOutputSchema,
} from "../contracts/guidanceContracts.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
bindRegularFileCleanup,
RetainedRegularFileCleanupFailure,
} from "./RegularFileRead.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../domain/result.js";
import {
parseGuidanceSections,
type GuideSection,
} from "./GuidanceSections.js";
const SKILL_ROOT = `../../skills/${PRODUCT_IDENTITY.skillName}/`;
const SOURCE = "installed_rea_guidance" as const;
const MAX_GUIDE_FILE_BYTES = 1_048_576;
const MAX_GUIDE_BUNDLE_BYTES = 4_194_304;
const sha256 = (value: string | Uint8Array): string =>
createHash("sha256").update(value).digest("hex");
interface GuidanceBundle {
readonly digest: string;
readonly sections: readonly GuideSection[];
}
const readPackagedGuide = (
relativePath: string,
resources: ArtifactResourceScope,
): Promise<string> =>
readSkillFile(
fileURLToPath(new URL(`${SKILL_ROOT}${relativePath}`, import.meta.url)),
resources,
MAX_GUIDE_FILE_BYTES,
);
type GuidanceBundleRead =
| { readonly kind: "available"; readonly bundle: GuidanceBundle }
| { readonly kind: "unavailable"; readonly message: string }
| { readonly kind: "cleanup"; readonly error: ProviderCleanupError };
const guidanceCleanupFailure = (
cause: unknown,
resources: ArtifactResourceScope,
operation: string,
): ProviderCleanupError => {
const observation = ArtifactReaderFailure.cleanupObservation(
cause,
resources.cleanupResources,
);
const error = new ProviderCleanupError(
"rea",
resources.cleanupResources,
{ phase: "managed_guidance_read", reason: observation.reason },
{
operation,
cause,
cleanup: observation,
remediationAction:
"Close REA so it can retry releasing the retained guidance file handle. Do not repeat the request until cleanup completes.",
},
);
bindRegularFileCleanup(cause, error);
return error;
};
const readBundle = async (
readGuide: (relativePath: string) => Promise<string>,
resources: ArtifactResourceScope,
operation: string,
): Promise<GuidanceBundleRead> => {
const files = new Map<string, string>();
let bundleBytes = 0;
try {
for (const relativePath of MANAGED_SKILL_FILES) {
const content = await readGuide(relativePath);
bundleBytes += Buffer.byteLength(content, "utf8");
if (bundleBytes > MAX_GUIDE_BUNDLE_BYTES)
return {
kind: "unavailable",
message:
"Installed REA guidance exceeds the 4 MiB bundle allowance; no partial bundle was indexed.",
};
files.set(relativePath, content.replaceAll("\r\n", "\n"));
}
} catch (cause: unknown) {
return cause instanceof RetainedRegularFileCleanupFailure ||
(cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
? {
kind: "cleanup",
error: guidanceCleanupFailure(cause, resources, operation),
}
: {
kind: "unavailable",
message:
"Installed REA guidance is missing or unreadable; no complete bundle could be indexed.",
};
}
const digest = sha256(
[...files]
.map(([path, content]) => `${path}\0${sha256(content)}\n`)
.join(""),
);
const sections = parseGuidanceSections(files);
if (sections === undefined)
return {
kind: "unavailable",
message:
"Installed REA guidance exceeds the 4,096-section or 25,165,824 expanded UTF-16-code-unit index allowance; no partial index was used.",
};
return { kind: "available", bundle: { digest, sections } };
};
const words = (text: string): string[] => [
...new Set(text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []),
];
const GENERIC_QUERY_WORDS: ReadonlySet<string> = new Set([
"a",
"about",
"an",
"and",
"are",
"as",
"at",
"be",
"been",
"by",
"can",
"could",
"did",
"do",
"does",
"for",
"from",
"how",
"i",
"in",
"is",
"it",
"my",
"of",
"on",
"or",
"should",
"so",
"that",
"the",
"their",
"there",
"they",
"this",
"to",
"was",
"we",
"were",
"what",
"when",
"where",
"which",
"who",
"why",
"will",
"with",
"would",
"you",
"your",
]);
const sectionLocator = (
bundle: GuidanceBundle,
section: GuideSection,
): string => {
const identity = JSON.stringify({
v: 1,
bundle: bundle.digest,
guide: section.guide,
ordinal: section.ordinal,
section: sha256(section.content),
});
return `rea-guide-v1.${Buffer.from(identity).toString("base64url")}`;
};
type GuidanceReader = (
relativePath: string,
resources: ArtifactResourceScope,
) => Promise<string>;
type GuidanceResult<Value> = Result<
Value,
AnalysisInputError | ProviderCleanupError
>;
/** Admit the complete request once; file reads belong to its independent child. */
const runGuidanceOperation = async <Value>(
resources: ArtifactResourceScope,
operationName: string,
operation: (child: ArtifactResourceScope) => Promise<GuidanceResult<Value>>,
): Promise<GuidanceResult<Value>> => {
try {
return await resources.run(async () => {
const child = new ArtifactResourceScope();
try {
return await operation(child);
} finally {
if (child.hasPending)
resources.retain({
kind: "lease",
resource: "managed guidance operation",
lease: new AnalysisCleanupLease(() => child.close(), {
get cleanupResources() {
return child.cleanupResources;
},
}),
});
else await child.close();
}
});
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
return err(guidanceCleanupFailure(cause, resources, operationName));
throw cause;
}
};
/** Reader seams receive the operation child rather than re-entering the root. */
export const searchGuidanceWithReader = (
input: unknown,
readGuide: GuidanceReader,
resources: ArtifactResourceScope,
): Promise<GuidanceResult<SearchGuidanceOutput>> =>
runGuidanceOperation(resources, "search_guidance", (child) =>
searchGuidanceInScope(input, (path) => readGuide(path, child), child),
);
export const readGuidanceSectionWithReader = (
input: unknown,
readGuide: GuidanceReader,
resources: ArtifactResourceScope,
): Promise<GuidanceResult<ReadGuidanceSectionOutput>> =>
runGuidanceOperation(resources, "read_guidance_section", (child) =>
readGuidanceSectionInScope(input, (path) => readGuide(path, child), child),
);
const invalidInput = (
operation: string,
input: unknown,
issues: readonly z.core.$ZodIssue[],
): Result<never, AnalysisInputError> =>
err(analysisInputErrorFromIssues(operation, issues, input));
export const searchGuidance = async (
input: unknown,
resources: ArtifactResourceScope,
): Promise<
Result<SearchGuidanceOutput, AnalysisInputError | ProviderCleanupError>
> => searchGuidanceWithReader(input, readPackagedGuide, resources);
/** Reader seam keeps packaged-guide drift and missing-file behavior testable. */
const searchGuidanceInScope = async (
input: unknown,
readGuide: (relativePath: string) => Promise<string>,
resources: ArtifactResourceScope,
): Promise<
Result<SearchGuidanceOutput, AnalysisInputError | ProviderCleanupError>
> => {
const parsed = searchGuidanceInputSchema.safeParse(input);
if (!parsed.success)
return invalidInput("search_guidance", input, parsed.error.issues);
const cursor =
parsed.data.cursor === undefined
? undefined
: decodeGuidanceCursor(parsed.data.cursor);
if (parsed.data.cursor !== undefined && cursor === undefined)
return invalidInput("search_guidance", input, [
{
code: "custom",
path: ["cursor"],
message: "Invalid guidance continuation token",
},
]);
const queryDigest = sha256(parsed.data.query);
if (cursor !== undefined && cursor.query !== queryDigest)
return invalidInput("search_guidance", input, [
{
code: "custom",
path: ["cursor"],
message: "The continuation token belongs to a different query",
},
]);
const loaded = await readBundle(readGuide, resources, "search_guidance");
if (loaded.kind === "cleanup") return err(loaded.error);
if (loaded.kind === "unavailable")
return ok(
searchGuidanceOutputSchema.parse({
status: "bundle_unavailable",
source: SOURCE,
content_kind: "documentation",
message: loaded.message,
}),
);
const bundle = loaded.bundle;
if (cursor !== undefined && cursor.bundle !== bundle.digest)
return ok(
searchGuidanceOutputSchema.parse({
status: "stale_continuation",
source: SOURCE,
content_kind: "documentation",
message:
"The installed guidance changed since this search page was issued. Restart the search to continue with current content.",
}),
);
const queryWords = words(parsed.data.query);
const specificQueryWords = queryWords.filter(
(word) => !GENERIC_QUERY_WORDS.has(word),
);
const queryWordsToMatch = specificQueryWords;
const ranked = bundle.sections
.flatMap((section) => {
const sectionWords = new Set(words(section.content));
const matchedSpecific = queryWordsToMatch.filter((word) =>
sectionWords.has(word),
);
if (
queryWordsToMatch.length === 0 ||
matchedSpecific.length !== queryWordsToMatch.length
)
return [];
const matched = queryWords.filter((word) => sectionWords.has(word));
const headingWords = new Set(words(section.heading));
const score =
matched.length +
matched.filter((word) => headingWords.has(word)).length * 2;
return {
section,
matched,
score,
wordCount: sectionWords.size,
};
})
.sort(
(left, right) =>
right.score - left.score ||
// Prefer a focused recipe over a long overview when term scores tie.
left.wordCount - right.wordCount ||
compareText(left.section.guide, right.section.guide) ||
left.section.ordinal - right.section.ordinal,
);
if (ranked.length === 0)
return ok(
searchGuidanceOutputSchema.parse({
status: "no_matches",
source: SOURCE,
content_kind: "documentation",
query: parsed.data.query,
message:
"No bundled guidance sections matched this query. This does not indicate whether an analysis capability is available.",
total_matches: 0,
}),
);
const offset = cursor?.offset ?? 0;
if (cursor !== undefined && offset >= ranked.length)
return invalidInput("search_guidance", input, [
{
code: "custom",
path: ["cursor"],
message:
"The continuation token does not select a remaining result page",
},
]);
const page = ranked.slice(offset, offset + parsed.data.limit);
const hasMore = offset + page.length < ranked.length;
return ok(
searchGuidanceOutputSchema.parse({
status: "matches",
source: SOURCE,
content_kind: "documentation",
package: `${PRODUCT_IDENTITY.packageName}@${PRODUCT_IDENTITY.packageVersion}`,
bundle_sha256: bundle.digest,
query: parsed.data.query,
matches: page.map(({ section, matched }) => ({
guide: section.guide,
heading: section.heading,
locator: sectionLocator(bundle, section),
matched_terms: matched,
relevance: `Matched ${matched.join(", ")} in this section.`,
context: contextFor(section.content, matched),
})),
total_matches: ranked.length,
offset,
limit: parsed.data.limit,
has_more: hasMore,
...(hasMore
? {
next_cursor: encodeGuidanceCursor({
v: 1,
bundle: bundle.digest,
query: queryDigest,
offset: offset + page.length,
}),
}
: {}),
}),
);
};
export const readGuidanceSection = async (
input: unknown,
resources: ArtifactResourceScope,
): Promise<
Result<ReadGuidanceSectionOutput, AnalysisInputError | ProviderCleanupError>
> => readGuidanceSectionWithReader(input, readPackagedGuide, resources);
/** Reader seam keeps packaged-guide drift and missing-file behavior testable. */
const readGuidanceSectionInScope = async (
input: unknown,
readGuide: (relativePath: string) => Promise<string>,
resources: ArtifactResourceScope,
): Promise<
Result<ReadGuidanceSectionOutput, AnalysisInputError | ProviderCleanupError>
> => {
const parsed = readGuidanceSectionInputSchema.safeParse(input);
if (!parsed.success)
return invalidInput("read_guidance_section", input, parsed.error.issues);
const identity = decodeLocator(parsed.data.locator);
if (identity === undefined)
return invalidInput("read_guidance_section", input, [
{
code: "custom",
path: ["locator"],
message: "Malformed or unsupported guidance locator",
},
]);
const loaded = await readBundle(
readGuide,
resources,
"read_guidance_section",
);
if (loaded.kind === "cleanup") return err(loaded.error);
if (loaded.kind === "unavailable")
return ok(
readGuidanceSectionOutputSchema.parse({
status: "bundle_unavailable",
source: SOURCE,
content_kind: "documentation",
message: loaded.message,
}),
);
const bundle = loaded.bundle;
if (identity.bundle !== bundle.digest)
return ok(
readGuidanceSectionOutputSchema.parse({
status: "stale_locator",
source: SOURCE,
content_kind: "documentation",
message:
"The installed guidance changed since this locator was issued. Search again for a current locator.",
}),
);
const section = bundle.sections.find(
(candidate) =>
candidate.guide === identity.guide &&
candidate.ordinal === identity.ordinal &&
sha256(candidate.content) === identity.section,
);
if (section === undefined)
return ok(
readGuidanceSectionOutputSchema.parse({
status: "stale_locator",
source: SOURCE,
content_kind: "documentation",
message:
"The selected section is no longer present in the installed guidance. Search again.",
}),
);
return ok(
readGuidanceSectionOutputSchema.parse({
status: "section",
source: SOURCE,
content_kind: "documentation",
package: `${PRODUCT_IDENTITY.packageName}@${PRODUCT_IDENTITY.packageVersion}`,
bundle_sha256: bundle.digest,
guide: section.guide,
heading: section.heading,
locator: parsed.data.locator,
content: section.content,
}),
);
};
interface LocatorIdentity {
readonly v: 1;
readonly bundle: string;
readonly guide: string;
readonly ordinal: number;
readonly section: string;
}
interface GuidanceCursor {
readonly v: 1;
readonly bundle: string;
readonly query: string;
readonly offset: number;
}
const encodeGuidanceCursor = (cursor: GuidanceCursor): string =>
`rea-guidance-page-v1.${Buffer.from(JSON.stringify(cursor)).toString("base64url")}`;
const decodeGuidanceCursor = (cursor: string): GuidanceCursor | undefined => {
const prefix = "rea-guidance-page-v1.";
if (!cursor.startsWith(prefix)) return undefined;
try {
const value: unknown = JSON.parse(
Buffer.from(cursor.slice(prefix.length), "base64url").toString("utf8"),
);
if (typeof value !== "object" || value === null) return undefined;
const item = value as Partial<GuidanceCursor>;
if (
item.v !== 1 ||
typeof item.bundle !== "string" ||
!/^[a-f0-9]{64}$/u.test(item.bundle) ||
typeof item.query !== "string" ||
!/^[a-f0-9]{64}$/u.test(item.query) ||
typeof item.offset !== "number" ||
!Number.isInteger(item.offset) ||
item.offset < 1
)
return undefined;
return item as GuidanceCursor;
} catch {
return undefined;
}
};
const decodeLocator = (locator: string): LocatorIdentity | undefined => {
if (!locator.startsWith("rea-guide-v1.")) return undefined;
try {
const value: unknown = JSON.parse(
Buffer.from(locator.slice("rea-guide-v1.".length), "base64url").toString(
"utf8",
),
);
if (typeof value !== "object" || value === null) return undefined;
const item = value as Partial<LocatorIdentity>;
if (
item.v !== 1 ||
typeof item.bundle !== "string" ||
!/^[a-f0-9]{64}$/u.test(item.bundle) ||
typeof item.guide !== "string" ||
!(MANAGED_SKILL_FILES as readonly string[]).includes(item.guide) ||
!Number.isInteger(item.ordinal) ||
typeof item.section !== "string" ||
!/^[a-f0-9]{64}$/u.test(item.section)
)
return undefined;
return item as LocatorIdentity;
} catch {
return undefined;
}
};
const contextFor = (content: string, matched: readonly string[]): string => {
const lines = content.split("\n");
const heading = lines[0] ?? "";
const terms = new Set(matched);
let bestIndex = -1;
let bestScore = 0;
lines.slice(1).forEach((line, index) => {
const score = words(line).filter((word) => terms.has(word)).length;
if (score > bestScore) {
bestScore = score;
bestIndex = index + 1;
}
});
const excerptStart = bestIndex < 0 ? 1 : Math.max(1, bestIndex - 1);
const excerptEnd = bestIndex < 0 ? 4 : bestIndex + 2;
return [heading, ...lines.slice(excerptStart, excerptEnd)]
.join(" ")
.replaceAll(/\s+/gu, " ")
.slice(0, 500);
};
const compareText = (left: string, right: string): number =>
left < right ? -1 : left > right ? 1 : 0;
+123
View File
@@ -0,0 +1,123 @@
/** Parsed documentation facts; callers never reinterpret Markdown boundaries. */
export interface GuideSection {
readonly guide: string;
readonly heading: string;
readonly content: string;
readonly ordinal: number;
}
// The shipped bundle has tens of headings. These ceilings bound object count
// and UTF-16 text retained by a complete index before allocating section text.
const MAX_SECTIONS = 4096;
// ATX has at most six enclosing levels over the admitted 4 MiB bundle.
const MAX_EXPANDED_CHARACTERS = 6 * 4_194_304;
const blank = (value: string | undefined): boolean =>
value === " " || value === "\t";
/** ATX closing hashes and ASCII whitespace, scanned once from each end. */
const headingTitle = (raw: string): string => {
let start = 0;
let end = raw.length;
while (start < end && blank(raw[start])) start += 1;
while (end > start && blank(raw[end - 1])) end -= 1;
let hashes = end;
while (hashes > start && raw[hashes - 1] === "#") hashes -= 1;
if (hashes < end && (hashes === start || blank(raw[hashes - 1]))) {
end = hashes;
while (end > start && blank(raw[end - 1])) end -= 1;
}
return raw.slice(start, end);
};
interface Heading {
readonly level: number;
readonly title: string;
readonly start: number;
end: number;
}
const atxHeading = (
line: string,
): Pick<Heading, "level" | "title"> | undefined => {
const match = /^ {0,3}(#{1,6})(?:[ \t]+(.*)|$)/u.exec(line);
if (match?.[1] === undefined) return undefined;
const raw = match[2] ?? "";
// Closing hashes need whitespace; literal C# and escaped hashes survive.
const title = headingTitle(raw);
return { level: match[1].length, title };
};
const readHeadings = (
content: string,
remaining: number,
): Heading[] | undefined => {
const headings: Heading[] = [];
const enclosing: Heading[] = [];
let fence: { marker: string; length: number } | undefined;
let start = 0;
while (start < content.length) {
const newline = content.indexOf("\n", start);
const end = newline < 0 ? content.length : newline;
const line = content.slice(start, end);
const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(line);
const marker = fenceMatch?.[1];
if (fence !== undefined) {
if (
marker?.[0] === fence.marker &&
marker.length >= fence.length &&
/^[ \t]*$/u.test(fenceMatch?.[2] ?? "")
)
fence = undefined;
} else if (
marker !== undefined &&
!(marker[0] === "`" && fenceMatch?.[2]?.includes("`"))
) {
fence = { marker: marker[0] ?? "", length: marker.length };
} else {
const parsed = atxHeading(line);
if (parsed !== undefined) {
if (headings.length >= remaining) return undefined;
while ((enclosing.at(-1)?.level ?? 0) >= parsed.level) {
const parent = enclosing.pop();
if (parent !== undefined) parent.end = start;
}
const heading = { ...parsed, start, end: content.length };
headings.push(heading);
enclosing.push(heading);
}
}
start = newline < 0 ? content.length : newline + 1;
}
return headings;
};
/** Bound the whole bundle before materializing an expanded section index. */
export const parseGuidanceSections = (
files: ReadonlyMap<string, string>,
): readonly GuideSection[] | undefined => {
const documents: { guide: string; content: string; headings: Heading[] }[] =
[];
let count = 0;
let expanded = 0;
for (const [guide, content] of files) {
const headings = readHeadings(content, MAX_SECTIONS - count);
if (headings === undefined) return undefined;
count += headings.length;
for (const heading of headings) {
expanded += heading.end - heading.start;
if (expanded > MAX_EXPANDED_CHARACTERS) return undefined;
}
documents.push({ guide, content, headings });
}
const sections: GuideSection[] = [];
for (const { guide, content, headings } of documents)
for (const [ordinal, heading] of headings.entries())
sections.push({
guide,
ordinal,
heading: heading.title,
content: content.slice(heading.start, heading.end).trim(),
});
return sections;
};
+10 -4
View File
@@ -43,21 +43,27 @@ export const isManagedSkillManifest = (content: string): boolean =>
export const readSkillFile = (
path: string,
resources?: ArtifactResourceScope,
maxBytes = Number.MAX_SAFE_INTEGER,
): Promise<string> =>
readSkillFileSnapshot(path, resources).then(({ bytes }) =>
readSkillFileSnapshot(path, resources, maxBytes).then(({ bytes }) =>
bytes.toString("utf8"),
);
const readSkillFileSnapshot = async (
path: string,
resources?: ArtifactResourceScope,
maxBytes = Number.MAX_SAFE_INTEGER,
): Promise<SkillFileSnapshot> =>
withRegularFile(
path,
async (handle, stats) => {
const bytes = await readBoundedFileBytes(handle, stats.size, {
expectedSize: stats.size,
});
const bytes = await readBoundedFileBytes(
handle,
Math.min(stats.size, maxBytes),
{
expectedSize: stats.size,
},
);
if (bytes === undefined || bytes.length !== stats.size)
throw new RegularFileChangedError(path);
return { bytes, mode: stats.mode & 0o7777, state: stats };
+19 -1
View File
@@ -79,9 +79,26 @@ import { excludeDeclaredSensitiveArguments } from "./cli/explicitSensitiveArgume
import { rejectSurplusCliArguments } from "./cli/strictPositionals.js";
import { presentCliArgumentErrors } from "./cli/argumentErrors.js";
import { completionHelpNote, createCliCompletions } from "./cli/completions.js";
import {
closeCliGuidanceResources,
registerGuidanceCommands,
} from "./cli/guidanceCommands.js";
import { closeCliJavaScriptResources } from "./cli/javascriptApplicationAnalysis.js";
import { registerGuidedWorkflowCommands } from "./cli/guidedWorkflowCommands.js";
export { closeCliJavaScriptResources } from "./cli/javascriptApplicationAnalysis.js";
/** Close every process-owned resource used by CLI commands. */
export const closeCliResources = async (): Promise<void> => {
const results = await Promise.allSettled([
closeCliJavaScriptResources(),
closeCliGuidanceResources(),
]);
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
);
if (failures.length === 1) throw failures[0];
if (failures.length > 1)
throw new AggregateError(failures, "CLI resource cleanup failed");
};
/**
* Build the one-shot Incur CLI without starting Hopper at import time.
@@ -153,6 +170,7 @@ export const createCli = (
const cli = timedCli as unknown as CliInstance;
registerSetupCommands(cli, logger, environment);
registerGuidanceCommands(cli, logger);
registerGuidedWorkflowCommands(cli, logger);
registerReverseEngineeringCommands(cli, logger, environment);
registerCutterCommands(cli, logger, environment);
+120
View File
@@ -0,0 +1,120 @@
import { z } from "incur";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import {
readGuidanceSection,
searchGuidance,
} from "../application/GuidanceAccess.js";
import {
readGuidanceSectionInputSchema,
searchGuidanceLimitSchema,
searchGuidanceInputSchema,
} from "../contracts/guidanceContracts.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
let retainedGuidanceResources: ArtifactResourceScope | undefined;
const guidanceResources = (): ArtifactResourceScope => {
retainedGuidanceResources ??= new ArtifactResourceScope();
return retainedGuidanceResources;
};
/** Release CLI-owned guidance descriptors, retaining failed cleanup for retry. */
export const closeCliGuidanceResources = async (): Promise<void> => {
const resources = retainedGuidanceResources;
if (resources === undefined) return;
await resources.close();
if (retainedGuidanceResources === resources)
retainedGuidanceResources = undefined;
};
/** Register offline search and retrieval for the installed REA guide bundle. */
export const registerGuidanceCommands = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.guidance, {
description: "Search or read the installed REA workflow guidance",
args: z.object({}),
options: z
.object({
action: z
.enum(["search", "read"])
.default("search")
.describe("Guidance action to perform: search or read"),
query: searchGuidanceInputSchema.shape.query
.optional()
.describe(
"Required with search; all non-generic query words must match",
),
locator: readGuidanceSectionInputSchema.shape.locator
.optional()
.describe("Required with --action read; locator returned by search"),
limit: searchGuidanceLimitSchema
.optional()
.describe("Maximum search matches; search only, defaults to 8"),
cursor: searchGuidanceInputSchema.shape.cursor
.optional()
.describe("Continue a search page; search only"),
})
.superRefine((options, context) => {
if (options.action === "search") {
if (options.query === undefined)
context.addIssue({
code: "custom",
path: ["query"],
message: "--query is required with --action search",
});
if (options.locator !== undefined)
context.addIssue({
code: "custom",
path: ["locator"],
message: "--locator can only be used with --action read",
});
} else {
if (options.locator === undefined)
context.addIssue({
code: "custom",
path: ["locator"],
message: "--locator is required with --action read",
});
for (const option of ["query", "limit", "cursor"] as const)
if (options[option] !== undefined)
context.addIssue({
code: "custom",
path: [option],
message: `--${option} can only be used with --action search`,
});
}
}),
run: ({ options }) =>
logCliCommand(logger, CLI_COMMANDS.guidance, async () => {
const result =
options.action === "search"
? await searchGuidance(
{
...(options.query === undefined
? {}
: { query: options.query }),
...(options.limit === undefined
? {}
: { limit: options.limit }),
...(options.cursor === undefined
? {}
: { cursor: options.cursor }),
},
guidanceResources(),
)
: await readGuidanceSection(
{
locator: options.locator,
},
guidanceResources(),
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
});
};
+1
View File
@@ -29,6 +29,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectNativeDispatchMetadata: "inspect-native-dispatch-metadata",
capabilities: "capabilities",
providers: "providers",
guidance: "guidance",
guidedWorkflows: "guided-workflows",
function: "function",
annotateNativeFunction: "annotate-native-function",
+31
View File
@@ -27,6 +27,12 @@ import { appleApplicationProjectionResultSchema } from "../domain/apple/appleApp
import { harmonyApplicationProjectionResultSchema } from "../domain/harmony/harmonyApplication.js";
import type { ToolContract } from "./toolContractTypes.js";
import { toolContractMetadata } from "./toolEffects.js";
import {
readGuidanceSectionInputSchema,
readGuidanceSectionOutputSchema,
searchGuidanceInputSchema,
searchGuidanceOutputSchema,
} from "./guidanceContracts.js";
import {
guidedWorkflowsInputSchema,
guidedWorkflowsOutputSchema,
@@ -73,6 +79,21 @@ const retained = (evidence_id: string) => ({
/** Provider-neutral graph workflow contracts shared by MCP and CLI adapters. */
export const APPLICATION_TOOL_CONTRACTS = [
{
name: "search_guidance",
...toolContractMetadata("search_guidance"),
description:
"Search only the installed REA workflow guidance. Every non-generic query word must occur in a section. Returns ranked sections, bounded pages, and locators for read_guidance_section. No target or provider is accessed.",
kind: "application",
inputSchema: searchGuidanceInputSchema,
outputSchema: searchGuidanceOutputSchema,
examples: [
{
title: "Find exact JavaScript module guidance",
input: { query: "exact JavaScript module" },
},
],
},
{
name: "guided_workflows",
...toolContractMetadata("guided_workflows"),
@@ -96,6 +117,16 @@ export const APPLICATION_TOOL_CONTRACTS = [
},
],
},
{
name: "read_guidance_section",
...toolContractMetadata("read_guidance_section"),
description:
"Retrieve a section selected by search_guidance from the installed REA guidance bundle. Reports stale locators and unavailable bundles explicitly. Returns documentation, not target observations or Evidence.",
kind: "application",
inputSchema: readGuidanceSectionInputSchema,
outputSchema: readGuidanceSectionOutputSchema,
examples: [],
},
{
name: "trace_application_feature",
...toolContractMetadata("trace_application_feature"),
+91
View File
@@ -0,0 +1,91 @@
import { z } from "zod";
export const searchGuidanceLimitSchema = z.number().int().min(1).max(20);
export const searchGuidanceInputSchema = z.strictObject({
query: z
.string()
.min(1)
.max(2048)
.describe(
"Lexical query: every non-generic word must occur in the section; generic-only queries return no matches.",
)
.refine((value) => value.trim().length > 0),
limit: searchGuidanceLimitSchema.default(8),
cursor: z.string().min(1).max(4096).optional(),
});
export const readGuidanceSectionInputSchema = z.strictObject({
locator: z.string().min(1).max(4096),
});
export const searchGuidanceOutputSchema = z.discriminatedUnion("status", [
z.strictObject({
status: z.literal("matches"),
source: z.literal("installed_rea_guidance"),
content_kind: z.literal("documentation"),
package: z.string(),
bundle_sha256: z.string().length(64),
query: z.string(),
matches: z.array(
z.strictObject({
guide: z.string(),
heading: z.string(),
locator: z.string(),
matched_terms: z.array(z.string()),
relevance: z.string(),
context: z.string(),
}),
),
total_matches: z.number().int().nonnegative(),
offset: z.number().int().nonnegative(),
limit: z.number().int().positive(),
has_more: z.boolean(),
next_cursor: z.string().min(1).max(4096).optional(),
}),
z.strictObject({
status: z.literal("no_matches"),
source: z.literal("installed_rea_guidance"),
content_kind: z.literal("documentation"),
query: z.string(),
message: z.string(),
total_matches: z.literal(0),
}),
z.strictObject({
status: z.literal("bundle_unavailable"),
source: z.literal("installed_rea_guidance"),
content_kind: z.literal("documentation"),
message: z.string(),
}),
z.strictObject({
status: z.literal("stale_continuation"),
source: z.literal("installed_rea_guidance"),
content_kind: z.literal("documentation"),
message: z.string(),
}),
]);
export const readGuidanceSectionOutputSchema = z.discriminatedUnion("status", [
z.strictObject({
status: z.literal("section"),
source: z.literal("installed_rea_guidance"),
content_kind: z.literal("documentation"),
package: z.string(),
bundle_sha256: z.string().length(64),
guide: z.string(),
heading: z.string(),
locator: z.string(),
content: z.string(),
}),
z.strictObject({
status: z.enum(["stale_locator", "bundle_unavailable"]),
source: z.literal("installed_rea_guidance"),
content_kind: z.literal("documentation"),
message: z.string(),
}),
]);
export type SearchGuidanceOutput = z.output<typeof searchGuidanceOutputSchema>;
export type ReadGuidanceSectionOutput = z.output<
typeof readGuidanceSectionOutputSchema
>;
+4
View File
@@ -704,6 +704,8 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
trace_application_feature: evidence,
trace_javascript_semantics: evidence,
compare_application_versions: evidence,
search_guidance: effects(),
read_guidance_section: effects(),
guided_workflows: effects(),
compare_source_to_bundle: evidence,
compare_javascript_export_shapes: evidence,
@@ -793,6 +795,8 @@ export const TARGET_CONTENT_FREE_TOOLS: ReadonlySet<string> = new Set([
"inspect_analysis_settings",
"update_analysis_settings",
"export_evidence_bundle",
"search_guidance",
"read_guidance_section",
"guided_workflows",
"record_unknown",
]);
+2
View File
@@ -10,6 +10,7 @@ import { registerEnvironmentBundleTool } from "./registerApplicationTools/enviro
import { registerReconstructionObligationLedgerTool } from "./registerApplicationTools/obligationLedger.js";
import { registerProjectMobileApplicationGraphTools } from "./registerApplicationTools/projectMobileApplicationGraph.js";
import type { ApplicationToolRegistration } from "./registerApplicationTools/types.js";
import { registerGuidanceTools } from "./registerGuidanceTools.js";
import { registerGuidedWorkflowTool } from "./registerGuidedWorkflowTool.js";
/** Register provider-neutral JavaScript application graph workflows. */
@@ -17,6 +18,7 @@ export const registerApplicationTools = (
server: EvidenceMcpServer,
options: ApplicationToolRegistration,
): void => {
registerGuidanceTools(server, options.artifactResources);
registerGuidedWorkflowTool(server);
registerTraceFeatureTool(server, options);
registerTraceJavaScriptSemanticsTool(server, options);
+1 -1
View File
@@ -9,7 +9,7 @@ import type { WithAdmittedAnalysis } from "../analysisAdmission.js";
/** Shared services for registering JavaScript application graph workflows. */
export interface ApplicationToolRegistration {
readonly artifactResources?: ArtifactResourceScope;
readonly artifactResources: ArtifactResourceScope;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
readonly logger: Logger;
readonly evidenceById: EvidenceReader["evidenceById"] | undefined;
+45
View File
@@ -0,0 +1,45 @@
import {
readGuidanceSection,
searchGuidance,
} from "../application/GuidanceAccess.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { toolContract } from "../contracts/toolContracts.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Register provider-free search and retrieval over the installed guide bundle. */
export const registerGuidanceTools = (
server: EvidenceMcpServer,
resources: ArtifactResourceScope,
): void => {
const search = toolContract("search_guidance");
server.registerTool(
search.name,
applicationToolRegistrationOptions(search),
async (input) => {
const result = await searchGuidance(input, resources);
return server.delivery.toCallToolResult(
result.ok
? { ok: true, value: jsonValueSchema.parse(result.value) }
: result,
search,
);
},
);
const read = toolContract("read_guidance_section");
server.registerTool(
read.name,
applicationToolRegistrationOptions(read),
async (input) => {
const result = await readGuidanceSection(input, resources);
return server.delivery.toCallToolResult(
result.ok
? { ok: true, value: jsonValueSchema.parse(result.value) }
: result,
read,
);
},
);
};
@@ -0,0 +1,39 @@
import { afterEach, expect, it, vi } from "vitest";
import { closeCliResources } from "../../../src/cli.js";
import * as javascript from "../../../src/cli/javascriptApplicationAnalysis.js";
import * as guidance from "../../../src/cli/guidanceCommands.js";
afterEach(() => vi.restoreAllMocks());
it.each([
[false, false],
[true, false],
[false, true],
[true, true],
])(
"settles both CLI owners when JavaScript fails=%s and guidance fails=%s",
async (jsFails, guideFails) => {
const jsError = new Error("JavaScript resources retained");
const guideError = new Error("Guidance descriptor retained");
const js = vi
.spyOn(javascript, "closeCliJavaScriptResources")
.mockImplementation(async () => {
if (jsFails) throw jsError;
});
const guides = vi
.spyOn(guidance, "closeCliGuidanceResources")
.mockImplementation(async () => {
if (guideFails) throw guideError;
});
const closing = closeCliResources();
if (jsFails && guideFails)
await expect(closing).rejects.toMatchObject({
errors: [jsError, guideError],
});
else if (jsFails || guideFails)
await expect(closing).rejects.toBe(jsFails ? jsError : guideError);
else await expect(closing).resolves.toBeUndefined();
expect(js).toHaveBeenCalledOnce();
expect(guides).toHaveBeenCalledOnce();
},
);
@@ -8,6 +8,7 @@ import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js";
/** Canonical MCP operations reached by CLI commands with different names. */
const CLI_COMMAND_TOOL_ALIASES = Object.freeze({
"environment-build": ["export_environment_bundle"],
guidance: ["search_guidance", "read_guidance_section"],
analyze: ["binary_overview"],
inspect: ["binary_overview"],
decompile: ["procedure_pseudo_code"],
+7 -7
View File
@@ -105,7 +105,7 @@ describe("doctor configuration dispatch", () => {
const imported = "CLI command runtime imported";
await installCliRuntime(
workspace,
`process.stderr.write(${JSON.stringify(`${imported}\n`)}); export const closeCliJavaScriptResources = () => {}; export const createCli = () => ({ serve: (args, options) => options.stdout(JSON.stringify({ adapter: "cli", args })) });\n`,
`process.stderr.write(${JSON.stringify(`${imported}\n`)}); export const closeCliResources = () => {}; export const createCli = () => ({ serve: (args, options) => options.stdout(JSON.stringify({ adapter: "cli", args })) });\n`,
);
// Both token ownership and output/configuration admission use production code.
await workspace.write(
@@ -194,7 +194,7 @@ describe("executable dispatcher", () => {
async ({ processes, workspace }) => {
await installCliRuntime(
workspace,
'export const closeCliJavaScriptResources = () => {}; export const createCli = () => ({ serve: (_args, options) => { options.stdout("first document\\n"); throw new RangeError("Invalid string length"); } });\n',
'export const closeCliResources = () => {}; export const createCli = () => ({ serve: (_args, options) => { options.stdout("first document\\n"); throw new RangeError("Invalid string length"); } });\n',
'export const createCliResultOutput = () => ({ handled: false, failed: false, captureSdkOutput: () => false, finishSdkOutput: async () => {}, recoverFormattingFailure: () => { process.stdout.write("unexpected second document"); return true; } });\n',
);
const result = await processes.run(
@@ -213,7 +213,7 @@ describe("executable dispatcher", () => {
async ({ processes, workspace }) => {
await installCliRuntime(
workspace,
'export const closeCliJavaScriptResources = () => {}; export const createCli = () => ({ serve: (args) => process.stdout.write(JSON.stringify({ adapter: "cli", args })) });\n',
'export const closeCliResources = () => {}; export const createCli = () => ({ serve: (args) => process.stdout.write(JSON.stringify({ adapter: "cli", args })) });\n',
);
const scripts = workspace.path("scripts");
await workspace.mkdir("dist/mcp");
@@ -335,7 +335,7 @@ describe("executable dispatcher JavaScript shutdown", () => {
async (cleanupFails, { processes, workspace }) => {
await installCliRuntime(
workspace,
`export const closeCliJavaScriptResources = async () => { ${cleanupFails ? 'throw new Error("worker still owned");' : ""} }; export const createCli = () => ({ serve: () => { throw Object.assign(new Error("broken pipe"), { code: "EPIPE" }); } });\n`,
`export const closeCliResources = async () => { ${cleanupFails ? 'throw new Error("worker still owned");' : ""} }; export const createCli = () => ({ serve: () => { throw Object.assign(new Error("broken pipe"), { code: "EPIPE" }); } });\n`,
);
const result = await processes.run(
process.execPath,
@@ -368,7 +368,7 @@ describe("executable dispatcher JavaScript shutdown", () => {
: 'process.stdout.write("complete document\\n");';
await installCliRuntime(
workspace,
`export const closeCliJavaScriptResources = () => { globalThis.cleaned = true; ${cleanupFails ? 'return Promise.reject(new Error("worker still owned"));' : ""} }; export const createCli = () => ({ serve: (_args, options) => { options.stdout("pending document"); options.exit(0); } });\n`,
`export const closeCliResources = () => { globalThis.cleaned = true; ${cleanupFails ? 'return Promise.reject(new Error("worker still owned"));' : ""} }; export const createCli = () => ({ serve: (_args, options) => { options.stdout("pending document"); options.exit(0); } });\n`,
`export const createCliResultOutput = () => ({ handled: false, failed: false, captureSdkOutput: () => true, finishSdkOutput: async () => { if (!globalThis.cleaned) throw new Error("output preceded cleanup"); await new Promise(resolve => setTimeout(resolve, 10)); ${finish} } });\n`,
);
const result = await processes.run(
@@ -395,7 +395,7 @@ describe("executable dispatcher JavaScript shutdown", () => {
async ({ processes, workspace }) => {
await installCliRuntime(
workspace,
'export const closeCliJavaScriptResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { options.exit(0); } });\n',
'export const closeCliResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { options.exit(0); } });\n',
);
const result = await processes.run(
process.execPath,
@@ -412,7 +412,7 @@ describe("executable dispatcher JavaScript shutdown", () => {
async ({ processes, workspace }) => {
await installCliRuntime(
workspace,
'export const closeCliJavaScriptResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { process.stdout.emit("error", Object.assign(new Error("broken pipe"), { code: "EPIPE" })); options.exit(0); } });\n',
'export const closeCliResources = () => Promise.reject(new Error("worker still owned")); export const createCli = () => ({ serve: (_args, options) => { process.stdout.emit("error", Object.assign(new Error("broken pipe"), { code: "EPIPE" })); options.exit(0); } });\n',
);
const result = await processes.run(
process.execPath,
+128
View File
@@ -0,0 +1,128 @@
import { afterAll, expect, it } from "vitest";
import { createCli, closeCliResources } from "../../../src/cli.js";
afterAll(async () => closeCliResources());
const run = async (arguments_: string[]) => {
const cli = createCli({});
let stdout = "";
let exitCode = 0;
const previousExitCode = process.exitCode;
process.exitCode = 0;
try {
await cli.serve([...arguments_, "--json"], {
env: {},
exit: (code) => {
exitCode = code;
},
stdout: (chunk) => {
stdout += chunk;
},
});
return { exitCode: process.exitCode || exitCode, stdout };
} finally {
process.exitCode = previousExitCode ?? 0;
}
};
it("searches and reads packaged guidance through the provider-free CLI", async () => {
const found = await run(["guidance", "--query", "exact JavaScript module"]);
expect(found.exitCode, found.stdout).toBe(0);
const search = JSON.parse(found.stdout) as {
readonly status: string;
readonly matches: readonly {
readonly heading: string;
readonly locator: string;
}[];
};
expect(search.status).toBe("matches");
const selected = search.matches.find(
({ heading }) => heading === "Read a module without repeating analysis",
);
expect(selected).toBeDefined();
const read = await run([
"guidance",
"--action",
"read",
"--locator",
selected?.locator ?? "",
]);
expect(read.exitCode, read.stdout).toBe(0);
expect(JSON.parse(read.stdout)).toMatchObject({
status: "section",
heading: selected?.heading,
locator: selected?.locator,
source: "installed_rea_guidance",
});
const ignoredLimit = await run([
"guidance",
"--action",
"read",
"--locator",
selected?.locator ?? "",
"--limit",
"2",
]);
expect(ignoredLimit.exitCode).not.toBe(0);
});
it("reports no matches and rejects invalid CLI inputs", async () => {
const absent = await run(["guidance", "--query", "qzxv"]);
expect(absent.exitCode).toBe(0);
expect(JSON.parse(absent.stdout)).toMatchObject({
status: "no_matches",
total_matches: 0,
});
const empty = await run(["guidance", "--query", " "]);
expect(empty.exitCode).not.toBe(0);
const malformed = await run([
"guidance",
"--action",
"read",
"--locator",
"../guide.md",
]);
expect(malformed.exitCode).not.toBe(0);
expect(malformed.stdout).toContain("locator");
const incompatible = await run([
"guidance",
"--action",
"read",
"--locator",
"looks-like-a-locator",
"--query",
"module",
]);
expect(incompatible.exitCode).not.toBe(0);
});
it("continues a CLI search with a query- and bundle-bound cursor", async () => {
const first = await run(["guidance", "--query", "module", "--limit", "1"]);
const firstPage = JSON.parse(first.stdout) as {
readonly next_cursor?: string;
readonly matches: readonly { readonly locator: string }[];
};
expect(firstPage.next_cursor).toBeDefined();
const next = await run([
"guidance",
"--query",
"module",
"--limit",
"1",
"--cursor",
firstPage.next_cursor ?? "",
]);
expect(next.exitCode, next.stdout).toBe(0);
const nextPage = JSON.parse(next.stdout) as {
readonly matches: readonly { readonly locator: string }[];
};
expect(nextPage.matches[0]?.locator).not.toBe(firstPage.matches[0]?.locator);
const wrongQuery = await run([
"guidance",
"--query",
"different",
"--cursor",
firstPage.next_cursor ?? "",
]);
expect(wrongQuery.exitCode).not.toBe(0);
});
@@ -22,6 +22,7 @@ import {
canonicalSkillNeedsInstall,
existingSkillDestinations,
installCanonicalSkill,
readSkillFile,
readInstalledSkillIdentity,
skillDestinations,
type SkillInstallFileOperations,
@@ -37,6 +38,21 @@ import { SkillFilePublication } from "../../../src/application/SkillFilePublicat
import { z } from "zod";
import { skillReferenceIssues } from "../../../scripts/lib/docs-facts.mjs";
describe("managed skill file read boundary", () => {
it("rejects linked and oversized managed guide files", async () => {
const root = await createTestTempDirectory("rea-guidance-file-boundary-");
const target = join(root, "guide.md");
const link = join(root, "linked-guide.md");
const oversized = join(root, "oversized-guide.md");
await writeFile(target, "safe guide content");
await symlink(target, link);
await writeFile(oversized, "larger than limit");
await expect(readSkillFile(link)).rejects.toThrow();
await expect(readSkillFile(oversized, undefined, 4)).rejects.toThrow();
});
});
const expectPermissionMode = async (
path: string,
expected: number,
@@ -12,10 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js";
import {
runDoctorOwned,
type DoctorHost,
} from "../../../src/application/Doctor.js";
import { runDoctorOwned } from "../../../src/application/Doctor.js";
import {
readSkillFile,
skillDestinations,
@@ -96,7 +93,7 @@ describe("setup skill backup cleanup ownership", () => {
if (stage !== "verification") await writeFile(backup, original);
const close = failFirstCloses(backup);
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
createDoctorHostFixture({ platform: "linux", homeDirectory: home }),
{ HOME: home },
);
@@ -127,6 +124,7 @@ describe("setup skill backup cleanup ownership", () => {
{ approved: true, installHopper: false, structured: true },
{
...host,
nodeVersion: "24.22.1",
initialSetupState: async () => {
throw primary;
},
@@ -188,7 +186,7 @@ describe("skill regular-file cleanup ownership", () => {
await writeFile(manifest, "stale managed skill\n");
const close = failFirstCloses(manifest);
const host = systemSetupHost(
{ platform: "linux", homeDirectory: home } as DoctorHost,
createDoctorHostFixture({ platform: "linux", homeDirectory: home }),
{ HOME: home },
);
+53
View File
@@ -0,0 +1,53 @@
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();
}
});
@@ -7,7 +7,7 @@ import { Ajv2020 } from "ajv/dist/2020.js";
import { createApplicationMcpHarness } from "../fixtures/applicationMcpHarness.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { closeCliJavaScriptResources, createCli } from "../../src/cli.js";
import { closeCliResources, createCli } from "../../src/cli.js";
import { createEvidence, parseEvidence } from "../../src/domain/evidence.js";
import type { JsonValue } from "../../src/domain/jsonValue.js";
import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js";
@@ -20,7 +20,7 @@ const SEMANTIC_GRAPH_ID = `jsrg_${"0".repeat(64)}`;
// In-process serve keeps the CLI worker for this file. Stop it before the
// worker process exits so the private runtime root is not left behind.
afterAll(closeCliJavaScriptResources);
afterAll(closeCliResources);
it("reports invalid nested semantic graph Evidence as caller input through CLI and SDK MCP", async () => {
const root = await createTestTempDirectory("rea-semantic-trace-input-");
+4 -6
View File
@@ -19,14 +19,12 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
createAnalysisExecution,
type AnalysisClient,
type AnalysisProvider,
type AnalysisProviderCandidate,
type CapabilityDescriptor,
type ProviderIdentity,
} from "../../../src/application/AnalysisProvider.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import type {
AnalysisProvider,
AnalysisProviderCandidate,
CapabilityDescriptor,
ProviderIdentity,
} from "../../../src/application/AnalysisProvider.js";
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
@@ -0,0 +1,127 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, expect, it } from "vitest";
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
import {
readGuidanceSectionOutputSchema,
searchGuidanceInputSchema,
searchGuidanceOutputSchema,
} from "../../../src/contracts/guidanceContracts.js";
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
import { registerGuidanceTools } from "../../../src/server/registerGuidanceTools.js";
import { ToolResultDelivery } from "../../../src/server/ToolResultDelivery.js";
const open: {
client: Client;
server: EvidenceMcpServer;
resources: ArtifactResourceScope;
}[] = [];
const guidanceOutput = <T>(
schema: { parse(value: unknown): T },
value: unknown,
): T => {
// MCP's legacy output-schema projection wraps root-level union schemas.
// Newer transports pass the object unchanged.
if (typeof value === "object" && value !== null && "result" in value)
return schema.parse(value.result);
return schema.parse(value);
};
afterEach(async () => {
await Promise.all(
open
.splice(0)
.flatMap(({ client, server, resources }) => [
client.close(),
server.close(),
resources.close(),
]),
);
});
it("advertises strict provider-free contracts and returns packaged guide results over MCP", async () => {
const server = new EvidenceMcpServer(
{ name: "guidance-test", version: "1" },
{ capabilities: { tools: { listChanged: true } } },
undefined,
new ToolResultDelivery(1024 * 1024),
);
const resources = new ArtifactResourceScope();
registerGuidanceTools(server, resources);
const client = new Client({ name: "guidance-client", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
open.push({ client, server, resources });
const listed = await client.listTools();
expect(listed.tools.map(({ name }) => name)).toEqual(
expect.arrayContaining(["search_guidance", "read_guidance_section"]),
);
expect(
searchGuidanceInputSchema.safeParse({
query: "module",
unexpected: true,
}).success,
).toBe(false);
const invalidLocator = await client.callTool({
name: "read_guidance_section",
arguments: { locator: "../../outside.md" },
});
expect(invalidLocator.isError).toBe(true);
const invalidLimit = await client.callTool({
name: "search_guidance",
arguments: { query: "module", limit: 21 },
});
expect(invalidLimit.isError).toBe(true);
const response = await client.callTool({
name: "search_guidance",
arguments: { query: "exact JavaScript module" },
});
expect(response.isError).not.toBe(true);
const parsed = guidanceOutput(
searchGuidanceOutputSchema,
response.structuredContent,
);
expect(parsed).toMatchObject({
status: "matches",
source: "installed_rea_guidance",
});
if (parsed.status !== "matches") throw new Error("Expected guidance matches");
const selected = parsed.matches.find(
({ heading }) => heading === "Read a module without repeating analysis",
);
expect(selected).toBeDefined();
const section = await client.callTool({
name: "read_guidance_section",
arguments: { locator: selected?.locator },
});
expect(section.isError).not.toBe(true);
expect(
guidanceOutput(readGuidanceSectionOutputSchema, section.structuredContent),
).toMatchObject({
status: "section",
heading: selected?.heading,
source: "installed_rea_guidance",
});
const firstPage = await client.callTool({
name: "search_guidance",
arguments: { query: "module", limit: 1 },
});
const firstResult = guidanceOutput(
searchGuidanceOutputSchema,
firstPage.structuredContent,
);
if (firstResult.status !== "matches" || firstResult.next_cursor === undefined)
throw new Error("Expected a guidance continuation token");
const nextPage = await client.callTool({
name: "search_guidance",
arguments: {
query: "module",
limit: 1,
cursor: firstResult.next_cursor,
},
});
expect(
guidanceOutput(searchGuidanceOutputSchema, nextPage.structuredContent),
).toMatchObject({ status: "matches", offset: 1 });
});
@@ -21,7 +21,9 @@ describe("target-content provenance audit", () => {
"export_evidence_bundle",
"guided_workflows",
"inspect_analysis_settings",
"read_guidance_section",
"record_unknown",
"search_guidance",
"update_analysis_settings",
]);
});
+1 -1
View File
@@ -129,7 +129,7 @@ const stopChild = async (
graceMs: number,
): Promise<void> => {
if (child.exitCode !== null || child.signalCode !== null) return;
// EOF lets the worker await closeCliJavaScriptResources before exiting.
// EOF lets the worker await closeCliResources before exiting.
if (!child.stdin.writableEnded) child.stdin.end();
if (await waitForExit(child, graceMs)) return;
signalChild(child, "SIGTERM");
@@ -40,7 +40,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
process.exit(1);
}
const { closeCliJavaScriptResources, createCli } =
const { closeCliResources, createCli } =
await import("../../../dist/cli-runtime/cli.js");
const { createCliResultOutput } =
await import("../../../dist/cli-runtime/cliResultOutput.js");
@@ -106,7 +106,7 @@ try {
await inputStopped;
await chain;
if (completion.kind === "failed") throw completion.cause;
}, [closeCliJavaScriptResources]);
}, [closeCliResources]);
} catch (cause) {
console.error(cause);
process.exit(1);