fix(artifacts): publish addressable inspection sources through both adapters

This commit is contained in:
morluto committed 2026-10-24 02:50:51 +08:00
1 parent b885f44bef
commit bd8e1fb2c1
6 files changed
+136 -27

No files matched your search

+11 -1
View File
@@ -1,3 +1,4 @@
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
@@ -84,7 +85,16 @@ export const deliverAnalysisEvidence = async (
});
if (!headroom.ok)
return err(headroom.error.retainPartialObservation(evidence));
const bundle = createImmutableEvidenceBundle([evidence]);
const sourceEvidence =
evidence.operation === "inspect_artifact"
? readArtifactInspectionSources(evidence.normalized_result)
: ok([]);
if (!sourceEvidence.ok)
return err(sourceEvidence.error.retainPartialObservation(evidence));
const bundle = createImmutableEvidenceBundle([
...sourceEvidence.value,
evidence,
]);
const delivered =
detail === "summary"
? inspectAnalysisViewValidated(
+17 -4
View File
@@ -1,3 +1,4 @@
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js";
import { z } from "incur";
import { parseEvidence, type Evidence } from "../domain/evidence.js";
@@ -97,6 +98,9 @@ export const deliverCliAnalysisEvidence = async (
detail,
evidenceOutput,
...(input.view === undefined ? {} : { view: input.view }),
...(input.sourceEvidence === undefined
? {}
: { sourceEvidence: input.sourceEvidence }),
});
};
@@ -127,13 +131,22 @@ export const publishCliAnalysisEvidence = async (
return projectAnalysisError(
delivered.error.retainPartialObservation(complete),
);
const inspectionSources =
complete.operation === "inspect_artifact"
? readArtifactInspectionSources(complete.normalized_result)
: ok([]);
if (!inspectionSources.ok)
return projectAnalysisError(
inspectionSources.error.retainPartialObservation(complete),
);
const published = await publishEvidenceDelivery({
bundle: createEvidenceBundle([
...new Map(
[...(input.sourceEvidence ?? []), complete].map((record) => [
record.evidence_id,
record,
]),
[
...(input.sourceEvidence ?? []),
...inspectionSources.value,
complete,
].map((record) => [record.evidence_id, record]),
).values(),
]),
complete,
@@ -123,10 +123,13 @@ export const artifactInventorySummarySchema = z.strictObject({
limitation_count: z.number().int().nonnegative(),
});
const inputError = (message: string): AnalysisInputError =>
const inputError = (
field: "source" | "view",
message: string,
): AnalysisInputError =>
new AnalysisInputError("inspect_analysis_view", undefined, [
{
path: ["source"],
path: [field],
reason: "invalid_value",
message,
},
@@ -221,7 +224,7 @@ const sourceFailure = (
if (cause instanceof z.ZodError)
return err(analysisViewResultInputError(input, cause));
if (!(cause instanceof ArtifactInventoryValidationError)) throw cause;
return err(inputError(cause.message));
return err(inputError("source", cause.message));
};
/** Project exact source records; exhausted pages cover available records, not runtime behavior. */
@@ -233,12 +236,16 @@ export const projectArtifactInventoryView = (
if (operation !== "inventory_artifact" && operation !== "inspect_artifact")
return err(
inputError(
"source",
"Inventory views require inventory_artifact or inspect_artifact Evidence.",
),
);
if (view.kind !== "summary" && view.kind !== "inventory")
return err(
inputError("Artifact Evidence supports summary and inventory views."),
inputError(
"view",
"Artifact Evidence supports summary and inventory views.",
),
);
const parsed = parseSource(parent);
if (!parsed.ok) return parsed;
+26
View File
@@ -1,3 +1,10 @@
import { readEvidencePayload } from "./evidenceValidation.js";
import type { EvidenceIntegrityError } from "./evidenceErrors.js";
import { ok, type Result } from "./result.js";
import {
canBorrowParsedJsonSnapshot,
immutableJsonSnapshotMember,
} from "./immutableJson.js";
import {
artifactIntegrityPairOf,
artifactSha256PairSchema,
@@ -437,3 +444,22 @@ const inspectionCoverage = (
if (inventory.coverage.status !== "complete") return "partial";
return "complete-within-substeps";
};
/** Own the atomic source closure published or retained with an inspection. */
export const readArtifactInspectionSources = (
result: unknown,
): Result<readonly Evidence[], EvidenceIntegrityError> => {
const parsed = readEvidencePayload(artifactInspectionResultSchema, result);
if (!parsed.ok) return parsed;
const inspection = canBorrowParsedJsonSnapshot(result, parsed.value)
? result
: parsed.value;
const substeps = immutableJsonSnapshotMember(inspection, "substeps");
return ok(
inspection.substeps.map((step, index) => {
const ownedStep = immutableJsonSnapshotMember(substeps, String(index));
immutableJsonSnapshotMember(ownedStep, "evidence");
return step.evidence;
}),
);
};
+4 -15
View File
@@ -1,13 +1,9 @@
import {
canBorrowParsedJsonSnapshot,
immutableJsonSnapshotMember,
} from "../domain/immutableJson.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { ARTIFACT_TOOL_CONTRACTS } from "../contracts/artifactToolContracts.js";
import { createArtifactExtractionDestination } from "../application/artifacts/ArtifactExtractionDestination.js";
import { registerEvidenceTools } from "./registerEvidenceTools.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
import {
retainedSummaryDelivery,
withoutDeliveryDetail,
@@ -40,16 +36,9 @@ export const registerArtifactTools = (
parametersForEvidence: decodeWithoutDelivery,
sourceEvidence: (operation, result) => {
if (operation !== "inspect_artifact") return [];
const parsed = artifactInspectionResultSchema.parse(result);
const inspection = canBorrowParsedJsonSnapshot(result, parsed)
? result
: parsed;
const substeps = immutableJsonSnapshotMember(inspection, "substeps");
return inspection.substeps.map((step, index) => {
const ownedStep = immutableJsonSnapshotMember(substeps, String(index));
immutableJsonSnapshotMember(ownedStep, "evidence");
return step.evidence;
});
const parsed = readArtifactInspectionSources(result);
if (!parsed.ok) throw parsed.error;
return parsed.value;
},
});
};
+67 -3
View File
@@ -38,7 +38,10 @@ import { artifactInspectionResultSchema } from "../../../src/domain/artifactInsp
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { z } from "zod";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import {
createEvidenceBundle,
parseEvidenceBundle,
} from "../../../src/domain/evidenceBundle.js";
import { inspectEvidenceBundle } from "../../../src/application/investigation/InspectEvidenceBundle.js";
cliTest(
@@ -264,11 +267,72 @@ cliTest(
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("main.js", new TextReader("console.log('portable');\n"));
await writeFile(archive, await writer.close());
const bundlePath = join(root, "inspection-bundle.json");
const inspected = await cli.run({
arguments: ["inspect-artifact", archive, "--json"],
arguments: [
"inspect-artifact",
archive,
"--detail",
"summary",
"--evidence_output",
bundlePath,
"--json",
],
});
expect(inspected.exitCode).toBe(0);
const parent = parseEvidence(inspected.json);
const savedBundle = parseEvidenceBundle(
JSON.parse(await readFile(bundlePath, "utf8")),
);
const parent = savedBundle.records.find(
(record) => record.operation === "inspect_artifact",
)!;
const atomicId = artifactInspectionResultSchema.parse(
parent.normalized_result,
).substeps[0]!.evidence_id;
expect(savedBundle.records.map((record) => record.evidence_id)).toContain(
atomicId,
);
const call = await cli.sharedMcpCall?.();
if (call === undefined) throw new Error("Expected shared MCP session");
expect(
(await call("import_evidence_bundle", { path: bundlePath })).isError,
).not.toBe(true);
const atomicView = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: atomicId },
view: {
kind: "inventory",
collection: "occurrences",
logical_paths: ["main.js"],
},
});
expect(atomicView.isError).not.toBe(true);
expect(
parseEvidence(atomicView.structuredContent).normalized_result,
).toMatchObject({
parent_evidence_id: atomicId,
items: [expect.objectContaining({ logical_path: "main.js" })],
});
const unsupportedRequest = join(root, "unsupported-view.json");
const unsupportedInput = {
source: { kind: "inline", evidence: parent },
view: { kind: "page", collection: "sections" },
};
await writeFile(unsupportedRequest, JSON.stringify(unsupportedInput));
const unsupported = await cli.run({
arguments: ["inspect-analysis-view", unsupportedRequest, "--json"],
});
expect(unsupported.exitCode).toBe(1);
expect(unsupported.json).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["view"],
message: "Artifact Evidence supports summary and inventory views.",
},
],
},
});
await unlink(archive);
const request = join(root, "view.json");
await writeFile(