refactor: remove cross-version investigation workflow

This commit is contained in:
morluto committed 2026-09-29 08:06:43 +08:00
1 parent 0d96275b68
commit 81996df7a5
42 files changed
+66 -3750

No files matched your search

-9
View File
@@ -159,19 +159,10 @@ type CapabilityAvailability =
readonly availabilityCode?: ProviderRejectionCode;
};
interface CapabilityLimits {
readonly maxResults: number | null;
readonly maxPayloadBytes: number | null;
readonly timeoutMs: number | null;
}
export type CapabilityDescriptor = CapabilityAvailability & {
readonly provider: ProviderIdentity;
readonly operation: Exclude<AnalysisOperation, "health">;
readonly pagination: "none" | "offset" | "cursor";
readonly exhaustive: boolean;
readonly effects: CapabilityEffects;
readonly limits: CapabilityLimits;
readonly limitations: readonly string[];
};
-9
View File
@@ -18,7 +18,6 @@ import type {
UnknownStatus,
UpdateUnknownInput,
} from "../domain/residualUnknown.js";
import type { InvestigationWorkspace } from "../domain/investigationWorkspace.js";
import type { ReconstructionCoverageWorkspace } from "../domain/reconstructionCoverage.js";
import type {
AnalysisOperation,
@@ -58,14 +57,6 @@ export interface BinarySessionPort extends AnalysisOperationPort {
importAnalysisSnapshot(
snapshot: AnalysisSnapshot,
): Result<number, AnalysisError>;
retainInvestigationWorkspace(
workspace: InvestigationWorkspace,
): "added" | "duplicate";
investigationWorkspace(
workspaceId: string,
revision: number,
): InvestigationWorkspace | undefined;
investigationWorkspaces(): readonly InvestigationWorkspace[];
retainReconstructionCoverageWorkspace(
workspace: ReconstructionCoverageWorkspace,
): "added" | "duplicate";
+1 -30
View File
@@ -10,10 +10,6 @@ import {
type AnalysisError,
type UnknownRegistryError,
} from "../domain/errors.js";
import {
parseInvestigationWorkspace,
type InvestigationWorkspace,
} from "../domain/investigationWorkspace.js";
import {
parseReconstructionCoverageWorkspace,
type ReconstructionCoverageWorkspace,
@@ -42,14 +38,13 @@ export interface ActiveAnalysisBinding {
readonly profile: AnalysisProfileCommitment | null;
}
/** Owns session evidence, snapshots, workspaces, and residual unknowns. */
/** Owns session evidence, snapshots, and residual unknowns. */
export abstract class BinarySessionRecords {
readonly #evidence = new EvidenceLedger({
maxRecords: 10_000,
maxBytes: 64 * 1024 * 1024,
});
readonly #snapshot = new AnalysisSnapshotCache();
readonly #investigationWorkspaces = new Map<string, InvestigationWorkspace>();
readonly #coverageWorkspaces = new Map<
string,
ReconstructionCoverageWorkspace
@@ -216,30 +211,6 @@ export abstract class BinarySessionRecords {
}
}
retainInvestigationWorkspace(
workspace: InvestigationWorkspace,
): "added" | "duplicate" {
const parsed = parseInvestigationWorkspace(workspace);
const key = workspaceKey(parsed.workspace_id, parsed.revision);
if (this.#investigationWorkspaces.has(key)) return "duplicate";
this.#investigationWorkspaces.set(key, parsed);
return "added";
}
investigationWorkspace(
workspaceId: string,
revision: number,
): InvestigationWorkspace | undefined {
const workspace = this.#investigationWorkspaces.get(
workspaceKey(workspaceId, revision),
);
return workspace === undefined ? undefined : structuredClone(workspace);
}
investigationWorkspaces(): readonly InvestigationWorkspace[] {
return sortedWorkspaces(this.#investigationWorkspaces.values());
}
retainReconstructionCoverageWorkspace(
workspace: ReconstructionCoverageWorkspace,
): "added" | "duplicate" {
-7
View File
@@ -193,8 +193,6 @@ const capabilityStatus = (
available: descriptor.available,
reason: descriptor.reason,
availability_code: descriptor.availabilityCode ?? null,
pagination: descriptor.pagination,
exhaustive: descriptor.exhaustive,
effects: {
mutates_artifact: descriptor.effects.mutatesArtifact,
launches_process: descriptor.effects.launchesProcess,
@@ -204,10 +202,5 @@ const capabilityStatus = (
changes_permissions: descriptor.effects.changesPermissions,
requires_root: descriptor.effects.requiresRoot,
},
limits: {
max_results: descriptor.limits.maxResults,
max_payload_bytes: descriptor.limits.maxPayloadBytes,
timeout_ms: descriptor.limits.timeoutMs,
},
limitations: [...descriptor.limitations],
});
@@ -34,8 +34,6 @@ const status = (
: (capability.availability_code ?? null),
input_contract_version: 1,
output_contract_version: 1,
pagination: "none" as const,
exhaustive: true,
effects: {
mutates_artifact: false,
launches_process: false,
@@ -45,11 +43,6 @@ const status = (
changes_permissions: false,
requires_root: false,
},
limits: {
max_results: null,
max_payload_bytes: null,
timeout_ms: null,
},
limitations: [],
})),
});
-219
View File
@@ -1,219 +0,0 @@
import type { ArtifactLimits } from "../artifacts/ArtifactReader.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import {
AnalysisCancelledError,
ArtifactOperationError,
type AnalysisError,
} from "../domain/errors.js";
import type {
CrossVersionInvestigationInput,
InvestigationRunOptions,
InvestigationRunTarget,
} from "../domain/investigationWorkspace.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import {
paginateArtifactInventory,
type ArtifactIntegrityPolicy,
type ArtifactInventorySnapshot,
} from "./ArtifactInventory.js";
import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.js";
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
import { resolveArtifactIntegrityPolicy } from "./ArtifactInventory/policy.js";
export const AUTOMATIC_RUN_LIMITATION =
"Automatic cross-version runs compare shipped artifact structure only; they do not execute either version.";
export interface VersionSnapshots {
readonly left: ArtifactInventorySnapshot;
readonly right: ArtifactInventorySnapshot;
}
export interface InventoryEvidencePages {
readonly left: readonly Evidence[];
readonly right: readonly Evidence[];
}
/** Injectable artifact scan boundary used by paired version inventory. */
export type VersionInventoryScanner = typeof scanAuthorizedArtifactInventory;
/** Inputs shared by the two scans in one version comparison. */
export interface VersionInventoryScanOptions {
readonly leftPath: string;
readonly rightPath: string;
readonly limits: ArtifactLimits;
readonly signal?: AbortSignal;
readonly integrity: ArtifactIntegrityPolicy;
}
/** Start both independent version scans while preserving left/right identity. */
export const scanVersionInventories = async (
options: VersionInventoryScanOptions,
scanner: VersionInventoryScanner = scanAuthorizedArtifactInventory,
): Promise<VersionSnapshots> => {
const [left, right] = await Promise.all([
scanner(options.leftPath, options.limits, {
signal: options.signal,
integrity: options.integrity,
}),
scanner(options.rightPath, options.limits, {
signal: options.signal,
integrity: options.integrity,
}),
]);
return { left, right };
};
/** Authorize and scan both inputs once under the same bounded traversal policy. */
export const scanVersions = async (
input: CrossVersionInvestigationInput,
signal?: AbortSignal,
integrityContinueEnabled = false,
): Promise<Result<VersionSnapshots, AnalysisError>> => {
try {
if (signal?.aborted === true)
return err(new AnalysisCancelledError("find_changed_behavior"));
const limits = artifactLimits(input.options);
const integrity = resolveArtifactIntegrityPolicy(
input.integrity_policy === "fail"
? { mode: "fail" }
: {
mode: input.integrity_policy,
maxMismatches: input.max_integrity_mismatches,
},
integrityContinueEnabled,
);
return ok(
await scanVersionInventories({
leftPath: input.left_path,
rightPath: input.right_path,
limits,
...(signal === undefined ? {} : { signal }),
integrity,
}),
);
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure)
return err(
new ArtifactOperationError(
"inventory_artifact",
cause.reason,
cause.details,
),
);
return err(new ArtifactOperationError("inventory_artifact", "io"));
}
};
/** Project complete snapshots into bounded, comparable Evidence page sets. */
export const createInventoryEvidencePages = (
input: CrossVersionInvestigationInput,
snapshots: VersionSnapshots,
): Result<InventoryEvidencePages, AnalysisError> => {
const left = inventoryPages(input.left_path, snapshots.left, input);
const right = inventoryPages(input.right_path, snapshots.right, input);
if (left.length > 100 || right.length > 100)
return err(new ArtifactOperationError("inventory_artifact", "limit"));
return ok({ left, right });
};
/** Reduce a snapshot to the content commitments used by run identity. */
export const targetFor = (
snapshot: ArtifactInventorySnapshot,
): InvestigationRunTarget => ({
root_sha256: snapshot.manifest.root_sha256,
graph_sha256: snapshot.manifest.graph_sha256,
manifest_id: snapshot.manifest.manifest_id,
format: snapshot.manifest.root_format,
});
/** Preserve scan and execution limitations in stable order. */
export const runLimitations = (snapshots: VersionSnapshots): string[] =>
[
...new Set([
...snapshots.left.limitations.map((item) => `Left: ${item}`),
...snapshots.right.limitations.map((item) => `Right: ${item}`),
AUTOMATIC_RUN_LIMITATION,
]),
].sort((left, right) => left.localeCompare(right));
const inventoryPages = (
path: string,
snapshot: ArtifactInventorySnapshot,
input: CrossVersionInvestigationInput,
): Evidence[] => {
const options = input.options;
const count = Math.max(
1,
Math.ceil(snapshot.nodes.length / options.page_size),
Math.ceil(snapshot.occurrences.length / options.page_size),
Math.ceil(snapshot.edges.length / options.page_size),
);
return Array.from({ length: count }, (_, index) =>
inventoryPage(path, snapshot, input, index * options.page_size),
);
};
const inventoryPage = (
path: string,
snapshot: ArtifactInventorySnapshot,
input: CrossVersionInvestigationInput,
offset: number,
): Evidence => {
const options = input.options;
const result = paginateArtifactInventory(snapshot, {
nodeOffset: offset,
nodeLimit: options.page_size,
occurrenceOffset: offset,
occurrenceLimit: options.page_size,
edgeOffset: offset,
edgeLimit: options.page_size,
});
return createEvidence(
{
path,
sha256: snapshot.manifest.root_sha256,
format: snapshot.manifest.root_format,
},
ARTIFACT_GRAPH_PROVIDER,
{
operation: "inventory_artifact",
parameters: {
node_offset: offset,
node_limit: options.page_size,
occurrence_offset: offset,
occurrence_limit: options.page_size,
edge_offset: offset,
edge_limit: options.page_size,
max_entries: options.max_entries,
max_total_bytes: options.max_total_bytes,
max_entry_bytes: options.max_entry_bytes,
max_compression_ratio: options.max_compression_ratio,
max_depth: options.max_depth,
max_path_bytes: options.max_path_bytes,
integrity_policy: input.integrity_policy,
integrity_continue_approved: input.integrity_continue_approved,
max_integrity_mismatches: input.max_integrity_mismatches,
},
result: jsonValueSchema.parse(result),
rawResult: null,
limitations: result.limitations,
locations: result.occurrences.items.map(
({ logical_path: logicalPath }) => ({
kind: "artifact-path" as const,
path: logicalPath,
}),
),
},
);
};
const artifactLimits = (options: InvestigationRunOptions): ArtifactLimits => ({
maxEntries: options.max_entries,
maxTotalBytes: options.max_total_bytes,
maxEntryBytes: options.max_entry_bytes,
maxCompressionRatio: options.max_compression_ratio,
maxDepth: options.max_depth,
maxPathBytes: options.max_path_bytes,
});
@@ -1,498 +0,0 @@
import { type Evidence } from "../domain/evidence.js";
import {
createEvidenceBundle,
type EvidenceFilePolicy,
} from "../domain/evidenceBundle.js";
import {
AnalysisCancelledError,
AnalysisInputError,
EvidenceIntegrityError,
InvestigationWorkspaceError,
type AnalysisError,
} from "../domain/errors.js";
import {
createLegacyInvestigationRunIdentity,
createInvestigationRunIdentity,
createInvestigationWorkspace,
crossVersionInvestigationInputSchema,
investigationRunSchema,
reviseInvestigationWorkspace,
type CrossVersionInvestigationInput,
type InvestigationRun,
type InvestigationWorkspace,
} from "../domain/investigationWorkspace.js";
import { err, ok, type Result } from "../domain/result.js";
import type { BinarySessionPort } from "./BinarySession.js";
import type { ProgressReporter } from "./ProgressReporter.js";
import {
createInventoryEvidencePages,
runLimitations,
scanVersions,
targetFor,
type InventoryEvidencePages,
type VersionSnapshots,
} from "./CrossVersionInventory.js";
import {
readInvestigationWorkspace,
writeInvestigationWorkspace,
} from "./InvestigationWorkspaceStore.js";
import {
createArtifactComparisonEvidence,
createChangedBehaviorEvidence,
} from "./CrossVersionInvestigationEvidence.js";
import { selectCompletedInvestigationReplay } from "./CrossVersionInvestigationReplay.js";
import {
investigationEvidenceIds as evidenceIds,
mergeInvestigationEvidence as mergeEvidence,
recordsForInvestigationRun as recordsForRun,
replaceInvestigationRun as replaceRun,
} from "./CrossVersionInvestigationCollections.js";
export interface CrossVersionInvestigationOutcome {
readonly evidence: Evidence;
readonly workspace: InvestigationWorkspace;
readonly reused: boolean;
}
/** Runtime authority and cancellation capabilities for one investigation. */
export interface CrossVersionInvestigationExecution {
readonly session?: BinarySessionPort;
readonly signal?: AbortSignal;
readonly progress?: ProgressReporter;
readonly integrityContinueEnabled?: boolean;
readonly authorizeWorkspaceWrite?: () => Promise<Result<null, AnalysisError>>;
}
/** Run or resume a deterministic cross-version artifact investigation. */
export const runCrossVersionInvestigation = async (
input: unknown,
policy: EvidenceFilePolicy,
execution: CrossVersionInvestigationExecution,
): Promise<Result<CrossVersionInvestigationOutcome, AnalysisError>> => {
const parsed = crossVersionInvestigationInputSchema.safeParse(input);
if (!parsed.success)
return err(
new AnalysisInputError("find_changed_behavior", {
cause: parsed.error,
}),
);
return runCrossVersionInvestigationValidated(parsed.data, policy, execution);
};
/** Run input already parsed by a trusted adapter boundary. */
export const runCrossVersionInvestigationValidated = async (
input: CrossVersionInvestigationInput,
policy: EvidenceFilePolicy,
execution: CrossVersionInvestigationExecution,
): Promise<Result<CrossVersionInvestigationOutcome, AnalysisError>> => {
const initial = await readInvestigationWorkspace(
input.workspace_path,
policy,
);
if (!initial.ok) return initial;
const preflight = validateWorkspaceRequest(initial.value, input);
if (!preflight.ok) return preflight;
if (isCancelled(execution.signal))
return err(new AnalysisCancelledError("find_changed_behavior"));
if (input.replay_run_id !== undefined) {
const replay = selectCompletedInvestigationReplay(initial.value, input);
if (!replay.ok) return replay;
return completeOutcome(
replay.value.workspace,
replay.value.run,
true,
execution.session,
);
}
await execution.progress?.report({
phase: "scan_versions",
completed: 0,
total: 4,
message: "Scanning both version inputs",
});
const snapshots = await scanVersions(
input,
execution.signal,
execution.integrityContinueEnabled,
);
if (!snapshots.ok) return snapshots;
await execution.progress?.report({
phase: "scan_versions",
completed: 1,
total: 4,
message: "Version inputs scanned",
});
return continueInvestigation({
input,
initial: initial.value,
snapshots: snapshots.value,
policy,
session: execution.session,
signal: execution.signal,
progress: execution.progress,
authorizeWorkspaceWrite: execution.authorizeWorkspaceWrite,
});
};
interface RunState {
readonly workspace: InvestigationWorkspace;
readonly run: InvestigationRun;
}
const continueInvestigation = async (context: {
readonly input: CrossVersionInvestigationInput;
readonly initial: InvestigationWorkspace | null;
readonly snapshots: VersionSnapshots;
readonly policy: EvidenceFilePolicy;
readonly session: BinarySessionPort | undefined;
readonly signal: AbortSignal | undefined;
readonly progress: ProgressReporter | undefined;
readonly authorizeWorkspaceWrite:
| (() => Promise<Result<null, AnalysisError>>)
| undefined;
}): Promise<Result<CrossVersionInvestigationOutcome, AnalysisError>> => {
const left = targetFor(context.snapshots.left);
const right = targetFor(context.snapshots.right);
const identity = createInvestigationRunIdentity({
left,
right,
options: context.input.options,
integrity_policy: context.input.integrity_policy,
integrity_continue_approved: context.input.integrity_continue_approved,
max_integrity_mismatches: context.input.max_integrity_mismatches,
});
const legacyIdentity = createLegacyInvestigationRunIdentity({
left,
right,
options: context.input.options,
});
const existing = context.initial?.runs.find(
(run) =>
run.run_id === identity.runId ||
(context.input.integrity_policy === "fail" &&
!context.input.integrity_continue_approved &&
context.input.max_integrity_mismatches === 10 &&
"legacy_request_identity" in run &&
run.max_integrity_mismatches === 10 &&
run.run_id === legacyIdentity.runId),
);
if (context.initial !== null && existing?.status === "complete")
return completeOutcome(context.initial, existing, true, context.session);
if (context.authorizeWorkspaceWrite !== undefined) {
const authorized = await context.authorizeWorkspaceWrite();
if (!authorized.ok) return authorized;
}
const pages = createInventoryEvidencePages(context.input, context.snapshots);
if (!pages.ok) return pages;
if (isCancelled(context.signal))
return err(new AnalysisCancelledError("find_changed_behavior"));
const inventoried = await ensureInventoryCheckpoint({
input: context.input,
current: context.initial,
existing,
pages: pages.value,
snapshots: context.snapshots,
policy: context.policy,
});
if (!inventoried.ok) return inventoried;
await context.progress?.report({
phase: "inventory_versions",
completed: 2,
total: 4,
message: "Both artifact inventories checkpointed",
});
if (isCancelled(context.signal))
return err(new AnalysisCancelledError("find_changed_behavior"));
const compared = await ensureComparisonCheckpoint(
context.input,
inventoried.value,
context.policy,
);
if (!compared.ok) return compared;
await context.progress?.report({
phase: "compare_artifacts",
completed: 3,
total: 4,
message: "Artifact comparison checkpointed",
});
if (isCancelled(context.signal))
return err(new AnalysisCancelledError("find_changed_behavior"));
const finalized = await finalizeInvestigation(
context.input,
compared.value,
context.policy,
context.session,
);
await context.progress?.report({
phase: "changed_behavior",
completed: 4,
total: 4,
message: finalized.ok ? "Investigation completed" : "Investigation failed",
terminal: true,
});
return finalized;
};
const isCancelled = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
const ensureInventoryCheckpoint = async (input: {
readonly input: CrossVersionInvestigationInput;
readonly current: InvestigationWorkspace | null;
readonly existing: InvestigationRun | undefined;
readonly pages: InventoryEvidencePages;
readonly snapshots: VersionSnapshots;
readonly policy: EvidenceFilePolicy;
}): Promise<Result<RunState, AnalysisError>> => {
if (input.existing !== undefined) {
const consistent = validateInventoryCheckpoint(input.existing, input.pages);
if (!consistent.ok) return consistent;
return input.current === null
? err(new EvidenceIntegrityError("Investigation workspace is missing"))
: ok({ workspace: input.current, run: input.existing });
}
const left = targetFor(input.snapshots.left);
const right = targetFor(input.snapshots.right);
const identity = createInvestigationRunIdentity({
left,
right,
options: input.input.options,
integrity_policy: input.input.integrity_policy,
integrity_continue_approved: input.input.integrity_continue_approved,
max_integrity_mismatches: input.input.max_integrity_mismatches,
});
const run = investigationRunSchema.parse({
schema_version: 1,
run_id: identity.runId,
request_sha256: identity.requestSha256,
left,
right,
options: input.input.options,
integrity_policy: input.input.integrity_policy,
integrity_continue_approved: input.input.integrity_continue_approved,
max_integrity_mismatches: input.input.max_integrity_mismatches,
status: "running",
completed_stages: ["inventory_left", "inventory_right"],
left_inventory_evidence_ids: evidenceIds(input.pages.left),
right_inventory_evidence_ids: evidenceIds(input.pages.right),
comparison_evidence_id: null,
result_evidence_id: null,
limitations: runLimitations(input.snapshots),
});
const saved = await checkpoint({
path: input.input.workspace_path,
name: input.input.workspace_name,
current: input.current,
records: [...input.pages.left, ...input.pages.right],
run,
policy: input.policy,
});
return saved.ok ? ok({ workspace: saved.value, run }) : saved;
};
const ensureComparisonCheckpoint = async (
input: CrossVersionInvestigationInput,
state: RunState,
policy: EvidenceFilePolicy,
): Promise<Result<RunState, AnalysisError>> => {
if (state.run.comparison_evidence_id !== null) return ok(state);
const inventory = inventoryEvidence(state.workspace, state.run);
if (!inventory.ok) return inventory;
const evidence = createArtifactComparisonEvidence(
inventory.value,
input.options.change_limit,
);
const run = investigationRunSchema.parse({
...state.run,
completed_stages: [
"inventory_left",
"inventory_right",
"compare_artifacts",
],
comparison_evidence_id: evidence.evidence_id,
});
const saved = await checkpoint({
path: input.workspace_path,
name: input.workspace_name,
current: state.workspace,
records: [evidence],
run,
policy,
});
return saved.ok ? ok({ workspace: saved.value, run }) : saved;
};
const finalizeInvestigation = async (
input: CrossVersionInvestigationInput,
state: RunState,
policy: EvidenceFilePolicy,
session?: BinarySessionPort,
): Promise<Result<CrossVersionInvestigationOutcome, AnalysisError>> => {
const comparison = comparisonEvidenceFor(state.workspace, state.run);
if (!comparison.ok) return comparison;
const evidence = createChangedBehaviorEvidence(
state.workspace,
state.run,
comparison.value,
);
const run = investigationRunSchema.parse({
...state.run,
status: "complete",
completed_stages: [
"inventory_left",
"inventory_right",
"compare_artifacts",
"find_changed_behavior",
],
result_evidence_id: evidence.evidence_id,
});
const saved = await checkpoint({
path: input.workspace_path,
name: input.workspace_name,
current: state.workspace,
records: [evidence],
run,
policy,
});
return saved.ok ? completeOutcome(saved.value, run, false, session) : saved;
};
const comparisonEvidenceFor = (
workspace: InvestigationWorkspace,
run: InvestigationRun,
): Result<Evidence, EvidenceIntegrityError> => {
const evidence = workspace.bundle.records.find(
({ evidence_id: id }) => id === run.comparison_evidence_id,
);
return evidence === undefined
? err(
new EvidenceIntegrityError(
"Investigation comparison checkpoint is missing Evidence",
),
)
: ok(evidence);
};
const checkpoint = async (input: {
readonly path: string;
readonly name: string;
readonly current: InvestigationWorkspace | null;
readonly records: readonly Evidence[];
readonly run: InvestigationRun;
readonly policy: EvidenceFilePolicy;
}): Promise<Result<InvestigationWorkspace, AnalysisError>> => {
const records = mergeEvidence(
input.current?.bundle.records ?? [],
input.records,
);
const bundle = createEvidenceBundle(
records,
input.current?.bundle.unknowns ?? [],
);
const runs = replaceRun(input.current?.runs ?? [], input.run);
const next =
input.current === null
? createInvestigationWorkspace(input.name, bundle, runs)
: reviseInvestigationWorkspace(input.current, bundle, runs);
const written = await writeInvestigationWorkspace(
next,
input.path,
input.current?.revision ?? null,
input.policy,
);
return written.ok ? ok(next) : written;
};
const completeOutcome = (
workspace: InvestigationWorkspace,
run: InvestigationRun,
reused: boolean,
session?: BinarySessionPort,
): Result<CrossVersionInvestigationOutcome, AnalysisError> => {
if (run.result_evidence_id === null)
return err(
new EvidenceIntegrityError("Completed investigation has no result"),
);
const evidence = workspace.bundle.records.find(
({ evidence_id: id }) => id === run.result_evidence_id,
);
if (evidence === undefined)
return err(
new EvidenceIntegrityError(
"Completed investigation result Evidence is missing",
),
);
if (session !== undefined) {
const records = recordsForRun(workspace, run);
const imported = session.importEvidenceBundle(
createEvidenceBundle(records),
);
if (!imported.ok) return imported;
}
return ok({ evidence, workspace, reused });
};
const validateWorkspaceRequest = (
workspace: InvestigationWorkspace | null,
input: CrossVersionInvestigationInput,
): Result<null, InvestigationWorkspaceError> => {
if (workspace !== null && workspace.name !== input.workspace_name)
return err(new InvestigationWorkspaceError("update", "name-conflict"));
if (
input.expected_workspace_revision !== undefined &&
workspace?.revision !== input.expected_workspace_revision
)
return err(new InvestigationWorkspaceError("update", "revision-conflict"));
return ok(null);
};
const validateInventoryCheckpoint = (
run: InvestigationRun,
pages: {
readonly left: readonly Evidence[];
readonly right: readonly Evidence[];
},
): Result<null, EvidenceIntegrityError> =>
JSON.stringify(run.left_inventory_evidence_ids) ===
JSON.stringify(evidenceIds(pages.left)) &&
JSON.stringify(run.right_inventory_evidence_ids) ===
JSON.stringify(evidenceIds(pages.right))
? ok(null)
: err(
new EvidenceIntegrityError(
"Investigation inventory checkpoint disagrees with current content",
),
);
const inventoryEvidence = (
workspace: InvestigationWorkspace | null,
run: InvestigationRun,
): Result<
{ readonly left: readonly Evidence[]; readonly right: readonly Evidence[] },
EvidenceIntegrityError
> => {
if (workspace === null)
return err(
new EvidenceIntegrityError("Investigation workspace is missing"),
);
const byId = new Map(
workspace.bundle.records.map((record) => [record.evidence_id, record]),
);
const left = run.left_inventory_evidence_ids.flatMap((id) => {
const evidence = byId.get(id);
return evidence === undefined ? [] : [evidence];
});
const right = run.right_inventory_evidence_ids.flatMap((id) => {
const evidence = byId.get(id);
return evidence === undefined ? [] : [evidence];
});
return left.length === run.left_inventory_evidence_ids.length &&
right.length === run.right_inventory_evidence_ids.length
? ok({ left, right })
: err(
new EvidenceIntegrityError(
"Investigation inventory checkpoint is incomplete",
),
);
};
@@ -1,45 +0,0 @@
import type { Evidence } from "../domain/evidence.js";
import type {
InvestigationRun,
InvestigationWorkspace,
} from "../domain/investigationWorkspace.js";
export const recordsForInvestigationRun = (
workspace: InvestigationWorkspace,
run: InvestigationRun,
): Evidence[] => {
const ids = new Set([
...run.left_inventory_evidence_ids,
...run.right_inventory_evidence_ids,
...(run.comparison_evidence_id === null
? []
: [run.comparison_evidence_id]),
...(run.result_evidence_id === null ? [] : [run.result_evidence_id]),
]);
return workspace.bundle.records.filter(({ evidence_id: id }) => ids.has(id));
};
export const mergeInvestigationEvidence = (
current: readonly Evidence[],
additions: readonly Evidence[],
): Evidence[] => {
const records = new Map(
current.map((record) => [record.evidence_id, record]),
);
for (const record of additions)
if (!records.has(record.evidence_id))
records.set(record.evidence_id, record);
return [...records.values()];
};
export const replaceInvestigationRun = (
runs: readonly InvestigationRun[],
replacement: InvestigationRun,
): InvestigationRun[] => [
...runs.filter(({ run_id: id }) => id !== replacement.run_id),
replacement,
];
export const investigationEvidenceIds = (
records: readonly Evidence[],
): string[] => records.map(({ evidence_id: id }) => id);
@@ -1,91 +0,0 @@
import { compareArtifacts } from "../domain/artifactComparison.js";
import { findChangedBehavior } from "../domain/changedBehavior.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import {
investigationRunSummarySchema,
type InvestigationRun,
type InvestigationWorkspace,
} from "../domain/investigationWorkspace.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { AUTOMATIC_RUN_LIMITATION } from "./CrossVersionInventory.js";
import {
ARTIFACT_COMPARISON_PROVIDER,
CHANGED_BEHAVIOR_PROVIDER,
} from "./InvestigationProviders.js";
/** Derive deterministic artifact-comparison Evidence from inventory pages. */
export const createArtifactComparisonEvidence = (
inventory: {
readonly left: readonly Evidence[];
readonly right: readonly Evidence[];
},
limit: number,
): Evidence => {
const comparison = compareArtifacts(
inventory.left,
inventory.right,
0,
limit,
);
const leftIds = evidenceIds(inventory.left);
const rightIds = evidenceIds(inventory.right);
return createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
predicateType: "rea.artifact-comparison/v1",
operation: "compare_artifacts",
parameters: {
left_evidence_ids: leftIds,
right_evidence_ids: rightIds,
offset: 0,
limit,
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
evidenceLinks: [...leftIds, ...rightIds],
});
};
/** Derive the deterministic final report for one comparison checkpoint. */
export const createChangedBehaviorEvidence = (
workspace: InvestigationWorkspace,
run: InvestigationRun,
comparison: Evidence,
): Evidence => {
const limit = Math.min(run.options.change_limit, 100);
const changed = findChangedBehavior([comparison], 0, limit);
const summary = investigationRunSummarySchema.parse({
schema_version: 1,
workspace_id: workspace.workspace_id,
run_id: run.run_id,
left_manifest_id: run.left.manifest_id,
right_manifest_id: run.right.manifest_id,
inventory_evidence_count:
run.left_inventory_evidence_ids.length +
run.right_inventory_evidence_ids.length,
comparison_evidence_id: comparison.evidence_id,
limitations: run.limitations,
});
return createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, {
predicateType: "rea.changed-behavior/v1",
operation: "find_changed_behavior",
parameters: {
workspace_id: workspace.workspace_id,
run_id: run.run_id,
comparison_evidence_ids: [comparison.evidence_id],
offset: 0,
limit,
},
result: jsonValueSchema.parse({
...changed,
investigation_run: summary,
}),
confidence: "derived",
authority: "analyst-inference",
limitations: [...changed.limitations, AUTOMATIC_RUN_LIMITATION],
evidenceLinks: changed.evidence_links,
});
};
const evidenceIds = (records: readonly Evidence[]): string[] =>
records.map(({ evidence_id: id }) => id);
@@ -1,341 +0,0 @@
import { isDeepStrictEqual } from "node:util";
import {
artifactInventoryResultSchema,
type ArtifactInventoryResult,
} from "../domain/artifactGraph.js";
import { parseArtifactInventoryEvidence } from "../domain/artifactInventoryEvidence.js";
import type { Evidence } from "../domain/evidence.js";
import {
EvidenceIntegrityError,
InvestigationWorkspaceError,
} from "../domain/errors.js";
import type {
CrossVersionInvestigationInput,
InvestigationRun,
InvestigationRunTarget,
InvestigationWorkspace,
} from "../domain/investigationWorkspace.js";
import { err, ok, type Result } from "../domain/result.js";
import {
createArtifactComparisonEvidence,
createChangedBehaviorEvidence,
} from "./CrossVersionInvestigationEvidence.js";
import { AUTOMATIC_RUN_LIMITATION } from "./CrossVersionInventory.js";
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
/** Validated workspace state returned by an explicit cache-only selector. */
export interface CompletedInvestigationReplay {
readonly workspace: InvestigationWorkspace;
readonly run: InvestigationRun;
}
interface ValidatedInventory {
readonly records: readonly Evidence[];
readonly result: ReturnType<typeof parseArtifactInventoryEvidence>;
}
/** Select and verify one caller-identified complete run without input access. */
export const selectCompletedInvestigationReplay = (
workspace: InvestigationWorkspace | null,
input: CrossVersionInvestigationInput,
): Result<
CompletedInvestigationReplay,
InvestigationWorkspaceError | EvidenceIntegrityError
> => {
const run = workspace?.runs.find(
({ run_id: runId }) => runId === input.replay_run_id,
);
if (
workspace === null ||
run === undefined ||
run.status !== "complete" ||
!controlsMatch(run, input)
)
return replayConflict();
const records = new Map(
workspace.bundle.records.map((record) => [record.evidence_id, record]),
);
const left = inventoryRecords(records, run.left_inventory_evidence_ids);
const right = inventoryRecords(records, run.right_inventory_evidence_ids);
if (!left.ok) return left;
if (!right.ok) return right;
if (
!pathsMatch(left.value, input.left_path) ||
!pathsMatch(right.value, input.right_path)
)
return replayConflict();
const validated = validateLinkedEvidence(
workspace,
run,
left.value,
right.value,
);
return validated.ok ? ok({ workspace, run }) : validated;
};
const controlsMatch = (
run: InvestigationRun,
input: CrossVersionInvestigationInput,
): boolean =>
isDeepStrictEqual(run.options, input.options) &&
run.integrity_policy === input.integrity_policy &&
run.integrity_continue_approved === input.integrity_continue_approved &&
run.max_integrity_mismatches === input.max_integrity_mismatches;
const inventoryRecords = (
records: ReadonlyMap<string, Evidence>,
evidenceIds: readonly string[],
): Result<readonly Evidence[], EvidenceIntegrityError> => {
if (new Set(evidenceIds).size !== evidenceIds.length)
return replayIntegrity("Investigation replay repeats inventory Evidence");
const selected = evidenceIds.flatMap((evidenceId) => {
const evidence = records.get(evidenceId);
return evidence === undefined ? [] : [evidence];
});
return selected.length === evidenceIds.length
? ok(selected)
: replayIntegrity("Investigation replay is missing inventory Evidence");
};
const pathsMatch = (records: readonly Evidence[], path: string): boolean =>
records.every(({ subject }) => subject?.local_path === path);
const validateLinkedEvidence = (
workspace: InvestigationWorkspace,
run: InvestigationRun,
leftRecords: readonly Evidence[],
rightRecords: readonly Evidence[],
): Result<null, EvidenceIntegrityError> => {
try {
const left = validateInventory(leftRecords, run.left, run);
const right = validateInventory(rightRecords, run.right, run);
if (!left.ok) return left;
if (!right.ok) return right;
const expectedLimitations = runLimitations(left.value, right.value);
if (!isDeepStrictEqual(run.limitations, expectedLimitations))
return replayIntegrity(
"Investigation replay limitations disagree with inventory Evidence",
);
const comparison = createArtifactComparisonEvidence(
{ left: left.value.records, right: right.value.records },
run.options.change_limit,
);
const storedComparison = evidenceFor(workspace, run.comparison_evidence_id);
if (
storedComparison === undefined ||
!isDeepStrictEqual(storedComparison, comparison)
)
return replayIntegrity(
"Investigation replay comparison Evidence is inconsistent",
);
const result = createChangedBehaviorEvidence(workspace, run, comparison);
const storedResult = evidenceFor(workspace, run.result_evidence_id);
return storedResult !== undefined && isDeepStrictEqual(storedResult, result)
? ok(null)
: replayIntegrity("Investigation replay result Evidence is inconsistent");
} catch (cause: unknown) {
return replayIntegrity(
"Investigation replay linked Evidence is invalid",
cause,
);
}
};
const validateInventory = (
records: readonly Evidence[],
target: InvestigationRunTarget,
run: InvestigationRun,
): Result<ValidatedInventory, EvidenceIntegrityError> => {
const pages = records.map((record) =>
artifactInventoryResultSchema.parse(record.normalized_result),
);
const expectedPageCount = Math.max(
1,
Math.ceil((pages[0]?.manifest.node_count ?? 0) / run.options.page_size),
Math.ceil(
(pages[0]?.manifest.occurrence_count ?? 0) / run.options.page_size,
),
Math.ceil((pages[0]?.manifest.edge_count ?? 0) / run.options.page_size),
);
if (records.length !== expectedPageCount)
return replayIntegrity(
"Investigation replay inventory Evidence pagination is inconsistent",
);
if (!sameSnapshotMetadata(pages))
return replayIntegrity(
"Investigation replay inventory pages disagree on snapshot metadata",
);
for (const [index, record] of records.entries()) {
const page = pages[index];
if (page === undefined || !inventoryRecordMatches(record, page, run, index))
return replayIntegrity(
"Investigation replay inventory Evidence is inconsistent",
);
}
const result = parseArtifactInventoryEvidence(records);
if (!result.inventory.complete || !targetMatches(result.inventory, target))
return replayIntegrity(
"Investigation replay inventory commitment is inconsistent",
);
return ok({ records, result });
};
const inventoryRecordMatches = (
record: Evidence,
page: ArtifactInventoryResult,
run: InvestigationRun,
index: number,
): boolean => {
const offset = index * run.options.page_size;
const parameters = {
node_offset: offset,
node_limit: run.options.page_size,
occurrence_offset: offset,
occurrence_limit: run.options.page_size,
edge_offset: offset,
edge_limit: run.options.page_size,
max_entries: run.options.max_entries,
max_total_bytes: run.options.max_total_bytes,
max_entry_bytes: run.options.max_entry_bytes,
max_compression_ratio: run.options.max_compression_ratio,
max_depth: run.options.max_depth,
max_path_bytes: run.options.max_path_bytes,
integrity_policy: run.integrity_policy,
integrity_continue_approved: run.integrity_continue_approved,
max_integrity_mismatches: run.max_integrity_mismatches,
};
const locations = page.occurrences.items.map(({ logical_path: path }) => ({
kind: "artifact-path" as const,
path,
}));
return (
record.subject !== null &&
record.subject.name === record.subject.local_path.split("/").at(-1) &&
record.subject.digest.sha256 === page.manifest.root_sha256 &&
record.subject.format === page.manifest.root_format &&
record.subject.architecture === null &&
isDeepStrictEqual(record.provider, ARTIFACT_GRAPH_PROVIDER) &&
record.predicate_type === "rea.analysis/v2" &&
record.operation === "inventory_artifact" &&
isDeepStrictEqual(record.parameters, parameters) &&
record.raw_result === null &&
record.confidence === "observed" &&
record.authority === "shipped-artifact" &&
record.environment === null &&
isDeepStrictEqual(record.limitations, page.limitations) &&
isDeepStrictEqual(record.locations, locations) &&
record.evidence_links.length === 0 &&
isDeepStrictEqual(page.limits, traversalLimits(run)) &&
pageMatches(
page.nodes,
offset,
run.options.page_size,
page.manifest.node_count,
) &&
pageMatches(
page.occurrences,
offset,
run.options.page_size,
page.manifest.occurrence_count,
) &&
pageMatches(
page.edges,
offset,
run.options.page_size,
page.manifest.edge_count,
)
);
};
const sameSnapshotMetadata = (
pages: readonly ArtifactInventoryResult[],
): boolean => {
const first = pages[0];
return (
first !== undefined &&
pages.every(
(page) =>
isDeepStrictEqual(page.manifest, first.manifest) &&
isDeepStrictEqual(page.limits, first.limits) &&
isDeepStrictEqual(page.provenance, first.provenance) &&
isDeepStrictEqual(
page.integrity_contradictions,
first.integrity_contradictions,
) &&
isDeepStrictEqual(page.limitations, first.limitations),
)
);
};
const pageMatches = (
page: {
readonly items: readonly unknown[];
readonly offset: number;
readonly limit: number;
readonly total: number;
readonly next_offset: number | null;
},
offset: number,
limit: number,
total: number,
): boolean =>
page.offset === offset &&
page.limit === limit &&
page.total === total &&
page.items.length === Math.max(0, Math.min(limit, page.total - offset)) &&
page.next_offset === (offset + limit < page.total ? offset + limit : null);
const traversalLimits = (run: InvestigationRun) => ({
max_entries: run.options.max_entries,
max_total_bytes: run.options.max_total_bytes,
max_entry_bytes: run.options.max_entry_bytes,
max_compression_ratio: run.options.max_compression_ratio,
max_depth: run.options.max_depth,
max_path_bytes: run.options.max_path_bytes,
});
const targetMatches = (
inventory: ReturnType<typeof parseArtifactInventoryEvidence>["inventory"],
target: InvestigationRunTarget,
): boolean =>
inventory.manifest.root_sha256 === target.root_sha256 &&
inventory.manifest.graph_sha256 === target.graph_sha256 &&
inventory.manifest.manifest_id === target.manifest_id &&
inventory.manifest.root_format === target.format;
const runLimitations = (
left: ValidatedInventory,
right: ValidatedInventory,
): string[] =>
[
...new Set([
...left.result.inventory.limitations.map((item) => `Left: ${item}`),
...right.result.inventory.limitations.map((item) => `Right: ${item}`),
AUTOMATIC_RUN_LIMITATION,
]),
].sort((first, second) => first.localeCompare(second));
const evidenceFor = (
workspace: InvestigationWorkspace,
evidenceId: string | null,
): Evidence | undefined =>
evidenceId === null
? undefined
: workspace.bundle.records.find(
({ evidence_id: current }) => current === evidenceId,
);
const replayConflict = (): Result<never, InvestigationWorkspaceError> =>
err(new InvestigationWorkspaceError("read", "revision-conflict"));
const replayIntegrity = (
message: string,
cause?: unknown,
): Result<never, EvidenceIntegrityError> =>
err(
new EvidenceIntegrityError(
message,
cause === undefined ? undefined : { cause },
),
);
@@ -39,7 +39,6 @@ const grantSchema = z.object({
});
const storeSchema = z.object({
schema_version: z.literal(1),
project_id: z.string().regex(/^project_[a-f0-9]{64}$/u),
project_root: z.string(),
grants: z.array(grantSchema).max(1_000),
@@ -133,7 +132,6 @@ export const writeProjectPermissionStore = async (
const project = await identifyPermissionProject(projectRoot);
if (!project.ok) return project;
const candidate = storeSchema.safeParse({
schema_version: 1,
project_id: project.value.id,
project_root: project.value.root,
grants,
@@ -1,4 +1,4 @@
import { InvestigationWorkspaceError } from "../domain/errors.js";
import { WorkspaceStorageError } from "../domain/errors.js";
import type { EvidenceFilePolicy } from "../domain/evidenceBundle.js";
import {
parseReconstructionCoverageWorkspace,
@@ -10,29 +10,27 @@ import {
readRevisionedWorkspace,
writeRevisionedWorkspace,
type RevisionedWorkspaceCodec,
} from "./InvestigationWorkspaceStore.js";
} from "./RevisionedWorkspaceStore.js";
const codec: RevisionedWorkspaceCodec<ReconstructionCoverageWorkspace> = {
parse: parseReconstructionCoverageWorkspace,
serialize: serializeReconstructionCoverageWorkspace,
validateNext: (current, next, expectedRevision) => {
if ((current?.revision ?? null) !== expectedRevision)
return err(
new InvestigationWorkspaceError("update", "revision-conflict"),
);
return err(new WorkspaceStorageError("update", "revision-conflict"));
if (current === null)
return next.revision === 1 && next.previous_revision_sha256 === null
? ok(null)
: err(new InvestigationWorkspaceError("update", "revision-conflict"));
: err(new WorkspaceStorageError("update", "revision-conflict"));
if (
current.name !== next.name ||
current.workspace_id !== next.workspace_id
)
return err(new InvestigationWorkspaceError("update", "name-conflict"));
return err(new WorkspaceStorageError("update", "name-conflict"));
return next.revision === current.revision + 1 &&
next.previous_revision_sha256 === current.revision_sha256
? ok(null)
: err(new InvestigationWorkspaceError("update", "revision-conflict"));
: err(new WorkspaceStorageError("update", "revision-conflict"));
},
};
@@ -41,7 +39,7 @@ export const readReconstructionCoverageWorkspace = (
path: string,
policy: EvidenceFilePolicy,
): Promise<
Result<ReconstructionCoverageWorkspace | null, InvestigationWorkspaceError>
Result<ReconstructionCoverageWorkspace | null, WorkspaceStorageError>
> => readRevisionedWorkspace(path, policy, codec);
/** Atomically append one reconstruction coverage workspace CAS revision. */
@@ -53,7 +51,7 @@ export const writeReconstructionCoverageWorkspace = (
): Promise<
Result<
{ readonly path: string; readonly bytes: number },
InvestigationWorkspaceError
WorkspaceStorageError
>
> =>
writeRevisionedWorkspace(path, policy, {
@@ -11,16 +11,11 @@ import writeFileAtomic from "write-file-atomic";
import type { EvidenceFilePolicy } from "../domain/evidenceBundle.js";
import { isJsonWithinLimits } from "../domain/jsonLimits.js";
import { InvestigationWorkspaceError } from "../domain/errors.js";
import {
parseInvestigationWorkspace,
serializeInvestigationWorkspace,
type InvestigationWorkspace,
} from "../domain/investigationWorkspace.js";
import { WorkspaceStorageError } from "../domain/errors.js";
import { err, ok, type Result } from "../domain/result.js";
import { canonicalizeConfiguredRoots } from "./ConfiguredRoots.js";
type WorkspaceResult<Value> = Result<Value, InvestigationWorkspaceError>;
type WorkspaceResult<Value> = Result<Value, WorkspaceStorageError>;
/** Parser, serializer, and CAS transition owned by one revisioned workspace. */
export interface RevisionedWorkspaceCodec<Document> {
@@ -33,20 +28,6 @@ export interface RevisionedWorkspaceCodec<Document> {
) => WorkspaceResult<null>;
}
const investigationWorkspaceCodec: RevisionedWorkspaceCodec<InvestigationWorkspace> =
{
parse: parseInvestigationWorkspace,
serialize: serializeInvestigationWorkspace,
validateNext: validateNextRevision,
};
/** Read a validated workspace, returning null only when it does not exist. */
export const readInvestigationWorkspace = async (
path: string,
policy: EvidenceFilePolicy,
): Promise<WorkspaceResult<InvestigationWorkspace | null>> =>
readRevisionedWorkspace(path, policy, investigationWorkspaceCodec);
/** Read one root-confined, owner-only revisioned workspace document. */
export const readRevisionedWorkspace = async <Document>(
path: string,
@@ -54,14 +35,14 @@ export const readRevisionedWorkspace = async <Document>(
codec: RevisionedWorkspaceCodec<Document>,
): Promise<WorkspaceResult<Document | null>> => {
if (policy.roots.length === 0)
return err(new InvestigationWorkspaceError("read", "disabled"));
return err(new WorkspaceStorageError("read", "disabled"));
try {
const destination = await resolveDestination(path, policy.roots);
if (destination === null)
return err(new InvestigationWorkspaceError("read", "outside-root"));
return err(new WorkspaceStorageError("read", "outside-root"));
return await readWorkspaceFile(destination, policy, codec);
} catch (cause: unknown) {
return err(new InvestigationWorkspaceError("read", "io", { cause }));
return err(new WorkspaceStorageError("read", "io", { cause }));
}
};
@@ -71,21 +52,6 @@ export interface RevisionedWorkspaceWriteInput<Document> {
readonly codec: RevisionedWorkspaceCodec<Document>;
}
/** Atomically append one CAS-linked workspace revision under an exclusive lock. */
export const writeInvestigationWorkspace = async (
workspace: InvestigationWorkspace,
path: string,
expectedRevision: number | null,
policy: EvidenceFilePolicy,
): Promise<
WorkspaceResult<{ readonly path: string; readonly bytes: number }>
> =>
writeRevisionedWorkspace(path, policy, {
document: workspace,
expectedRevision,
codec: investigationWorkspaceCodec,
});
/** Atomically append one validated CAS-linked revision under an exclusive lock. */
export const writeRevisionedWorkspace = async <Document>(
path: string,
@@ -96,23 +62,21 @@ export const writeRevisionedWorkspace = async <Document>(
> => {
const { document, expectedRevision, codec } = input;
if (policy.roots.length === 0)
return err(new InvestigationWorkspaceError("update", "disabled"));
return err(new WorkspaceStorageError("update", "disabled"));
let encoded: string;
try {
encoded = codec.serialize(document);
} catch (cause: unknown) {
return err(
new InvestigationWorkspaceError("update", "integrity", { cause }),
);
return err(new WorkspaceStorageError("update", "integrity", { cause }));
}
const bytes = Buffer.byteLength(encoded, "utf8");
if (bytes > policy.maxBytes)
return err(new InvestigationWorkspaceError("update", "too-large"));
return err(new WorkspaceStorageError("update", "too-large"));
let lock: StoreFileLock | undefined;
try {
const destination = await resolveDestination(path, policy.roots);
if (destination === null)
return err(new InvestigationWorkspaceError("update", "outside-root"));
return err(new WorkspaceStorageError("update", "outside-root"));
const acquired = await acquireLock(destination);
if (!acquired.ok) return acquired;
lock = acquired.value;
@@ -131,7 +95,7 @@ export const writeRevisionedWorkspace = async <Document>(
});
return ok({ path: resolve(path), bytes });
} catch (cause: unknown) {
return err(new InvestigationWorkspaceError("update", "io", { cause }));
return err(new WorkspaceStorageError("update", "io", { cause }));
} finally {
if (lock !== undefined) await lock.release();
}
@@ -163,26 +127,24 @@ const readWorkspaceFile = async <Document>(
});
if (stats === undefined) return ok(null);
if (!stats.isFile() || stats.isSymbolicLink())
return err(new InvestigationWorkspaceError("read", "not-file"));
return err(new WorkspaceStorageError("read", "not-file"));
if (stats.size > policy.maxBytes)
return err(new InvestigationWorkspaceError("read", "too-large"));
return err(new WorkspaceStorageError("read", "too-large"));
const encoded = await readFile(destination);
if (encoded.byteLength > policy.maxBytes)
return err(new InvestigationWorkspaceError("read", "too-large"));
return err(new WorkspaceStorageError("read", "too-large"));
let decoded: unknown;
try {
decoded = JSON.parse(encoded.toString("utf8"));
} catch (cause: unknown) {
return err(
new InvestigationWorkspaceError("read", "invalid-json", { cause }),
);
return err(new WorkspaceStorageError("read", "invalid-json", { cause }));
}
if (!isJsonWithinLimits(decoded, policy))
return err(new InvestigationWorkspaceError("read", "too-large"));
return err(new WorkspaceStorageError("read", "too-large"));
try {
return ok(codec.parse(decoded));
} catch (cause: unknown) {
return err(new InvestigationWorkspaceError("read", "integrity", { cause }));
return err(new WorkspaceStorageError("read", "integrity", { cause }));
}
};
@@ -198,7 +160,7 @@ const acquireLock = async (
return ok(await createStoreFileLock(path));
} catch (retryCause: unknown) {
return err(
new InvestigationWorkspaceError(
new WorkspaceStorageError(
"update",
isAlreadyExists(retryCause) ? "locked" : "io",
{ cause: retryCause },
@@ -207,7 +169,7 @@ const acquireLock = async (
}
}
return err(
new InvestigationWorkspaceError(
new WorkspaceStorageError(
"update",
isAlreadyExists(cause) ? "locked" : "io",
{ cause },
@@ -216,26 +178,6 @@ const acquireLock = async (
}
};
function validateNextRevision(
current: InvestigationWorkspace | null,
next: InvestigationWorkspace,
expectedRevision: number | null,
): WorkspaceResult<null> {
if ((current?.revision ?? null) !== expectedRevision)
return err(new InvestigationWorkspaceError("update", "revision-conflict"));
if (current === null) {
return next.revision === 1 && next.previous_revision_digest === null
? ok(null)
: err(new InvestigationWorkspaceError("update", "revision-conflict"));
}
if (current.name !== next.name || current.workspace_id !== next.workspace_id)
return err(new InvestigationWorkspaceError("update", "name-conflict"));
return next.revision === current.revision + 1 &&
next.previous_revision_digest === current.revision_digest
? ok(null)
: err(new InvestigationWorkspaceError("update", "revision-conflict"));
}
const isFileNotFound = (cause: unknown): boolean =>
errorCode(cause) === "ENOENT";
-2
View File
@@ -8,7 +8,6 @@ import { registerCoreAnalysisCommands } from "./cli/coreAnalysisCommands.js";
import { registerUtilityCommands } from "./cli/utilityCommands.js";
import { registerArtifactCommands } from "./cli/artifactCommands.js";
import { registerManagedCommands } from "./cli/managedCommands.js";
import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
import { registerProcessCommands } from "./cliProcessCommands.js";
import { registerPolicyCommands } from "./cliPolicyCommands.js";
@@ -56,7 +55,6 @@ export const createCli = (): CliInstance => {
registerUtilityCommands(cli, logger);
registerArtifactCommands(cli, logger);
registerManagedCommands(cli, logger);
registerInvestigationCommands(cli, logger);
registerEvidenceCommands(cli, logger);
registerProcessCommands(cli, logger);
registerPolicyCommands(cli, logger);
+3 -3
View File
@@ -252,8 +252,8 @@ const registerCoverageCommands = (
authority,
{
path: parsed.data.workspace_path,
readCapability: "investigation_workspace_read",
writeCapability: "investigation_workspace_write",
readCapability: "reconstruction_coverage_read",
writeCapability: "reconstruction_coverage_write",
operation: CLI_COMMANDS.commitReconstructionCoverage,
},
);
@@ -282,7 +282,7 @@ const registerCoverageCommands = (
),
};
const authorized = await authorizeRootPermission(authority, {
capability: "investigation_workspace_read",
capability: "reconstruction_coverage_read",
roots: [parsed.data.workspace_path],
access: "read",
operation: CLI_COMMANDS.queryReconstructionCoverage,
-1
View File
@@ -35,7 +35,6 @@ export const CLI_COMMANDS = Object.freeze({
evidenceImport: "evidence-import",
evidenceExport: "evidence-export",
compare: "compare",
investigateVersions: "investigate-versions",
captureProcess: "capture-process",
compareProcessCaptures: "compare-process-captures",
policy: "policy",
+1 -18
View File
@@ -78,22 +78,7 @@ export const registerEvidenceCommands = (
left: z.string().describe("Left Evidence bundle JSON path"),
right: z.string().describe("Right Evidence bundle JSON path"),
}),
options: z.object({
offset: z
.number()
.int()
.min(0)
.default(0)
.describe("Zero-based changed-record offset"),
limit: z
.number()
.int()
.min(1)
.max(500)
.default(100)
.describe("Maximum changed records to return"),
}),
run: ({ args, options }) =>
run: ({ args }) =>
logCliCommand(logger, "compare", async () => {
const config = parseConfig(process.env);
if (!config.ok) return cliError(config.error);
@@ -105,8 +90,6 @@ export const registerEvidenceCommands = (
const compared = await compareEvidenceBundlesCommand({
leftPath: args.left,
rightPath: args.right,
offset: options.offset,
limit: options.limit,
policy: config.value.evidenceFilePolicy,
});
return compared.ok ? compared.value : cliError(compared.error);
-190
View File
@@ -1,190 +0,0 @@
import { Cli, z } from "incur";
import { runCrossVersionInvestigation } from "./application/CrossVersionInvestigation.js";
import { logCliCommand } from "./cliLogging.js";
import { parseConfig } from "./config.js";
import { projectAnalysisError, type AnalysisError } from "./domain/errors.js";
import type { Logger } from "./logger.js";
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
import { authorizeFileReadWithDeferredWrite } from "./application/DeferredFileAuthorization.js";
import { CLI_COMMANDS } from "./cliCommandNames.js";
const investigationOptionsSchema = z.object({
yes: z
.boolean()
.default(false)
.describe("Approve persistent workspace writes"),
workspaceName: z
.string()
.trim()
.min(1)
.max(200)
.default("default")
.describe("Logical name recorded in the investigation workspace"),
expectedRevision: z
.number()
.int()
.min(1)
.optional()
.describe("Required current workspace revision for an atomic update"),
replayRunId: z
.string()
.regex(/^run_[a-f0-9]{64}$/u)
.optional()
.describe("Replay one verified complete workspace run without rescanning"),
maxEntries: z
.number()
.int()
.min(1)
.max(50_000)
.default(10_000)
.describe("Maximum artifact entries to inspect per version"),
maxTotalBytes: z
.number()
.int()
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(1_073_741_824)
.describe("Maximum total uncompressed artifact content to inspect"),
maxEntryBytes: z
.number()
.int()
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(268_435_456)
.describe("Maximum uncompressed content to inspect for one entry"),
pageSize: z
.number()
.int()
.min(1)
.max(500)
.default(500)
.describe("Maximum graph items loaded per investigation page"),
changeLimit: z
.number()
.int()
.min(1)
.max(500)
.default(500)
.describe("Maximum version changes to record"),
integrityPolicy: z
.enum(["fail", "record-and-continue"])
.default("fail")
.describe("Behavior when declared artifact integrity does not match"),
integrityContinueApproved: z
.boolean()
.default(false)
.describe("Approve continuing after recorded integrity mismatches"),
maxIntegrityMismatches: z
.number()
.int()
.min(1)
.max(100)
.default(10)
.describe("Maximum integrity mismatches to record before stopping"),
});
/** Register the persistent cross-version CLI workflow. */
export const registerInvestigationCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.investigateVersions, {
description:
"Run or resume a persistent cross-version artifact investigation",
args: z.object({
leftPath: z.string().describe("Earlier artifact or directory path"),
rightPath: z.string().describe("Later artifact or directory path"),
workspacePath: z.string().describe("Persistent workspace JSON path"),
}),
options: investigationOptionsSchema,
alias: {
workspaceName: "workspace-name",
expectedRevision: "expected-revision",
replayRunId: "replay-run-id",
maxEntries: "max-entries",
maxTotalBytes: "max-total-bytes",
maxEntryBytes: "max-entry-bytes",
pageSize: "page-size",
changeLimit: "change-limit",
integrityPolicy: "integrity-policy",
integrityContinueApproved: "integrity-continue-approved",
maxIntegrityMismatches: "max-integrity-mismatches",
yes: "y",
},
run: ({ args, options }) =>
logCliCommand(logger, "investigate-versions", () =>
runInvestigation(args, options),
),
});
};
const runInvestigation = async (
args: {
readonly leftPath: string;
readonly rightPath: string;
readonly workspacePath: string;
},
options: z.infer<typeof investigationOptionsSchema>,
) => {
if (!options.yes)
return {
error: "ApprovalRequired",
message: "Persistent workspace writes require explicit --yes approval.",
};
const config = parseConfig(process.env);
if (!config.ok) return cliError(config.error);
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) return cliError(authority.error);
const workspaceAuthorization = await authorizeFileReadWithDeferredWrite(
authority.value,
{
path: args.workspacePath,
readCapability: "investigation_workspace_read",
writeCapability: "investigation_workspace_write",
operation: "investigate_versions",
},
);
if (!workspaceAuthorization.ok) return cliError(workspaceAuthorization.error);
const investigated = await runCrossVersionInvestigation(
{
approved: true,
workspace_path: args.workspacePath,
workspace_name: options.workspaceName,
...(options.expectedRevision === undefined
? {}
: { expected_workspace_revision: options.expectedRevision }),
...(options.replayRunId === undefined
? {}
: { replay_run_id: options.replayRunId }),
left_path: args.leftPath,
right_path: args.rightPath,
integrity_policy: options.integrityPolicy,
integrity_continue_approved: options.integrityContinueApproved,
max_integrity_mismatches: options.maxIntegrityMismatches,
options: {
max_entries: options.maxEntries,
max_total_bytes: options.maxTotalBytes,
max_entry_bytes: options.maxEntryBytes,
max_compression_ratio: 1_000,
max_depth: 20,
max_path_bytes: 4_096,
page_size: options.pageSize,
change_limit: options.changeLimit,
},
},
config.value.evidenceFilePolicy,
{
integrityContinueEnabled: config.value.artifactIntegrityContinueEnabled,
authorizeWorkspaceWrite: workspaceAuthorization.value.authorizeWrite,
},
);
return investigated.ok
? investigated.value.evidence
: cliError(investigated.error);
};
const cliError = (error: AnalysisError) => ({
error: "Analysis failed",
...projectAnalysisError(error),
});
-1
View File
@@ -113,7 +113,6 @@ export const INVESTIGATION_EXAMPLES = {
},
verify_reconstruction: {
specification: {
schema_version: 1,
name: "Terminal compatibility",
claims: [
{
+4 -4
View File
@@ -407,22 +407,22 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"compare_artifacts",
"Compare two bounded sets of inventory_artifact Evidence pages by logical occurrence path, content identity, metadata, and graph relations. Pages must share and satisfy their graph commitment; every delta cites both sets, and gaps yield truncated or unknown, never equivalence.",
"Compare complete inventory_artifact Evidence sets by logical occurrence path, content identity, metadata, and graph relations. Every delta cites both sets, and gaps yield truncated or unknown, never equivalence. Returns every change inline.",
artifactComparisonInputSchema,
),
session(
"compare_functions",
"Compare two explicit bounded sets of analyze_function Evidence pages across identity, exact provider text, calls, references, strings, and address-normalized CFG topology. Missing or provider-incompatible facets remain truncated or unknown; every conclusion cites both Evidence sets.",
"Compare two explicit sets of analyze_function Evidence across identity, exact provider text, calls, references, strings, and address-normalized CFG topology. Missing or provider-incompatible facets remain truncated or unknown; every conclusion cites both Evidence sets.",
functionComparisonInputSchema,
),
session(
"compare_bundles",
"Compare two canonical Evidence v2 bundles by exact record membership, explicit one-to-one observation pairs, and complete residual-unknown revision histories. Missing bundle members describe omission only, never behavioral equivalence; output is digest-anchored and deterministically paginated.",
"Compare two canonical Evidence bundles by exact record membership, explicit one-to-one observation pairs, and complete residual-unknown revision histories. Missing bundle members describe omission only, never behavioral equivalence; output is digest-anchored and returns every change inline.",
bundleComparisonInputSchema,
),
session(
"find_changed_behavior",
"Aggregate validated comparison Evidence, or automatically run and resume a persistent cross-version artifact investigation beneath an approved evidence root. Runtime observations remain distinct from static behavior candidates; missing or incomplete comparisons produce unresolved findings, never causal claims.",
"Aggregate validated process and artifact comparison Evidence. Runtime observations remain distinct from static behavior candidates; missing or incomplete comparisons produce unresolved findings, never causal claims. Returns every finding inline.",
changedBehaviorInputSchema,
),
session(
+5 -5
View File
@@ -40,7 +40,7 @@ const ANALYSIS_ERROR_TAGS = [
"EvidenceIntegrityError",
"EvidenceLimitError",
"EvidenceFileError",
"InvestigationWorkspaceError",
"WorkspaceStorageError",
"UnknownRegistryError",
"HopperTimeoutError",
"HopperCancelledError",
@@ -351,9 +351,9 @@ export class EvidenceFileError extends AnalysisError {
}
}
/** Persistent investigation workspace access or CAS validation failed. */
export class InvestigationWorkspaceError extends AnalysisError {
readonly _tag = "InvestigationWorkspaceError";
/** A persisted reconstruction coverage workspace could not be read or updated. */
export class WorkspaceStorageError extends AnalysisError {
readonly _tag = "WorkspaceStorageError";
constructor(
readonly operation: "read" | "update",
@@ -370,7 +370,7 @@ export class InvestigationWorkspaceError extends AnalysisError {
| "io",
options?: ErrorOptions,
) {
super(`Investigation workspace ${operation} failed: ${reason}`, options);
super(`Workspace storage ${operation} failed: ${reason}`, options);
}
}
-484
View File
@@ -1,484 +0,0 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import {
evidenceBundleSchema,
parseEvidenceBundle,
type EvidenceBundle,
} from "./evidenceBundle.js";
import type { JsonValue } from "./jsonValue.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
const artifactFormatSchema =
evidenceBundleSchema.shape.artifacts.element.shape.format;
/** Bounded controls for one automatic, provider-neutral artifact comparison. */
const investigationRunOptionsSchema = z.object({
max_entries: z.number().int().min(1).max(50_000).default(10_000),
max_total_bytes: z
.number()
.int()
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(1_073_741_824),
max_entry_bytes: z
.number()
.int()
.min(1)
.max(Number.MAX_SAFE_INTEGER)
.default(268_435_456),
max_compression_ratio: z.number().min(1).max(100_000).default(1_000),
max_depth: z.number().int().min(0).max(100).default(20),
max_path_bytes: z.number().int().min(1).max(65_535).default(4_096),
page_size: z.number().int().min(1).max(500).default(500),
change_limit: z.number().int().min(1).max(500).default(500),
});
const DEFAULT_INVESTIGATION_RUN_OPTIONS = {
max_entries: 10_000,
max_total_bytes: 1_073_741_824,
max_entry_bytes: 268_435_456,
max_compression_ratio: 1_000,
max_depth: 20,
max_path_bytes: 4_096,
page_size: 500,
change_limit: 500,
} as const;
export type InvestigationRunOptions = z.infer<
typeof investigationRunOptionsSchema
>;
/** Explicit write approval and two local versions for an automatic run. */
const crossVersionInvestigationFacts = {
approved: z.literal(true),
workspace_path: z.string().min(1).max(4_096),
workspace_name: z.string().trim().min(1).max(200).default("default"),
expected_workspace_revision: z.number().int().min(1).optional(),
replay_run_id: z
.string()
.regex(/^run_[a-f0-9]{64}$/u)
.optional(),
left_path: z.string().min(1).max(4_096),
right_path: z.string().min(1).max(4_096),
options: investigationRunOptionsSchema.default(
DEFAULT_INVESTIGATION_RUN_OPTIONS,
),
} as const;
export const crossVersionInvestigationInputSchema = z.union([
z.object({
...crossVersionInvestigationFacts,
integrity_policy: z.literal("fail").default("fail"),
integrity_continue_approved: z.literal(false).default(false),
max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
}),
z.object({
...crossVersionInvestigationFacts,
integrity_policy: z.literal("record-and-continue"),
integrity_continue_approved: z.literal(true),
max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
}),
]);
export type CrossVersionInvestigationInput = z.infer<
typeof crossVersionInvestigationInputSchema
>;
const targetSchema = z.object({
root_sha256: digestSchema,
graph_sha256: digestSchema,
manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u),
format: artifactFormatSchema,
});
export type InvestigationRunTarget = z.infer<typeof targetSchema>;
const investigationRunIdentityShape = {
schema_version: z.literal(1),
run_id: z.string().regex(/^run_[a-f0-9]{64}$/u),
request_sha256: digestSchema,
left: targetSchema,
right: targetSchema,
options: investigationRunOptionsSchema,
left_inventory_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
right_inventory_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
limitations: z.array(z.string().max(4_096)).max(100),
};
const investigationRunContextShape = {
...investigationRunIdentityShape,
integrity_policy: z.enum(["fail", "record-and-continue"]),
integrity_continue_approved: z.boolean(),
max_integrity_mismatches: z.number().int().min(1).max(100),
};
const inventoryRunStateShape = {
status: z.literal("running"),
completed_stages: z.tuple([
z.literal("inventory_left"),
z.literal("inventory_right"),
]),
comparison_evidence_id: z.null(),
result_evidence_id: z.null(),
};
const comparisonRunStateShape = {
status: z.literal("running"),
completed_stages: z.tuple([
z.literal("inventory_left"),
z.literal("inventory_right"),
z.literal("compare_artifacts"),
]),
comparison_evidence_id: evidenceIdSchema,
result_evidence_id: z.null(),
};
const completedRunStateShape = {
status: z.literal("complete"),
completed_stages: z.tuple([
z.literal("inventory_left"),
z.literal("inventory_right"),
z.literal("compare_artifacts"),
z.literal("find_changed_behavior"),
]),
comparison_evidence_id: evidenceIdSchema,
result_evidence_id: evidenceIdSchema,
};
const investigationRunStateSchemas = [
z.object({ ...investigationRunContextShape, ...inventoryRunStateShape }),
z.object({ ...investigationRunContextShape, ...comparisonRunStateShape }),
z.object({ ...investigationRunContextShape, ...completedRunStateShape }),
] as const;
const legacyIdentityRunStateSchemas = [
z.object({
...investigationRunContextShape,
...inventoryRunStateShape,
legacy_request_identity: z.literal(true),
}),
z.object({
...investigationRunContextShape,
...comparisonRunStateShape,
legacy_request_identity: z.literal(true),
}),
z.object({
...investigationRunContextShape,
...completedRunStateShape,
legacy_request_identity: z.literal(true),
}),
] as const;
export const investigationRunSchema = z
.union([
z.union(legacyIdentityRunStateSchemas),
z.union(investigationRunStateSchemas),
])
.superRefine((run, context) => {
if (!("legacy_request_identity" in run)) return;
const legacyIdentity = createLegacyInvestigationRunIdentity(run);
if (
run.integrity_policy !== "fail" ||
run.integrity_continue_approved ||
run.max_integrity_mismatches !== 10 ||
run.run_id !== legacyIdentity.runId ||
run.request_sha256 !== legacyIdentity.requestSha256
)
context.addIssue({
code: "custom",
path: ["legacy_request_identity"],
message:
"Legacy request identity is valid only for migrated strict runs",
});
});
export type InvestigationRun = z.infer<typeof investigationRunSchema>;
export const investigationRunSummarySchema = z.object({
schema_version: z.literal(1),
workspace_id: z.string().regex(/^ws_[a-f0-9]{64}$/u),
run_id: z.string().regex(/^run_[a-f0-9]{64}$/u),
left_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u),
right_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u),
inventory_evidence_count: z.number().int().min(2).max(200),
comparison_evidence_id: evidenceIdSchema,
limitations: z.array(z.string().max(4_096)).max(100),
});
/** Durable schema for one revisioned cross-version investigation workspace. */
export const investigationWorkspaceSchema = z.object({
workspace_version: z.literal(1),
workspace_id: z.string().regex(/^ws_[a-f0-9]{64}$/u),
name: z.string().trim().min(1).max(200),
revision: z.number().int().min(1),
previous_revision_digest: z
.string()
.regex(/^wrev_[a-f0-9]{64}$/u)
.nullable(),
revision_digest: z.string().regex(/^wrev_[a-f0-9]{64}$/u),
bundle: evidenceBundleSchema,
runs: z.array(investigationRunSchema).max(1_000),
});
const legacyInvestigationRunSchema = z.union([
z.strictObject({
...investigationRunIdentityShape,
...inventoryRunStateShape,
}),
z.strictObject({
...investigationRunIdentityShape,
...comparisonRunStateShape,
}),
z.strictObject({
...investigationRunIdentityShape,
...completedRunStateShape,
}),
]);
const legacyInvestigationWorkspaceSchema = investigationWorkspaceSchema
.extend({
runs: z.array(legacyInvestigationRunSchema).max(1_000),
})
.strict();
export type InvestigationWorkspace = z.infer<
typeof investigationWorkspaceSchema
>;
/** Derive a stable run identity from content commitments and bounded controls. */
export const createInvestigationRunIdentity = (input: {
readonly left: InvestigationRunTarget;
readonly right: InvestigationRunTarget;
readonly options: InvestigationRunOptions;
readonly integrity_policy: CrossVersionInvestigationInput["integrity_policy"];
readonly integrity_continue_approved: boolean;
readonly max_integrity_mismatches: number;
}): { readonly runId: string; readonly requestSha256: string } => {
const requestSha256 = digestCanonical({
schema: "rea.cross-version-investigation-request/v1",
left: input.left,
right: input.right,
options: input.options,
integrity: {
policy: input.integrity_policy,
approved: input.integrity_continue_approved,
max_mismatches: input.max_integrity_mismatches,
},
});
return { runId: `run_${requestSha256}`, requestSha256 };
};
/** Build the first canonical workspace revision. */
export const createInvestigationWorkspace = (
name: string,
bundle: EvidenceBundle,
runs: readonly InvestigationRun[],
): InvestigationWorkspace =>
buildWorkspace({
workspaceId: `ws_${digestCanonical({ schema: "rea.workspace/v1", name })}`,
name,
revision: 1,
previousRevisionDigest: null,
bundle,
runs,
});
/** Create the next immutable workspace revision. */
export const reviseInvestigationWorkspace = (
current: InvestigationWorkspace,
bundle: EvidenceBundle,
runs: readonly InvestigationRun[],
): InvestigationWorkspace =>
buildWorkspace({
workspaceId: current.workspace_id,
name: current.name,
revision: current.revision + 1,
previousRevisionDigest: current.revision_digest,
bundle,
runs,
});
/** Parse a workspace and verify identities, revision digest, and references. */
export const parseInvestigationWorkspace = (
input: unknown,
): InvestigationWorkspace => {
if (isLegacyWorkspace(input)) return migrateLegacyWorkspace(input);
const parsed = investigationWorkspaceSchema.parse(input);
const bundle = parseEvidenceBundle(parsed.bundle);
const canonical = buildWorkspace({
workspaceId: parsed.workspace_id,
name: parsed.name,
revision: parsed.revision,
previousRevisionDigest: parsed.previous_revision_digest,
bundle,
runs: parsed.runs,
});
if (parsed.workspace_id !== workspaceIdFor(parsed.name))
throw new TypeError("Investigation workspace ID does not match its name");
if (parsed.revision_digest !== canonical.revision_digest)
throw new TypeError("Investigation workspace revision digest is invalid");
if (JSON.stringify(parsed) !== JSON.stringify(canonical))
throw new TypeError("Investigation workspace is not canonical");
validateRunReferences(canonical);
return canonical;
};
/** Encode a validated workspace as byte-stable canonical JSON. */
export const serializeInvestigationWorkspace = (
workspace: InvestigationWorkspace,
): string => canonicalJson(parseInvestigationWorkspace(workspace));
const buildWorkspace = (input: {
readonly workspaceId: string;
readonly name: string;
readonly revision: number;
readonly previousRevisionDigest: string | null;
readonly bundle: EvidenceBundle;
readonly runs: readonly InvestigationRun[];
}): InvestigationWorkspace => {
const runs = [...input.runs]
.map((run) => investigationRunSchema.parse(run))
.sort((left, right) => left.run_id.localeCompare(right.run_id));
assertUnique(
runs.map(({ run_id: id }) => id),
"investigation run ID",
);
const semantic = {
workspace_version: 1,
workspace_id: input.workspaceId,
name: input.name,
revision: input.revision,
previous_revision_digest: input.previousRevisionDigest,
bundle: parseEvidenceBundle(input.bundle),
runs,
} satisfies JsonValue;
return investigationWorkspaceSchema.parse({
...semantic,
revision_digest: `wrev_${digestCanonical(semantic)}`,
});
};
const validateRunReferences = (workspace: InvestigationWorkspace): void => {
const evidenceIds = new Set(
workspace.bundle.records.map(({ evidence_id: id }) => id),
);
for (const run of workspace.runs) {
const expectedIdentity = createInvestigationRunIdentity(run);
const legacyIdentity = createLegacyInvestigationRunIdentity(run);
if (
(run.run_id !== expectedIdentity.runId ||
run.request_sha256 !== expectedIdentity.requestSha256) &&
(!("legacy_request_identity" in run) ||
run.integrity_policy !== "fail" ||
run.integrity_continue_approved ||
run.max_integrity_mismatches !== 10 ||
run.run_id !== legacyIdentity.runId ||
run.request_sha256 !== legacyIdentity.requestSha256)
)
throw new TypeError("Investigation run identity is invalid");
const references = [
...run.left_inventory_evidence_ids,
...run.right_inventory_evidence_ids,
...(run.comparison_evidence_id === null
? []
: [run.comparison_evidence_id]),
...(run.result_evidence_id === null ? [] : [run.result_evidence_id]),
];
if (references.some((evidenceId) => !evidenceIds.has(evidenceId)))
throw new TypeError("Investigation run references missing Evidence");
}
};
/** Derive the pre-integrity-policy run identity for strict workspace migration. */
export const createLegacyInvestigationRunIdentity = (input: {
readonly left: InvestigationRunTarget;
readonly right: InvestigationRunTarget;
readonly options: InvestigationRunOptions;
}): { readonly runId: string; readonly requestSha256: string } => {
const requestSha256 = digestCanonical({
schema: "rea.cross-version-investigation-request/v1",
left: input.left,
right: input.right,
options: input.options,
});
return { runId: `run_${requestSha256}`, requestSha256 };
};
const isLegacyWorkspace = (input: unknown): boolean => {
if (typeof input !== "object" || input === null || !("runs" in input))
return false;
const runs = input.runs;
return (
Array.isArray(runs) &&
runs.some(
(run) =>
typeof run === "object" && run !== null && !("integrity_policy" in run),
)
);
};
const migrateLegacyWorkspace = (input: unknown): InvestigationWorkspace => {
const parsed = legacyInvestigationWorkspaceSchema.parse(input);
const bundle = parseEvidenceBundle(parsed.bundle);
const runs = [...parsed.runs].sort((left, right) =>
left.run_id.localeCompare(right.run_id),
);
const semantic = {
workspace_version: 1,
workspace_id: parsed.workspace_id,
name: parsed.name,
revision: parsed.revision,
previous_revision_digest: parsed.previous_revision_digest,
bundle,
runs,
} satisfies JsonValue;
if (parsed.workspace_id !== workspaceIdFor(parsed.name))
throw new TypeError("Investigation workspace ID does not match its name");
if (parsed.revision_digest !== `wrev_${digestCanonical(semantic)}`)
throw new TypeError("Investigation workspace revision digest is invalid");
const canonical = legacyInvestigationWorkspaceSchema.parse({
...semantic,
revision_digest: parsed.revision_digest,
});
if (JSON.stringify(parsed) !== JSON.stringify(canonical))
throw new TypeError("Investigation workspace is not canonical");
for (const run of runs) {
const identity = createLegacyInvestigationRunIdentity(run);
if (
run.run_id !== identity.runId ||
run.request_sha256 !== identity.requestSha256
)
throw new TypeError("Investigation run identity is invalid");
}
const migrated = buildWorkspace({
workspaceId: parsed.workspace_id,
name: parsed.name,
revision: parsed.revision,
previousRevisionDigest: parsed.previous_revision_digest,
bundle,
runs: runs.map((run) => ({
...run,
legacy_request_identity: true as const,
integrity_policy: "fail" as const,
integrity_continue_approved: false,
max_integrity_mismatches: 10,
})),
});
validateRunReferences(migrated);
return migrated;
};
const workspaceIdFor = (name: string): string =>
`ws_${digestCanonical({ schema: "rea.workspace/v1", name })}`;
const digestCanonical = (value: JsonValue): string =>
createHash("sha256").update(canonicalJson(value)).digest("hex");
const canonicalJson = (value: JsonValue): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError("Investigation workspace could not canonicalize data");
return encoded;
};
const assertUnique = (values: readonly string[], description: string): void => {
if (new Set(values).size !== values.length)
throw new TypeError(`Duplicate ${description}`);
};
+2 -2
View File
@@ -12,8 +12,8 @@ export const PERMISSION_CAPABILITIES = [
"v8_inspector_observe",
"evidence_read",
"evidence_write",
"investigation_workspace_read",
"investigation_workspace_write",
"reconstruction_coverage_read",
"reconstruction_coverage_write",
"snapshot_read",
"snapshot_write",
"artifact_extract",
+1 -1
View File
@@ -30,7 +30,7 @@ interface GeneratedPayload {
}
const GENERATED_PAYLOAD_JSON =
'{"auxiliaryProviders":[{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"inventory_artifact","pagination":"offset","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"inspect_artifact","pagination":"none","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":true,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":120000},"operation":"extract_artifact","pagination":"offset","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null}],"identity":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"}},{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":false,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_macho","pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_signature","pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"inspect_plist","pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":null,"timeoutMs":30000},"operation":"list_architectures","pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"exhaustive":true,"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"limits":{"maxPayloadBytes":4194304,"maxResults":500,"timeoutMs":30000},"operation":"demangle_swift","pagination":"none","provider":{"id":"native-macos","name":"macOS native inspection utilities","vLine truncated
'{"auxiliaryProviders":[{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"operation":"inventory_artifact","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"operation":"inspect_artifact","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":true,"mutatesArtifact":false,"requiresRoot":false},"limitations":["DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.","ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only."],"operation":"extract_artifact","provider":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"},"reason":null}],"identity":{"id":"rea-artifact-graph","name":"REA safe artifact graph provider","version":"1"}},{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"operation":"inspect_macho","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"operation":"inspect_signature","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"operation":"inspect_plist","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"operation":"list_architectures","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":true,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["Availability and textual formats depend on the installed macOS/Xcode toolchain."],"operation":"demangle_swift","provider":{"id":"native-macos","name":"macOS native inspection utilities","version":null},"reason":null}],"identity":{"id":"native-macos","name":"macOS native inspection utilities","version":null}},{"capabilities":[{"available":true,"effects":{"changesPermissions":false,"launchesProcess":false,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesystem":false,"mutatesArtifact":false,"requiresRoot":false},"limitations":["This capability inventories PE/CLI identity only; inspect_managed_members admits bounded metadata members, signatures, and CIL anchors.","It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code."],"operation":"inspect_managed_artifact","provider":{"id":"rea-dotnet-static","name":"REA managed static analysis provider","version":"1"},"reason":null},{"available":true,"effects":{"changesPermissions":false,"launchesProcess":false,"mayAccessNetwork":false,"mayShowUi":false,"mayWriteFilesysLine truncated
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
if (!isGeneratedPayload(generatedPayload))
throw new TypeError("Generated MCP catalog payload is invalid");
+5 -120
View File
@@ -1,38 +1,22 @@
import type { McpServer } from "@modelcontextprotocol/server";
import type { z } from "zod";
import type { BinarySessionPort } from "../application/BinarySession.js";
import { runCrossVersionInvestigationValidated } from "../application/CrossVersionInvestigation.js";
import {
authorizeFileReadWithDeferredWrite,
type DeferredFileWriteAuthorization,
} from "../application/DeferredFileAuthorization.js";
import { SESSION_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import { buildCallPath } from "../domain/callPath.js";
import {
changedBehaviorInputSchema,
changedBehaviorResultSchema,
findChangedBehavior,
} from "../domain/changedBehavior.js";
import { findChangedBehavior } from "../domain/changedBehavior.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { verifyReconstruction } from "../domain/reconstructionVerification.js";
import { correlateStaticAndRuntime } from "../domain/staticRuntimeCorrelation.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import {
comparisonClosure,
evidenceClosure,
functionEvidenceIds,
investigationContext,
isIncomplete,
recordWorkflowEvidence,
verifyCoverageReadiness,
} from "./registerInvestigationTools/helpers.js";
import type { InvestigationToolPolicies } from "./registerInvestigationTools/types.js";
import {
runDerivedOperation,
type DerivedOperationContext,
} from "./runDerivedOperation.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import {
CALL_PATH_PROVIDER,
CHANGED_BEHAVIOR_PROVIDER,
@@ -52,9 +36,8 @@ export const registerInvestigationTools = (
(typeof SESSION_TOOL_CONTRACTS)[12],
(typeof SESSION_TOOL_CONTRACTS)[13],
],
policies: InvestigationToolPolicies,
): void => {
registerChangedBehavior(server, session, contracts[0], policies);
registerChangedBehavior(server, session, contracts[0]);
registerCallPath(server, session, contracts[1]);
registerStaticRuntime(server, session, contracts[2]);
registerReconstruction(server, session, contracts[3]);
@@ -64,23 +47,11 @@ const registerChangedBehavior = (
server: McpServer,
session: BinarySessionPort,
contract: (typeof SESSION_TOOL_CONTRACTS)[10],
policies: InvestigationToolPolicies,
): void => {
server.registerTool(
contract.name,
contractOptions(contract),
async (input, context) => {
const investigationRun = input.investigation_run;
if (investigationRun !== undefined)
return runAutomaticInvestigation({
server,
session,
contract,
policies,
investigationRun,
unknownRegistryApproved: input.unknown_registry_approved,
context,
});
const closure = evidenceClosure(
session,
comparisonClosure(input.comparisons),
@@ -88,7 +59,7 @@ const registerChangedBehavior = (
if (!closure.ok) return toCallToolResult(closure, contract);
const links = closure.value;
const computed = await runDerivedOperation(context, contract.name, () =>
findChangedBehavior(input.comparisons, input.offset, input.limit),
findChangedBehavior(input.comparisons),
);
if (!computed.ok) return toCallToolResult(computed, contract);
const result = computed.value;
@@ -99,8 +70,6 @@ const registerChangedBehavior = (
comparison_evidence_ids: input.comparisons.map(
({ evidence_id: id }) => id,
),
offset: input.offset,
limit: input.limit,
},
result: jsonValueSchema.parse(result),
confidence: "derived",
@@ -133,81 +102,6 @@ const registerChangedBehavior = (
);
};
const runAutomaticInvestigation = async (input: {
readonly server: McpServer;
readonly session: BinarySessionPort;
readonly contract: (typeof SESSION_TOOL_CONTRACTS)[10];
readonly policies: InvestigationToolPolicies;
readonly investigationRun: NonNullable<
z.output<typeof changedBehaviorInputSchema>["investigation_run"]
>;
readonly unknownRegistryApproved: true | undefined;
readonly context: DerivedOperationContext;
}) => {
const { policies, investigationRun, contract, session } = input;
const progress = mcpProgressReporter(input.context);
let workspaceAuthorization: DeferredFileWriteAuthorization | undefined;
if (policies.permissionAuthority !== undefined) {
const authority = policies.permissionAuthority;
const authorizedWorkspace = await authorizeFileReadWithDeferredWrite(
authority,
{
path: investigationRun.workspace_path,
readCapability: "investigation_workspace_read",
writeCapability: "investigation_workspace_write",
operation: contract.name,
},
);
if (!authorizedWorkspace.ok)
return toCallToolResult(authorizedWorkspace, contract);
workspaceAuthorization = authorizedWorkspace.value;
}
const investigated = await runCrossVersionInvestigationValidated(
investigationRun,
policies.evidenceFiles,
{
...investigationContext({
session,
signal: input.context.mcpReq.signal,
integrityContinueEnabled:
policies.integrityContinueEnabled?.() ?? false,
progress,
}),
...(workspaceAuthorization === undefined
? {}
: { authorizeWorkspaceWrite: workspaceAuthorization.authorizeWrite }),
},
);
if (!investigated.ok) return toCallToolResult(investigated, contract);
session.retainInvestigationWorkspace(investigated.value.workspace);
const result = changedBehaviorResultSchema.parse(
investigated.value.evidence.normalized_result,
);
const toolResult = toCallToolResult(
recordWorkflowEvidence(
session,
investigated.value.evidence,
input.unknownRegistryApproved,
isIncomplete(result.behavior_status),
{
question: `Automatic changed behavior run ${result.investigation_run?.run_id ?? "unknown"} remains ${result.behavior_status}`,
domain: "changed-behavior",
requiredAuthority: "controlled-replay",
requiredConfidence: "observed",
probes: [
{
operation: "capture_process_scenario",
rationale:
"Capture both versions under the same bounded scenario and environment.",
},
],
},
),
contract,
);
return toolResult;
};
const registerCallPath = (
server: McpServer,
session: BinarySessionPort,
@@ -294,8 +188,6 @@ const registerStaticRuntime = (
operation: contract.name,
parameters: {
mapping_count: input.mappings.length,
offset: input.offset,
limit: input.limit,
},
result: jsonValueSchema.parse(result),
confidence: "inferred",
@@ -343,12 +235,7 @@ const registerReconstruction = (
if (!coverage.ok) return toCallToolResult(coverage, contract);
const owned = session.exportEvidenceBundle();
const computed = await runDerivedOperation(context, contract.name, () =>
verifyReconstruction(
input.specification,
owned,
input.offset,
input.limit,
),
verifyReconstruction(input.specification, owned),
);
if (!computed.ok) return toCallToolResult(computed, contract);
const result = computed.value;
@@ -361,8 +248,6 @@ const registerReconstruction = (
parameters: {
specification_sha256: result.specification_sha256,
claim_ids: input.specification.claims.map(({ claim_id: id }) => id),
offset: input.offset,
limit: input.limit,
},
result: jsonValueSchema.parse(result),
confidence: "derived",
@@ -1,5 +1,4 @@
import type { BinarySessionPort } from "../../application/BinarySession.js";
import type { ProgressReporter } from "../../application/ProgressReporter.js";
import type { Evidence } from "../../domain/evidence.js";
import {
EvidenceIntegrityError,
@@ -13,29 +12,6 @@ import type { z } from "zod";
import type { reconstructionVerificationInputSchema } from "../../domain/reconstructionVerification.js";
import { AnalysisInputError } from "../../domain/errors.js";
/** Build the execution context passed to cross-version investigation. */
export const investigationContext = ({
session,
signal,
integrityContinueEnabled,
progress,
}: {
readonly session: BinarySessionPort;
readonly signal: AbortSignal;
readonly integrityContinueEnabled: boolean;
readonly progress?: ProgressReporter;
}): {
readonly session: BinarySessionPort;
readonly signal: AbortSignal;
readonly integrityContinueEnabled: boolean;
readonly progress?: ProgressReporter;
} => ({
session,
signal,
integrityContinueEnabled,
...(progress === undefined ? {} : { progress }),
});
export const isIncomplete = (status: string): boolean =>
status === "unknown" || status === "truncated";
@@ -1,13 +1,3 @@
import type { EvidenceFilePolicy } from "../../domain/evidenceBundle.js";
import type { PermissionAuthority } from "../../application/PermissionAuthority.js";
/** Policy inputs shared by all investigation workflow tools. */
export interface InvestigationToolPolicies {
readonly evidenceFiles: EvidenceFilePolicy;
readonly permissionAuthority?: PermissionAuthority;
readonly integrityContinueEnabled?: () => boolean;
}
/** Fields used to build an approved residual unknown from a workflow result. */
export interface WorkflowUnknownInput {
readonly question: string;
+1 -11
View File
@@ -412,17 +412,7 @@ export const registerSessionTools = (
staticRuntimeContract,
reconstructionContract,
] as const;
registerInvestigationTools(server, session, investigationContracts, {
evidenceFiles: evidenceFilePolicy,
...(options.artifactIntegrityContinueEnabled === undefined
? {}
: {
integrityContinueEnabled: options.artifactIntegrityContinueEnabled,
}),
...(options.permissionAuthority === undefined
? {}
: { permissionAuthority: options.permissionAuthority }),
});
registerInvestigationTools(server, session, investigationContracts);
registerRecordAndReplayTools(server, session, logger);
registerContextTools(server, session);
};