From dbe2c97af34361b11b9504e37611dd47715b52cb Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 07:13:32 +0800 Subject: [PATCH] refactor(ownership): consolidate snapshot and comparison facts Retain authenticated immutable snapshot payloads while keeping mutable exports independent. Derive artifact metadata comparisons once and reuse the owning native trace limits. --- src/contracts/enhancedInputs.ts | 6 +- src/domain/analysisSnapshot.test.ts | 91 ++++++++++++++++++ src/domain/analysisSnapshot.ts | 18 +++- src/domain/artifactComparison.ts | 118 ++++++++++++------------ src/domain/artifactInventoryEvidence.ts | 20 ++-- 5 files changed, 174 insertions(+), 79 deletions(-) diff --git a/src/contracts/enhancedInputs.ts b/src/contracts/enhancedInputs.ts index 22cc9b0ce1..40e5001934 100644 --- a/src/contracts/enhancedInputs.ts +++ b/src/contracts/enhancedInputs.ts @@ -3,6 +3,7 @@ import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMet import { nativeProcedureInputSchema } from "../domain/native/nativeProcedureInput.js"; import { decompileDirectoryInputSchema } from "./decompileDirectory.js"; import { nativeValueTraceInputSchema } from "../domain/native/nativeValueTrace.js"; +import { nativeInvestigationTraceLimitsSchema } from "../domain/native/nativeInvestigationGraph.js"; const traceLiteralInputSchema = z.strictObject({ query: z.string().min(1), case_sensitive: z.boolean().default(false), @@ -64,16 +65,13 @@ export const enhancedInputSchemas = { .optional(), direction: callRelationshipDirectionSchema, }), - trace_native_ui_action: z.strictObject({ + trace_native_ui_action: nativeInvestigationTraceLimitsSchema.extend({ action: z .string() .min(1) .describe( "A unique compiled UI action selector, interface object ID, native symbol or exact function address", ), - max_depth: z.int().min(0).max(32).default(8), - max_nodes: z.int().min(1).max(2_000).default(250), - max_edges: z.int().min(1).max(5_000).default(500), }), } as const; diff --git a/src/domain/analysisSnapshot.test.ts b/src/domain/analysisSnapshot.test.ts index acaa36f435..76cf22f7dc 100644 --- a/src/domain/analysisSnapshot.test.ts +++ b/src/domain/analysisSnapshot.test.ts @@ -5,6 +5,7 @@ import { analysisSnapshotSchema, analysisQueryId, parseAnalysisSnapshot, + immutableAnalysisSnapshot, snapshotBinding, snapshotEvidenceForQuery, snapshotTarget, @@ -639,3 +640,93 @@ describe("analysis snapshot Evidence binding", () => { } }); }); + +describe("analysis snapshot payload ownership", () => { + it.each(["provider", "workflow"] as const)( + "shares authenticated %s payloads only in immutable snapshots", + (kind) => { + const { snapshot } = snapshotWithHistoricalEvidence(); + const entry = snapshot.entries[0]; + if (entry === undefined) throw new Error("Missing fixture query"); + entry.execution.raw_result = { source: "raw" }; + const evidence = createEvidence( + ANALYSIS_SNAPSHOT_TARGET, + ANALYSIS_SNAPSHOT_PROVIDER, + { + operation: entry.operation, + parameters: entry.parameters, + result: entry.execution.result, + rawResult: entry.execution.raw_result, + analysisProfile: ANALYSIS_SNAPSHOT_PROFILE, + confidence: kind === "provider" ? "observed" : "derived", + }, + ); + const input = { + ...snapshot, + entries: kind === "provider" ? [entry] : [], + workflow_entries: + kind === "workflow" + ? [ + { + ...entry, + execution: { + ...entry.execution, + analysis_profile: ANALYSIS_SNAPSHOT_PROFILE, + }, + }, + ] + : [], + evidence_bundle: createEvidenceBundle([evidence]), + }; + const original = structuredClone(input); + const immutable = immutableAnalysisSnapshot(input); + const immutableEntry = ( + kind === "provider" ? immutable.entries : immutable.workflow_entries + )[0]; + const immutableEvidence = immutable.evidence_bundle.records[0]; + if (immutableEntry === undefined || immutableEvidence === undefined) + throw new Error("Missing immutable payload"); + expect(immutableEntry.execution.result).toBe( + immutableEvidence.normalized_result, + ); + expect(immutableEntry.execution.raw_result).toBe( + immutableEvidence.raw_result, + ); + expect(Object.isFrozen(immutableEntry.execution.result)).toBe(true); + expect(Object.isFrozen(immutableEntry.execution.raw_result)).toBe(true); + expect(Object.isFrozen(immutable)).toBe(true); + expect(immutableAnalysisSnapshot(immutable)).toBe(immutable); + expect(parseAnalysisSnapshot(immutable)).toBe(immutable); + expect(input).toEqual(original); + expect(Object.isFrozen(entry.execution.result)).toBe(false); + expect(Object.isFrozen(entry.execution.raw_result)).toBe(false); + const mutable = parseAnalysisSnapshot(input); + const mutableEntry = ( + kind === "provider" ? mutable.entries : mutable.workflow_entries + )[0]; + const mutableEvidence = mutable.evidence_bundle.records[0]; + if (mutableEntry === undefined || mutableEvidence === undefined) + throw new Error("Missing mutable payload"); + expect(mutableEntry.execution.result).not.toBe( + mutableEvidence.normalized_result, + ); + expect(mutableEntry.execution.raw_result).not.toBe( + mutableEvidence.raw_result, + ); + mutableEntry.execution.result = { changed: true }; + mutableEntry.execution.raw_result = { changed: true }; + expect(mutableEvidence).toEqual(evidence); + expect(input).toEqual(original); + const tampered = structuredClone(input); + const tamperedEntry = ( + kind === "provider" ? tampered.entries : tampered.workflow_entries + )[0]; + if (tamperedEntry === undefined) throw new Error("Missing tamper query"); + tamperedEntry.execution.result = { changed: true }; + expect(() => immutableAnalysisSnapshot(tampered)).toThrow( + /differs from its Evidence/u, + ); + expect(Object.isFrozen(tamperedEntry.execution.result)).toBe(false); + }, + ); +}); diff --git a/src/domain/analysisSnapshot.ts b/src/domain/analysisSnapshot.ts index 2b998ccb9a..a4722fa3c2 100644 --- a/src/domain/analysisSnapshot.ts +++ b/src/domain/analysisSnapshot.ts @@ -607,13 +607,21 @@ export const createAnalysisSnapshotEntry = (input: { const validatedSnapshots = new WeakMap(); /** Parse a provider/profile-exact snapshot and verify its entries and evidence. */ -export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => { +export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => + authenticateSnapshot(input).parsed; + +const authenticateSnapshot = ( + input: unknown, +): { + readonly parsed: AnalysisSnapshot; + readonly restored: AnalysisSnapshot; +} => { if (typeof input === "object" && input !== null) { const owned = validatedSnapshots.get(input); - if (owned !== undefined) return owned; + if (owned !== undefined) return { parsed: owned, restored: owned }; } const parsed = analysisSnapshotSchema.parse(input); - restoreSnapshot( + const restored = restoreSnapshot( parsed, parseEvidenceBundle(parsed.evidence_bundle), parsed.entries.map((entry) => ({ @@ -635,12 +643,12 @@ export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => { ), })), ); - return parsed; + return { parsed, restored }; }; /** Own an authenticated immutable cache while preserving mutable caller exports. */ export const immutableAnalysisSnapshot = (input: unknown): AnalysisSnapshot => - sealSnapshot(parseAnalysisSnapshot(input)); + sealSnapshot(authenticateSnapshot(input).restored); const sealSnapshot = (snapshot: AnalysisSnapshot): AnalysisSnapshot => { for (const entry of [...snapshot.entries, ...snapshot.workflow_entries]) { diff --git a/src/domain/artifactComparison.ts b/src/domain/artifactComparison.ts index 54f976cada..78fdedf3e8 100644 --- a/src/domain/artifactComparison.ts +++ b/src/domain/artifactComparison.ts @@ -289,19 +289,13 @@ const compareOccurrences = ( } const leftOccurrence = leftMatches[0]; const rightOccurrence = rightMatches[0]; - if ( - leftOccurrence !== undefined && - rightOccurrence !== undefined && - asarPackingUnknown({ leftOccurrence, rightOccurrence }) - ) - hasUnknownAsarPacking = true; - if ( - leftOccurrence !== undefined && - rightOccurrence !== undefined && - nodeSeaMetadataUnknown({ leftOccurrence, rightOccurrence }) - ) - hasUnknownNodeSeaMetadata = true; + const metadata = occurrenceMetadata({ leftOccurrence, rightOccurrence }); + if (leftOccurrence !== undefined && rightOccurrence !== undefined) { + if (metadata.asarPacking === "unknown") hasUnknownAsarPacking = true; + if (metadata.nodeSea === "unknown") hasUnknownNodeSeaMetadata = true; + } const change = classifyPath({ + metadata, path, leftOccurrence, rightOccurrence, @@ -337,6 +331,7 @@ const occurrencesByPath = ( }; interface ClassifyPathInput { + readonly metadata: OccurrenceMetadata; readonly path: string; readonly leftOccurrence: ArtifactOccurrence | undefined; readonly rightOccurrence: ArtifactOccurrence | undefined; @@ -419,7 +414,8 @@ const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => { if (declaration.handled) return declaration.change; const availability = classifyAvailability(input, base); if (availability !== null) - return asarPackingChanged(input) || nodeSeaMetadataChanged(input) + return input.metadata.asarPacking === "changed" || + input.metadata.nodeSea === "changed" ? { ...base, classification: "changed", @@ -427,11 +423,7 @@ const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => { } : availability; const dimensions = changedDimensions(input); - if ( - pyInstallerMetadataUnknown(input) || - asarPackingUnknown(input) || - nodeSeaMetadataUnknown(input) - ) { + if (Object.values(input.metadata).includes("unknown")) { if (dimensions.length === 0) return { ...base, @@ -450,35 +442,52 @@ type OccurrencePair = Pick< "leftOccurrence" | "rightOccurrence" >; -const asarPackingUnknown = (input: OccurrencePair): boolean => - (input.leftOccurrence?.unpacked === undefined) !== - (input.rightOccurrence?.unpacked === undefined); +type MetadataComparison = "unchanged" | "changed" | "unknown"; -const asarPackingChanged = (input: OccurrencePair): boolean => - input.leftOccurrence?.unpacked !== undefined && - input.rightOccurrence?.unpacked !== undefined && - input.leftOccurrence.unpacked !== input.rightOccurrence.unpacked; +interface OccurrenceMetadata { + readonly asarPacking: MetadataComparison; + readonly nodeSea: MetadataComparison; + readonly pyInstaller: MetadataComparison; +} -const nodeSeaMetadataUnknown = (input: OccurrencePair): boolean => - (input.leftOccurrence?.format_metadata?.kind === "node-sea-entry") !== - (input.rightOccurrence?.format_metadata?.kind === "node-sea-entry"); - -const nodeSeaMetadataChanged = (input: OccurrencePair): boolean => { - const left = input.leftOccurrence?.format_metadata; - const right = input.rightOccurrence?.format_metadata; - return ( - left?.kind === "node-sea-entry" && - right?.kind === "node-sea-entry" && - canonicalJson(nodeSeaEntryDeclaration(left)) !== - canonicalJson(nodeSeaEntryDeclaration(right)) - ); +const compareOptionalMetadata = ( + left: T | undefined, + right: T | undefined, + declaration: (value: T) => unknown, +): MetadataComparison => { + if (left === undefined || right === undefined) + return left === right ? "unchanged" : "unknown"; + return canonicalJson(declaration(left)) === canonicalJson(declaration(right)) + ? "unchanged" + : "changed"; }; -const pyInstallerMetadataUnknown = (input: ClassifyPathInput): boolean => - (isPyInstallerEntry(input.leftOccurrence) && - !isPyInstallerEntry(input.rightOccurrence)) || - (isPyInstallerEntry(input.rightOccurrence) && - !isPyInstallerEntry(input.leftOccurrence)); +/** Interpret each pair's format declarations once, preserving historical omission. */ +const occurrenceMetadata = (input: OccurrencePair): OccurrenceMetadata => { + const left = input.leftOccurrence; + const right = input.rightOccurrence; + return { + asarPacking: compareOptionalMetadata( + left?.unpacked, + right?.unpacked, + (value) => value, + ), + nodeSea: compareOptionalMetadata( + left?.format_metadata?.kind === "node-sea-entry" + ? left.format_metadata + : undefined, + right?.format_metadata?.kind === "node-sea-entry" + ? right.format_metadata + : undefined, + nodeSeaEntryDeclaration, + ), + pyInstaller: compareOptionalMetadata( + isPyInstallerEntry(left) ? left.format_metadata : undefined, + isPyInstallerEntry(right) ? right.format_metadata : undefined, + pyInstallerEntryDeclaration, + ), + }; +}; const isPyInstallerEntry = ( occurrence: ArtifactOccurrence | undefined, @@ -518,13 +527,7 @@ const classifyPyInstallerDeclaration = ( dimensions: ["availability"], }, }; - const changed = - canonicalJson( - pyInstallerEntryDeclaration(input.leftOccurrence.format_metadata), - ) !== - canonicalJson( - pyInstallerEntryDeclaration(input.rightOccurrence.format_metadata), - ); + const changed = input.metadata.pyInstaller === "changed"; return { handled: true, change: changed @@ -534,6 +537,7 @@ const classifyPyInstallerDeclaration = ( }; const changedDimensions = (input: { + readonly metadata: OccurrenceMetadata; readonly leftNode: ArtifactNode | undefined; readonly rightNode: ArtifactNode | undefined; readonly leftOccurrence: ArtifactOccurrence | undefined; @@ -579,14 +583,15 @@ const changedDimensions = (input: { const metadataChanged = ( input: { + readonly metadata: OccurrenceMetadata; readonly leftOccurrence: ArtifactOccurrence | undefined; readonly rightOccurrence: ArtifactOccurrence | undefined; }, left: ArtifactNode, right: ArtifactNode, ): boolean => - asarPackingChanged(input) || - nodeSeaMetadataChanged(input) || + input.metadata.asarPacking === "changed" || + input.metadata.nodeSea === "changed" || left.media_type !== right.media_type || left.architecture !== right.architecture || left.content_state !== right.content_state || @@ -595,14 +600,7 @@ const metadataChanged = ( JSON.stringify(left.limitations) !== JSON.stringify(right.limitations) || JSON.stringify(input.leftOccurrence?.limitations) !== JSON.stringify(input.rightOccurrence?.limitations) || - (isPyInstallerEntry(input.leftOccurrence) && - isPyInstallerEntry(input.rightOccurrence) && - canonicalJson( - pyInstallerEntryDeclaration(input.leftOccurrence.format_metadata), - ) !== - canonicalJson( - pyInstallerEntryDeclaration(input.rightOccurrence.format_metadata), - )); + input.metadata.pyInstaller === "changed"; const nodeFor = ( occurrence: ArtifactOccurrence | undefined, diff --git a/src/domain/artifactInventoryEvidence.ts b/src/domain/artifactInventoryEvidence.ts index c19de27874..c91f0dff16 100644 --- a/src/domain/artifactInventoryEvidence.ts +++ b/src/domain/artifactInventoryEvidence.ts @@ -246,11 +246,7 @@ const validateInspectionSubject = ( throw new ArtifactInventoryValidationError( "Artifact inspection subject does not match its inventory manifest", ); - if ( - subject === null || - subject.digest.sha256 !== manifest.root_sha256 || - subject.format !== manifest.root_format - ) + if (!subjectMatchesInventoryRoot(subject, manifest)) throw new ArtifactInventoryValidationError( "Artifact inspection Evidence subject does not match its root manifest", ); @@ -295,16 +291,20 @@ const parseInventoryEvidencePage = ( } }; +const subjectMatchesInventoryRoot = ( + subject: Evidence["subject"], + manifest: ArtifactInventoryResult["manifest"], +): boolean => + subject !== null && + subject.digest.sha256 === manifest.root_sha256 && + subject.format === manifest.root_format; + /** Both inventory carriers require the same authenticated root identity. */ export const validateArtifactInventorySubject = ( subject: Evidence["subject"], manifest: ArtifactInventoryResult["manifest"], ): void => { - if ( - subject === null || - subject.digest.sha256 !== manifest.root_sha256 || - subject.format !== manifest.root_format - ) + if (!subjectMatchesInventoryRoot(subject, manifest)) throw new ArtifactInventoryValidationError( "Artifact inventory Evidence subject does not match its root digest", );