feat(javascript): return a retained summary view on request (#1568)

analyze_javascript_application returns the complete application graph
inline, which repeats tens of kilobytes even for a few source lines and
exceeds the default 10 MiB MCP frame for a 300-function source file.

The new optional detail input keeps complete delivery by default. With
detail "summary" the server records the complete analysis Evidence in
the session and returns its inspect_analysis_view summary, whose
parent_evidence_id lets callers page modules, select items, or pass the
retained reference to application workflows. A server without Evidence
retention refuses summary detail with capability_unavailable rather
than returning a summary whose analysis cannot be reached.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-10 07:37:37 -07:00
committed by GitHub
parent 649661dc8b
commit c19e15a2b3
9 changed files with 279 additions and 7 deletions
@@ -1,4 +1,4 @@
import { expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
@@ -9,7 +9,13 @@ import {
analysisViewJavaScriptEvidence,
analysisViewLayoutEvidence,
} from "../../../tests/fixtures/analysisView.js";
import { inspectAnalysisView } from "./AnalysisViewService.js";
import {
inspectAnalysisView,
summarizeRetainedAnalysis,
} from "./AnalysisViewService.js";
import type { Evidence } from "../../domain/evidence.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { err, ok } from "../../domain/result.js";
it.each(["/fixtures/unknown.exe", ""])(
"preserves recorded empty native identity fields: path=%j",
@@ -266,3 +272,47 @@ it("rejects contradictory subject and analysis artifact digests", () => {
error: { _tag: "EvidenceIntegrityError" },
});
});
describe("summarizeRetainedAnalysis", () => {
const parent = analysisViewJavaScriptEvidence(
analysisViewJavaScriptAnalysisWithSource(),
);
it("retains the complete Evidence before projecting its summary", () => {
const retained = new Map<string, Evidence>();
const summary = summarizeRetainedAnalysis(parent, {
recordEvidence: (evidence) => {
retained.set(evidence.evidence_id, evidence);
return ok("added");
},
evidenceById: (evidenceId) => retained.get(evidenceId),
});
if (!summary.ok) throw summary.error;
expect(retained.get(parent.evidence_id)).toBe(parent);
expect(summary.value.evidence_links).toEqual([parent.evidence_id]);
expect(summary.value.normalized_result).toMatchObject({
kind: "summary",
parent_evidence_id: parent.evidence_id,
parent_operation: "analyze_javascript_application",
});
});
it("refuses a summary whose complete Evidence cannot be retained", () => {
const unavailable = summarizeRetainedAnalysis(parent, {
recordEvidence: undefined,
evidenceById: undefined,
});
expect(unavailable.ok).toBe(false);
if (unavailable.ok) return;
expect(unavailable.error._tag).toBe("AnalysisCapabilityUnavailableError");
expect(unavailable.error.userMessage).toContain("detail complete");
const conflict = new EvidenceIntegrityError("ledger conflict");
expect(
summarizeRetainedAnalysis(parent, {
recordEvidence: () => err(conflict),
evidenceById: () => parent,
}),
).toEqual(err(conflict));
});
});
@@ -3,7 +3,9 @@ import { z } from "zod";
import { ANALYSIS_VIEW_PROVIDER } from "../InvestigationProviders.js";
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
import { resolveEvidenceInput } from "../EvidenceInputResolver.js";
import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import {
@@ -76,6 +78,47 @@ export const inspectAnalysisViewValidated = (
return ok(createAnalysisViewEvidence(input, parent, projected.value));
};
/** Session Evidence ownership required to deliver a view instead of a complete record. */
export interface AnalysisViewRetention {
readonly evidenceById: EvidenceLookup | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
}
/**
* Retain complete analysis Evidence in the session, then project its summary
* view from the retained record. Callers page or select the rest through
* inspect_analysis_view with the view's parent Evidence ID.
*/
export const summarizeRetainedAnalysis = (
evidence: Evidence,
retention: AnalysisViewRetention,
): Result<Evidence, AnalysisError> => {
if (
retention.recordEvidence === undefined ||
retention.evidenceById === undefined
)
return err(
new AnalysisCapabilityUnavailableError(
ANALYSIS_VIEW_PROVIDER.id,
evidence.operation,
"summary detail requires a server session that retains Evidence for later views",
{
userMessage:
"This server does not retain Evidence. Select detail complete to receive the complete analysis inline.",
},
),
);
const recorded = retention.recordEvidence(evidence);
if (!recorded.ok) return recorded;
return inspectAnalysisViewValidated(
{
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
view: { kind: "summary" },
},
retention.evidenceById,
);
};
const resolveAnalysisViewSource = (
source: InspectAnalysisViewInput["source"],
lookup: EvidenceLookup | undefined,