feat(analysis-view): project rich captures from Evidence files and template path pairs (#5293)

Project large local Evidence captures through the authenticated file-source boundary and add guarded opt-in sibling-pair path inference. Preserve request cancellation, resource ownership and file-relative validation diagnostics.

Validated relevant source and compiled CLI/MCP/environment boundaries, static/build/docs/catalog gates, and a real isolated Gitea 28.2.0 capture through CLI, default stdio MCP and HTTP replay. Current-head automated review passed; unrelated long-running platform jobs may still be pending.

Co-authored-by: Edgar Arakelyan <edgar.arakelyan@scale.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-20 10:33:52 +08:00
1 parent 5fa4defcbc
commit f93a0335a8
28 files changed
+1227 -66

No files matched your search

@@ -1,3 +1,4 @@
import { setImmediate } from "node:timers/promises";
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js";
import { z } from "zod";
@@ -6,7 +7,10 @@ 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 {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../../domain/analysisErrorCore.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import {
@@ -29,6 +33,10 @@ import { err, ok, type Result } from "../../domain/result.js";
import { WebNetworkApiSurfaceAccumulator } from "../../domain/analysisView/WebNetworkApiSurfaceAccumulator.js";
import { EnvironmentManifestAccumulator } from "../../domain/analysisView/EnvironmentManifestAccumulator.js";
import { admitAnalysisViewData } from "../../domain/analysisView/analysisViewDataAdmission.js";
import {
readEvidenceFileSource,
type EvidenceFileReadOptions,
} from "./evidenceFileSource.js";
/** Authenticate a selected-view request and project already completed Evidence. */
export const inspectAnalysisView = (
@@ -48,6 +56,58 @@ export const inspectAnalysisView = (
return inspectAnalysisViewValidated(parsed.data, lookup);
};
/** Validate a request whose source may be an Evidence file, then project it. */
export const inspectAnalysisViewWithFiles = async (
rawInput: unknown,
lookup?: EvidenceLookup,
options: EvidenceFileReadOptions = {},
): Promise<Result<Evidence, AnalysisError>> => {
const parsed = inspectAnalysisViewInputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
INSPECT_ANALYSIS_VIEW_OPERATION,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
return inspectAnalysisViewValidatedWithFiles(parsed.data, lookup, options);
};
/**
* 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.
*/
export const inspectAnalysisViewValidatedWithFiles = async (
input: InspectAnalysisViewInput,
lookup?: EvidenceLookup,
options: EvidenceFileReadOptions = {},
): Promise<Result<Evidence, AnalysisError>> => {
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<boolean> => {
await setImmediate();
return options.signal?.aborted === true;
};
// A request cancelled during authentication reports cancellation, even when authentication failed.
if (await cancelled())
return err(new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION));
if (!loaded.ok) return loaded;
const id = loaded.value.evidence_id;
const projected = inspectAnalysisViewValidated(
{ ...input, source: { kind: "retained-evidence", evidence_id: id } },
(evidenceId) => (evidenceId === id ? loaded.value : lookup?.(evidenceId)),
);
// A request cancelled during projection delivers and records nothing.
return (await cancelled())
? err(new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION))
: projected;
};
/** Authenticate borrowed inline Evidence after adapter envelope validation. */
export const inspectAnalysisViewValidated = (
input: InspectAnalysisViewInput,
@@ -182,8 +242,28 @@ const resolveAnalysisViewSource = (
): Result<Evidence, AnalysisError> => {
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,
});
};
/** Parse complete Evidence, reporting schema issues under the given request path. */
const authenticateEvidence = (
value: unknown,
path: readonly (string | number)[],
input: unknown,
): Result<Evidence, AnalysisError> => {
try {
return ok(parseEvidence(source.evidence));
return ok(parseEvidence(value));
} catch (cause: unknown) {
if (cause instanceof z.ZodError)
return err(
@@ -191,9 +271,9 @@ const resolveAnalysisViewSource = (
INSPECT_ANALYSIS_VIEW_OPERATION,
cause.issues.map((issue) => ({
...issue,
path: ["source", "evidence", ...issue.path],
path: [...path, ...issue.path],
})),
{ source },
input,
{ cause },
),
);