diff --git a/src/application/EvidenceBundleCommands.ts b/src/application/EvidenceBundleCommands.ts index 3205f867ca..d3dc913995 100644 --- a/src/application/EvidenceBundleCommands.ts +++ b/src/application/EvidenceBundleCommands.ts @@ -7,16 +7,12 @@ import { type PublishedEvidenceFile, } from "../domain/evidenceErrors.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; -import { - AnalysisCancelledError, - AnalysisInputError, -} from "../domain/analysisErrorCore.js"; +import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import type { Evidence } from "../domain/evidence.js"; import { createImmutableEvidenceBundle, type EvidenceBundle, } from "../domain/evidenceBundle.js"; -import type { InspectAnalysisViewInput } from "../domain/analysisView/analysisView.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -153,53 +149,6 @@ export const publishEvidenceDelivery = async (input: { return ok({ delivered: input.delivered, evidenceBundle: written.value }); }; -/** Resolve the caller's exact retained parent from an authenticated portable bundle. */ -export const inspectAnalysisViewFromBundle = async ( - input: InspectAnalysisViewInput, - bundlePath: string, - resources: ArtifactResourceScope, - signal?: AbortSignal, -): Promise> => { - if (input.source.kind !== "retained-evidence") - return err( - new AnalysisInputError("inspect_analysis_view", undefined, [ - { - path: ["source", "kind"], - reason: "invalid_value", - message: - "--evidence-bundle requires a retained-evidence source with the exact parent evidence_id; inline Evidence already supplies its source", - }, - ]), - ); - const loaded = await readImmutableEvidenceBundle( - bundlePath, - signal, - resources, - "inspect_analysis_view", - ); - // Authentication and projection are synchronous; dispatch queued interrupts before accepting either result. - await dispatchQueuedInterrupts(); - if (!loaded.ok) return loaded; - if (signal?.aborted) - return err( - new AnalysisCancelledError("inspect_analysis_view", { - cause: signal.reason, - }), - ); - const projected = inspectAnalysisViewValidated(input, (id) => - loaded.value.records.find((record) => record.evidence_id === id), - ); - await dispatchQueuedInterrupts(); - if (projected.ok && signal?.aborted) - return err( - new AnalysisCancelledError("inspect_analysis_view", { - cause: signal.reason, - partialObservation: projected.value, - }), - ); - return projected; -}; - /** Publish a completed bundle and retry its exact staged-output cleanup owner. */ const persistEvidenceBundleCommand = async ( bundle: EvidenceBundle, diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index 5e76a87e06..cb3e96ec8a 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -23,7 +23,6 @@ import { import { inspectAnalysisView, inspectAnalysisViewValidated, - inspectAnalysisViewWithFiles, summarizeRetainedAnalysis, } from "./AnalysisViewService.js"; import type { Evidence } from "../../domain/evidence.js"; @@ -42,7 +41,7 @@ import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; it.each(["layout", "javascript"])( "carries the retained %s source snapshot into repeatable shared views", - (kind) => { + async (kind) => { const parent = kind === "layout" ? analysisViewLayoutEvidence() @@ -56,9 +55,13 @@ it.each(["layout", "javascript"])( source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view: { kind: "summary" }, } as const; - const first = inspectAnalysisView(input, (id) => ledger.forAnalysis(id)); + const first = await inspectAnalysisView(input, (id) => + ledger.forAnalysis(id), + ); if (!first.ok) throw first.error; - const repeated = inspectAnalysisView(input, (id) => ledger.forAnalysis(id)); + const repeated = await inspectAnalysisView(input, (id) => + ledger.forAnalysis(id), + ); if (!repeated.ok) throw repeated.error; expect(repeated.value.normalized_result).toEqual( first.value.normalized_result, @@ -71,7 +74,7 @@ it.each(["layout", "javascript"])( it.each(["/fixtures/unknown.exe", ""])( "preserves recorded empty native identity fields: path=%j", - (path) => { + async (path) => { const original = functionDossierSchema.parse(ghidraFunctionDossier()); const parent = createEvidence( { path, format: "pe", sha256: "c".repeat(64) }, @@ -89,7 +92,7 @@ it.each(["/fixtures/unknown.exe", ""])( }, }, ); - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "native", @@ -110,7 +113,7 @@ it.each(["/fixtures/unknown.exe", ""])( }, ); -it("reports inline Evidence identity failures at the complete source path", () => { +it("reports inline Evidence identity failures at the complete source path", async () => { const parent = analysisViewLayoutEvidence(); const input = { source: { @@ -121,7 +124,7 @@ it("reports inline Evidence identity failures at the complete source path", () = } as const; for (const result of [ - inspectAnalysisView(input), + await inspectAnalysisView(input), inspectAnalysisViewValidated(input), ]) expect(result).toMatchObject({ @@ -139,7 +142,7 @@ it("reports inline Evidence identity failures at the complete source path", () = }); }); -it("projects a small native view from an authenticated >10 MiB retained dossier", () => { +it("projects a small native view from an authenticated >10 MiB retained dossier", async () => { const original = functionDossierSchema.parse(ghidraFunctionDossier()); const parent = createEvidence( { path: "/fixtures/large.exe", format: "pe", sha256: "c".repeat(64) }, @@ -163,7 +166,7 @@ it("projects a small native view from an authenticated >10 MiB retained dossier" limit: 128, }, }; - const view = inspectAnalysisView(input, (id) => + const view = await inspectAnalysisView(input, (id) => id === parent.evidence_id ? parent : undefined, ); if (!view.ok) throw view.error; @@ -182,15 +185,15 @@ it("projects a small native view from an authenticated >10 MiB retained dossier" ]); expect(JSON.stringify(view.value).length).toBeLessThan(12000); expect(JSON.stringify(parent).length).toBeGreaterThan(10 * 1024 * 1024); - expect(inspectAnalysisView(input, () => undefined)).toMatchObject({ + expect(await inspectAnalysisView(input, () => undefined)).toMatchObject({ ok: false, error: { _tag: "EvidenceIntegrityError" }, }); }); -it("projects inline layout Evidence and retains a derived view record", () => { +it("projects inline layout Evidence and retains a derived view record", async () => { const parent = analysisViewLayoutEvidence(); - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "item", @@ -211,13 +214,13 @@ it("projects inline layout Evidence and retains a derived view record", () => { }); }); -it("resolves same-session retained JavaScript Evidence without re-running analysis", () => { +it("resolves same-session retained JavaScript Evidence without re-running analysis", async () => { const parent = analysisViewJavaScriptEvidence( analysisViewJavaScriptAnalysisWithSource(), ); const lookup = (evidenceId: string) => evidenceId === parent.evidence_id ? parent : undefined; - const summary = inspectAnalysisView( + const summary = await inspectAnalysisView( { source: { kind: "retained-evidence", @@ -236,7 +239,7 @@ it("resolves same-session retained JavaScript Evidence without re-running analys expect(summary.value.normalized_result).not.toHaveProperty( "summary.semantic_graph", ); - const item = inspectAnalysisView( + const item = await inspectAnalysisView( { source: { kind: "retained-evidence", @@ -256,8 +259,8 @@ it("resolves same-session retained JavaScript Evidence without re-running analys ); }); -it("rejects stale retained IDs and unsupported parent operations", () => { - const missing = inspectAnalysisView( +it("rejects stale retained IDs and unsupported parent operations", async () => { + const missing = await inspectAnalysisView( { source: { kind: "retained-evidence", @@ -283,7 +286,7 @@ it("rejects stale retained IDs and unsupported parent operations", () => { result: { path: "/artifacts/crash.core" }, }, ); - const rejected = inspectAnalysisView({ + const rejected = await inspectAnalysisView({ source: { kind: "inline", evidence: unsupported }, view: { kind: "summary" }, }); @@ -292,7 +295,7 @@ it("rejects stale retained IDs and unsupported parent operations", () => { error: { _tag: "AnalysisUnsupportedTargetError" }, }); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: analysisViewLayoutEvidence() }, view: { kind: "summary", approval: true }, }), @@ -302,7 +305,7 @@ it("rejects stale retained IDs and unsupported parent operations", () => { }); }); -it("preserves an absent parent subject instead of guessing its format", () => { +it("preserves an absent parent subject instead of guessing its format", async () => { const analysis = analysisViewJavaScriptAnalysisWithSource(); const original = analysisViewJavaScriptEvidence(analysis); const parent = createEvidence(undefined, original.provider, { @@ -311,7 +314,7 @@ it("preserves an absent parent subject instead of guessing its format", () => { result: original.normalized_result, limitations: original.limitations, }); - const result = inspectAnalysisView( + const result = await inspectAnalysisView( { source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view: { kind: "summary" }, @@ -325,7 +328,7 @@ it("preserves an absent parent subject instead of guessing its format", () => { ]); }); -it("rejects contradictory subject and analysis artifact digests", () => { +it("rejects contradictory subject and analysis artifact digests", async () => { const analysis = analysisViewJavaScriptAnalysisWithSource(); const original = analysisViewJavaScriptEvidence(analysis); const parent = createEvidence( @@ -341,7 +344,7 @@ it("rejects contradictory subject and analysis artifact digests", () => { result: original.normalized_result, }, ); - const result = inspectAnalysisView( + const result = await inspectAnalysisView( { source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view: { kind: "summary" }, @@ -398,7 +401,7 @@ describe("summarizeRetainedAnalysis", () => { }); }); -it("seals inferred and literal HAR pages, including a null artifact digest", () => { +it("seals inferred and literal HAR pages, including a null artifact digest", async () => { const capture = analysisViewCaptureFixture(); capture.artifact.sha256 = null; const parent = analysisViewCaptureEvidence(capture); @@ -407,7 +410,7 @@ it("seals inferred and literal HAR pages, including a null artifact digest", () [0, "inferred"], [11, "inferred"], ] as const) { - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "api-surface", offset, limit: 1 }, }); @@ -417,7 +420,7 @@ it("seals inferred and literal HAR pages, including a null artifact digest", () expect(parseEvidence(result.value)).toEqual(result.value); } expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: analysisViewLayoutEvidence() }, view: { kind: "api-surface" }, }), @@ -427,7 +430,7 @@ it("seals inferred and literal HAR pages, including a null artifact digest", () }); }); -it("rejects a null projected digest under a present parent subject", () => { +it("rejects a null projected digest under a present parent subject", async () => { const capture = analysisViewCaptureFixture(); capture.artifact.sha256 = null; const parent = createEvidence( @@ -441,14 +444,14 @@ it("rejects a null projected digest under a present parent subject", () => { }, ); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "api-surface" }, }), ).toMatchObject({ ok: false, error: { _tag: "EvidenceIntegrityError" } }); }); -it("refuses an object-dense borrowed capture before making a full schema clone", () => { +it("refuses an object-dense borrowed capture before making a full schema clone", async () => { const capture = analysisViewObjectDenseCapture(); expect(Buffer.byteLength(JSON.stringify(capture))).toBeLessThan( WEB_NETWORK_CAPTURE_LIMITS.inputBytes, @@ -464,7 +467,7 @@ it("refuses an object-dense borrowed capture before making a full schema clone", { kind: "environment-manifest" }, { kind: "environment-state" }, ] as const) { - const result = inspectAnalysisView( + const result = await inspectAnalysisView( { source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view, @@ -482,11 +485,11 @@ it("refuses an object-dense borrowed capture before making a full schema clone", expect(ledger.forAnalysis(parent.evidence_id)).toBe(borrowed); }); -it("seals an inferred environment manifest and a derived manifest without templated groups", () => { +it("seals an inferred environment manifest and a derived manifest without templated groups", async () => { const parent = analysisViewCaptureEvidence( environmentManifestCaptureFixture(), ); - const inferred = inspectAnalysisView({ + const inferred = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "environment-manifest", template_rules: ["opaque-token"] }, }); @@ -510,7 +513,7 @@ it("seals an inferred environment manifest and a derived manifest without templa const literal = environmentManifestCaptureFixture(); literal.artifact.sha256 = null; literal.records = [3, 4, 8].map((ordinal) => literal.records[ordinal]!); - const derived = inspectAnalysisView({ + const derived = await inspectAnalysisView({ source: { kind: "inline", evidence: analysisViewCaptureEvidence(literal) }, view: { kind: "environment-manifest" }, }); @@ -525,7 +528,7 @@ it("seals an inferred environment manifest and a derived manifest without templa ]), }); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: analysisViewLayoutEvidence() }, view: { kind: "environment-manifest" }, }), @@ -535,9 +538,9 @@ it("seals an inferred environment manifest and a derived manifest without templa }); }); -it("seals an inferred environment state whose values are the recorded ones", () => { +it("seals an inferred environment state whose values are the recorded ones", async () => { const parent = analysisViewCaptureEvidence(environmentStateCaptureFixture()); - const sealed = inspectAnalysisView({ + const sealed = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "environment-state", template_rules: ["opaque-token"] }, }); @@ -566,36 +569,39 @@ it.each([ "api-surface", "environment-manifest", "environment-state", -] as const)("rejects inline accessors before parsing a %s view", (kind) => { - let accessed = false; - const evidence = { - get normalized_result() { - accessed = true; - throw new Error("Evidence must be admitted before parsing"); - }, - }; - const input = { - source: { kind: "inline", evidence }, - view: { kind }, - }; - const parsed = inspectAnalysisViewInputSchema.parse(input); - if (parsed.source.kind !== "inline") throw new Error("Expected inline input"); - expect(parsed.source.evidence).toBe(evidence); - expect(inspectAnalysisView(input)).toMatchObject({ - ok: false, - error: { - _tag: "AnalysisInputError", - issues: [{ path: ["source", "evidence", "normalized_result"] }], - }, - }); - expect(accessed).toBe(false); - expect( - inspectAnalysisView({ +] as const)( + "rejects inline accessors before parsing a %s view", + async (kind) => { + let accessed = false; + const evidence = { + get normalized_result() { + accessed = true; + throw new Error("Evidence must be admitted before parsing"); + }, + }; + const input = { + source: { kind: "inline", evidence }, + view: { kind }, + }; + const parsed = inspectAnalysisViewInputSchema.parse(input); + if (parsed.source.kind !== "inline") + throw new Error("Expected inline input"); + expect(parsed.source.evidence).toBe(evidence); + expect(await inspectAnalysisView(input)).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + issues: [{ path: ["source", "evidence", "normalized_result"] }], + }, + }); + expect(accessed).toBe(false); + const ordinary = await inspectAnalysisView({ source: { kind: "inline", evidence: analysisViewCaptureEvidence() }, view: { kind }, - }).ok, - ).toBe(true); -}); + }); + expect(ordinary.ok).toBe(true); + }, +); it.each([ "summary", @@ -605,7 +611,7 @@ it.each([ "environment-state", ] as const)( "rejects nested, hidden and inherited inline accessors for %s", - (kind) => { + async (kind) => { const parent = analysisViewCaptureEvidence(); let accessed = false; const getter = () => { @@ -643,10 +649,11 @@ it.each([ view: { kind }, }; for (const result of [ - inspectAnalysisView(input), - inspectAnalysisViewValidated( - inspectAnalysisViewInputSchema.parse(input), - ), + await inspectAnalysisView(input), + inspectAnalysisViewValidated({ + source: input.source, + view: inspectAnalysisViewInputSchema.parse(input).view, + }), ]) expect(result).toMatchObject({ ok: false, @@ -668,7 +675,7 @@ it.each([ ["inherited iterator", Symbol.iterator, true], ] as const)( "rejects array %s accessors without invoking them", - (_name, key, inherited) => { + async (_name, key, inherited) => { const parent = analysisViewCaptureEvidence(); let accessed = false; const array: unknown[] = []; @@ -687,7 +694,7 @@ it.each([ "environment-manifest", "environment-state", ] as const) { - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: { ...parent, raw_result: array } }, view: { kind }, }); @@ -700,7 +707,7 @@ it.each([ }, ); -it("rejects callable array overrides and subclass species before parsing", () => { +it("rejects callable array overrides and subclass species before parsing", async () => { const parent = analysisViewCaptureEvidence(); let invoked = false; const callback = () => { @@ -724,7 +731,7 @@ it("rejects callable array overrides and subclass species before parsing", () => "environment-manifest", "environment-state", ] as const) { - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: { ...parent, raw_result: array } }, view: { kind }, }); @@ -742,10 +749,10 @@ it.each([ "api-surface", "environment-manifest", "environment-state", -] as const)("preserves ordinary inline Evidence for %s", (kind) => { +] as const)("preserves ordinary inline Evidence for %s", async (kind) => { const parent = analysisViewCaptureEvidence(); const before = JSON.stringify(parent); - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind }, }); @@ -760,7 +767,7 @@ it.each([ expect(Object.isFrozen(parent)).toBe(false); }); -it("rejects executable constructor values before schema plain-object checks", () => { +it("rejects executable constructor values before schema plain-object checks", async () => { const parent = analysisViewCaptureEvidence(); let invoked = false; const constructor = () => undefined; @@ -778,7 +785,7 @@ it("rejects executable constructor values before schema plain-object checks", () "environment-state", ] as const) { expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: { ...parent, raw_result: { constructor } }, @@ -790,7 +797,7 @@ it("rejects executable constructor values before schema plain-object checks", () expect(invoked).toBe(false); }); -it("rejects inline proxy values before reflection or parsing can run traps", () => { +it("rejects inline proxy values before reflection or parsing can run traps", async () => { const parent = analysisViewCaptureEvidence(); let invoked = false; const trap = () => { @@ -811,7 +818,7 @@ it("rejects inline proxy values before reflection or parsing can run traps", () "environment-manifest", "environment-state", ] as const) { - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: { ...parent, raw_result: value } }, view: { kind }, }); @@ -823,7 +830,7 @@ it("rejects inline proxy values before reflection or parsing can run traps", () expect(invoked).toBe(false); }); -it("keeps ignored array metadata inert through parsing and projection", () => { +it("keeps ignored array metadata inert through parsing and projection", async () => { const capture = analysisViewCaptureFixture(); const parent = createEvidence( { @@ -856,7 +863,7 @@ it("keeps ignored array metadata inert through parsing and projection", () => { "environment-manifest", "environment-state", ] as const) { - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind }, }); @@ -867,19 +874,19 @@ it("keeps ignored array metadata inert through parsing and projection", () => { expect(parseEvidence(parent).raw_result).toEqual([]); }); -it("keeps inline-envelope admission live alongside capture validation and grouping", () => { +it("keeps inline-envelope admission live alongside capture validation and grouping", async () => { // Each clone alone fits; both graphs coexist while the view is projected. const capture = analysisViewObjectDenseCapture(40_000); const parent = analysisViewCaptureEvidence(capture); for (const kind of ["summary", "capture"] as const) { - const projected = inspectAnalysisView({ + const projected = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind }, }); if (!projected.ok) throw projected.error; expect(projected.value.evidence_links).toEqual([parent.evidence_id]); } - const retained = inspectAnalysisView( + const retained = await inspectAnalysisView( { source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view: { kind: "api-surface" }, @@ -888,7 +895,7 @@ it("keeps inline-envelope admission live alongside capture validation and groupi ); if (!retained.ok) throw retained.error; expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "api-surface" }, }), @@ -898,7 +905,7 @@ it("keeps inline-envelope admission live alongside capture validation and groupi }); }); -it("admits raw_result and parameters before authenticating a portable capture", () => { +it("admits raw_result and parameters before authenticating a portable capture", async () => { const dense = analysisViewObjectDenseCapture(); const parent = analysisViewCaptureEvidence(); for (const field of ["raw_result", "parameters"] as const) @@ -907,7 +914,7 @@ it("admits raw_result and parameters before authenticating a portable capture", "environment-manifest", "environment-state", ] as const) { - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: { ...parent, [field]: dense } }, view: { kind }, }); @@ -927,18 +934,18 @@ const writeEvidenceFile = async (value: unknown): Promise => { return path; }; -describe("inspectAnalysisViewWithFiles", () => { +describe("inspectAnalysisView", () => { const view = manifestView; it("projects an Evidence file exactly as the same Evidence inline", async () => { const parent = analysisViewCaptureEvidence( environmentManifestCaptureFixture(), ); - const fromFile = await inspectAnalysisViewWithFiles({ + const fromFile = await inspectAnalysisView({ source: { kind: "evidence-file", path: await writeEvidenceFile(parent) }, view, }); - const inline = inspectAnalysisView({ + const inline = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view, }); @@ -961,7 +968,7 @@ describe("inspectAnalysisViewWithFiles", () => { ); const parent = analysisViewCaptureEvidence(capture); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view, }), @@ -969,7 +976,7 @@ describe("inspectAnalysisViewWithFiles", () => { ok: false, error: { _tag: "AnalysisResourceConstraintError", resource: "memory" }, }); - const fromFile = await inspectAnalysisViewWithFiles({ + const fromFile = await inspectAnalysisView({ source: { kind: "evidence-file", path: await writeEvidenceFile(parent) }, view, }); @@ -981,7 +988,7 @@ describe("inspectAnalysisViewWithFiles", () => { it("refuses a relative path, a missing file and JSON that is not Evidence", async () => { expect( - await inspectAnalysisViewWithFiles({ + await inspectAnalysisView({ source: { kind: "evidence-file", path: "capture.json" }, view, }), @@ -991,12 +998,12 @@ describe("inspectAnalysisViewWithFiles", () => { "absent.json", ); expect( - await inspectAnalysisViewWithFiles({ + await inspectAnalysisView({ source: { kind: "evidence-file", path: missing }, view, }), ).toMatchObject({ ok: false, error: { _tag: "EvidenceFileError" } }); - const notEvidence = await inspectAnalysisViewWithFiles({ + const notEvidence = await inspectAnalysisView({ source: { kind: "evidence-file", path: await writeEvidenceFile({ hello: 1 }), @@ -1028,7 +1035,7 @@ describe("inspectAnalysisViewWithFiles", () => { const parent = analysisViewCaptureEvidence( environmentManifestCaptureFixture(), ); - const result = await inspectAnalysisViewWithFiles( + const result = await inspectAnalysisView( { source: { kind: "evidence-file", @@ -1053,7 +1060,7 @@ describe("inspectAnalysisViewWithFiles", () => { environmentManifestCaptureFixture(), ); const path = await writeEvidenceFile(parent); - const running = inspectAnalysisViewWithFiles( + const running = inspectAnalysisView( { source: { kind: "evidence-file", path }, view }, undefined, { signal: controller.signal }, @@ -1067,21 +1074,9 @@ describe("inspectAnalysisViewWithFiles", () => { }); expect(result.ok ? undefined : result.error.cause).toBe(reason); }); - - it("leaves file sources to the file-aware entry point", () => { - expect( - inspectAnalysisView({ - source: { kind: "evidence-file", path: "/tmp/capture.json" }, - view, - }), - ).toMatchObject({ - ok: false, - error: { _tag: "AnalysisCapabilityUnavailableError" }, - }); - }); }); -describe("inspectAnalysisViewWithFiles scenario parents", () => { +describe("inspectAnalysisView scenario parents", () => { const view = manifestView; it("accepts from a file a scenario capture whose response bodies exceed the inline budget", async () => { @@ -1096,7 +1091,7 @@ describe("inspectAnalysisViewWithFiles scenario parents", () => { ), ); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view, }), @@ -1104,7 +1099,7 @@ describe("inspectAnalysisViewWithFiles scenario parents", () => { ok: false, error: { _tag: "AnalysisResourceConstraintError", resource: "memory" }, }); - const fromFile = await inspectAnalysisViewWithFiles({ + const fromFile = await inspectAnalysisView({ source: { kind: "evidence-file", path: await writeEvidenceFile(parent) }, view, }); diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index 659d98dc24..a5127e94ca 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -1,4 +1,6 @@ import { setImmediate } from "node:timers/promises"; +import { dispatchQueuedInterrupts } from "../../process/queuedInterrupts.js"; +import { readImmutableEvidenceBundle } from "../EvidenceBundleFiles.js"; import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; import { z } from "zod"; @@ -10,6 +12,7 @@ import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, + AnalysisInputError, AnalysisCapabilityUnavailableError, } from "../../domain/analysisErrorCore.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; @@ -40,29 +43,25 @@ import { type EvidenceFileReadOptions, } from "./evidenceFileSource.js"; -/** Authenticate a selected-view request and project already completed Evidence. */ -export const inspectAnalysisView = ( - rawInput: unknown, - lookup?: EvidenceLookup, -): Result => { - const parsed = inspectAnalysisViewInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues( - INSPECT_ANALYSIS_VIEW_OPERATION, - parsed.error.issues, - rawInput, - { cause: parsed.error }, - ), - ); - return inspectAnalysisViewValidated(parsed.data, lookup); -}; +/** A parsed request whose source can be projected without filesystem access. */ +export interface ReadyAnalysisViewInput { + readonly source: Exclude< + InspectAnalysisViewInput["source"], + { kind: "evidence-file" } + >; + readonly view: AnalysisViewRequest; +} -/** Validate a request whose source may be an Evidence file, then project it. */ -export const inspectAnalysisViewWithFiles = async ( +/** Source acquisition controls shared by CLI and MCP; bundles are selected by the CLI. */ +export interface AnalysisViewReadOptions extends EvidenceFileReadOptions { + readonly evidenceBundlePath?: string; +} + +/** Validate one view request, resolve its selected source, then project completed Evidence. */ +export const inspectAnalysisView = async ( rawInput: unknown, lookup?: EvidenceLookup, - options: EvidenceFileReadOptions = {}, + options: AnalysisViewReadOptions = {}, ): Promise> => { const parsed = inspectAnalysisViewInputSchema.safeParse(rawInput); if (!parsed.success) @@ -74,21 +73,29 @@ export const inspectAnalysisViewWithFiles = async ( { cause: parsed.error }, ), ); - return inspectAnalysisViewValidatedWithFiles(parsed.data, lookup, options); + if (options.evidenceBundlePath !== undefined) + return inspectBundleView(parsed.data, options.evidenceBundlePath, options); + const source = parsed.data.source; + if (source.kind === "evidence-file") + return inspectFileView({ ...parsed.data, source }, lookup, options); + return inspectAnalysisViewValidated({ ...parsed.data, source }, lookup); }; /** * Read and authenticate an Evidence-file source, then project it as retained * Evidence: its strings are bounded by the file reader rather than charged as - * inline input. Other sources project unchanged. + * inline input. */ -export const inspectAnalysisViewValidatedWithFiles = async ( - input: InspectAnalysisViewInput, +const inspectFileView = async ( + input: InspectAnalysisViewInput & { + readonly source: Extract< + InspectAnalysisViewInput["source"], + { kind: "evidence-file" } + >; + }, lookup?: EvidenceLookup, options: EvidenceFileReadOptions = {}, ): Promise> => { - if (input.source.kind !== "evidence-file") - return inspectAnalysisViewValidated(input, lookup); const loaded = await readEvidenceFileSource(input.source.path, options); // Authentication and projection are synchronous; yield so a pending abort is dispatched before each check. const cancelled = async (): Promise => { @@ -118,9 +125,56 @@ export const inspectAnalysisViewValidatedWithFiles = async ( return projected; }; -/** Authenticate borrowed inline Evidence after adapter envelope validation. */ -export const inspectAnalysisViewValidated = ( +/** Resolve the caller's exact retained parent from an authenticated portable bundle. */ +const inspectBundleView = async ( input: InspectAnalysisViewInput, + bundlePath: string, + { resources, signal }: EvidenceFileReadOptions, +): Promise> => { + if (input.source.kind !== "retained-evidence") + return err( + new AnalysisInputError("inspect_analysis_view", undefined, [ + { + path: ["source", "kind"], + reason: "invalid_value", + message: + "--evidence-bundle requires a retained-evidence source with the exact parent evidence_id; inline Evidence already supplies its source", + }, + ]), + ); + const loaded = await readImmutableEvidenceBundle( + bundlePath, + signal, + resources, + "inspect_analysis_view", + ); + // Authentication and projection are synchronous; dispatch queued interrupts before accepting either result. + await dispatchQueuedInterrupts(); + if (!loaded.ok) return loaded; + if (signal?.aborted) + return err( + new AnalysisCancelledError("inspect_analysis_view", { + cause: signal.reason, + }), + ); + const projected = inspectAnalysisViewValidated( + { source: input.source, view: input.view }, + (id) => loaded.value.records.find((record) => record.evidence_id === id), + ); + await dispatchQueuedInterrupts(); + if (projected.ok && signal?.aborted) + return err( + new AnalysisCancelledError("inspect_analysis_view", { + cause: signal.reason, + partialObservation: projected.value, + }), + ); + return projected; +}; + +/** Project a parsed request whose selected source is inline or retained. */ +export const inspectAnalysisViewValidated = ( + input: ReadyAnalysisViewInput, lookup?: EvidenceLookup, ): Result => { const apiSurfaceAccumulator = @@ -265,20 +319,11 @@ export const projectRetainedAnalysis = ( }; const resolveAnalysisViewSource = ( - source: InspectAnalysisViewInput["source"], + source: ReadyAnalysisViewInput["source"], lookup: EvidenceLookup | undefined, ): Result => { if (source.kind === "retained-evidence") return resolveEvidenceInput(source, lookup); - // Files are read by inspectAnalysisViewValidatedWithFiles, which the CLI and MCP adapters call. - if (source.kind === "evidence-file") - return err( - new AnalysisCapabilityUnavailableError( - ANALYSIS_VIEW_PROVIDER.id, - INSPECT_ANALYSIS_VIEW_OPERATION, - "evidence-file sources are read before projection", - ), - ); return authenticateEvidence(source.evidence, ["source", "evidence"], { source, }); @@ -317,7 +362,7 @@ const authenticateEvidence = ( }; const createAnalysisViewEvidence = ( - input: InspectAnalysisViewInput, + input: ReadyAnalysisViewInput, parent: Evidence, result: AnalysisViewResult, ): Evidence => { diff --git a/src/application/artifacts/ArtifactComparisonEvidence.test.ts b/src/application/artifacts/ArtifactComparisonEvidence.test.ts index b6f540e800..ea2baf0438 100644 --- a/src/application/artifacts/ArtifactComparisonEvidence.test.ts +++ b/src/application/artifacts/ArtifactComparisonEvidence.test.ts @@ -14,9 +14,9 @@ import { } from "../../domain/evidenceErrors.js"; import { resolveArtifactComparisonEvidence } from "./ArtifactComparisonEvidence.js"; -const artifactSummary = () => { +const artifactSummary = async () => { const parent = ARTIFACT_COMPARISON_EXAMPLE.left; - const summary = inspectAnalysisView( + const summary = await inspectAnalysisView( { source: { kind: "inline", evidence: parent }, view: { kind: "summary" }, @@ -66,8 +66,8 @@ const recreateSummary = ( ); }; -it("reports a missing retained parent instead of treating summary facts as inventory", () => { - const { summary } = artifactSummary(); +it("reports a missing retained parent instead of treating summary facts as inventory", async () => { + const { summary } = await artifactSummary(); const resolved = resolveArtifactComparisonEvidence(summary, undefined); expect(resolved.ok).toBe(false); if (!resolved.ok) { @@ -76,8 +76,8 @@ it("reports a missing retained parent instead of treating summary facts as inven } }); -it("uses retained Evidence resolution to reject a mismatched parent identity", () => { - const { summary } = artifactSummary(); +it("uses retained Evidence resolution to reject a mismatched parent identity", async () => { + const { summary } = await artifactSummary(); const otherParent = ARTIFACT_COMPARISON_EXAMPLE.right; const resolved = resolveArtifactComparisonEvidence( summary, @@ -90,8 +90,8 @@ it("uses retained Evidence resolution to reject a mismatched parent identity", ( } }); -it("rejects a summary that declares the wrong parent operation", () => { - const { parent, summary } = artifactSummary(); +it("rejects a summary that declares the wrong parent operation", async () => { + const { parent, summary } = await artifactSummary(); const summaryResult = artifactInventorySummaryViewResultSchema.parse( summary.normalized_result, ); @@ -108,8 +108,8 @@ it("rejects a summary that declares the wrong parent operation", () => { expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError); }); -it("rejects summary parameters that do not identify the projected parent", () => { - const { parent, summary } = artifactSummary(); +it("rejects summary parameters that do not identify the projected parent", async () => { + const { parent, summary } = await artifactSummary(); const forged = recreateSummary(summary, summary.normalized_result, { ...summary.parameters, parent_evidence_id: `ev_${"f".repeat(64)}`, @@ -122,8 +122,8 @@ it("rejects summary parameters that do not identify the projected parent", () => expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError); }); -it("rejects summary facts that do not reproject from the complete parent", () => { - const { parent, summary } = artifactSummary(); +it("rejects summary facts that do not reproject from the complete parent", async () => { + const { parent, summary } = await artifactSummary(); const summaryResult = artifactInventorySummaryViewResultSchema.parse( summary.normalized_result, ); @@ -146,8 +146,8 @@ it("rejects summary facts that do not reproject from the complete parent", () => expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError); }); -it("accepts display-only subject changes excluded from Evidence identity", () => { - const { parent, summary } = artifactSummary(); +it("accepts display-only subject changes excluded from Evidence identity", async () => { + const { parent, summary } = await artifactSummary(); if (summary.subject === null) throw new Error("artifact summary fixture must identify its subject"); const displayOnly = parseEvidence({ diff --git a/src/application/environment/environmentReplay.test.ts b/src/application/environment/environmentReplay.test.ts index 6b6b7f111a..fcd1dd5f72 100644 --- a/src/application/environment/environmentReplay.test.ts +++ b/src/application/environment/environmentReplay.test.ts @@ -41,7 +41,7 @@ const replay = ( }; /** The fixture source with /api/users/2's response replaced by HAR base64 content the decoder retained. */ -const base64Source = (bytes: Buffer): EnvironmentSource => { +const base64Source = async (bytes: Buffer): Promise => { const capture = environmentManifestCaptureFixture(); const record = capture.records.find( (entry) => @@ -68,7 +68,7 @@ const base64Source = (bytes: Buffer): EnvironmentSource => { }, ]; const evidence = analysisViewCaptureEvidence(capture); - const manifest = inspectAnalysisView({ + const manifest = await inspectAnalysisView({ source: { kind: "inline", evidence }, view: { kind: "environment-manifest" }, }); @@ -378,15 +378,17 @@ describe("replayEnvironmentTool bodies", () => { ).toMatchObject({ body: exact, replay: { truncated: false } }); }); - it("decodes retained HAR base64 bytes that are UTF-8 and keeps other bytes as base64", () => { - const decoded = base64Source(Buffer.from('{"login":"decoded"}', "utf8")); + it("decodes retained HAR base64 bytes that are UTF-8 and keeps other bytes as base64", async () => { + const decoded = await base64Source( + Buffer.from('{"login":"decoded"}', "utf8"), + ); expect(replay("get_users_by_id", { id: "2" }, decoded)).toMatchObject({ body: { login: "decoded" }, replay: { body_encoding: "utf-8" }, }); const binary = Buffer.from([0xff, 0xfe, 0x00, 0x41]); expect( - replay("get_users_by_id", { id: "2" }, base64Source(binary)), + replay("get_users_by_id", { id: "2" }, await base64Source(binary)), ).toMatchObject({ body: binary.toString("base64"), replay: { body_encoding: "base64", body_recorded: true }, @@ -396,12 +398,12 @@ describe("replayEnvironmentTool bodies", () => { replay( "get_users_by_id", { id: "2" }, - base64Source(Buffer.from([0xef, 0xbb, 0xbf, 0x41])), + await base64Source(Buffer.from([0xef, 0xbb, 0xbf, 0x41])), ).body, ).toBe("\uFEFFA"); }); - it("decodes only the replayable prefix of a large retained base64 body and flags it truncated", () => { + it("decodes only the replayable prefix of a large retained base64 body and flags it truncated", async () => { for (const [fill, encoding] of [ [0x61, "utf-8"], [0xff, "base64"], @@ -410,7 +412,7 @@ describe("replayEnvironmentTool bodies", () => { const result = replay( "get_users_by_id", { id: "2" }, - base64Source(large), + await base64Source(large), ); expect(result.replay).toMatchObject({ truncated: true, @@ -427,7 +429,7 @@ describe("replayEnvironmentTool bodies", () => { // Under the limit in recorded bytes, even though its base64 spelling is longer. const small = Buffer.alloc(200 * 1024, 0xff); expect( - replay("get_users_by_id", { id: "2" }, base64Source(small)), + replay("get_users_by_id", { id: "2" }, await base64Source(small)), ).toMatchObject({ body: small.toString("base64"), replay: { truncated: false, body_encoding: "base64" }, diff --git a/src/application/environment/environmentScenario.test.ts b/src/application/environment/environmentScenario.test.ts index c326b5c0ca..7995f5d291 100644 --- a/src/application/environment/environmentScenario.test.ts +++ b/src/application/environment/environmentScenario.test.ts @@ -49,8 +49,8 @@ describe("environment manifests from capture_browser_scenario parents", () => { ); }); - it("refuses a scenario capture that selected neither response bodies nor header values", () => { - const refused = inspectAnalysisView({ + it("refuses a scenario capture that selected neither response bodies nor header values", async () => { + const refused = await inspectAnalysisView({ source: { kind: "inline", evidence: scenarioCaptureEvidence( @@ -70,8 +70,8 @@ describe("environment manifests from capture_browser_scenario parents", () => { }); }); - it("names the views a scenario capture supports when another is requested", () => { - const refused = inspectAnalysisView({ + it("names the views a scenario capture supports when another is requested", async () => { + const refused = await inspectAnalysisView({ source: { kind: "inline", evidence: scenarioCaptureEvidence(scenarioCaptureFixture()), diff --git a/src/application/environment/environmentSource.test.ts b/src/application/environment/environmentSource.test.ts index c1312eb876..9c53c2f4a8 100644 --- a/src/application/environment/environmentSource.test.ts +++ b/src/application/environment/environmentSource.test.ts @@ -123,9 +123,9 @@ describe("loadEnvironmentSource", () => { expect(projected.message).toContain("different capture"); }); - it("refuses manifests of another view kind, non-capture evidence and non-Evidence JSON", () => { + it("refuses manifests of another view kind, non-capture evidence and non-Evidence JSON", async () => { const fixture = environmentServeFixture(); - const surface = inspectAnalysisView({ + const surface = await inspectAnalysisView({ source: { kind: "inline", evidence: fixture.capture }, view: { kind: "api-surface" }, }); @@ -161,14 +161,14 @@ describe("loadEnvironmentSource", () => { ); }); - it("indexes nothing for a manifest that cites no records and leaves tampered captures to authentication", () => { + it("indexes nothing for a manifest that cites no records and leaves tampered captures to authentication", async () => { const fixture = environmentServeFixture(); const capture = analysisViewCaptureEvidence({ ...analysisViewCaptureFixture(), records: [], total_records: 0, }); - const manifest = inspectAnalysisView({ + const manifest = await inspectAnalysisView({ source: { kind: "inline", evidence: capture }, view: { kind: "environment-manifest" }, }); @@ -302,7 +302,7 @@ describe("loadEnvironmentSource payload attribution", () => { }); describe("loadEnvironmentSource recorded request lines", () => { - it("keeps the delimiter of an explicit empty query in the recorded exchange", () => { + it("keeps the delimiter of an explicit empty query in the recorded exchange", async () => { const capture = environmentManifestCaptureFixture(); const record = capture.records.find((entry) => JSON.stringify(entry.reported).includes("/api/users/2"), @@ -314,7 +314,7 @@ describe("loadEnvironmentSource recorded request lines", () => { reported.request.url = `${reported.request.url}?`; record.reported = reported; const evidence = analysisViewCaptureEvidence(capture); - const manifest = inspectAnalysisView({ + const manifest = await inspectAnalysisView({ source: { kind: "inline", evidence }, view: { kind: "environment-manifest" }, }); diff --git a/src/application/environment/environmentState.test.ts b/src/application/environment/environmentState.test.ts index eae81e0577..9f6ec8988e 100644 --- a/src/application/environment/environmentState.test.ts +++ b/src/application/environment/environmentState.test.ts @@ -82,7 +82,7 @@ describe("loadEnvironmentState", () => { expect(state.entities.length).toBeGreaterThan(0); }); - it("refuses non-Evidence JSON, another view kind, another capture's state and other template rules", () => { + it("refuses non-Evidence JSON, another view kind, another capture's state and other template rules", async () => { const notEvidence = loadEnvironmentState({}, source); if (notEvidence.ok || !(notEvidence.error instanceof AnalysisInputError)) throw new Error("Expected an input refusal"); @@ -113,7 +113,7 @@ describe("loadEnvironmentState", () => { ], }, }); - const ruleless = inspectAnalysisView({ + const ruleless = await inspectAnalysisView({ source: { kind: "inline", evidence: fixture.capture }, view: { kind: "environment-state", template_rules: [] }, }); @@ -263,7 +263,7 @@ describe("deriveEnvironmentState", () => { }); }); - it("projects the authenticated capture as retained Evidence, so a capture inline admission refuses still yields its state", () => { + it("projects the authenticated capture as retained Evidence, so a capture inline admission refuses still yields its state", async () => { const capture = timedCaptureEvidence( Array.from({ length: 20 }, (_, index) => [ "GET", @@ -271,7 +271,7 @@ describe("deriveEnvironmentState", () => { { id: index, text: "x".repeat(200_000) }, ]), ); - const inline = inspectAnalysisView({ + const inline = await inspectAnalysisView({ source: { kind: "inline", evidence: capture }, view: { kind: "environment-state", template_rules: [] }, }); diff --git a/src/application/native/NativeFunctionMatchingViews.test.ts b/src/application/native/NativeFunctionMatchingViews.test.ts index 72da044d7a..cc3eda5030 100644 --- a/src/application/native/NativeFunctionMatchingViews.test.ts +++ b/src/application/native/NativeFunctionMatchingViews.test.ts @@ -15,12 +15,12 @@ it("pages individual function facts without transferring an entire ambiguous ali }); if (!matched.ok) throw matched.error; const source = { kind: "inline", evidence: matched.value.evidence }; - const view = (query: unknown) => { - const result = inspectAnalysisView({ source, view: query }); + const view = async (query: unknown) => { + const result = await inspectAnalysisView({ source, view: query }); if (!result.ok) throw result.error; return analysisViewResultSchema.parse(result.value.normalized_result); }; - const groups = view({ + const groups = await view({ kind: "function-matches", collection: "matches", fields: ["basis"], @@ -36,7 +36,7 @@ it("pages individual function facts without transferring an entire ambiguous ali if (groups.kind !== "function-matches") throw new Error("Wrong view"); expect(groups.items[0]).not.toHaveProperty("left"); const member = `${matched.value.sources[0]!.evidence_id}/123`; - const functions = view({ + const functions = await view({ kind: "function-matches", collection: "functions", function_ids: [member], @@ -60,7 +60,7 @@ it("pages individual function facts without transferring an entire ambiguous ali }); expect(JSON.stringify(functions).length).toBeLessThan(6000); expect( - view({ + await view({ kind: "function-matches", collection: "functions", names: [], @@ -68,7 +68,7 @@ it("pages individual function facts without transferring an entire ambiguous ali }), ).toMatchObject({ coverage: { total: 0 } }); expect( - view({ + await view({ kind: "function-matches", collection: "functions", names: ["same"], @@ -77,7 +77,7 @@ it("pages individual function facts without transferring an entire ambiguous ali }), ).toMatchObject({ coverage: { total: 500, next_offset: 1 } }); expect( - view({ + await view({ kind: "function-matches", collection: "functions", sides: ["right"], @@ -88,9 +88,13 @@ it("pages individual function facts without transferring an entire ambiguous ali coverage: { examined: 1, total: 500, exhausted: true, next_offset: null }, }); expect( - view({ kind: "function-matches", collection: "functions", states: [] }), + await view({ + kind: "function-matches", + collection: "functions", + states: [], + }), ).toMatchObject({ coverage: { total: 0, exhausted: true } }); - expect(view({ kind: "summary" })).toMatchObject({ + expect(await view({ kind: "summary" })).toMatchObject({ summary: { scope: "provider-observed-inputs", summary: { unresolved: 1 }, diff --git a/src/cli/analysisEvidenceDelivery.ts b/src/cli/analysisEvidenceDelivery.ts index ede9c060b5..c51653c096 100644 --- a/src/cli/analysisEvidenceDelivery.ts +++ b/src/cli/analysisEvidenceDelivery.ts @@ -8,7 +8,7 @@ import { ok, type Result } from "../domain/result.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; import { publishEvidenceDelivery } from "../application/EvidenceBundleCommands.js"; -import { inspectAnalysisView } from "../application/analysisView/AnalysisViewService.js"; +import { inspectAnalysisViewValidated } from "../application/analysisView/AnalysisViewService.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; @@ -118,7 +118,7 @@ export const publishCliAnalysisEvidence = async ( // unreported bundle; publication then owns every post-commit outcome. const delivered = detail === "summary" - ? inspectAnalysisView({ + ? inspectAnalysisViewValidated({ source: { kind: "inline", evidence: complete }, view: input.view ?? { kind: "summary" }, }) diff --git a/src/cli/analysisViewCommands.ts b/src/cli/analysisViewCommands.ts index 74aa05e892..09adfae6ee 100644 --- a/src/cli/analysisViewCommands.ts +++ b/src/cli/analysisViewCommands.ts @@ -1,7 +1,6 @@ import { z } from "incur"; -import { inspectAnalysisViewValidatedWithFiles } from "../application/analysisView/AnalysisViewService.js"; -import { inspectAnalysisViewFromBundle } from "../application/EvidenceBundleCommands.js"; +import { inspectAnalysisView } from "../application/analysisView/AnalysisViewService.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ANALYSIS_VIEW_PROVIDER } from "../application/InvestigationProviders.js"; import { readEvidenceBundle } from "../application/EvidenceBundleFiles.js"; @@ -21,7 +20,6 @@ import { localPathStringSchema } from "../domain/localPath.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; -import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisView.js"; /** Project selected views of completed analysis Evidence through the CLI. */ export const registerAnalysisViewCommands = ( cli: CliInstance, @@ -103,34 +101,17 @@ export const registerAnalysisViewCommands = ( if (!input.ok) return input.error; // An evidence-file path is resolved against the operator's working directory. const value = resolveCliJsonPaths(input.value, [["source", "path"]]); - const parsed = inspectAnalysisViewInputSchema.safeParse(value); - if (!parsed.success) - return { - error: "Application workflow failed", - ...projectAnalysisError( - analysisInputErrorFromIssues( - CLI_COMMANDS.inspectAnalysisView, - parsed.error.issues, - value, - ), - ), - }; const resources = new ArtifactResourceScope(); const bundlePath = options.evidenceBundle; const result = await finishAnalysisCommand( () => - bundlePath === undefined - ? inspectAnalysisViewValidatedWithFiles( - parsed.data, - undefined, - { signal, resources }, - ) - : inspectAnalysisViewFromBundle( - parsed.data, - bundlePath, - resources, - signal, - ), + inspectAnalysisView(value, undefined, { + signal, + resources, + ...(bundlePath === undefined + ? {} + : { evidenceBundlePath: bundlePath }), + }), resources, ANALYSIS_VIEW_PROVIDER.id, "inspect_analysis_view", diff --git a/src/server/registerAnalysisViewTool.ts b/src/server/registerAnalysisViewTool.ts index 67df1ba83a..6b6828d9a8 100644 --- a/src/server/registerAnalysisViewTool.ts +++ b/src/server/registerAnalysisViewTool.ts @@ -2,7 +2,7 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; -import { inspectAnalysisViewWithFiles } from "../application/analysisView/AnalysisViewService.js"; +import { inspectAnalysisView } from "../application/analysisView/AnalysisViewService.js"; import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; @@ -25,7 +25,7 @@ export const registerAnalysisViewTool = ( const handler = createEvidenceToolHandler( server, (input, options) => - inspectAnalysisViewWithFiles(input, evidenceById, { + inspectAnalysisView(input, evidenceById, { ...(options.signal === undefined ? {} : { signal: options.signal }), ...(artifactResources === undefined ? {} diff --git a/tests/boundary/cli/analysisViewCli.test.ts b/tests/boundary/cli/analysisViewCli.test.ts index 9cd12c8f78..271df8b80c 100644 --- a/tests/boundary/cli/analysisViewCli.test.ts +++ b/tests/boundary/cli/analysisViewCli.test.ts @@ -72,7 +72,7 @@ cliTest( ], }); expect(result.exitCode).toBe(0); - const expected = inspectAnalysisView({ + const expected = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view, }); @@ -191,7 +191,7 @@ cliTest( limit: 1, }, }; - const expected = inspectAnalysisView(input); + const expected = await inspectAnalysisView(input); if (!expected.ok) throw expected.error; await writeFile(request, JSON.stringify(input)); const response = await cli.run({ @@ -348,7 +348,7 @@ cliTest( source: { kind: "inline" as const, evidence }, view: { kind: "summary" as const }, }; - const application = inspectAnalysisView(request); + const application = await inspectAnalysisView(request); await writeFile(path, JSON.stringify(request)); const reply = await cli.run({ arguments: ["inspect-analysis-view", path, "--json"], @@ -397,7 +397,7 @@ cliTest( source: { kind: "inline" as const, evidence: reseal(inspection, nested) }, view: { kind: "summary" as const }, }; - const application = inspectAnalysisView(request); + const application = await inspectAnalysisView(request); expect(application.ok).toBe(false); if (application.ok) throw new Error("Expected invalid nested Evidence"); expect(application.error.cause).toMatchObject({ @@ -664,7 +664,7 @@ cliTest("rejects a request that omits source", async ({ cli }) => { expect(result.json).toMatchObject({ code: "invalid_request", category: "invalid_input", - details: { operation: "inspect-analysis-view" }, + details: { operation: "inspect_analysis_view" }, }); }); @@ -820,7 +820,7 @@ cliTest( parent_evidence_id: parent.evidence_id, }); // The file source projects exactly what the same Evidence projects inline. - const inline = inspectAnalysisView({ + const inline = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view, }); diff --git a/tests/boundary/cli/environmentServeCli.test.ts b/tests/boundary/cli/environmentServeCli.test.ts index a05066475e..7f4bd05afa 100644 --- a/tests/boundary/cli/environmentServeCli.test.ts +++ b/tests/boundary/cli/environmentServeCli.test.ts @@ -134,7 +134,7 @@ cliTest( url.search = query.toString(); request.url = url.href; const capture = analysisViewCaptureEvidence(records); - const manifest = inspectAnalysisView({ + const manifest = await inspectAnalysisView({ source: { kind: "inline", evidence: capture }, view: { kind: "environment-manifest" }, }); diff --git a/tests/boundary/cli/managedMemberSummaryCli.test.ts b/tests/boundary/cli/managedMemberSummaryCli.test.ts index 57632379f5..7ffa4ca429 100644 --- a/tests/boundary/cli/managedMemberSummaryCli.test.ts +++ b/tests/boundary/cli/managedMemberSummaryCli.test.ts @@ -144,7 +144,7 @@ cliTest( expect(parent.locations).toEqual(complete.locations); expect(parent.parameters).toEqual({ type_names: typeNames }); expect(receipt.evidence_links).toEqual([parent.evidence_id]); - const portable = inspectAnalysisView({ + const portable = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "summary" }, }); diff --git a/tests/boundary/filesystem/desktopApplication.test.ts b/tests/boundary/filesystem/desktopApplication.test.ts index 55aeb22d03..00bfdce792 100644 --- a/tests/boundary/filesystem/desktopApplication.test.ts +++ b/tests/boundary/filesystem/desktopApplication.test.ts @@ -113,13 +113,13 @@ it("joins real PE/CLI reference facts and independent engine markers without pic expect(probe.path).toBe( join(await realpath(root), files.get(probe.file_id)!.path), ); - const summary = inspectAnalysisView({ + const summary = await inspectAnalysisView({ source: { kind: "inline", evidence }, view: { kind: "summary" }, }); if (!summary.ok) throw summary.error; expect(JSON.stringify(summary.value)).not.toContain('"files":['); - const selected = inspectAnalysisView({ + const selected = await inspectAnalysisView({ source: { kind: "inline", evidence }, view: { kind: "desktop-application", @@ -203,7 +203,7 @@ it("retains bounded coverage and stable pages without claiming absence after a t entries_examined: 2, }); expect(report.files).toHaveLength(2); - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence }, view: { kind: "desktop-application", @@ -497,7 +497,7 @@ it.skipIf(process.platform === "win32").each(["path", "kind"] as const)( files_probed: 0, bytes_read: 0, }); - const page = inspectAnalysisView({ + const page = await inspectAnalysisView({ source: { kind: "inline", evidence: after.evidence }, view: { kind: "desktop-application", @@ -999,7 +999,7 @@ it("bounds diagnostic previews while preserving complete caller-provided issue f source, evidence.provider, ); - const summarized = inspectAnalysisView({ + const summarized = await inspectAnalysisView({ source: { kind: "inline", evidence: callerEvidence }, view: { kind: "summary" }, }); @@ -1024,7 +1024,7 @@ it("bounds diagnostic previews while preserving complete caller-provided issue f }, }); for (const ids of [undefined, []]) { - const selected = inspectAnalysisView({ + const selected = await inspectAnalysisView({ source: { kind: "inline", evidence: callerEvidence }, view: { kind: "desktop-application", diff --git a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts index 960886b02e..f4968d6687 100644 --- a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts +++ b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts @@ -135,7 +135,7 @@ it.skipIf(process.platform === "win32")( offset = page.coverage.next_offset ?? manifest.text.length; } while (offset < manifest.text.length); expect(text).toBe(manifest.text); - const portable = inspectAnalysisView({ + const portable = await inspectAnalysisView({ source: { kind: "inline", evidence: parseEvidence(JSON.parse(before)) }, view: { kind: "summary" }, }); @@ -156,7 +156,7 @@ it.skipIf(process.platform === "win32")( 30000, ); -it("preserves partial/unknown declarations and Unicode text while rejecting missing APK identity and malformed source", () => { +it("preserves partial/unknown declarations and Unicode text while rejecting missing APK identity and malformed source", async () => { const report = { engine: { name: "jadx", @@ -192,7 +192,10 @@ it("preserves partial/unknown declarations and Unicode text while rejecting miss ); const parent = evidence(); const source = { kind: "inline", evidence: parent }; - const summary = inspectAnalysisView({ source, view: { kind: "summary" } }); + const summary = await inspectAnalysisView({ + source, + view: { kind: "summary" }, + }); if (!summary.ok) throw summary.error; const overview = androidPackageSummaryViewResultSchema.parse( summary.value.normalized_result, @@ -200,7 +203,7 @@ it("preserves partial/unknown declarations and Unicode text while rejecting miss expect(overview.summary.package_name).toBeNull(); expect(overview.summary.manifest.status).toBe("partial"); expect(overview.limitations).toEqual(parent.limitations); - const page = inspectAnalysisView({ + const page = await inspectAnalysisView({ source, view: { kind: "android", facet: "manifest", offset: 0, limit: 4 }, }); @@ -218,7 +221,7 @@ it("preserves partial/unknown declarations and Unicode text while rejecting miss expect(result.unknowns.join(" ")).toContain("partial"); for (const offset of [2]) expect( - inspectAnalysisView({ + await inspectAnalysisView({ source, view: { kind: "android", facet: "manifest", offset, limit: 2 }, }), @@ -228,7 +231,7 @@ it("preserves partial/unknown declarations and Unicode text while rejecting miss evidence({ ...report, class_count: -1 }), ]) expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: invalid }, view: { kind: "summary" }, }), diff --git a/tests/boundary/mcp/commandObservationViews.test.ts b/tests/boundary/mcp/commandObservationViews.test.ts index fae3d31a1d..a5e092e024 100644 --- a/tests/boundary/mcp/commandObservationViews.test.ts +++ b/tests/boundary/mcp/commandObservationViews.test.ts @@ -254,16 +254,18 @@ it("rejects summary without retention before executing either native command", a expect(run).not.toHaveBeenCalled(); }); -it("rejects malformed historical GDB facts and conflicting source commands", () => { +it("rejects malformed historical GDB facts and conflicting source commands", async () => { const source = gdbEvidence({ session_id: sessionId, command: "a different command", }); expect( - inspectAnalysisView({ - source: { kind: "inline", evidence: source }, - view: { kind: "summary" }, - }).ok, + ( + await inspectAnalysisView({ + source: { kind: "inline", evidence: source }, + view: { kind: "summary" }, + }) + ).ok, ).toBe(false); const malformed = createEvidence(undefined, source.provider, { operation: "gdb_console", @@ -271,16 +273,18 @@ it("rejects malformed historical GDB facts and conflicting source commands", () result: { ...capture, records: [123] }, }); expect( - inspectAnalysisView({ - source: { kind: "inline", evidence: malformed }, - view: { kind: "summary" }, - }).ok, + ( + await inspectAnalysisView({ + source: { kind: "inline", evidence: malformed }, + view: { kind: "summary" }, + }) + ).ok, ).toBe(false); }); it.each([true, false, null])( "preserves opaque objdump capture and unknown/change facts (%s)", - (changed) => { + async (changed) => { const report = { stdout: "opaque output 🧪\n", stderr: "diagnostic", @@ -307,7 +311,7 @@ it.each([true, false, null])( rawResult: report, }, ); - const selected = inspectAnalysisView({ + const selected = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "summary" }, }); @@ -330,10 +334,12 @@ it.each([true, false, null])( }, ); expect( - inspectAnalysisView({ - source: { kind: "inline", evidence: conflicting }, - view: { kind: "summary" }, - }).ok, + ( + await inspectAnalysisView({ + source: { kind: "inline", evidence: conflicting }, + view: { kind: "summary" }, + }) + ).ok, ).toBe(false); } }, diff --git a/tests/boundary/mcp/dartAotSummaryMcp.test.ts b/tests/boundary/mcp/dartAotSummaryMcp.test.ts index 9962bb74f3..4b4d28a4ff 100644 --- a/tests/boundary/mcp/dartAotSummaryMcp.test.ts +++ b/tests/boundary/mcp/dartAotSummaryMcp.test.ts @@ -141,7 +141,7 @@ it("honors explicit ABI and old full requests, validates SDK schemas and reuses .exportEvidenceBundle() .records.find((record) => record.evidence_id === parent.evidence_id), ).toEqual(parent); - const portable = inspectAnalysisView({ + const portable = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "summary" }, }); @@ -149,7 +149,7 @@ it("honors explicit ABI and old full requests, validates SDK schemas and reuses expect(portable.value.normalized_result).toEqual(view); }, 30000); -it("preserves null snapshot facts and pool lower bounds after exhaustion, rejecting malformed and conflicting identities", () => { +it("preserves null snapshot facts and pool lower bounds after exhaustion, rejecting malformed and conflicting identities", async () => { const report = { target: { path: "/source.apk", bytes: 10, sha256: "a".repeat(64) }, abi: "arm64-v8a", @@ -191,7 +191,7 @@ it("preserves null snapshot facts and pool lower bounds after exhaustion, reject }, ); for (const parent of [evidence(), evidence(report, null)]) { - const summary = inspectAnalysisView({ + const summary = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "summary" }, }); @@ -203,7 +203,7 @@ it("preserves null snapshot facts and pool lower bounds after exhaustion, reject expect(result.summary.string_pool.package_uri_count_at_least).toBe(1000); expect(result.summary.string_pool.returned_counts.package_uris).toBe(1); expect(result.limitations).toEqual(parent.limitations); - const page = inspectAnalysisView({ + const page = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "dart-aot", @@ -222,13 +222,13 @@ it("preserves null snapshot facts and pool lower bounds after exhaustion, reject }); } expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: evidence(report, "c".repeat(64)) }, view: { kind: "summary" }, }), ).toMatchObject({ ok: false, error: { _tag: "EvidenceIntegrityError" } }); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: evidence({ @@ -243,7 +243,7 @@ it("preserves null snapshot facts and pool lower bounds after exhaustion, reject }), ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } }); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: evidence() }, view: { kind: "android", facet: "manifest", offset: 0, limit: 1 }, }), diff --git a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts index 0d32bd6e31..788a17a3a2 100644 --- a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts @@ -135,7 +135,7 @@ it("returns a first-call overview and pages the unchanged source after its file .exportEvidenceBundle() .records.find((x) => x.evidence_id === parent.evidence_id), ).toEqual(parent); - const portable = inspectAnalysisView({ + const portable = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "summary" }, }); @@ -175,7 +175,7 @@ it("keeps malformed metadata, empty returned rows and historical null subjects e { kind: "summary" }, { kind: "managed", collection: "attributes", offset: 0, limit: 3 }, ]) { - const result = inspectAnalysisView({ + const result = await inspectAnalysisView({ source: { kind: "inline", evidence: historical }, view, }); @@ -209,7 +209,7 @@ it("keeps malformed metadata, empty returned rows and historical null subjects e }, ); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: incompatible }, view: { kind: "summary" }, }), diff --git a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts index e08355988a..be0d05eb45 100644 --- a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts @@ -176,7 +176,7 @@ it("summarizes selected members and pages every original collection after the fi expect(JSON.stringify(session.evidenceById(parent.evidence_id))).toBe( original, ); - const portable = inspectAnalysisView({ + const portable = await inspectAnalysisView({ source: { kind: "inline", evidence: parent }, view: { kind: "summary" }, }); @@ -211,7 +211,7 @@ it("keeps collections that historical member Evidence never examined unknown", a limitations: parent.limitations, authority: parent.authority, }); - const summary = inspectAnalysisView({ + const summary = await inspectAnalysisView({ source: { kind: "inline", evidence: historical }, view: { kind: "summary" }, }); @@ -227,13 +227,13 @@ it("keeps collections that historical member Evidence never examined unknown", a expect(view.unknowns.join(" ")).toContain("no recorded subject"); for (const collection of ["exported_types", "user_strings"] as const) expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: historical }, view: { kind: "managed-members", collection, offset: 0, limit: 1 }, }), ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } }); expect( - inspectAnalysisView({ + await inspectAnalysisView({ source: { kind: "inline", evidence: historical }, view: { kind: "managed", collection: "attributes", offset: 0, limit: 1 }, }), diff --git a/tests/fixtures/browserScenarioCapture.ts b/tests/fixtures/browserScenarioCapture.ts index 09bc1ad687..3a9067671a 100644 --- a/tests/fixtures/browserScenarioCapture.ts +++ b/tests/fixtures/browserScenarioCapture.ts @@ -1,6 +1,7 @@ +import { analysisViewRequestSchema } from "../../src/domain/analysisView/analysisView.js"; import { createHash } from "node:crypto"; -import { inspectAnalysisView } from "../../src/application/analysisView/AnalysisViewService.js"; +import { inspectAnalysisViewValidated } from "../../src/application/analysisView/AnalysisViewService.js"; import { createBrowserScenarioEvidence } from "../../src/application/BrowserScenarioEvidence.js"; import { sanitizeBrowserUrl } from "../../src/domain/browserObservation.js"; import { browserScenarioSchema } from "../../src/domain/browserScenario.js"; @@ -304,12 +305,12 @@ export const scenarioCaptureEvidence = ( export const scenarioServeFixture = ( capture: Evidence = scenarioCaptureEvidence(), ): EnvironmentServeFixture => { - const manifest = inspectAnalysisView({ + const manifest = inspectAnalysisViewValidated({ source: { kind: "inline", evidence: capture }, - view: { + view: analysisViewRequestSchema.parse({ kind: "environment-manifest", template_rules: [...ALL_TEMPLATE_RULES], - }, + }), }); if (!manifest.ok) throw manifest.error; return { manifest: manifest.value, capture }; diff --git a/tests/fixtures/environmentServe.ts b/tests/fixtures/environmentServe.ts index 3021cbcd22..354a9efddb 100644 --- a/tests/fixtures/environmentServe.ts +++ b/tests/fixtures/environmentServe.ts @@ -1,7 +1,8 @@ +import { analysisViewRequestSchema } from "../../src/domain/analysisView/analysisView.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { inspectAnalysisView } from "../../src/application/analysisView/AnalysisViewService.js"; +import { inspectAnalysisViewValidated } from "../../src/application/analysisView/AnalysisViewService.js"; import { loadEnvironmentSource, type EnvironmentSource, @@ -39,9 +40,12 @@ export const environmentServeFixture = ( const capture = analysisViewCaptureEvidence( environmentManifestCaptureFixture(), ); - const manifest = inspectAnalysisView({ + const manifest = inspectAnalysisViewValidated({ source: { kind: "inline", evidence: capture }, - view: { kind: "environment-manifest", template_rules: [...templateRules] }, + view: analysisViewRequestSchema.parse({ + kind: "environment-manifest", + template_rules: [...templateRules], + }), }); if (!manifest.ok) throw manifest.error; return { manifest: manifest.value, capture }; @@ -118,9 +122,12 @@ export const environmentStateServeFixture = ( ), ): EnvironmentStateServeFixture => { const project = (kind: "environment-manifest" | "environment-state") => { - const projected = inspectAnalysisView({ + const projected = inspectAnalysisViewValidated({ source: { kind: "inline", evidence: capture }, - view: { kind, template_rules: [...ALL_TEMPLATE_RULES] }, + view: analysisViewRequestSchema.parse({ + kind, + template_rules: [...ALL_TEMPLATE_RULES], + }), }); if (!projected.ok) throw projected.error; return projected.value; diff --git a/tests/process-global/analysisViewFileCancellation.test.ts b/tests/process-global/analysisViewFileCancellation.test.ts index 1a5f9a8f52..375bb6d70e 100644 --- a/tests/process-global/analysisViewFileCancellation.test.ts +++ b/tests/process-global/analysisViewFileCancellation.test.ts @@ -7,7 +7,7 @@ import { } from "../fixtures/analysisView.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { readEvidenceFileSource } from "../../src/application/analysisView/evidenceFileSource.js"; -import { inspectAnalysisViewWithFiles } from "../../src/application/analysisView/AnalysisViewService.js"; +import { inspectAnalysisView } from "../../src/application/analysisView/AnalysisViewService.js"; // This module mock needs an isolated graph even if another service imports the workflow first. vi.mock("../../src/application/analysisView/evidenceFileSource.js", { @@ -35,7 +35,7 @@ it("prefers cancellation observed after failed Evidence-file authentication", as return loaded; }); expect( - await inspectAnalysisViewWithFiles( + await inspectAnalysisView( { source: { kind: "evidence-file", path }, view: { kind: "environment-manifest" }, diff --git a/tests/process-global/managedBundleReadGuards.test.ts b/tests/process-global/managedBundleReadGuards.test.ts index 0f14247f4d..8e42cebf56 100644 --- a/tests/process-global/managedBundleReadGuards.test.ts +++ b/tests/process-global/managedBundleReadGuards.test.ts @@ -28,10 +28,8 @@ import { parseImmutableEvidenceBundle, } from "../../src/domain/evidenceBundle.js"; import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js"; -import { - deliverAnalysisEvidence, - inspectAnalysisViewFromBundle, -} from "../../src/application/EvidenceBundleCommands.js"; +import { deliverAnalysisEvidence } from "../../src/application/EvidenceBundleCommands.js"; +import { inspectAnalysisView } from "../../src/application/analysisView/AnalysisViewService.js"; import { readImmutableEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js"; import { requireAuthenticationHeadroom } from "../../src/application/JsonAuthenticationHeadroom.js"; import { SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE } from "../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js"; @@ -215,14 +213,13 @@ it("preserves the view operation and caller reason when bundle I/O is cancelled" const reason = new Error("cancel managed bundle page"); const resources = new ArtifactResourceScope(); try { - const pending = inspectAnalysisViewFromBundle( + const pending = inspectAnalysisView( { source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view: { kind: "summary" }, }, - path, - resources, - controller.signal, + undefined, + { evidenceBundlePath: path, resources, signal: controller.signal }, ); await held.firstRead; controller.abort(reason); @@ -369,7 +366,7 @@ it.each(["JSON assembly", "bundle authentication"])( const resources = new ArtifactResourceScope(); try { expect( - await inspectAnalysisViewFromBundle( + await inspectAnalysisView( { source: { kind: "retained-evidence", @@ -377,8 +374,8 @@ it.each(["JSON assembly", "bundle authentication"])( }, view: { kind: "summary" }, }, - path, - resources, + undefined, + { evidenceBundlePath: path, resources }, ), ).toMatchObject({ ok: false,