import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js"; import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import type { Evidence } from "../../domain/evidence.js"; import type { EvidenceBundle } from "../../domain/evidenceBundle.js"; import type { JsonValue } from "../../domain/jsonValue.js"; import { evidenceBundleForTarget } from "../../domain/evidenceBundle.js"; import { EvidenceFileError, EvidenceIntegrityError, AnalysisSnapshotMismatchError, } from "../../domain/evidenceErrors.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import type { UnknownRegistryError } from "../../domain/unknownRegistryError.js"; import type { RecordUnknownInput, ResidualUnknown, UnknownStatus, UpdateUnknownInput, } from "../../domain/residualUnknown.js"; import type { UnknownPage, UnknownQuery, } from "../investigation/InvestigationRecordPort.js"; import { err, ok, type Result } from "../../domain/result.js"; import type { AnalysisExecution, AnalysisOperation, } from "../AnalysisProvider.js"; import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js"; import { InvestigationRecords } from "../investigation/InvestigationRecords.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { writeAnalysisSnapshot as writeSnapshotFile } from "./AnalysisSnapshotFiles.js"; const SNAPSHOT_MUTATION_RECOVERY = "Export session observations through export_evidence_bundle if needed. Close the active target without saving a snapshot, then reopen it before importing or saving a snapshot."; export interface ActiveAnalysisBinding { readonly target: BinaryTarget; readonly profile: AnalysisProfileCommitment | null; } /** Validated composed-workflow payload bound to the active target and profile. */ export interface WorkflowSnapshotRecordInput { readonly operation: AnalysisOperation; readonly parameters: Readonly>; readonly execution: Parameters< AnalysisSnapshotCache["recordWorkflow"] >[0]["execution"]; } /** Own binary snapshots and coordinate the composed investigation records. */ export abstract class BinarySessionRecords { readonly #records: InvestigationRecords; readonly #writeResources = new ArtifactResourceScope(); readonly #snapshot = new AnalysisSnapshotCache(); #snapshotInvalidated = false; readonly #snapshotListeners = new Set<() => void | Promise>(); constructor(records: InvestigationRecords = new InvestigationRecords()) { this.#records = records; } /** Observe changes to the mutable current analysis snapshot resource. */ onAnalysisSnapshotChanged(listener: () => void | Promise): () => void { this.#snapshotListeners.add(listener); return () => this.#snapshotListeners.delete(listener); } recordEvidence( evidence: Evidence, ): Result<"added" | "duplicate", EvidenceIntegrityError> { const recorded = this.#records.recordEvidence(evidence); if (recorded.ok && recorded.value === "added") this.#emitSnapshotChanged(); return recorded; } hasEvidence(evidenceId: string): boolean { return this.#records.hasEvidence(evidenceId); } evidenceById(evidenceId: string): Evidence | undefined { return this.#records.evidenceById(evidenceId); } /** Borrow immutable investigation Evidence without copying a complete graph. */ evidenceForAnalysis(evidenceId: string): Evidence | undefined { return this.#records.evidenceForAnalysis(evidenceId); } exportEvidenceBundle(): EvidenceBundle { return this.#records.exportEvidenceBundle(); } /** Borrow a sealed bundle for complete serialization without cloning retained graphs. */ evidenceBundleForSerialization(): EvidenceBundle { return this.#records.evidenceBundleForSerialization(); } /** Persist one provider-neutral snapshot under this session's resource owner. */ writeAnalysisSnapshot( snapshot: AnalysisSnapshot, path: string, overwrite: boolean, ): Promise< Result<{ readonly path: string; readonly bytes: number }, AnalysisError> > { return writeSnapshotFile(snapshot, path, overwrite, this.#writeResources); } /** Retry and release staged snapshot files at the owning session shutdown. */ async closeWriteResources(): Promise> { try { await this.#writeResources.retryCleanup(); return ok(null); } catch (cause: unknown) { const observation = ArtifactReaderFailure.cleanupObservation( cause, "analysis snapshot staging", ); return err( new EvidenceFileError("write", "io", { cause, cleanup: { reason: observation.reason, resources: observation.resources, }, }), ); } } importEvidenceBundle( bundle: unknown, ): Result { const imported = this.#records.mergeEvidenceBundle(bundle); if (!imported.ok) return imported; if (imported.value.metadataChanged) this.invalidateSnapshot(); else if (imported.value.changed) this.#emitSnapshotChanged(); return ok(imported.value.recordsAdded); } protected abstract activeAnalysisBinding(): ActiveAnalysisBinding | undefined; exportAnalysisSnapshot(): Result { const active = this.activeAnalysisBinding(); if (this.#snapshotInvalidated) return err( new EvidenceIntegrityError( "Analysis snapshots are unavailable because analysis metadata may have changed", { userMessage: `Analysis snapshots are unavailable because analysis metadata may have changed. ${SNAPSHOT_MUTATION_RECOVERY}`, }, ), ); const target = active?.target; const profile = active?.profile ?? undefined; if (target !== undefined && profile === undefined) return err( new EvidenceIntegrityError( "Analysis snapshots require a concrete provider analysis profile", ), ); return this.#snapshot.export( target, profile, target === undefined ? this.#records.exportEvidenceBundle() : evidenceBundleForTarget( this.#records.exportEvidenceBundle(), target.sha256, ), ); } importAnalysisSnapshot( snapshot: AnalysisSnapshot, ): Result { const active = this.activeAnalysisBinding(); if (active !== undefined && this.#snapshotInvalidated) return err( new EvidenceIntegrityError( "Analysis snapshots cannot be imported because analysis metadata may have changed", { userMessage: `Analysis snapshots cannot be imported because analysis metadata may have changed. ${SNAPSHOT_MUTATION_RECOVERY}`, }, ), ); if (active?.profile === null) return err( new AnalysisSnapshotMismatchError( "Analysis snapshot profile_mismatch: the active target has no concrete analysis profile", ), ); const imported = this.#snapshot.import( snapshot, active === undefined ? undefined : { target: active.target, profile: active.profile }, (bundle) => { const imported = this.#records.mergeEvidenceBundle(bundle); return imported.ok ? ok(imported.value.recordsAdded) : imported; }, ); if (imported.ok) this.#emitSnapshotChanged(); return imported; } protected matchesSnapshot( target: BinaryTarget, profile: AnalysisProfileCommitment | null, ): boolean { return this.#snapshot.matches(target, profile ?? undefined); } protected selectSnapshot( target: BinaryTarget, profile: AnalysisProfileCommitment, ): void { this.#snapshot.select(target, profile); this.#emitSnapshotChanged(); } protected lookupSnapshot( target: BinaryTarget, profile: AnalysisProfileCommitment, operation: AnalysisOperation, parameters: Readonly< Record >, ): AnalysisExecution | undefined { if (this.#snapshotInvalidated) return undefined; return this.#snapshot.lookup(target, profile, operation, parameters); } protected recordSnapshot( input: Parameters[0], ): void { if (this.#snapshotInvalidated) return; this.#snapshot.record(input); this.#emitSnapshotChanged(); } /** Retain one derived workflow result alongside its provider cache entries. */ recordWorkflowSnapshot( input: WorkflowSnapshotRecordInput, ): Result { const active = this.activeAnalysisBinding(); if (active === undefined || active.profile === null) return err( new EvidenceIntegrityError( "Workflow snapshot entries require an active concrete provider profile", ), ); if (this.#snapshotInvalidated) return ok(null); try { this.#snapshot.recordWorkflow({ target: active.target, profile: active.profile, ...input, }); this.#emitSnapshotChanged(); return ok(null); } catch (cause: unknown) { return err( new EvidenceIntegrityError( cause instanceof Error ? cause.message : "Workflow snapshot entry validation failed", { cause }, ), ); } } protected invalidateSnapshot(): void { this.#snapshot.clear(); this.#snapshotInvalidated = true; this.#emitSnapshotChanged(); } protected resetSnapshotInvalidation(): void { if (!this.#snapshotInvalidated) return; this.#snapshot.clear(); this.#snapshotInvalidated = false; this.#emitSnapshotChanged(); } protected clearSnapshot(): void { this.#snapshot.clear(); this.#emitSnapshotChanged(); } protected clearSessionRecords(): void { this.#records.clear(); this.#snapshot.clear(); this.#snapshotInvalidated = false; this.#emitSnapshotChanged(); } #emitSnapshotChanged(): void { for (const listener of this.#snapshotListeners) { try { const notification = listener(); if (notification !== undefined) void notification.catch((cause: unknown) => { // best-effort cleanup: async observer notifications must not reject unhandled. void cause; }); } catch (cause: unknown) { // External resource observers are best-effort; one callback must not // make a committed evidence mutation appear to fail. void cause; } } } recordUnknown( input: RecordUnknownInput, ): Result { const target = this.activeAnalysisBinding()?.target; const recorded = this.#records.recordUnknown(input, target); if (recorded.ok) this.#emitSnapshotChanged(); return recorded; } recordEvidenceWithUnknown( evidence: Evidence, input: RecordUnknownInput, ): Result { const recorded = this.#records.recordEvidenceWithUnknown(evidence, input); if (recorded.ok) this.#emitSnapshotChanged(); return recorded; } updateUnknown( input: UpdateUnknownInput, ): Result { const target = this.activeAnalysisBinding()?.target; const updated = this.#records.updateUnknown(input, target); if (updated.ok) this.#emitSnapshotChanged(); return updated; } listUnknowns( filters: { readonly status?: UnknownStatus; readonly severity?: ResidualUnknown["severity"]; readonly domain?: string; } = {}, ): ResidualUnknown[] { return this.#records.listUnknowns(filters); } listUnknownsPage(query: UnknownQuery = {}): UnknownPage { return this.#records.listUnknownsPage(query); } verifyUnknownResolution(unknownId: string): Result< { readonly valid: boolean; readonly truthVerified: boolean; readonly unknown: ResidualUnknown; }, UnknownRegistryError > { return this.#records.verifyUnknownResolution(unknownId); } }