diff --git a/scripts/verify-real-browser.mjs b/scripts/verify-real-browser.mjs index 21cfee8f92..0d9fb7213e 100644 --- a/scripts/verify-real-browser.mjs +++ b/scripts/verify-real-browser.mjs @@ -36,7 +36,7 @@ import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs"; import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs"; import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs"; -const REAL_BROWSER_STARTUP_TIMEOUT_MS = 30_000; +const REAL_BROWSER_STARTUP_TIMEOUT_MS = 60_000; const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret"; const SCENARIO_URL_SECRET_VALUE = "rea-browser-url-verifier-secret"; process.env.REA_BROWSER_VERIFIER_SECRET = SCENARIO_SECRET_VALUE; diff --git a/src/application/ArtifactExtraction.ts b/src/application/ArtifactExtraction.ts index d22f8b2efa..cbbcfcc73e 100644 --- a/src/application/ArtifactExtraction.ts +++ b/src/application/ArtifactExtraction.ts @@ -1,5 +1,6 @@ import { lstat, realpath } from "node:fs/promises"; +import { canonicalDigest } from "../domain/comparisonSemantics.js"; import { AsarArtifactReader } from "../artifacts/AsarArtifactReader.js"; import { ArtifactPathRegistry, @@ -23,7 +24,6 @@ import { } from "../domain/artifactGraph.js"; import type { BinaryTarget } from "../domain/binaryTarget.js"; import { scanArtifactInventory } from "./ArtifactInventory.js"; -import { digestCanonical } from "./ArtifactGraphConstruction.js"; /** Local extraction input with the output root chosen by the adapter. */ export interface ArtifactExtractionInput { @@ -188,14 +188,14 @@ const createExtractionResult = ( selected_occurrence_ids: selected .map(({ occurrence }) => occurrence.occurrence_id) .sort((left, right) => left.localeCompare(right)), - files_sha256: digestCanonical(extracted), + files_sha256: canonicalDigest(extracted, "Artifact"), output_root_alias: "$OUTPUT_ROOT" as const, }; return artifactExtractionResultSchema.parse({ manifest: inventory.manifest, extraction_manifest: { ...extractionSemantic, - extraction_id: `aex_${digestCanonical(extractionSemantic)}`, + extraction_id: `aex_${canonicalDigest(extractionSemantic, "Artifact")}`, }, output_root: input.outputRoot, artifacts: extracted, diff --git a/src/application/ArtifactGraphConstruction.ts b/src/application/ArtifactGraphConstruction.ts index e37055c6c1..187d5d6776 100644 --- a/src/application/ArtifactGraphConstruction.ts +++ b/src/application/ArtifactGraphConstruction.ts @@ -1,7 +1,4 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; - +import { canonicalDigest } from "../domain/comparisonSemantics.js"; import type { ArtifactEntry } from "../artifacts/ArtifactReader.js"; import type { ArtifactCommand, @@ -44,7 +41,7 @@ export const createOccurrence = ( path: string, parent: string | null, ): MutableOccurrence => ({ - occurrence_id: `occ_${digestCanonical({ path, kind: entry.kind })}`, + occurrence_id: `occ_${canonicalDigest({ path, kind: entry.kind }, "Artifact")}`, artifact_id: null, parent_occurrence_id: parent, logical_path: path, @@ -99,7 +96,7 @@ export const materializeDirectoryNodes = ( })) .sort((left, right) => compareDirectoryChildNames(left.name, right.name)); const node = createArtifactNode({ - sha256: digestCanonical({ kind: "directory", children }), + sha256: canonicalDigest({ kind: "directory", children }, "Artifact"), size: 0, kind: directory.logical_path.toLowerCase().endsWith(".framework") ? "framework" @@ -128,13 +125,16 @@ export const createRootNode = (input: { createArtifactNode({ sha256: input.digest?.sha256 ?? - digestCanonical({ - kind: "directory-root", - children: input.occurrences.map(({ logical_path, artifact_id }) => ({ - logical_path, - artifact_id, - })), - }), + canonicalDigest( + { + kind: "directory-root", + children: input.occurrences.map(({ logical_path, artifact_id }) => ({ + logical_path, + artifact_id, + })), + }, + "Artifact", + ), size: input.digest?.bytes ?? 0, kind: input.directory || @@ -159,7 +159,7 @@ export const createArtifactNode = (input: { readonly contentState: ArtifactNode["content_state"]; readonly limitations?: readonly string[]; }): ArtifactNode => ({ - artifact_id: `art_${digestCanonical({ sha256: input.sha256 })}`, + artifact_id: `art_${canonicalDigest({ sha256: input.sha256 }, "Artifact")}`, kind: input.kind, format: input.format, sha256: input.sha256, @@ -175,7 +175,7 @@ export const rootOccurrenceFor = ( node: ArtifactNode, declaredSize: number, ): MutableOccurrence => ({ - occurrence_id: `occ_${digestCanonical({ root: node.artifact_id })}`, + occurrence_id: `occ_${canonicalDigest({ root: node.artifact_id }, "Artifact")}`, artifact_id: node.artifact_id, parent_occurrence_id: null, logical_path: ".", @@ -197,11 +197,14 @@ export const rekeyOccurrences = ( for (const occurrence of occurrences) replacements.set( occurrence.occurrence_id, - `occ_${digestCanonical({ - root_artifact_id: rootArtifactId, - logical_path: occurrence.logical_path, - entry_kind: occurrence.entry_kind, - })}`, + `occ_${canonicalDigest( + { + root_artifact_id: rootArtifactId, + logical_path: occurrence.logical_path, + entry_kind: occurrence.entry_kind, + }, + "Artifact", + )}`, ); for (const occurrence of occurrences) { occurrence.occurrence_id = @@ -245,7 +248,7 @@ export const createArtifactEdges = ( logical_path: occurrence.logical_path, }; edges.push({ - edge_id: `edge_${digestCanonical(semantic)}`, + edge_id: `edge_${canonicalDigest(semantic, "Artifact")}`, ...semantic, producer: occurrence.entry_kind === "slice" ? (producer ?? null) : null, ordinal: edges.length, @@ -356,13 +359,6 @@ export const classifyArtifactContent = ( return byPath; }; -export const digestCanonical = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Artifact value is not canonical JSON"); - return createHash("sha256").update(encoded).digest("hex"); -}; - const relationFor = (path: string): ArtifactEdge["relation"] => { const lower = path.toLowerCase(); if (lower.endsWith(".map")) return "maps-source"; diff --git a/src/application/ArtifactInventory/scanCanonical.ts b/src/application/ArtifactInventory/scanCanonical.ts index b76ef8ca16..ea28fc56aa 100644 --- a/src/application/ArtifactInventory/scanCanonical.ts +++ b/src/application/ArtifactInventory/scanCanonical.ts @@ -15,13 +15,13 @@ import { import { createArtifactEdges, createRootNode, - digestCanonical, indexOccurrencesByPath, materializeDirectoryNodes, rekeyOccurrences, rootOccurrenceFor, type MutableOccurrence, } from "../ArtifactGraphConstruction.js"; +import { canonicalDigest } from "../../domain/comparisonSemantics.js"; import { classifyRoot } from "./classify.js"; import { hashReadable, type HashResult } from "./hash.js"; import { createReader, inventoryLimitations } from "./reader.js"; @@ -125,12 +125,15 @@ const buildInventorySnapshot = async ( await verifyRootDigest(path, rootDigest, signal); - const graphSha256 = digestCanonical({ - nodes: orderedNodes, - occurrences: orderedOccurrences, - edges: orderedEdges, - integrity_contradictions: integrityContradictions, - }); + const graphSha256 = canonicalDigest( + { + nodes: orderedNodes, + occurrences: orderedOccurrences, + edges: orderedEdges, + integrity_contradictions: integrityContradictions, + }, + "Artifact", + ); const manifest = buildManifest({ rootNode, graphSha256, @@ -168,12 +171,15 @@ const buildIntegrityContradictions = ( : occurrenceById.get(occurrence.parent_occurrence_id); const parentArtifactId = parent?.artifact_id ?? rootNode.artifact_id; return { - contradiction_id: `ic_${digestCanonical({ - root_artifact_id: rootNode.artifact_id, - logical_path: contradiction.logicalPath, - declared_sha256: contradiction.declaredSha256, - observed_sha256: contradiction.observedSha256, - })}`, + contradiction_id: `ic_${canonicalDigest( + { + root_artifact_id: rootNode.artifact_id, + logical_path: contradiction.logicalPath, + declared_sha256: contradiction.declaredSha256, + observed_sha256: contradiction.observedSha256, + }, + "Artifact", + )}`, occurrence_id: occurrence.occurrence_id, parent_artifact_id: parentArtifactId, logical_path: contradiction.logicalPath, @@ -235,10 +241,13 @@ const buildManifest = ({ readonly orderedEdges: readonly { edge_id: string }[]; }): ArtifactInventoryResult["manifest"] => artifactInventoryResultSchema.shape.manifest.parse({ - manifest_id: `agm_${digestCanonical({ - root_artifact_id: rootNode.artifact_id, - graph_sha256: graphSha256, - })}`, + manifest_id: `agm_${canonicalDigest( + { + root_artifact_id: rootNode.artifact_id, + graph_sha256: graphSha256, + }, + "Artifact", + )}`, root_artifact_id: rootNode.artifact_id, root_sha256: rootNode.sha256, root_format: rootNode.format, diff --git a/src/application/BrowserScenarioEvidence.ts b/src/application/BrowserScenarioEvidence.ts index 0f557eeb6f..3e71993d2f 100644 --- a/src/application/BrowserScenarioEvidence.ts +++ b/src/application/BrowserScenarioEvidence.ts @@ -1,3 +1,4 @@ +import { canonicalDigest } from "../domain/comparisonSemantics.js"; import type { ProviderIdentity } from "./AnalysisProvider.js"; import type { BrowserScenario } from "../domain/browserScenario.js"; import type { BrowserScenarioCapture } from "../domain/browserScenarioCapture.js"; @@ -8,12 +9,11 @@ import { type EvidenceObservation, } from "../domain/evidence.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; -import { digestCanonicalJson } from "../domain/browserScenarioNormalization.js"; const browserScenarioParameters = ( scenario: BrowserScenario, ): EvidenceObservation["parameters"] => ({ - scenario_sha256: digestCanonicalJson(scenario), + scenario_sha256: canonicalDigest(scenario, "Browser scenario"), browser_mode: scenario.browser.mode, ...(scenario.browser.mode === "launch" ? { browser_headless: scenario.browser.headless } diff --git a/src/application/DoctorScope.ts b/src/application/DoctorScope.ts index c6ead4dfe6..bd9af069b9 100644 --- a/src/application/DoctorScope.ts +++ b/src/application/DoctorScope.ts @@ -1,3 +1,4 @@ +import { uniqueSorted } from "../domain/canonicalOrdering.js"; import type { ClientRegistrationStatus, UnhealthyClientRegistrationStatus, @@ -175,8 +176,5 @@ const selectedSkillCheck = ( : { remediation: "Run rea setup to update the installed REA skill." }), }; -const uniqueSorted = (values: readonly string[]): readonly string[] => - [...new Set(values)].sort((left, right) => left.localeCompare(right)); - const doctorProviderCheckName = (providerId: string, name: string): boolean => name === providerId || name.startsWith(`${providerId}-`); diff --git a/src/application/ElectronActiveEvidence.ts b/src/application/ElectronActiveEvidence.ts index 0706a710aa..f4d6be03d1 100644 --- a/src/application/ElectronActiveEvidence.ts +++ b/src/application/ElectronActiveEvidence.ts @@ -1,3 +1,4 @@ +import { canonicalDigest } from "../domain/comparisonSemantics.js"; import type { ProviderIdentity } from "./AnalysisProvider.js"; import type { Evidence, EvidenceObservation } from "../domain/evidence.js"; import { createEvidence } from "../domain/evidence.js"; @@ -6,7 +7,6 @@ import type { ElectronActiveObservationResult, } from "../domain/electronActiveObservation.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; -import { digestCanonicalJson } from "../domain/browserScenarioNormalization.js"; type CanonicalElectronActiveObservationInput = ElectronActiveObservationInput & { @@ -52,5 +52,8 @@ const parameters = ( input: CanonicalElectronActiveObservationInput, ): EvidenceObservation["parameters"] => ({ ...scenarioProjection(input), - scenario_sha256: digestCanonicalJson(scenarioProjection(input)), + scenario_sha256: canonicalDigest( + scenarioProjection(input), + "Browser scenario", + ), }); diff --git a/src/application/ElectronBoundaryAnalysis.ts b/src/application/ElectronBoundaryAnalysis.ts index e362d85936..51b49d214f 100644 --- a/src/application/ElectronBoundaryAnalysis.ts +++ b/src/application/ElectronBoundaryAnalysis.ts @@ -1,3 +1,4 @@ +import { compareCodePoints } from "../domain/canonicalOrdering.js"; import type { ElectronIpcFinding } from "../domain/electronStaticAnalysisTypes.js"; import type { ElectronBoundarySummary } from "../domain/javascriptApplicationAnalysis.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; @@ -221,6 +222,3 @@ const resolvesToFile = (input: ResolveToFileInput): boolean => { const ipcRecordKey = (path: string, finding: ElectronIpcFinding): string => `${path}\0${String(finding.location.start.line)}:${String(finding.location.start.column)}\0${finding.side}\0${finding.operation}`; - -const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; diff --git a/src/application/JavaScriptArtifactFiles.ts b/src/application/JavaScriptArtifactFiles.ts index 0dd0b82d38..28b5d064d8 100644 --- a/src/application/JavaScriptArtifactFiles.ts +++ b/src/application/JavaScriptArtifactFiles.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { isAbsolute } from "node:path"; import type { Readable } from "node:stream"; +import { compareCodePoints } from "../domain/canonicalOrdering.js"; import { AsarArtifactReader } from "../artifacts/AsarArtifactReader.js"; import { ArtifactPathRegistry, @@ -331,6 +332,3 @@ const abortIfNeeded = (signal?: AbortSignal): void => { "JavaScript artifact reconstruction cancelled", ); }; - -const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; diff --git a/src/application/JavaScriptJsonModules.ts b/src/application/JavaScriptJsonModules.ts index a7d16985d5..ed29f07211 100644 --- a/src/application/JavaScriptJsonModules.ts +++ b/src/application/JavaScriptJsonModules.ts @@ -1,3 +1,4 @@ +import { compareCodePoints } from "../domain/canonicalOrdering.js"; import type { JavaScriptJsonModuleObservation } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; @@ -41,6 +42,3 @@ export const analyzeJavaScriptJsonModule = ( limitation: null, }; }; - -const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; diff --git a/src/application/JavaScriptModuleRelationships.ts b/src/application/JavaScriptModuleRelationships.ts index 461decdafb..b825e6933f 100644 --- a/src/application/JavaScriptModuleRelationships.ts +++ b/src/application/JavaScriptModuleRelationships.ts @@ -1,5 +1,6 @@ import { posix } from "node:path"; +import { compareCodePoints } from "../domain/canonicalOrdering.js"; import type { ApplicationNode } from "../domain/javascriptApplicationGraph.js"; import { completeApplicationCoverage, @@ -409,6 +410,3 @@ const isExportLink = (link: JavaScriptSemanticModuleLink): boolean => link.kind === "export" || link.kind === "re-export" || link.kind === "commonjs-export"; - -const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; diff --git a/src/application/JavaScriptSemanticGraphProjection.ts b/src/application/JavaScriptSemanticGraphProjection.ts index ffc6bdbe6e..776f343142 100644 --- a/src/application/JavaScriptSemanticGraphProjection.ts +++ b/src/application/JavaScriptSemanticGraphProjection.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "../domain/javascriptApplicationGraph.js"; +import { compareCodePoints } from "../domain/canonicalOrdering.js"; import type { JavaScriptSemanticGraphNode } from "../domain/javascriptSemanticGraph.js"; import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../domain/javascriptSemanticGraphSchemas.js"; import type { JavaScriptSemanticIr } from "../domain/javascriptSemanticIr.js"; diff --git a/src/browser/CdpCanonicalJsonDigest.ts b/src/browser/CdpCanonicalJsonDigest.ts deleted file mode 100644 index b7fa9f8d14..0000000000 --- a/src/browser/CdpCanonicalJsonDigest.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; - -/** Hash a value using the canonical JSON representation. */ -export const canonicalJsonDigest = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) throw new TypeError("Expected canonical JSON"); - return createHash("sha256").update(encoded).digest("hex"); -}; diff --git a/src/browser/CdpCaptureStorageFingerprints.ts b/src/browser/CdpCaptureStorageFingerprints.ts index c21d7611c4..80f81c9740 100644 --- a/src/browser/CdpCaptureStorageFingerprints.ts +++ b/src/browser/CdpCaptureStorageFingerprints.ts @@ -6,6 +6,7 @@ import type { InspectWebPageInput, WebPageInspection, } from "../domain/browserObservation.js"; +import { compareCodePoints } from "../domain/canonicalOrdering.js"; import { CdpConnection } from "./CdpConnection.js"; import { optionalCdpCommand } from "./CdpOptionalCommand.js"; import { recordValue, recordsValue, stringValue } from "./CdpCaptureValues.js"; @@ -484,6 +485,3 @@ const decodeBase64 = (value: string): Buffer | null => { return null; return Buffer.from(value, "base64"); }; - -const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; diff --git a/src/browser/CdpElectronInspection.ts b/src/browser/CdpElectronInspection.ts index 3aeae1d0bb..ab50e59183 100644 --- a/src/browser/CdpElectronInspection.ts +++ b/src/browser/CdpElectronInspection.ts @@ -1,3 +1,4 @@ +import { canonicalDigest } from "../domain/comparisonSemantics.js"; import type { ProgressReporter } from "../application/ProgressReporter.js"; import type { ElectronPageInspection, @@ -7,7 +8,6 @@ import { BrowserObservationError } from "../domain/errors.js"; import type { CdpConnection, CdpEvent } from "./CdpConnection.js"; import type { CdpEndpointDiscovery, CdpEndpointTarget } from "./CdpEndpoint.js"; import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; -import { canonicalJsonDigest } from "./CdpCanonicalJsonDigest.js"; import { delayWithCancellation, numberValue, @@ -289,7 +289,7 @@ const captureResources = async ( ? null : Math.max(0, numberValue(resource.contentSize) ?? 0), }; - const resourceKey = `electron_resource_${canonicalJsonDigest(item)}`; + const resourceKey = `electron_resource_${canonicalDigest(item, "CDP capture")}`; if (seen.has(resourceKey)) continue; seen.add(resourceKey); resources.push({ diff --git a/src/browser/CdpElectronScripts.ts b/src/browser/CdpElectronScripts.ts index ac86896ab8..e394334ffb 100644 --- a/src/browser/CdpElectronScripts.ts +++ b/src/browser/CdpElectronScripts.ts @@ -2,10 +2,10 @@ import type { ElectronPageInspection, InspectElectronPageInput, } from "../domain/electronObservation.js"; +import { canonicalDigest } from "../domain/comparisonSemantics.js"; import { createWebTextArtifact } from "../domain/webContentArtifact.js"; import type { CdpConnection } from "./CdpConnection.js"; import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; -import { canonicalJsonDigest } from "./CdpCanonicalJsonDigest.js"; import { requiredRecord, stringValue } from "./CdpCaptureValues.js"; import type { ElectronScriptDraft } from "./CdpElectronScriptEvents.js"; import { authorizedElectronFile } from "./ElectronFileScope.js"; @@ -49,7 +49,7 @@ export const captureElectronScripts = async ( is_module: script.isModule, language: script.language, }; - const scriptKey = `electron_script_${canonicalJsonDigest(identity)}`; + const scriptKey = `electron_script_${canonicalDigest(identity, "CDP capture")}`; if (seen.has(scriptKey)) continue; seen.add(scriptKey); total += 1; diff --git a/src/contracts/artifactComparisonExample.ts b/src/contracts/artifactComparisonExample.ts index e5c0955eeb..dbb6834cc3 100644 --- a/src/contracts/artifactComparisonExample.ts +++ b/src/contracts/artifactComparisonExample.ts @@ -1,14 +1,11 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; - +import { canonicalDigest } from "../domain/comparisonSemantics.js"; import { createEvidence } from "../domain/evidence.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; const inventory = (digit: string) => { const sha = digit.repeat(64); - const artifactId = `art_${digestCanonical({ sha256: sha })}`; - const occurrenceId = `occ_${digestCanonical({ root: artifactId })}`; + const artifactId = `art_${canonicalDigest({ sha256: sha }, "Artifact example")}`; + const occurrenceId = `occ_${canonicalDigest({ root: artifactId }, "Artifact example")}`; const nodes = [ { artifact_id: artifactId, @@ -39,18 +36,24 @@ const inventory = (digit: string) => { limitations: [], }, ]; - const graphSha256 = digestCanonical({ - nodes, - occurrences, - edges: [], - integrity_contradictions: [], - }); + const graphSha256 = canonicalDigest( + { + nodes, + occurrences, + edges: [], + integrity_contradictions: [], + }, + "Artifact example", + ); return jsonValueSchema.parse({ manifest: { - manifest_id: `agm_${digestCanonical({ - root_artifact_id: artifactId, - graph_sha256: graphSha256, - })}`, + manifest_id: `agm_${canonicalDigest( + { + root_artifact_id: artifactId, + graph_sha256: graphSha256, + }, + "Artifact example", + )}`, root_artifact_id: artifactId, root_sha256: sha, root_format: "file", @@ -67,13 +70,6 @@ const inventory = (digit: string) => { }); }; -const digestCanonical = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Artifact example is not canonical JSON"); - return createHash("sha256").update(encoded).digest("hex"); -}; - const provider = { id: "rea-artifact", name: "REA artifact graph", diff --git a/src/domain/artifactInspection.ts b/src/domain/artifactInspection.ts index 3cbb3d2215..2c92386599 100644 --- a/src/domain/artifactInspection.ts +++ b/src/domain/artifactInspection.ts @@ -1,12 +1,11 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; import { z } from "zod"; import { artifactInventoryResultSchema, type ArtifactInventoryResult, } from "./artifactGraph.js"; +import { canonicalDigest } from "./comparisonSemantics.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js"; import { jsonValueSchema } from "./jsonValue.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; @@ -171,10 +170,13 @@ export const createArtifactInspection = ( }, substeps: [ { - substep_id: `ais_${digestCanonical({ - operation: "inventory_artifact", - evidence_id: inventoryEvidence.evidence_id, - })}`, + substep_id: `ais_${canonicalDigest( + { + operation: "inventory_artifact", + evidence_id: inventoryEvidence.evidence_id, + }, + "Artifact inspection", + )}`, operation: "inventory_artifact" as const, status: "completed" as const, evidence_id: inventoryEvidence.evidence_id, @@ -209,7 +211,7 @@ export const createArtifactInspection = ( }; return artifactInspectionResultSchema.parse({ ...semantic, - inspection_id: `ai_${digestCanonical(semantic)}`, + inspection_id: `ai_${canonicalDigest(semantic, "Artifact inspection")}`, }); }; @@ -255,7 +257,10 @@ const observation = ( value: jsonValueSchema.parse(value), evidence_id: evidenceId, }; - return { observation_id: `aio_${digestCanonical(semantic)}`, ...semantic }; + return { + observation_id: `aio_${canonicalDigest(semantic, "Artifact inspection")}`, + ...semantic, + }; }; const allRelationships = ( @@ -272,7 +277,7 @@ const allRelationships = ( evidence_id: evidenceId, }; return { - relationship_id: `air_${digestCanonical(semantic)}`, + relationship_id: `air_${canonicalDigest(semantic, "Artifact inspection")}`, ...semantic, }; }); @@ -299,7 +304,10 @@ const allHypotheses = ( limitation: "Format classification proposes a bounded follow-up; it is not semantic proof.", }; - return { hypothesis_id: `aih_${digestCanonical(semantic)}`, ...semantic }; + return { + hypothesis_id: `aih_${canonicalDigest(semantic, "Artifact inspection")}`, + ...semantic, + }; }); }; @@ -316,7 +324,7 @@ const contradictions = ( evidence_id: evidenceId, }; return { - contradiction_id: `aic_${digestCanonical(semantic)}`, + contradiction_id: `aic_${canonicalDigest(semantic, "Artifact inspection")}`, ...semantic, }; }); @@ -409,7 +417,10 @@ const branch = ( next_probe: nextProbe, evidence_id: evidenceId, }; - return { branch_id: `aib_${digestCanonical(semantic)}`, ...semantic }; + return { + branch_id: `aib_${canonicalDigest(semantic, "Artifact inspection")}`, + ...semantic, + }; }; const inspectionCoverage = ( @@ -419,15 +430,3 @@ const inspectionCoverage = ( if (Object.values(omissions).some((count) => count > 0)) return "truncated"; return "complete-within-substeps"; }; - -const digestCanonical = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Artifact inspection could not canonicalize"); - return createHash("sha256").update(encoded).digest("hex"); -}; - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort((left, right) => - left < right ? -1 : left > right ? 1 : 0, - ); diff --git a/src/domain/artifactInventoryEvidence.ts b/src/domain/artifactInventoryEvidence.ts index b85839cb82..0266a0d971 100644 --- a/src/domain/artifactInventoryEvidence.ts +++ b/src/domain/artifactInventoryEvidence.ts @@ -1,7 +1,3 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; - import { artifactInventoryResultSchema, type ArtifactInventoryResult, @@ -9,6 +5,7 @@ import { type ArtifactOccurrence, type IntegrityContradiction, } from "./artifactGraph.js"; +import { canonicalDigest, canonicalJson } from "./comparisonSemantics.js"; import { parseEvidence, type Evidence } from "./evidence.js"; import { artifactInspectionResultSchema } from "./artifactInspection.js"; @@ -81,7 +78,10 @@ const validateInventoryPage = (inventory: ArtifactInventoryResult): void => { "edge ID", ); for (const node of inventory.nodes) - if (node.artifact_id !== `art_${digestCanonical({ sha256: node.sha256 })}`) + if ( + node.artifact_id !== + `art_${canonicalDigest({ sha256: node.sha256 }, "Artifact inventory")}` + ) throw new TypeError("Artifact node ID is not content-addressed"); }; @@ -96,17 +96,29 @@ const assembleInventorySet = ( const first = pages[0]; if (first === undefined) throw new TypeError("Artifact inventory requires Evidence pages"); - const manifestJson = canonicalJson(first.inventory.manifest); + const manifestJson = canonicalJson( + first.inventory.manifest, + "Artifact inventory", + ); const nodes = new Map(); const occurrences = new Map(); const paths = new Map(); const edges = new Map(); for (const page of pages) { - if (canonicalJson(page.inventory.manifest) !== manifestJson) + if ( + canonicalJson(page.inventory.manifest, "Artifact inventory") !== + manifestJson + ) throw new TypeError("Artifact inventory pages do not share one manifest"); if ( - canonicalJson(page.inventory.integrity_contradictions) !== - canonicalJson(first.inventory.integrity_contradictions) + canonicalJson( + page.inventory.integrity_contradictions, + "Artifact inventory", + ) !== + canonicalJson( + first.inventory.integrity_contradictions, + "Artifact inventory", + ) ) throw new TypeError( "Artifact inventory pages do not share integrity contradictions", @@ -195,29 +207,38 @@ const validateCompleteInventory = (inventory: InventorySet): void => { throw new TypeError( "Integrity contradiction references a missing graph member", ); - const expectedId = `ic_${digestCanonical({ - root_artifact_id: inventory.manifest.root_artifact_id, - logical_path: contradiction.logical_path, - declared_sha256: contradiction.declared_sha256, - observed_sha256: contradiction.observed_sha256, - })}`; + const expectedId = `ic_${canonicalDigest( + { + root_artifact_id: inventory.manifest.root_artifact_id, + logical_path: contradiction.logical_path, + declared_sha256: contradiction.declared_sha256, + observed_sha256: contradiction.observed_sha256, + }, + "Artifact inventory", + )}`; if (contradiction.contradiction_id !== expectedId) throw new TypeError( "Integrity contradiction ID does not match its identity", ); } - const graphSha256 = digestCanonical({ - nodes: inventory.nodes, - occurrences: inventory.occurrences, - edges: inventory.edges, - integrity_contradictions: inventory.integrityContradictions, - }); + const graphSha256 = canonicalDigest( + { + nodes: inventory.nodes, + occurrences: inventory.occurrences, + edges: inventory.edges, + integrity_contradictions: inventory.integrityContradictions, + }, + "Artifact inventory", + ); if (graphSha256 !== inventory.manifest.graph_sha256) throw new TypeError("Artifact graph commitment does not match its members"); - const manifestId = `agm_${digestCanonical({ - root_artifact_id: inventory.manifest.root_artifact_id, - graph_sha256: graphSha256, - })}`; + const manifestId = `agm_${canonicalDigest( + { + root_artifact_id: inventory.manifest.root_artifact_id, + graph_sha256: graphSha256, + }, + "Artifact inventory", + )}`; if (manifestId !== inventory.manifest.manifest_id) throw new TypeError("Artifact manifest ID does not match its commitment"); }; @@ -237,12 +258,15 @@ const validateOccurrence = ( throw new TypeError("Artifact occurrence references a missing parent"); const expectedId = occurrence.logical_path === "." - ? `occ_${digestCanonical({ root: inventory.manifest.root_artifact_id })}` - : `occ_${digestCanonical({ - root_artifact_id: inventory.manifest.root_artifact_id, - logical_path: occurrence.logical_path, - entry_kind: occurrence.entry_kind, - })}`; + ? `occ_${canonicalDigest({ root: inventory.manifest.root_artifact_id }, "Artifact inventory")}` + : `occ_${canonicalDigest( + { + root_artifact_id: inventory.manifest.root_artifact_id, + logical_path: occurrence.logical_path, + entry_kind: occurrence.entry_kind, + }, + "Artifact inventory", + )}`; if (occurrence.occurrence_id !== expectedId) throw new TypeError("Artifact occurrence ID does not match its identity"); }; @@ -265,7 +289,9 @@ const validateEdge = ( occurrence_id: edge.occurrence_id, logical_path: edge.logical_path, }; - if (edge.edge_id !== `edge_${digestCanonical(semantic)}`) + if ( + edge.edge_id !== `edge_${canonicalDigest(semantic, "Artifact inventory")}` + ) throw new TypeError("Artifact edge ID does not match its identity"); }; @@ -278,7 +304,8 @@ const mergeExact = ( const previous = output.get(key); if ( previous !== undefined && - canonicalJson(previous) !== canonicalJson(value) + canonicalJson(previous, "Artifact inventory") !== + canonicalJson(value, "Artifact inventory") ) throw new TypeError(`Artifact inventory has conflicting ${label} pages`); output.set(key, value); @@ -288,13 +315,3 @@ const assertUnique = (values: readonly string[], label: string): void => { if (new Set(values).size !== values.length) throw new TypeError(`Artifact inventory contains a duplicate ${label}`); }; - -const digestCanonical = (value: unknown): string => - createHash("sha256").update(canonicalJson(value)).digest("hex"); - -const canonicalJson = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Artifact inventory could not canonicalize graph data"); - return encoded; -}; diff --git a/src/domain/browserScenarioDiff.ts b/src/domain/browserScenarioDiff.ts index 83cb8e8949..bc62730117 100644 --- a/src/domain/browserScenarioDiff.ts +++ b/src/domain/browserScenarioDiff.ts @@ -23,7 +23,6 @@ import { } from "./browserScenarioDiffValues.js"; import { commitBrowserScenarioNormalization, - digestCanonicalJson, digestNormalizedScenarioValue, } from "./browserScenarioNormalization.js"; diff --git a/src/domain/browserScenarioNormalization.ts b/src/domain/browserScenarioNormalization.ts index 24f6a2f080..08c889ab95 100644 --- a/src/domain/browserScenarioNormalization.ts +++ b/src/domain/browserScenarioNormalization.ts @@ -1,6 +1,4 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; +import { canonicalDigest } from "./comparisonSemantics.js"; import type { BrowserScenarioArtifactKind, @@ -30,7 +28,10 @@ export const commitBrowserScenarioNormalization = ( left.rule_id < right.rule_id ? -1 : left.rule_id > right.rule_id ? 1 : 0, ); const commitment = { built_in_rules: [...BUILT_IN_RULES], rules }; - return { ...commitment, sha256: digestCanonicalJson(commitment) }; + return { + ...commitment, + sha256: canonicalDigest(commitment, "Browser scenario"), + }; }; /** Digest one artifact after applying committed exact-literal rules. */ @@ -38,14 +39,8 @@ export const digestNormalizedScenarioValue = ( value: unknown, artifact: BrowserScenarioArtifactKind, rules: readonly BrowserScenarioNormalizationRule[], -): string => digestCanonicalJson(normalize(value, artifact, rules)); - -/** Compute a canonical SHA-256 commitment for JSON-compatible domain data. */ -export const digestCanonicalJson = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) throw new TypeError("Expected canonical JSON"); - return createHash("sha256").update(encoded).digest("hex"); -}; +): string => + canonicalDigest(normalize(value, artifact, rules), "Browser scenario"); const normalize = ( value: unknown, diff --git a/src/domain/canonicalOrdering.ts b/src/domain/canonicalOrdering.ts new file mode 100644 index 0000000000..104efe2af5 --- /dev/null +++ b/src/domain/canonicalOrdering.ts @@ -0,0 +1,15 @@ +/** + * Canonical code-point ordering shared across domain and application layers. + * + * Compares strings by Unicode code point (`<`/`>`) so graph ordering, + * fingerprints, and deterministic tie-breaks stay stable regardless of + * locale. Prefer this over `String.prototype.localeCompare`, which is + * locale-dependent. + */ +export const compareCodePoints = (left: string, right: string): number => + left < right ? -1 : left > right ? 1 : 0; + +/** Deduplicate and sort strings with canonical code-point ordering. */ +export const uniqueSorted = ( + values: readonly Value[], +): Value[] => [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/changedBehavior.ts b/src/domain/changedBehavior.ts index 5e7b761121..1c0a24ce9e 100644 --- a/src/domain/changedBehavior.ts +++ b/src/domain/changedBehavior.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { artifactComparisonResultSchema } from "./artifactComparison.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js"; import { functionComparisonResultSchema } from "./functionComparison.js"; import { @@ -386,9 +387,6 @@ const compareFindings = (left: Finding, right: Finding): number => "en", ); -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort((left, right) => left.localeCompare(right, "en")); - const assertUnique = (values: readonly string[], label: string): void => { if (new Set(values).size !== values.length) throw new TypeError(`Changed-behavior analysis rejects duplicate ${label}`); diff --git a/src/domain/electronStaticAnalysisBrowser.ts b/src/domain/electronStaticAnalysisBrowser.ts index c2d2e06420..7693d89abc 100644 --- a/src/domain/electronStaticAnalysisBrowser.ts +++ b/src/domain/electronStaticAnalysisBrowser.ts @@ -15,11 +15,11 @@ import { electronStaticValue, objectProperty, } from "./electronStaticAnalysisValues.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js"; import { argumentNode, calleeName, - compareCodePoints, propertyName, range, staticPath, diff --git a/src/domain/electronStaticAnalysisNative.ts b/src/domain/electronStaticAnalysisNative.ts index 2886240910..8dd762baeb 100644 --- a/src/domain/electronStaticAnalysisNative.ts +++ b/src/domain/electronStaticAnalysisNative.ts @@ -1,11 +1,11 @@ import * as t from "@babel/types"; +import { compareCodePoints } from "./canonicalOrdering.js"; import type { ElectronNativeAddonBindingFinding } from "./electronStaticAnalysisTypes.js"; import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js"; import { argumentNode, calleeName, - compareCodePoints, propertyName, range, stringValue, diff --git a/src/domain/electronStaticAnalysisValues.ts b/src/domain/electronStaticAnalysisValues.ts index b333d5e969..549b9e8155 100644 --- a/src/domain/electronStaticAnalysisValues.ts +++ b/src/domain/electronStaticAnalysisValues.ts @@ -1,8 +1,8 @@ import * as t from "@babel/types"; +import { compareCodePoints } from "./canonicalOrdering.js"; import type { ElectronStaticValue } from "./electronStaticAnalysisTypes.js"; import { - compareCodePoints, propertyName, sourceSlice, } from "./javascriptStaticAnalysisHelpers.js"; diff --git a/src/domain/functionComparison.ts b/src/domain/functionComparison.ts index 9cf9b2d003..eaf0d54d1f 100644 --- a/src/domain/functionComparison.ts +++ b/src/domain/functionComparison.ts @@ -1,14 +1,11 @@ +import { canonicalJson } from "./comparisonSemantics.js"; import { parseFunctionEvidence } from "./functionDossierEvidence.js"; import { functionMatch } from "./functionComparisonNormalization.js"; import { functionComparisonResultSchema, type FunctionComparisonResult, } from "./functionComparisonSchemas.js"; -import { - overallStatus, - summarize, - canonicalJson, -} from "./functionComparisonResults.js"; +import { overallStatus, summarize } from "./functionComparisonResults.js"; import { compareDimensions } from "./functionComparisonDimensions.js"; export { @@ -25,7 +22,8 @@ export const compareFunctions = ( const right = parseFunctionEvidence(rightInput); const links = [left.evidence[0].evidence_id, right.evidence[0].evidence_id]; const providersDiffer = - canonicalJson(left.provider) !== canonicalJson(right.provider); + canonicalJson(left.provider, "Function comparison") !== + canonicalJson(right.provider, "Function comparison"); const dimensions = compareDimensions(left, right, links, providersDiffer); const match = functionMatch(left, right); const changes = dimensions.filter(({ status }) => status !== "unchanged"); diff --git a/src/domain/functionComparisonDimensions.ts b/src/domain/functionComparisonDimensions.ts index 58cea51513..b0b30b8907 100644 --- a/src/domain/functionComparisonDimensions.ts +++ b/src/domain/functionComparisonDimensions.ts @@ -1,3 +1,4 @@ +import { canonicalJson } from "./comparisonSemantics.js"; import { diffLines } from "diff"; import type { @@ -21,7 +22,6 @@ import type { FunctionDimension, } from "./functionComparisonSchemas.js"; import { - canonicalJson, dimensionResult, unresolvedDimension as buildUnresolvedDimension, } from "./functionComparisonResults.js"; @@ -289,7 +289,10 @@ const compareReferences = ( true, providersDiffer, ); - if (canonicalJson(leftProjection) !== canonicalJson(rightProjection)) + if ( + canonicalJson(leftProjection, "Function comparison") !== + canonicalJson(rightProjection, "Function comparison") + ) return dimensionResult({ dimension: "references", status: "changed", @@ -448,8 +451,8 @@ const compareValues = ( ): FunctionDimension => { if (!leftComplete || !rightComplete) return unresolvedDimension(dimension, "unknown", links, null, null, []); - const leftJson = canonicalJson(left); - const rightJson = canonicalJson(right); + const leftJson = canonicalJson(left, "Function comparison"); + const rightJson = canonicalJson(right, "Function comparison"); return dimensionResult({ dimension, status: leftJson === rightJson ? "unchanged" : "changed", diff --git a/src/domain/functionComparisonResults.ts b/src/domain/functionComparisonResults.ts index a55f7a4ff8..d635cc317f 100644 --- a/src/domain/functionComparisonResults.ts +++ b/src/domain/functionComparisonResults.ts @@ -1,7 +1,4 @@ -import { - canonicalDigest, - canonicalJson as encodeCanonicalJson, -} from "./comparisonSemantics.js"; +import { canonicalDigest } from "./comparisonSemantics.js"; import type { DimensionName, FunctionComparisonResult, @@ -93,9 +90,5 @@ export const summarize = (dimensions: readonly FunctionDimension[]) => ({ unknown: dimensions.filter(({ status }) => status === "unknown").length, }); -export const canonicalJson = (value: unknown): string => { - return encodeCanonicalJson(value, "Function comparison"); -}; - const digest = (value: unknown): string => canonicalDigest(value, "Function comparison"); diff --git a/src/domain/javascriptApplicationChangeGraph.ts b/src/domain/javascriptApplicationChangeGraph.ts index 28836ebeb9..dd52f6f90e 100644 --- a/src/domain/javascriptApplicationChangeGraph.ts +++ b/src/domain/javascriptApplicationChangeGraph.ts @@ -1,5 +1,4 @@ import { - compareCodePoints, createJavaScriptApplicationEdge, createJavaScriptApplicationGraph, createJavaScriptApplicationNode, @@ -8,6 +7,7 @@ import { type ApplicationNode, type JavaScriptApplicationGraph, } from "./javascriptApplicationGraph.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { ApplicationVersionComparisonItem } from "./javascriptApplicationVersionComparisonSchemas.js"; interface ChangeGraphInput { @@ -256,7 +256,3 @@ const uniqueEdges = (edges: readonly ApplicationEdge[]): ApplicationEdge[] => [...new Map(edges.map((edge) => [edge.edge_id, edge])).values()].sort( (left, right) => compareCodePoints(left.edge_id, right.edge_id), ); - -const uniqueSorted = ( - values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptApplicationGraph.ts b/src/domain/javascriptApplicationGraph.ts index 2863003b27..ef82887c71 100644 --- a/src/domain/javascriptApplicationGraph.ts +++ b/src/domain/javascriptApplicationGraph.ts @@ -1,7 +1,6 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; import { z } from "zod"; +import { canonicalDigest, canonicalJson } from "./comparisonSemantics.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { ApplicationGraphEvidence, @@ -20,22 +19,6 @@ import { type JavaScriptApplicationGraphInput, } from "./javascriptApplicationGraphSchemas.js"; -/** Compare strings by Unicode code point for canonical graph ordering. */ -export const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; - -const canonicalJson = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError( - "JavaScript Application Graph could not canonicalize data", - ); - return encoded; -}; - -const digestCanonical = (value: unknown): string => - createHash("sha256").update(canonicalJson(value)).digest("hex"); - const OBSERVATION_IDENTIFIER_STRATEGY = { strategy: "semantic-content-sha256" as const, stability: "observation-exact" as const, @@ -45,13 +28,9 @@ const EDGE_IDENTIFIER_STRATEGY = { stability: "relationship-exact" as const, }; -const uniqueSorted = ( - values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); - const limitKey = ( limit: ApplicationGraphEvidence["coverage"]["limits"][number], -) => canonicalJson(limit); +) => canonicalJson(limit, "JavaScript Application Graph"); const normalizeLimits = ( limits: ApplicationGraphEvidence["coverage"]["limits"], @@ -85,7 +64,7 @@ const nodeSemantic = (node: Pick) => ({ }); const nodeId = (node: Pick): string => - `jag_node_${digestCanonical(nodeSemantic(node))}`; + `jag_node_${canonicalDigest(nodeSemantic(node), "JavaScript Application Graph")}`; const observationSemantic = ( nodeIdentifier: string, @@ -102,14 +81,14 @@ const observationId = ( "observation_id" >, ): string => - `jag_observation_${digestCanonical( + `jag_observation_${canonicalDigest( observationSemantic(nodeIdentifier, observation), )}`; type EdgeSemantic = Omit; const edgeId = (edge: EdgeSemantic): string => - `jag_edge_${digestCanonical(edge)}`; + `jag_edge_${canonicalDigest(edge, "JavaScript Application Graph")}`; const sortedUniqueIssue = ( values: readonly string[], @@ -235,8 +214,11 @@ const checkNode = ( }); if ( node.identity.strategy === "structural-fingerprint" && - canonicalJson(node.identity.basis) !== - canonicalJson(uniqueSorted(node.identity.basis)) + canonicalJson(node.identity.basis, "JavaScript Application Graph") !== + canonicalJson( + uniqueSorted(node.identity.basis), + "JavaScript Application Graph", + ) ) context.addIssue({ code: "custom", @@ -376,7 +358,10 @@ const checkGraphInvariants = ( } const { graph_id: identifier, ...semantic } = graph; - if (identifier !== `jag_${digestCanonical(semantic)}`) + if ( + identifier !== + `jag_${canonicalDigest(semantic, "JavaScript Application Graph")}` + ) context.addIssue({ code: "custom", message: "Graph identifier does not match its semantic content", @@ -461,7 +446,7 @@ export const createJavaScriptApplicationGraph = ( }; return javascriptApplicationGraphSchema.parse({ ...semantic, - graph_id: `jag_${digestCanonical(semantic)}`, + graph_id: `jag_${canonicalDigest(semantic, "JavaScript Application Graph")}`, }); }; @@ -473,10 +458,17 @@ export const parseJavaScriptApplicationGraph = ( /** Compute the byte-stable SHA-256 commitment of a verified graph. */ export const computeJavaScriptApplicationGraphSha256 = ( input: unknown, -): string => digestCanonical(parseJavaScriptApplicationGraph(input)); +): string => + canonicalDigest( + parseJavaScriptApplicationGraph(input), + "JavaScript Application Graph", + ); /** Serialize a verified graph as RFC 8785 canonical JSON. */ export const serializeJavaScriptApplicationGraph = (input: unknown): string => - canonicalJson(parseJavaScriptApplicationGraph(input)); + canonicalJson( + parseJavaScriptApplicationGraph(input), + "JavaScript Application Graph", + ); export type { ApplicationEdge, ApplicationGraphEvidence, ApplicationNode }; diff --git a/src/domain/javascriptApplicationVersionComparison.ts b/src/domain/javascriptApplicationVersionComparison.ts index 39514d5284..4358565cae 100644 --- a/src/domain/javascriptApplicationVersionComparison.ts +++ b/src/domain/javascriptApplicationVersionComparison.ts @@ -1,7 +1,5 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; - +import { canonicalDigest } from "./comparisonSemantics.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import type { Evidence } from "./evidence.js"; import { buildJavaScriptApplicationChangeGraph } from "./javascriptApplicationChangeGraph.js"; import { @@ -9,7 +7,6 @@ import { type ApplicationVersionComparisonResult, } from "./javascriptApplicationVersionComparisonSchemas.js"; import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; -import { compareCodePoints } from "./javascriptApplicationGraph.js"; import { classifyJavaScriptApplicationVersions } from "./javascriptApplicationVersionItems.js"; import { matchJavaScriptApplicationVersions } from "./javascriptApplicationVersionKeys.js"; @@ -81,7 +78,7 @@ export const compareJavaScriptApplicationVersions = ( }; return applicationVersionComparisonResultSchema.parse({ ...semantic, - comparison_id: `javc_${digestCanonical(semantic)}`, + comparison_id: `javc_${canonicalDigest(semantic, "Application version comparison")}`, }); }; @@ -164,16 +161,3 @@ const comparisonLimitations = ( "At least one source graph is incomplete; unmatched entities on the opposite side remain unknown rather than added or removed.", ]), ]); - -const digestCanonical = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError( - "Application version comparison could not canonicalize data", - ); - return createHash("sha256").update(encoded).digest("hex"); -}; - -const uniqueSorted = ( - values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptApplicationVersionItems.ts b/src/domain/javascriptApplicationVersionItems.ts index c9692c174c..a47a4ffc13 100644 --- a/src/domain/javascriptApplicationVersionItems.ts +++ b/src/domain/javascriptApplicationVersionItems.ts @@ -3,9 +3,9 @@ import { canonicalDigest, canonicalJson, } from "./comparisonSemantics.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import type { Evidence } from "./evidence.js"; import { - compareCodePoints, type ApplicationEdge, type ApplicationNode, type JavaScriptApplicationGraph, @@ -441,7 +441,7 @@ const itemWithId = ( semantic: Semantic, ): Semantic & { readonly item_id: string } => ({ ...semantic, - item_id: `javc_item_${digestCanonical(semantic)}`, + item_id: `javc_item_${canonicalDigest(semantic, "Application version item")}`, }); const stringProperty = (node: ApplicationNode, key: string): string | null => { @@ -458,6 +458,3 @@ const stringProperty = (node: ApplicationNode, key: string): string | null => { const graphsComplete = (context: ItemContext): boolean => absenceClaimable(context.leftGraph.coverage) && absenceClaimable(context.rightGraph.coverage); - -const digestCanonical = (value: unknown): string => - canonicalDigest(value, "Application version item"); diff --git a/src/domain/javascriptApplicationVersionKeys.ts b/src/domain/javascriptApplicationVersionKeys.ts index 76512dda77..898fde60af 100644 --- a/src/domain/javascriptApplicationVersionKeys.ts +++ b/src/domain/javascriptApplicationVersionKeys.ts @@ -1,5 +1,5 @@ +import { compareCodePoints } from "./canonicalOrdering.js"; import type { ApplicationNode } from "./javascriptApplicationGraph.js"; -import { compareCodePoints } from "./javascriptApplicationGraph.js"; import type { JsonValue } from "./jsonValue.js"; /** One deterministic one-to-one match between two graph versions. */ diff --git a/src/domain/javascriptAstFingerprint.ts b/src/domain/javascriptAstFingerprint.ts index ebf53346cb..334f2490b4 100644 --- a/src/domain/javascriptAstFingerprint.ts +++ b/src/domain/javascriptAstFingerprint.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { propertyName } from "./javascriptAstValues.js"; /** Static CommonJS export names. */ @@ -125,6 +126,3 @@ const memberPath = (node: t.Node | null | undefined): string => { const stringValue = (node: t.Node | null | undefined): string | undefined => t.isStringLiteral(node) ? node.value : undefined; - -const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; diff --git a/src/domain/javascriptExportShapeComparison.ts b/src/domain/javascriptExportShapeComparison.ts index 38e78eecc7..af6f0afc82 100644 --- a/src/domain/javascriptExportShapeComparison.ts +++ b/src/domain/javascriptExportShapeComparison.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "./javascriptApplicationGraph.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import { digestExportShapeValue } from "./javascriptExportShapeComparisonIdentity.js"; import { javaScriptExportShapeComparisonResultSchema, @@ -209,7 +209,3 @@ const evidencePair = (left: string, right: string): [string, string] => [ left, right, ]; - -const uniqueSorted = ( - values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptExportShapeSelection.ts b/src/domain/javascriptExportShapeSelection.ts index 4a89db62b8..24609e5d6c 100644 --- a/src/domain/javascriptExportShapeSelection.ts +++ b/src/domain/javascriptExportShapeSelection.ts @@ -2,12 +2,12 @@ import type { ApplicationNode, JavaScriptApplicationGraph, } from "./javascriptApplicationGraph.js"; -import { compareCodePoints } from "./javascriptApplicationGraph.js"; import { projectedExportReturnShapesSchema, type JavaScriptExportShapeComparisonResult, type ProjectedExportReturnShapes, } from "./javascriptExportShapeComparisonSchemas.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; type Shape = ProjectedExportReturnShapes["static_return_shapes"][number]; type SelectorResult = JavaScriptExportShapeComparisonResult["left"]; diff --git a/src/domain/javascriptExportShapeVariants.ts b/src/domain/javascriptExportShapeVariants.ts index 9db1e888d9..1cc0630086 100644 --- a/src/domain/javascriptExportShapeVariants.ts +++ b/src/domain/javascriptExportShapeVariants.ts @@ -1,4 +1,3 @@ -import { compareCodePoints } from "./javascriptApplicationGraph.js"; import { canonicalExportShapeValue, digestExportShapeValue, @@ -8,6 +7,7 @@ import type { JavaScriptExportShapeComparisonResult, ProjectedExportReturnShapes, } from "./javascriptExportShapeComparisonSchemas.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { SelectedJavaScriptExport } from "./javascriptExportShapeSelection.js"; type Primitive = string | number | boolean | null; @@ -453,7 +453,3 @@ const isPrimitive = (value: unknown): value is Primitive => const capitalize = (value: string): string => `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`; - -const uniqueSorted = ( - values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptFeatureSeed.ts b/src/domain/javascriptFeatureSeed.ts index 5f5f8754e1..991da5d27c 100644 --- a/src/domain/javascriptFeatureSeed.ts +++ b/src/domain/javascriptFeatureSeed.ts @@ -1,5 +1,5 @@ +import { compareCodePoints } from "./canonicalOrdering.js"; import type { ApplicationNode } from "./javascriptApplicationGraph.js"; -import { compareCodePoints } from "./javascriptApplicationGraph.js"; import type { ApplicationFeatureSeed } from "./javascriptFeatureTraceSchemas.js"; import type { JsonValue } from "./jsonValue.js"; diff --git a/src/domain/javascriptFeatureTrace.ts b/src/domain/javascriptFeatureTrace.ts index ad46099ed3..2435486d6c 100644 --- a/src/domain/javascriptFeatureTrace.ts +++ b/src/domain/javascriptFeatureTrace.ts @@ -1,10 +1,7 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; - +import { canonicalDigest } from "./comparisonSemantics.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { Evidence } from "./evidence.js"; import { - compareCodePoints, createJavaScriptApplicationGraph, type ApplicationEdge, type ApplicationNode, @@ -87,7 +84,7 @@ export const traceApplicationFeature = ( }; return applicationFeatureTraceResultSchema.parse({ ...semantic, - trace_id: `jatr_${digestCanonical(semantic)}`, + trace_id: `jatr_${canonicalDigest(semantic, "Feature trace")}`, }); }; @@ -127,7 +124,7 @@ const noMatchResult = ( }; return applicationFeatureTraceResultSchema.parse({ ...semantic, - trace_id: `jatr_${digestCanonical(semantic)}`, + trace_id: `jatr_${canonicalDigest(semantic, "Feature trace")}`, }); }; @@ -206,7 +203,10 @@ const pathTo = ( ({ evidence }) => evidence.state === "inferred", ), }; - return { ...semantic, path_id: `jatp_${digestCanonical(semantic)}` }; + return { + ...semantic, + path_id: `jatp_${canonicalDigest(semantic, "Feature trace")}`, + }; }; const isTerminal = (node: ApplicationNode): boolean => @@ -253,14 +253,3 @@ const sourceCoverageLimitations = ( : [ "The source application graph is incomplete; unmatched seeds and frontiers remain unknown.", ]; - -const digestCanonical = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Feature trace could not canonicalize data"); - return createHash("sha256").update(encoded).digest("hex"); -}; - -const uniqueSorted = ( - values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptFeatureTraversal.ts b/src/domain/javascriptFeatureTraversal.ts index 0e5e154eb4..96f6932b1d 100644 --- a/src/domain/javascriptFeatureTraversal.ts +++ b/src/domain/javascriptFeatureTraversal.ts @@ -1,9 +1,9 @@ import { - compareCodePoints, type ApplicationEdge, type ApplicationNode, type JavaScriptApplicationGraph, } from "./javascriptApplicationGraph.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; interface AdjacencyEntry { readonly edge: ApplicationEdge; diff --git a/src/domain/javascriptNativeHandoff.ts b/src/domain/javascriptNativeHandoff.ts index f3bdbb5b33..646fbc0d7e 100644 --- a/src/domain/javascriptNativeHandoff.ts +++ b/src/domain/javascriptNativeHandoff.ts @@ -1,6 +1,6 @@ +import { compareCodePoints } from "./canonicalOrdering.js"; import type { Evidence } from "./evidence.js"; import { - compareCodePoints, type ApplicationEdge, type ApplicationNode, } from "./javascriptApplicationGraph.js"; diff --git a/src/domain/javascriptRuntimeLoadState.ts b/src/domain/javascriptRuntimeLoadState.ts index e6a17b0eaa..c8949061b2 100644 --- a/src/domain/javascriptRuntimeLoadState.ts +++ b/src/domain/javascriptRuntimeLoadState.ts @@ -1,7 +1,5 @@ -import { - compareCodePoints, - type ApplicationNode, -} from "./javascriptApplicationGraph.js"; +import { type ApplicationNode } from "./javascriptApplicationGraph.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js"; import type { JavaScriptRuntimeReconciliationItem, @@ -173,6 +171,3 @@ const state = ( runtime_node_ids: [...input.runtimeNodeIds], reason: input.reason, }); - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptRuntimeReconciliationActive.ts b/src/domain/javascriptRuntimeReconciliationActive.ts index 3d3e7c22cb..2c6fbf742b 100644 --- a/src/domain/javascriptRuntimeReconciliationActive.ts +++ b/src/domain/javascriptRuntimeReconciliationActive.ts @@ -1,8 +1,7 @@ -import { createHash } from "node:crypto"; import { isAbsolute } from "node:path"; +import { canonicalDigest } from "./comparisonSemantics.js"; import { isPathWithinRoot } from "./localPath.js"; -import canonicalize from "canonicalize"; import { z } from "zod"; import { @@ -45,7 +44,7 @@ export const parseActiveElectronCapture = ( kind: "electron-active", evidence, inspection: normalizeInspection(result), - captureSha256: digestCanonical(result), + captureSha256: canonicalDigest(result, "Runtime reconciliation"), scriptsCompleteWithinScope: false, }; }; @@ -84,7 +83,7 @@ const normalizeInspection = ( readonly completeness: BrowserCompleteness; } => ({ target: { - target_id: `electron-active:${digestCanonical(result.application.application_path).slice(0, 32)}`, + target_id: `electron-active:${canonicalDigest(result.application.application_path, "Runtime reconciliation").slice(0, 32)}`, type: "electron-application", title: result.application.application_path, attached: true, @@ -115,10 +114,3 @@ const normalizeInspection = ( }, }), }); - -const digestCanonical = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Runtime reconciliation could not canonicalize input"); - return createHash("sha256").update(encoded).digest("hex"); -}; diff --git a/src/domain/javascriptRuntimeReconciliationGraph.ts b/src/domain/javascriptRuntimeReconciliationGraph.ts index 30efec7c89..65b3d0bf67 100644 --- a/src/domain/javascriptRuntimeReconciliationGraph.ts +++ b/src/domain/javascriptRuntimeReconciliationGraph.ts @@ -1,5 +1,4 @@ import { - compareCodePoints, createJavaScriptApplicationGraph, createJavaScriptApplicationNode, type ApplicationEdge, @@ -10,6 +9,7 @@ import type { ParsedRuntimeCapture, ParsedStaticLayer, } from "./javascriptRuntimeReconciliationParsing.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { RuntimeProjection } from "./javascriptRuntimeReconciliationRuntime.js"; interface ReconciledGraphInput { @@ -246,6 +246,3 @@ const mergeNodes = (nodes: readonly ApplicationNode[]): ApplicationNode[] => { const uniqueEdges = (edges: readonly ApplicationEdge[]): ApplicationEdge[] => [ ...new Map(edges.map((edge) => [edge.edge_id, edge])).values(), ]; - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptRuntimeReconciliationMatching.ts b/src/domain/javascriptRuntimeReconciliationMatching.ts index 7c79986af5..22b13d9a91 100644 --- a/src/domain/javascriptRuntimeReconciliationMatching.ts +++ b/src/domain/javascriptRuntimeReconciliationMatching.ts @@ -1,13 +1,11 @@ import { - compareCodePoints, createJavaScriptApplicationEdge, type ApplicationEdge, type ApplicationGraphEvidence, } from "./javascriptApplicationGraph.js"; -import { - digestCanonical, - type ParsedStaticLayer, -} from "./javascriptRuntimeReconciliationParsing.js"; +import { canonicalDigest } from "./comparisonSemantics.js"; +import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import type { JavaScriptRuntimeReconciliationItem } from "./javascriptRuntimeReconciliationSchemas.js"; import type { RuntimeReconciliationEntity } from "./javascriptRuntimeReconciliationRuntime.js"; import { @@ -335,7 +333,7 @@ const createItem = ( candidate_static_nodes: candidateReferences.items, } satisfies Omit; return { - reconciliation_id: `jrr_item_${digestCanonical(semantic)}`, + reconciliation_id: `jrr_item_${canonicalDigest(semantic, "Runtime reconciliation")}`, ...semantic, }; }; diff --git a/src/domain/javascriptRuntimeReconciliationParsing.ts b/src/domain/javascriptRuntimeReconciliationParsing.ts index adaac16e7a..3fc8f074b5 100644 --- a/src/domain/javascriptRuntimeReconciliationParsing.ts +++ b/src/domain/javascriptRuntimeReconciliationParsing.ts @@ -1,5 +1,3 @@ -import { createHash } from "node:crypto"; -import canonicalize from "canonicalize"; import { z } from "zod"; import { @@ -22,15 +20,14 @@ import { type JavaScriptRuntimeObservation, type JavaScriptRuntimeLocation, } from "./javascriptRuntimeObservation.js"; +import { canonicalDigest } from "./comparisonSemantics.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { parseEvidence, type Evidence } from "./evidence.js"; import { javascriptApplicationAnalysisResultSchema, type JavaScriptApplicationAnalysisResult, } from "./javascriptApplicationAnalysis.js"; -import { - compareCodePoints, - type JavaScriptApplicationGraph, -} from "./javascriptApplicationGraph.js"; +import { type JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; import { parseActiveElectronCapture } from "./javascriptRuntimeReconciliationActive.js"; import type { ReconcileJavaScriptRuntimeInput } from "./javascriptRuntimeReconciliationSchemas.js"; @@ -144,13 +141,6 @@ export const parseRuntimeCaptures = ( compareCodePoints(left.evidence.evidence_id, right.evidence.evidence_id), ); -export const digestCanonical = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Runtime reconciliation could not canonicalize input"); - return createHash("sha256").update(encoded).digest("hex"); -}; - const parseStaticLayer = (layer: StaticLayerInput): ParsedStaticLayer => { const evidence = parseEvidence(layer.analysis); const result = javascriptApplicationAnalysisResultSchema.parse( @@ -175,11 +165,14 @@ const parseStaticLayer = (layer: StaticLayerInput): ParsedStaticLayer => { "JavaScript application Evidence subject disagrees with its result", ); return { - layerId: `jrl_${digestCanonical({ - role: layer.role, - evidence_id: evidence.evidence_id, - runtime_mappings: layer.runtime_mappings, - })}`, + layerId: `jrl_${canonicalDigest( + { + role: layer.role, + evidence_id: evidence.evidence_id, + runtime_mappings: layer.runtime_mappings, + }, + "Runtime reconciliation", + )}`, role: layer.role, evidence, result, @@ -215,7 +208,7 @@ const parseRuntimeCapture = (input: Evidence): ParsedRuntimeCapture => { kind: "browser", evidence, inspection, - captureSha256: digestCanonical(inspection), + captureSha256: canonicalDigest(inspection, "Runtime reconciliation"), scriptsCompleteWithinScope: scriptsComplete(inspection.completeness), }; } @@ -243,7 +236,7 @@ const parseRuntimeCapture = (input: Evidence): ParsedRuntimeCapture => { kind: "electron", evidence, inspection, - captureSha256: digestCanonical(inspection), + captureSha256: canonicalDigest(inspection, "Runtime reconciliation"), scriptsCompleteWithinScope: scriptsComplete(inspection.completeness), }; } @@ -266,7 +259,7 @@ const parseRuntimeCapture = (input: Evidence): ParsedRuntimeCapture => { kind: "v8-inspector", evidence, inspection, - captureSha256: digestCanonical(result), + captureSha256: canonicalDigest(result, "Runtime reconciliation"), scriptsCompleteWithinScope: false, }; } diff --git a/src/domain/javascriptRuntimeReconciliationResult.ts b/src/domain/javascriptRuntimeReconciliationResult.ts index 2ef80b23dc..4832248735 100644 --- a/src/domain/javascriptRuntimeReconciliationResult.ts +++ b/src/domain/javascriptRuntimeReconciliationResult.ts @@ -1,13 +1,11 @@ -import { - compareCodePoints, - type JavaScriptApplicationGraph, -} from "./javascriptApplicationGraph.js"; +import { type JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; +import { canonicalDigest } from "./comparisonSemantics.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import type { StaticLoadStateProjection } from "./javascriptRuntimeLoadState.js"; import type { RuntimeMatchingProjection } from "./javascriptRuntimeReconciliationMatching.js"; -import { - digestCanonical, - type ParsedRuntimeCapture, - type ParsedStaticLayer, +import type { + ParsedRuntimeCapture, + ParsedStaticLayer, } from "./javascriptRuntimeReconciliationParsing.js"; import { javascriptRuntimeReconciliationResultSchema, @@ -51,7 +49,7 @@ export const createJavaScriptRuntimeReconciliationResult = ( limitations: reconciliationLimitations(completion), }; return javascriptRuntimeReconciliationResultSchema.parse({ - reconciliation_id: `jrr_${digestCanonical(semantic)}`, + reconciliation_id: `jrr_${canonicalDigest(semantic, "Runtime reconciliation")}`, ...semantic, }); }; @@ -210,6 +208,3 @@ const countBy = , Key extends keyof Item>( key: Key, value: Item[Key], ): number => items.filter((item) => item[key] === value).length; - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptRuntimeReconciliationSchemas.ts b/src/domain/javascriptRuntimeReconciliationSchemas.ts index 03ff4edfed..61d0c550ec 100644 --- a/src/domain/javascriptRuntimeReconciliationSchemas.ts +++ b/src/domain/javascriptRuntimeReconciliationSchemas.ts @@ -1,11 +1,9 @@ import { z } from "zod"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { evidenceSchema } from "./evidence.js"; import { javascriptRuntimeUnresolvedLocationSchema } from "./javascriptRuntimeObservation.js"; -import { - compareCodePoints, - javascriptApplicationGraphSchema, -} from "./javascriptApplicationGraph.js"; +import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; import { digestSchema } from "./../domain/digests.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; diff --git a/src/domain/javascriptRuntimeStaticCandidates.ts b/src/domain/javascriptRuntimeStaticCandidates.ts index ffc3f46b2e..29714733e0 100644 --- a/src/domain/javascriptRuntimeStaticCandidates.ts +++ b/src/domain/javascriptRuntimeStaticCandidates.ts @@ -1,10 +1,10 @@ import { posix, relative, win32 } from "node:path"; import { - compareCodePoints, type ApplicationNode, type JavaScriptApplicationGraph, } from "./javascriptApplicationGraph.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js"; import type { RuntimeReconciliationEntity } from "./javascriptRuntimeReconciliationRuntime.js"; @@ -316,6 +316,3 @@ const uniqueMappedPaths = ( compareCodePoints(left.path, right.path) || compareCodePoints(left.basis, right.basis), ); - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptSemanticCallResolution.ts b/src/domain/javascriptSemanticCallResolution.ts index 021839bca3..af8d1d0dbc 100644 --- a/src/domain/javascriptSemanticCallResolution.ts +++ b/src/domain/javascriptSemanticCallResolution.ts @@ -4,13 +4,14 @@ import type { JavaScriptSemanticArgumentFlow, JavaScriptSemanticCallable, } from "./javascriptSemanticIr.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; import { resolveSemanticBindingState, type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; -import { compareCodePoints, range } from "./javascriptStaticAnalysisHelpers.js"; +import { range } from "./javascriptStaticAnalysisHelpers.js"; /** Resolution of one call target within the currently analyzed source. */ export interface LocalCallableResolution { diff --git a/src/domain/javascriptSemanticChildProcesses.ts b/src/domain/javascriptSemanticChildProcesses.ts index 1e18a1fa33..d1666bbc59 100644 --- a/src/domain/javascriptSemanticChildProcesses.ts +++ b/src/domain/javascriptSemanticChildProcesses.ts @@ -14,8 +14,9 @@ import { type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; -import { compareCodePoints, range } from "./javascriptStaticAnalysisHelpers.js"; +import { range } from "./javascriptStaticAnalysisHelpers.js"; import { dataEffectLiteralString, dataEffectMemberCallee, diff --git a/src/domain/javascriptSemanticDataEffects.ts b/src/domain/javascriptSemanticDataEffects.ts index 61a52d90e6..839c50fd9f 100644 --- a/src/domain/javascriptSemanticDataEffects.ts +++ b/src/domain/javascriptSemanticDataEffects.ts @@ -6,6 +6,7 @@ import type { JavaScriptSemanticConfigurationOperation, JavaScriptSemanticRequestOperation, } from "./javascriptSemanticIr.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { semanticStaticPropertyName } from "./javascriptSemanticProjection.js"; import { resolveSemanticBindingState, @@ -27,7 +28,7 @@ import { traverseDataEffects as traverseWithContext, type DataEffectTraversalContext as TraversalContext, } from "./javascriptSemanticDataEffectHelpers.js"; -import { compareCodePoints, range } from "./javascriptStaticAnalysisHelpers.js"; +import { range } from "./javascriptStaticAnalysisHelpers.js"; interface DataEffectAnalysis { readonly configurationOperations: readonly JavaScriptSemanticConfigurationOperation[]; diff --git a/src/domain/javascriptSemanticFingerprints.ts b/src/domain/javascriptSemanticFingerprints.ts index a41594135b..e630bbe5f0 100644 --- a/src/domain/javascriptSemanticFingerprints.ts +++ b/src/domain/javascriptSemanticFingerprints.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; +import { compareCodePoints } from "./canonicalOrdering.js"; import type { JavaScriptSemanticCallAnalysis } from "./javascriptSemanticCalls.js"; import type { JavaScriptSemanticCallable, @@ -17,7 +18,6 @@ import type { import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; -import { compareCodePoints } from "./javascriptStaticAnalysisHelpers.js"; interface SemanticFingerprintCollectionInput { readonly state: JavaScriptSemanticAnalysisState; diff --git a/src/domain/javascriptSemanticGraph.ts b/src/domain/javascriptSemanticGraph.ts index febe08ccae..510ffe0080 100644 --- a/src/domain/javascriptSemanticGraph.ts +++ b/src/domain/javascriptSemanticGraph.ts @@ -1,9 +1,7 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; import { z } from "zod"; -import { compareCodePoints } from "./javascriptApplicationGraph.js"; +import { canonicalDigest, canonicalJson } from "./comparisonSemantics.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js"; import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES, @@ -23,22 +21,6 @@ import { type JavaScriptSemanticGraphRelation, } from "./javascriptSemanticGraphSchemas.js"; -const canonicalJson = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError( - "JavaScript semantic graph could not canonicalize data", - ); - return encoded; -}; - -const digestCanonical = (value: unknown): string => - createHash("sha256").update(canonicalJson(value)).digest("hex"); - -const uniqueSorted = ( - values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); - const normalizeEvidence = ( evidence: ApplicationGraphEvidence, ): ApplicationGraphEvidence => ({ @@ -46,7 +28,10 @@ const normalizeEvidence = ( coverage: { ...evidence.coverage, limits: [...evidence.coverage.limits].sort((left, right) => - compareCodePoints(canonicalJson(left), canonicalJson(right)), + compareCodePoints( + canonicalJson(left, "JavaScript semantic graph"), + canonicalJson(right, "JavaScript semantic graph"), + ), ), }, limitations: uniqueSorted(evidence.limitations), @@ -56,7 +41,7 @@ const normalizeEvidence = ( const nodeId = ( node: Pick, ): string => - `jsrg_node_${digestCanonical({ kind: node.kind, identity: node.identity })}`; + `jsrg_node_${canonicalDigest({ kind: node.kind, identity: node.identity }, "JavaScript semantic graph")}`; /** Normalize one semantic entity and derive its artifact-version identifier. */ export const createJavaScriptSemanticGraphNode = ( @@ -93,7 +78,7 @@ export const createJavaScriptSemanticGraphRelation = ( }; return javaScriptSemanticRelationSchema.parse({ ...semantic, - relation_id: `jsrg_relation_${digestCanonical(semantic)}`, + relation_id: `jsrg_relation_${canonicalDigest(semantic, "JavaScript semantic graph")}`, }); }; @@ -108,7 +93,7 @@ export const createJavaScriptSemanticGraphUnknown = (input: unknown) => { }; return javaScriptSemanticUnknownSchema.parse({ ...semantic, - unknown_id: `jsrg_unknown_${digestCanonical(semantic)}`, + unknown_id: `jsrg_unknown_${canonicalDigest(semantic, "JavaScript semantic graph")}`, }); }; @@ -124,15 +109,21 @@ export const createJavaScriptSemanticFingerprint = (input: unknown) => { limitations: uniqueSorted(parsed.limitations), evidence: normalizeEvidence(parsed.evidence), }; - const fingerprintSha256 = digestCanonical(semantic.components); + const fingerprintSha256 = canonicalDigest( + semantic.components, + "JavaScript semantic graph", + ); return javaScriptSemanticFingerprintSchema.parse({ ...semantic, fingerprint_sha256: fingerprintSha256, - fingerprint_id: `jsrg_fingerprint_${digestCanonical({ - function_node_id: semantic.function_node_id, - algorithm: semantic.algorithm, - fingerprint_sha256: fingerprintSha256, - })}`, + fingerprint_id: `jsrg_fingerprint_${canonicalDigest( + { + function_node_id: semantic.function_node_id, + algorithm: semantic.algorithm, + fingerprint_sha256: fingerprintSha256, + }, + "JavaScript semantic graph", + )}`, }); }; @@ -159,8 +150,11 @@ const sortedUniqueIssue = ( const checkCoverage = (graph: GraphRecord, context: z.RefinementCtx): void => { const families = graph.coverage.families.map(({ family }) => family); if ( - canonicalJson(families) !== - canonicalJson(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES) + canonicalJson(families, "JavaScript semantic graph") !== + canonicalJson( + JAVASCRIPT_SEMANTIC_RELATION_FAMILIES, + "JavaScript semantic graph", + ) ) context.addIssue({ code: "custom", @@ -307,7 +301,10 @@ const checkRelations = ( ): void => { for (const [index, relation] of graph.relations.entries()) { const { relation_id: identifier, ...semantic } = relation; - if (identifier !== "jsrg_relation_" + digestCanonical(semantic)) + if ( + identifier !== + "jsrg_relation_" + canonicalDigest(semantic, "JavaScript semantic graph") + ) context.addIssue({ code: "custom", path: ["relations", index, "relation_id"], @@ -347,7 +344,10 @@ const checkUnknowns = ( ): void => { for (const [index, unknown] of graph.unknowns.entries()) { const { unknown_id: identifier, ...semantic } = unknown; - if (identifier !== "jsrg_unknown_" + digestCanonical(semantic)) + if ( + identifier !== + "jsrg_unknown_" + canonicalDigest(semantic, "JavaScript semantic graph") + ) context.addIssue({ code: "custom", path: ["unknowns", index, "unknown_id"], @@ -413,7 +413,8 @@ const checkFingerprints = ( message: "Fingerprint must name a function node", }); if ( - fingerprint.fingerprint_sha256 !== digestCanonical(fingerprint.components) + fingerprint.fingerprint_sha256 !== + canonicalDigest(fingerprint.components, "JavaScript semantic graph") ) context.addIssue({ code: "custom", @@ -422,11 +423,14 @@ const checkFingerprints = ( }); const expectedIdentifier = "jsrg_fingerprint_" + - digestCanonical({ - function_node_id: fingerprint.function_node_id, - algorithm: fingerprint.algorithm, - fingerprint_sha256: fingerprint.fingerprint_sha256, - }); + canonicalDigest( + { + function_node_id: fingerprint.function_node_id, + algorithm: fingerprint.algorithm, + fingerprint_sha256: fingerprint.fingerprint_sha256, + }, + "JavaScript semantic graph", + ); if (fingerprint.fingerprint_id !== expectedIdentifier) context.addIssue({ code: "custom", @@ -462,7 +466,10 @@ const checkGraph = (graph: GraphRecord, context: z.RefinementCtx): void => { message: "Non-complete graph coverage requires a limitation", }); const { graph_id: identifier, ...semantic } = graph; - if (identifier !== "jsrg_" + digestCanonical(semantic)) + if ( + identifier !== + "jsrg_" + canonicalDigest(semantic, "JavaScript semantic graph") + ) context.addIssue({ code: "custom", path: ["graph_id"], @@ -502,7 +509,10 @@ export const createJavaScriptSemanticGraph = ( coverage: { ...parsed.coverage, limits: [...parsed.coverage.limits].sort((left, right) => - compareCodePoints(canonicalJson(left), canonicalJson(right)), + compareCodePoints( + canonicalJson(left, "JavaScript semantic graph"), + canonicalJson(right, "JavaScript semantic graph"), + ), ), families: [...parsed.coverage.families] .map((family) => ({ @@ -515,7 +525,7 @@ export const createJavaScriptSemanticGraph = ( }; return javaScriptSemanticGraphSchema.parse({ ...semantic, - graph_id: `jsrg_${digestCanonical(semantic)}`, + graph_id: `jsrg_${canonicalDigest(semantic, "JavaScript semantic graph")}`, }); }; diff --git a/src/domain/javascriptSemanticPrimitives.ts b/src/domain/javascriptSemanticPrimitives.ts index e8c139dddb..501ae8ef48 100644 --- a/src/domain/javascriptSemanticPrimitives.ts +++ b/src/domain/javascriptSemanticPrimitives.ts @@ -4,8 +4,8 @@ import type { JavaScriptSemanticPrimitive, JavaScriptSemanticValue, } from "./javascriptSemanticIr.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { semanticPrimitiveKey } from "./javascriptSemanticProvenance.js"; -import { compareCodePoints } from "./javascriptStaticAnalysisHelpers.js"; /** Normalize one bounded collection of possible primitive values. */ export const semanticPrimitiveSet = ( diff --git a/src/domain/javascriptSemanticProjection.ts b/src/domain/javascriptSemanticProjection.ts index e007810dc6..3bd2034b66 100644 --- a/src/domain/javascriptSemanticProjection.ts +++ b/src/domain/javascriptSemanticProjection.ts @@ -14,13 +14,13 @@ import { type JavaScriptSemanticAnalysisState, type JavaScriptSemanticScopeState, } from "./javascriptSemanticState.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { evaluateSemanticBinding, evaluateSemanticProvenance, } from "./javascriptSemanticValues.js"; import { - compareCodePoints, propertyName, range, stringValue, diff --git a/src/domain/javascriptSemanticPromises.ts b/src/domain/javascriptSemanticPromises.ts index cace26ac59..4b320a2671 100644 --- a/src/domain/javascriptSemanticPromises.ts +++ b/src/domain/javascriptSemanticPromises.ts @@ -13,12 +13,9 @@ import { semanticResolutionBlocked, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; -import { - compareCodePoints, - range, - sourceRangesEqual, -} from "./javascriptStaticAnalysisHelpers.js"; +import { range, sourceRangesEqual } from "./javascriptStaticAnalysisHelpers.js"; import { assignedSemanticResultBindings, containsSemanticNode, diff --git a/src/domain/javascriptSemanticProvenance.ts b/src/domain/javascriptSemanticProvenance.ts index 1559d7c41c..255e457e05 100644 --- a/src/domain/javascriptSemanticProvenance.ts +++ b/src/domain/javascriptSemanticProvenance.ts @@ -3,7 +3,7 @@ import type { JavaScriptModuleOrigin, JavaScriptSemanticPrimitive, } from "./javascriptSemanticIr.js"; -import { compareCodePoints } from "./javascriptStaticAnalysisHelpers.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; /** Construct exact local provenance without inventing module origins. */ export const semanticLocalProvenance = (): JavaScriptBindingProvenance => ({ diff --git a/src/domain/javascriptSemanticQuery.ts b/src/domain/javascriptSemanticQuery.ts index 9ff6168a5f..5157247f37 100644 --- a/src/domain/javascriptSemanticQuery.ts +++ b/src/domain/javascriptSemanticQuery.ts @@ -1,9 +1,9 @@ -import { compareCodePoints } from "./javascriptApplicationGraph.js"; import type { JavaScriptSemanticGraph, JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation, } from "./javascriptSemanticGraph.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js"; import { isJavaScriptSemanticOwnershipRelation as ownershipRelation } from "./javascriptSemanticQueryRelations.js"; import { diff --git a/src/domain/javascriptSemanticQueryAssessment.ts b/src/domain/javascriptSemanticQueryAssessment.ts index 100fa29c20..db9499ecb0 100644 --- a/src/domain/javascriptSemanticQueryAssessment.ts +++ b/src/domain/javascriptSemanticQueryAssessment.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "./javascriptApplicationGraph.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import type { JavaScriptSemanticGraph } from "./javascriptSemanticGraph.js"; import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js"; import type { JavaScriptSemanticQueryResult } from "./javascriptSemanticQuerySchemas.js"; @@ -77,6 +77,3 @@ const queryLimitations = ( "Candidate relations remain ambiguous and do not establish a resolved semantic path.", ]), ]); - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptSemanticQueryIdentity.ts b/src/domain/javascriptSemanticQueryIdentity.ts index 3b7633de95..92ec930d71 100644 --- a/src/domain/javascriptSemanticQueryIdentity.ts +++ b/src/domain/javascriptSemanticQueryIdentity.ts @@ -2,11 +2,11 @@ import { createHash } from "node:crypto"; import canonicalize from "canonicalize"; -import { compareCodePoints } from "./javascriptApplicationGraph.js"; import type { JavaScriptSemanticGraph, JavaScriptSemanticGraphNode, } from "./javascriptSemanticGraph.js"; +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import type { JavaScriptSemanticQueryInput } from "./javascriptSemanticQuerySchemas.js"; /** Canonical JSON used by semantic query commitments and ordering. */ @@ -26,9 +26,6 @@ const digest = (value: unknown): string => .update(canonicalJavaScriptSemanticQueryJson(value)) .digest("hex"); -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareCodePoints); - /** Derive the stable identity for one graph-bound query. */ export const javaScriptSemanticQueryIdentifier = ( graph: JavaScriptSemanticGraph, diff --git a/src/domain/javascriptSemanticReturns.ts b/src/domain/javascriptSemanticReturns.ts index d4fc064442..c24e0f67ca 100644 --- a/src/domain/javascriptSemanticReturns.ts +++ b/src/domain/javascriptSemanticReturns.ts @@ -10,6 +10,7 @@ import type { ProjectedPropertyCoverage, ProjectedReturnField, } from "./javascriptExportShapeComparisonSchemas.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; import { resolveSemanticBindingState, @@ -17,7 +18,7 @@ import { type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; import { evaluateSemanticExpression } from "./javascriptSemanticValues.js"; -import { compareCodePoints, range } from "./javascriptStaticAnalysisHelpers.js"; +import { range } from "./javascriptStaticAnalysisHelpers.js"; import { semanticReturnCoverage } from "./javascriptSemanticCoverage.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; diff --git a/src/domain/javascriptSemanticValues.ts b/src/domain/javascriptSemanticValues.ts index b6bb95a8bc..913683ee86 100644 --- a/src/domain/javascriptSemanticValues.ts +++ b/src/domain/javascriptSemanticValues.ts @@ -13,10 +13,10 @@ import { type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; import { - compareCodePoints, propertyName, stringValue, } from "./javascriptStaticAnalysisHelpers.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { semanticStaticPropertyName } from "./javascriptAstValues.js"; import { semanticAmbiguousProvenance, diff --git a/src/domain/javascriptStaticAnalysisBundler.ts b/src/domain/javascriptStaticAnalysisBundler.ts index a353909917..b63a229c31 100644 --- a/src/domain/javascriptStaticAnalysisBundler.ts +++ b/src/domain/javascriptStaticAnalysisBundler.ts @@ -4,12 +4,12 @@ import { collectJavaScriptExports, fingerprintJavaScriptAst, } from "./javascriptAstFingerprint.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; import { addFindingOnce } from "./javascriptStaticAnalysisFindings.js"; import { argumentValue, calleeName, chunkRuntime, - compareCodePoints, factoryRequireName, moduleFactory, modulePropertyName, diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index 87ec76e250..00d8855ae9 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -11,6 +11,7 @@ import type { JavaScriptStaticStorage, } from "./javascriptStaticAnalysisTypes.js"; import { semanticStaticPropertyName } from "./javascriptAstValues.js"; +import { compareCodePoints } from "./canonicalOrdering.js"; export { propertyName, @@ -414,10 +415,6 @@ export const sortedUnique = ( export const sha256Text = (value: string): string => createHash("sha256").update(value).digest("hex"); -/** Compare strings by Unicode code point for canonical ordering. */ -export const compareCodePoints = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; - /** Recognized route-construction call suffixes. */ export const STATIC_ROUTE_CALL_NAMES = [ "route", diff --git a/src/domain/reconstructionUnknowns.ts b/src/domain/reconstructionUnknowns.ts index 2cfeb9b4a8..8af5593f1b 100644 --- a/src/domain/reconstructionUnknowns.ts +++ b/src/domain/reconstructionUnknowns.ts @@ -1,3 +1,4 @@ +import { uniqueSorted } from "./canonicalOrdering.js"; import type { ReconstructionClaim, ReconstructionClaimResult, @@ -129,6 +130,3 @@ const defaultProbe = ( rationale: "Capture complete artifact inventories under equal limits.", }; - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort((left, right) => left.localeCompare(right, "en")); diff --git a/src/domain/reconstructionVerification.ts b/src/domain/reconstructionVerification.ts index 47d44d2597..2a40fbdb9d 100644 --- a/src/domain/reconstructionVerification.ts +++ b/src/domain/reconstructionVerification.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { artifactComparisonResultSchema } from "./artifactComparison.js"; import { artifactInventoryResultSchema } from "./artifactGraph.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import { parseEvidence, type Evidence } from "./evidence.js"; import { parseEvidenceBundle } from "./evidenceBundle.js"; import { functionComparisonResultSchema } from "./functionComparison.js"; @@ -371,5 +372,3 @@ const count = ( const sameSet = (left: readonly string[], right: readonly string[]): boolean => left.length === right.length && uniqueSorted(left).every((id, index) => id === uniqueSorted(right)[index]); -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort((left, right) => left.localeCompare(right, "en")); diff --git a/src/domain/referenceSourceGraph.ts b/src/domain/referenceSourceGraph.ts index 857acbbe37..edc4bc9689 100644 --- a/src/domain/referenceSourceGraph.ts +++ b/src/domain/referenceSourceGraph.ts @@ -1,9 +1,8 @@ -import { createHash } from "node:crypto"; import { posix } from "node:path"; -import canonicalize from "canonicalize"; import { z } from "zod"; +import { canonicalDigest, canonicalJson } from "./comparisonSemantics.js"; import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; import { digestSchema } from "./../domain/digests.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; @@ -228,16 +227,6 @@ export const historicalSourceParseFailureKey = ( failure: HistoricalSourceGraphInput["parse_failures"][number], ): string => `${failure.path}\u0000${failure.parser}\u0000${failure.reason}`; -const canonicalJson = (value: unknown): string => { - const serialized = canonicalize(value); - if (serialized === undefined) - throw new TypeError("RFC 8785 canonicalization rejected a graph value"); - return serialized; -}; - -const digestCanonical = (value: unknown): string => - createHash("sha256").update(canonicalJson(value)).digest("hex"); - const relationshipKey = ( relationship: z.infer, ): string => @@ -420,8 +409,11 @@ function checkGraphInvariants( ); if ( - canonicalJson(graph.languages) !== - canonicalJson(historicalSourceLanguages(graph.entries)) + canonicalJson(graph.languages, "Reference source graph") !== + canonicalJson( + historicalSourceLanguages(graph.entries), + "Reference source graph", + ) ) context.addIssue({ code: "custom", @@ -429,8 +421,11 @@ function checkGraphInvariants( path: ["languages"], }); if ( - canonicalJson(graph.manifests) !== - canonicalJson(historicalSourceManifests(graph.entries)) + canonicalJson(graph.manifests, "Reference source graph") !== + canonicalJson( + historicalSourceManifests(graph.entries), + "Reference source graph", + ) ) context.addIssue({ code: "custom", @@ -479,7 +474,11 @@ const rootCommitment = ( const computeRootSha256 = ( entries: readonly z.infer[], exclusions: readonly z.infer[], -): string => digestCanonical(rootCommitment(entries, exclusions)); +): string => + canonicalDigest( + rootCommitment(entries, exclusions), + "Reference source graph", + ); /** Build and internally commit a normalized historical source graph. */ export const createHistoricalSourceGraph = ( @@ -499,7 +498,7 @@ export const parseHistoricalSourceGraph = ( /** Compute a deterministic graph commitment containing no absolute root path. */ export const computeHistoricalSourceGraphSha256 = (input: unknown): string => - digestCanonical(parseHistoricalSourceGraph(input)); + canonicalDigest(parseHistoricalSourceGraph(input), "Reference source graph"); /** Build a deterministic, relocation-independent manifest for a source graph. */ export const createHistoricalSourceManifest = ( @@ -517,7 +516,7 @@ export const createHistoricalSourceManifest = ( }; return historicalSourceManifestBaseSchema.parse({ ...semantic, - manifest_id: `hsm_${digestCanonical(semantic)}`, + manifest_id: `hsm_${canonicalDigest(semantic, "Reference source graph")}`, }); }; @@ -527,7 +526,9 @@ export const parseHistoricalSourceManifest = ( ): HistoricalSourceManifest => { const manifest = historicalSourceManifestBaseSchema.parse(input); const { manifest_id: manifestId, ...semantic } = manifest; - if (`hsm_${digestCanonical(semantic)}` !== manifestId) + if ( + `hsm_${canonicalDigest(semantic, "Reference source graph")}` !== manifestId + ) throw new TypeError("Historical source manifest identifier does not match"); return manifest; }; diff --git a/src/domain/sourceToBundleComparison.ts b/src/domain/sourceToBundleComparison.ts index 007f38d16a..076b6ad3f2 100644 --- a/src/domain/sourceToBundleComparison.ts +++ b/src/domain/sourceToBundleComparison.ts @@ -1,3 +1,4 @@ +import { compareCodePoints, uniqueSorted } from "./canonicalOrdering.js"; import { absenceClaimable, canonicalDigest } from "./comparisonSemantics.js"; import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; @@ -82,7 +83,7 @@ export const compareSourceToBundle = ( }; return sourceToBundleComparisonResultSchema.parse({ ...semanticResult, - comparison_id: `stbc_${digestCanonical(semanticResult)}`, + comparison_id: `stbc_${canonicalDigest(semanticResult, "Source-to-bundle comparison")}`, }); }; @@ -299,11 +300,14 @@ const mappingId = ( input: SourceToBundleComparisonInput, item: Omit, ): string => - `stbc_item_${digestCanonical({ - reference_root_sha256: input.reference.root_sha256, - application_graph_id: input.application.graph.graph_id, - ...item, - })}`; + `stbc_item_${canonicalDigest( + { + reference_root_sha256: input.reference.root_sha256, + application_graph_id: input.application.graph.graph_id, + ...item, + }, + "Source-to-bundle comparison", + )}`; const comparisonSummary = ( items: readonly SourceToBundleComparisonItem[], @@ -362,13 +366,4 @@ const compareCandidates = ( right: SourceToBundleCandidate, ): number => right.score - left.score || - compareText(left.current_node_id, right.current_node_id); - -const digestCanonical = (value: unknown): string => - canonicalDigest(value, "Source-to-bundle comparison"); - -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareText); - -const compareText = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; + compareCodePoints(left.current_node_id, right.current_node_id); diff --git a/src/domain/sourceToBundleSignals.ts b/src/domain/sourceToBundleSignals.ts index cdb6d5d4bb..5caac70d13 100644 --- a/src/domain/sourceToBundleSignals.ts +++ b/src/domain/sourceToBundleSignals.ts @@ -1,5 +1,6 @@ import { posix } from "node:path"; +import { compareCodePoints } from "./canonicalOrdering.js"; import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js"; import type { HistoricalSourceGraph } from "./referenceSourceGraph.js"; import { @@ -67,7 +68,7 @@ export const sourceBearingNodes = ( ): ApplicationNode[] => nodes .filter(({ kind }) => RELEVANT_NODE_KINDS.has(kind)) - .sort((left, right) => compareText(left.node_id, right.node_id)); + .sort((left, right) => compareCodePoints(left.node_id, right.node_id)); /** Keep historical source files in stable path order. */ export const historicalSourceFiles = ( @@ -78,7 +79,7 @@ export const historicalSourceFiles = ( (entry): entry is SourceFile => entry.kind === "file" && entry.classifications.includes("source"), ) - .sort((left, right) => compareText(left.path, right.path)); + .sort((left, right) => compareCodePoints(left.path, right.path)); /** Build deterministic candidate indices without assigning fuzzy matches. */ export const buildSourceToBundleCandidateIndex = ( @@ -125,7 +126,7 @@ export const candidateIdsForSource = ( const output: string[] = []; for (const group of groups) { if (group === undefined) continue; - for (const nodeId of [...group].sort(compareText)) { + for (const nodeId of [...group].sort(compareCodePoints)) { if (seen.has(nodeId)) continue; seen.add(nodeId); output.push(nodeId); @@ -233,7 +234,7 @@ const signal = ( kind, weight: signalWeight(kind), source_value: sourceValue, - current_values: [...new Set(currentValues)].sort(compareText), + current_values: [...new Set(currentValues)].sort(compareCodePoints), }); const candidateConfidence = ( @@ -293,7 +294,10 @@ const uniquePaths = (paths: readonly CurrentPath[]): CurrentPath[] => paths.map((path) => [`${path.kind}\0${path.value}`, path]), ).values(), ].sort((left, right) => - compareText(`${left.kind}\0${left.value}`, `${right.kind}\0${right.value}`), + compareCodePoints( + `${left.kind}\0${left.value}`, + `${right.kind}\0${right.value}`, + ), ); const pathSuffixes = (path: string): string[] => { @@ -310,6 +314,3 @@ const addIndexValue = ( values.add(nodeId); index.set(key, values); }; - -const compareText = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; diff --git a/src/domain/staticRuntimeCorrelation.ts b/src/domain/staticRuntimeCorrelation.ts index 194e49bb2a..3c16443441 100644 --- a/src/domain/staticRuntimeCorrelation.ts +++ b/src/domain/staticRuntimeCorrelation.ts @@ -4,6 +4,7 @@ import canonicalize from "canonicalize"; import { z } from "zod"; import { artifactComparisonResultSchema } from "./artifactComparison.js"; +import { uniqueSorted } from "./canonicalOrdering.js"; import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js"; import { functionComparisonResultSchema } from "./functionComparison.js"; import type { JsonValue } from "./jsonValue.js"; @@ -485,9 +486,6 @@ const assertUnique = (evidence: readonly Evidence[], label: string): void => { ); }; -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values)].sort((left, right) => left.localeCompare(right, "en")); - const sha256 = (value: string): string => createHash("sha256").update(value).digest("hex"); diff --git a/src/server/promptCompletion.ts b/src/server/promptCompletion.ts index 5b12b6c6b9..1aa788a527 100644 --- a/src/server/promptCompletion.ts +++ b/src/server/promptCompletion.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { uniqueSorted } from "../domain/canonicalOrdering.js"; import type { AnalysisOperationPort } from "../application/AnalysisProvider.js"; import type { BinarySessionPort } from "../application/BinarySession.js"; import type { PromptCompletionKind } from "../contracts/promptContracts.js"; @@ -41,7 +42,8 @@ export const createPromptCompletionSource = ( context, ); const prefix = normalize(value); - return uniqueSorted(candidates).filter((candidate) => + const nonEmpty = candidates.filter((candidate) => candidate.length > 0); + return uniqueSorted(nonEmpty).filter((candidate) => normalize(candidate).startsWith(prefix), ); }, @@ -182,11 +184,5 @@ const unknownCandidates = ( .filter(({ status }) => status !== "resolved") .map(({ unknown_id: id }) => id); -const uniqueSorted = (values: readonly string[]): string[] => - [...new Set(values.filter((value) => value.length > 0))].sort(compareText); - -const compareText = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; - const normalize = (value: string): string => value.normalize("NFKC").toLowerCase(); diff --git a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts index e85eb4ed8a..bcd1542c47 100644 --- a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts +++ b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { describe, expect, it } from "vitest"; -import { digestCanonical } from "../../../src/application/ArtifactGraphConstruction.js"; +import { canonicalDigest } from "../../../src/domain/comparisonSemantics.js"; import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; import { compareArtifacts } from "../../../src/domain/artifactComparison.js"; import type { ArtifactInventoryResult } from "../../../src/domain/artifactGraph.js"; @@ -54,23 +54,29 @@ const legacyDirectoryId = ( inventory: ArtifactInventoryResult, paths: readonly string[], ) => - `art_${digestCanonical({ - sha256: digestCanonical({ - kind: "directory", - children: paths - .map((path) => { - const child = occurrenceAt(inventory, path); - return { - name: path.split("/").at(-1), - artifact_id: child.artifact_id, - entry_kind: child.entry_kind, - }; - }) - .sort((left, right) => - String(left.name).localeCompare(String(right.name)), - ), - }), - })}`; + `art_${canonicalDigest( + { + sha256: canonicalDigest( + { + kind: "directory", + children: paths + .map((path) => { + const child = occurrenceAt(inventory, path); + return { + name: path.split("/").at(-1), + artifact_id: child.artifact_id, + entry_kind: child.entry_kind, + }; + }) + .sort((left, right) => + String(left.name).localeCompare(String(right.name)), + ), + }, + "Artifact", + ), + }, + "Artifact", + )}`; describe("virtual directory relative-path identity", () => { it("detects a renamed implicit intermediate directory", async () => {