From 765e809e366eff21b4b9c224f1a813ad64245ea3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:17:17 +0800 Subject: [PATCH] refactor: use canonical implementations and types directly Remove forwarding wrappers and shadow aliases, migrate callers and verification scripts to their owners, and share one exhaustive semantic resource-bound projection. --- AGENTS.md | 2 +- docs/architecture.mermaid | 2 +- scripts/verify/javascript/digests.mjs | 4 +- src/application/BrowserScenarioEvidence.ts | 4 +- src/application/DirectAnalysisStatus.ts | 21 +------ src/application/SetupClientConfiguration.ts | 4 +- .../javascript/ElectronActiveEvidence.ts | 4 +- .../javascript/ElectronBoundaryAnalysis.ts | 4 +- .../JavaScriptArtifactGraphBuilder.ts | 30 ++-------- .../javascript/JavaScriptJsonModules.ts | 4 +- .../JavaScriptModuleRelationships.ts | 30 +++------- .../JavaScriptSemanticGraphProjection.ts | 4 +- .../JavaScriptSemanticGraphValueProjection.ts | 8 ++- src/artifacts/ArtifactProviderMetadata.ts | 4 +- src/artifacts/DirectoryArtifactReader.ts | 4 +- .../extraction/ArtifactExtraction.ts | 12 ++-- .../inventory/ArtifactGraphConstruction.ts | 16 +++--- src/artifacts/inventory/scanCanonical.ts | 10 ++-- .../javascript/JavaScriptArtifactFiles.ts | 6 +- src/browser/CdpBrowserProvider.ts | 7 ++- src/browser/CdpCaptureStorageFingerprints.ts | 6 +- src/browser/CdpElectronInspection.ts | 4 +- src/browser/CdpElectronProvider.ts | 8 +-- src/browser/CdpElectronScripts.ts | 4 +- src/cli/utilityCommands.ts | 17 +----- src/composition/directAnalysis.ts | 8 +-- src/contracts/artifactComparisonExample.ts | 10 ++-- .../javascriptRuntimeReconciliationExample.ts | 8 +-- src/contracts/managed/managedToolContracts.ts | 8 +-- .../mobileApplicationGraphExamples.ts | 10 ++-- src/contracts/sessionLifecycleInputs.ts | 10 ++-- src/contracts/sessionStatusContract.ts | 8 +-- src/contracts/sessionToolContracts.ts | 8 +-- src/contracts/sessionToolSchemas.ts | 8 +-- src/domain/apple/appleApplication.test.ts | 16 +++--- src/domain/artifactComparison.ts | 8 +-- src/domain/artifactIdentity.ts | 16 +++--- src/domain/artifactInspection.ts | 16 +++--- src/domain/artifactInventoryEvidence.ts | 10 ++-- src/domain/browserScenarioNormalization.ts | 6 +- src/domain/browserScenarioValues.ts | 14 ++--- src/domain/callPath.ts | 8 +-- src/domain/canonicalOrdering.ts | 16 +----- src/domain/comparisonSemantics.ts | 10 ---- src/domain/firmware/firmwareAnalysis.ts | 7 ++- src/domain/functionComparison.test.ts | 11 ++-- src/domain/functionComparisonNormalization.ts | 4 +- src/domain/functionComparisonResults.ts | 4 +- .../javascript/electronActiveObservation.ts | 5 +- .../electronStaticAnalysisBrowser.ts | 6 +- .../electronStaticAnalysisNative.ts | 4 +- .../electronStaticAnalysisValues.ts | 4 +- .../javascriptApplicationChangeGraph.ts | 11 ++-- .../javascript/javascriptApplicationGraph.ts | 30 +++++----- .../javascriptApplicationVersionComparison.ts | 4 +- ...riptApplicationVersionComparisonSchemas.ts | 21 ++++--- .../javascriptApplicationVersionItems.ts | 23 ++++---- .../javascriptApplicationVersionKeys.ts | 12 ++-- .../javascript/javascriptAstFingerprint.ts | 4 +- .../javascriptExportShapeComparison.ts | 5 +- .../javascriptExportShapeComparisonSchemas.ts | 32 +++++------ .../javascriptExportShapeSelection.ts | 4 +- .../javascriptExportShapeVariants.ts | 5 +- .../javascript/javascriptFeatureSeed.ts | 8 ++- .../javascript/javascriptFeatureTrace.ts | 15 +++-- .../javascript/javascriptFeatureTraversal.ts | 10 ++-- .../javascript/javascriptNativeHandoff.ts | 14 +++-- .../javascriptRuntimeReconciliationActive.ts | 6 +- .../javascriptRuntimeReconciliationGraph.ts | 11 ++-- ...javascriptRuntimeReconciliationMatching.ts | 10 ++-- .../javascriptRuntimeReconciliationParsing.ts | 21 ++++--- .../javascriptRuntimeReconciliationResult.ts | 4 +- .../javascriptRuntimeReconciliationSchemas.ts | 4 +- .../javascriptRuntimeStaticCandidates.ts | 13 +++-- .../javascriptSemanticAnalysis.calls.test.ts | 6 +- .../javascriptSemanticAnalysis.fixture.ts | 6 ++ ...vascriptSemanticAnalysis.structure.test.ts | 15 ++--- .../javascriptSemanticCallResolution.ts | 8 ++- .../javascriptSemanticChildProcesses.ts | 4 +- .../javascript/javascriptSemanticCoverage.ts | 33 +++++++++++ .../javascriptSemanticDataEffects.ts | 4 +- .../javascriptSemanticFingerprints.ts | 4 +- .../javascript/javascriptSemanticGraph.ts | 50 ++++++++++------- src/domain/javascript/javascriptSemanticIr.ts | 18 ------ .../javascriptSemanticPrimitives.ts | 4 +- .../javascriptSemanticProjection.ts | 8 ++- .../javascript/javascriptSemanticPromises.ts | 4 +- .../javascriptSemanticProvenance.ts | 4 +- .../javascript/javascriptSemanticQuery.ts | 4 +- .../javascriptSemanticQueryIdentity.ts | 5 +- .../javascript/javascriptSemanticReturns.ts | 6 +- .../javascript/javascriptSemanticSlots.ts | 9 +-- .../javascript/javascriptSemanticValues.ts | 4 +- .../javascriptStaticAnalysisBundler.ts | 15 ++--- .../javascriptStaticAnalysisHelpers.ts | 23 ++++---- .../sourceToBundleComparison.test.ts | 4 +- .../javascript/sourceToBundleComparison.ts | 12 ++-- .../javascript/sourceToBundleSignals.ts | 14 +++-- src/domain/jsonShape.ts | 4 +- src/domain/localPath.ts | 4 -- .../managed/managedMemberComparisonMatch.ts | 14 ++--- src/domain/referenceSourceGraph.ts | 15 +++-- src/domain/referenceSourceImportParsing.ts | 5 +- src/domain/webCaptureDiff.ts | 56 ++++++++++--------- src/dotnet/ManagedStaticProviderMetadata.ts | 4 +- src/main/transport.ts | 7 +-- src/main/types.ts | 6 +- src/native/NativeMacOSProviderMetadata.ts | 4 +- src/reference/ReferenceSourceReaderEntries.ts | 6 +- src/reference/ReferenceSourceReaderFile.ts | 10 ++-- src/reference/ReferenceSourceReaderTypes.ts | 8 +-- .../ReferenceSourceReaderValidate.ts | 12 ++-- src/server/createServer.ts | 4 +- src/server/registerSessionStatusTool.ts | 4 +- src/server/registerSessionTools.ts | 10 ++-- src/server/sessionAvailabilityPolicy.ts | 8 +-- .../artifactDirectoryIdentity.test.ts | 10 ++-- .../process/cliEnvironmentSeam.test.ts | 2 +- .../javascriptArtifactReconstruction.test.ts | 20 +++++++ 119 files changed, 577 insertions(+), 629 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1acb82552..abfd2e301 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ See [docs/testing.md](docs/testing.md) for test scopes and verification lanes an ## Configuration & Environment Variables -Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep user-facing setup and provider configuration in [README.md](README.md) and the relevant guide under `docs/`; do not maintain a second environment-variable catalogue here. +Configuration is parsed and validated by `src/config/`. Keep user-facing setup and provider configuration in [README.md](README.md) and the relevant guide under `docs/`; do not maintain a second environment-variable catalogue here. ## Coding Style & Naming Conventions diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid index b02bfafbe..83c6e5cb2 100644 --- a/docs/architecture.mermaid +++ b/docs/architecture.mermaid @@ -58,7 +58,7 @@ graph TD end subgraph "Configuration" - CONFIG["src/config.ts + src/config/
Zod-validated env parsing
provider settings and origins"] + CONFIG["src/config/
Zod-validated env parsing
provider settings and origins"] end subgraph "Tests" diff --git a/scripts/verify/javascript/digests.mjs b/scripts/verify/javascript/digests.mjs index 98530b07b..03c61bd81 100644 --- a/scripts/verify/javascript/digests.mjs +++ b/scripts/verify/javascript/digests.mjs @@ -4,7 +4,7 @@ import assert from "node:assert/strict"; import { constants } from "node:buffer"; import { createHash } from "node:crypto"; -import { canonicalDigest } from "../../../dist/domain/comparisonSemantics.js"; +import { digestCanonicalValue } from "../../../dist/domain/canonicalDigest.js"; import { createEvidence, parseEvidence, @@ -23,7 +23,7 @@ for (let index = 0; index < count; index += 1) { expected.update(encodedLeaf); } expected.update("]}"); -assert.equal(canonicalDigest(value), expected.digest("hex")); +assert.equal(digestCanonicalValue(value), expected.digest("hex")); const evidence = createEvidence( undefined, diff --git a/src/application/BrowserScenarioEvidence.ts b/src/application/BrowserScenarioEvidence.ts index e2009b16e..5dae07c21 100644 --- a/src/application/BrowserScenarioEvidence.ts +++ b/src/application/BrowserScenarioEvidence.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "../domain/comparisonSemantics.js"; +import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import type { ProviderIdentity } from "./AnalysisProvider.js"; import type { BrowserScenario } from "../domain/browserScenario.js"; import type { BrowserScenarioCapture } from "../domain/browserScenarioCapture.js"; @@ -13,7 +13,7 @@ const browserScenarioParameters = ( scenario: BrowserScenario, ): EvidenceObservation["parameters"] => ({ ...jsonObjectSchema.parse(scenario), - scenario_sha256: canonicalDigest(scenario, "Browser scenario"), + scenario_sha256: digestCanonicalValue(scenario, "Browser scenario"), }); /** Create Evidence without retaining resolved scenario secret values. */ diff --git a/src/application/DirectAnalysisStatus.ts b/src/application/DirectAnalysisStatus.ts index 5548e61c4..cc6df7901 100644 --- a/src/application/DirectAnalysisStatus.ts +++ b/src/application/DirectAnalysisStatus.ts @@ -5,7 +5,8 @@ import { silentLogger } from "../logger.js"; import type { Logger } from "pino"; import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js"; -const runSessionStatus = async ( +/** Read target-free provider status and operation availability. */ +export const runSessionStatus = async ( dependencies: Pick< DirectAnalysisDependencies, "createBinarySession" | "readConfiguration" @@ -26,21 +27,3 @@ const runSessionStatus = async ( await session.close(); } }; - -/** List binary-session provider candidates and auxiliary operation availability. */ -export const runProviderStatus = ( - dependencies: Pick< - DirectAnalysisDependencies, - "createBinarySession" | "readConfiguration" - >, - logger: Logger = silentLogger, -) => runSessionStatus(dependencies, logger); - -/** List binary-session operation descriptors; this is not the full MCP catalog. */ -export const runCapabilityStatus = ( - dependencies: Pick< - DirectAnalysisDependencies, - "createBinarySession" | "readConfiguration" - >, - logger: Logger = silentLogger, -) => runSessionStatus(dependencies, logger); diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts index 2d5343a58..f88a1b5bf 100644 --- a/src/application/SetupClientConfiguration.ts +++ b/src/application/SetupClientConfiguration.ts @@ -26,13 +26,11 @@ import type { } from "./SetupTypes.js"; import type { SetupClient } from "./SupportedClients.js"; -const defaultCommand = (): readonly string[] => npxRegistrationCommand(); - /** Configure one supported client's native stdio MCP registration shape. */ export const configureClientConfiguration = ( client: SetupClient, environment: SetupProviderEnvironment = {}, - command: readonly string[] = defaultCommand(), + command: readonly string[] = npxRegistrationCommand(), ): Promise => { if (client.format === undefined || client.format === "unsupported") return Promise.resolve({ status: "failed", reason: "readback" }); diff --git a/src/application/javascript/ElectronActiveEvidence.ts b/src/application/javascript/ElectronActiveEvidence.ts index 9d8e199cb..489cd7caa 100644 --- a/src/application/javascript/ElectronActiveEvidence.ts +++ b/src/application/javascript/ElectronActiveEvidence.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "../../domain/comparisonSemantics.js"; +import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; import type { ProviderIdentity } from "../AnalysisProvider.js"; import type { Evidence, EvidenceObservation } from "../../domain/evidence.js"; import { createEvidence } from "../../domain/evidence.js"; @@ -44,6 +44,6 @@ const parameters = ( const scenario = jsonObjectSchema.parse(input); return { ...scenario, - scenario_sha256: canonicalDigest(scenario, "Electron scenario"), + scenario_sha256: digestCanonicalValue(scenario, "Electron scenario"), }; }; diff --git a/src/application/javascript/ElectronBoundaryAnalysis.ts b/src/application/javascript/ElectronBoundaryAnalysis.ts index c1f842317..a718f6ed0 100644 --- a/src/application/javascript/ElectronBoundaryAnalysis.ts +++ b/src/application/javascript/ElectronBoundaryAnalysis.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "../../domain/canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js"; import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; @@ -37,7 +37,7 @@ export const collectElectronIpcRecords = ( finding, })), ) - .sort((left, right) => compareCodePoints(left.key, right.key)); + .sort((left, right) => compareUnicodeCodePoints(left.key, right.key)); /** Match only exact literal channels and retain ambiguous matches separately. */ export const classifyElectronIpcPairings = ( diff --git a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts index 26d6b7b2e..0c39ce458 100644 --- a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts +++ b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts @@ -26,13 +26,11 @@ import { partialApplicationCoverage, } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import type { JavaScriptSemanticResourceLimit } from "../../domain/javascript/javascriptSemanticValueTypes.js"; -import { semanticCoverageResourceLimits } from "../../domain/javascript/javascriptSemanticCoverage.js"; import { - SEMANTIC_EXPRESSION_DEPTH_LIMIT, - SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT, - SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT, - semanticResourceLimitReason, -} from "../../domain/javascript/javascriptSemanticResourceLimits.js"; + semanticCoverageResourceLimits, + semanticResourceLimitCoverage, +} from "../../domain/javascript/javascriptSemanticCoverage.js"; +import { semanticResourceLimitReason } from "../../domain/javascript/javascriptSemanticResourceLimits.js"; import { addJavaScriptArtifactContainers, addJavaScriptArtifactFiles, @@ -138,7 +136,7 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => { return partialApplicationCoverage([], null); if (resourceLimits.length > 0) return partialApplicationCoverage( - resourceLimits.map(applicationGraphResourceLimit), + semanticResourceLimitCoverage(resourceLimits), null, ); if (unknownGap) return partialApplicationCoverage([], null); @@ -158,24 +156,6 @@ const semanticResourceLimits = ( ), ].sort(); -const applicationGraphResourceLimit = ( - resourceLimit: JavaScriptSemanticResourceLimit, -) => ({ - name: `javascript_semantic_${resourceLimit.replaceAll("-", "_")}`, - value: - resourceLimit === "primitive-candidates" - ? SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT - : resourceLimit === "primitive-bytes" - ? SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT - : SEMANTIC_EXPRESSION_DEPTH_LIMIT, - unit: - resourceLimit === "expression-depth" - ? ("depth" as const) - : resourceLimit === "primitive-bytes" - ? ("bytes" as const) - : ("items" as const), -}); - const graphLimitations = ( context: JavaScriptArtifactGraphContext, coverage: "complete" | "partial" | "unknown" | "unavailable", diff --git a/src/application/javascript/JavaScriptJsonModules.ts b/src/application/javascript/JavaScriptJsonModules.ts index 3733db5cb..ac18ea367 100644 --- a/src/application/javascript/JavaScriptJsonModules.ts +++ b/src/application/javascript/JavaScriptJsonModules.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "../../domain/canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import type { JavaScriptJsonModuleObservation } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; @@ -31,7 +31,7 @@ export const analyzeJavaScriptJsonModule = ( } const keys = typeof value === "object" && value !== null && !Array.isArray(value) - ? Object.keys(value).sort(compareCodePoints) + ? Object.keys(value).sort(compareUnicodeCodePoints) : []; const retained = keys; return { diff --git a/src/application/javascript/JavaScriptModuleRelationships.ts b/src/application/javascript/JavaScriptModuleRelationships.ts index 36868fb62..8e12f3beb 100644 --- a/src/application/javascript/JavaScriptModuleRelationships.ts +++ b/src/application/javascript/JavaScriptModuleRelationships.ts @@ -1,18 +1,16 @@ import { posix } from "node:path"; -import { compareCodePoints } from "../../domain/canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js"; import { completeApplicationCoverage, partialApplicationCoverage, } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; -import { semanticCoverageResourceLimits } from "../../domain/javascript/javascriptSemanticCoverage.js"; import { - SEMANTIC_EXPRESSION_DEPTH_LIMIT, - SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT, - SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT, - semanticResourceLimitReason, -} from "../../domain/javascript/javascriptSemanticResourceLimits.js"; + semanticCoverageResourceLimits, + semanticResourceLimitCoverage, +} from "../../domain/javascript/javascriptSemanticCoverage.js"; +import { semanticResourceLimitReason } from "../../domain/javascript/javascriptSemanticResourceLimits.js"; import type { JavaScriptModuleOrigin, JavaScriptSemanticModuleLink, @@ -81,7 +79,7 @@ export const addJavaScriptSourceModules = ( .flatMap(({ exportedName }) => exportedName === null ? [] : [exportedName], ) - .sort(compareCodePoints); + .sort(compareUnicodeCodePoints); const retainedExports = [...new Set(exports)]; const source = context.accumulator.addNode({ kind: "javascript-module", @@ -408,21 +406,7 @@ const semanticCoverage = ( if (semantic.ir.coverage.status === "complete" && resourceLimits.length === 0) return completeApplicationCoverage(); return partialApplicationCoverage( - resourceLimits.map((resourceLimit) => ({ - name: `javascript_semantic_${resourceLimit.replaceAll("-", "_")}`, - value: - resourceLimit === "primitive-candidates" - ? SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT - : resourceLimit === "primitive-bytes" - ? SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT - : SEMANTIC_EXPRESSION_DEPTH_LIMIT, - unit: - resourceLimit === "expression-depth" - ? ("depth" as const) - : resourceLimit === "primitive-bytes" - ? ("bytes" as const) - : ("items" as const), - })), + semanticResourceLimitCoverage(resourceLimits), semantic.ir.coverage.omittedCount, ); }; diff --git a/src/application/javascript/JavaScriptSemanticGraphProjection.ts b/src/application/javascript/JavaScriptSemanticGraphProjection.ts index b83f2e970..182a67612 100644 --- a/src/application/javascript/JavaScriptSemanticGraphProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphProjection.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "../../domain/canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js"; @@ -65,7 +65,7 @@ export const createSemanticCallableOwnerLookup = ( const rank = best === undefined ? 1 - : compareCodePoints(candidate.priority, best.priority) || + : compareUnicodeCodePoints(candidate.priority, best.priority) || best.order - candidate.order; if (node !== undefined && rank > 0) { best = candidate; diff --git a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts index bb2bbd520..cb2596b28 100644 --- a/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts @@ -1,10 +1,12 @@ import { semanticContainer, semanticPropertyPointer, - type JavaScriptSemanticSlot, } from "../../domain/javascript/javascriptSemanticSlots.js"; import type { JavaScriptSemanticBinding } from "../../domain/javascript/javascriptSemanticIr.js"; -import type { JavaScriptSemanticValue } from "../../domain/javascript/javascriptSemanticValueTypes.js"; +import type { + JavaScriptSemanticProperty, + JavaScriptSemanticValue, +} from "../../domain/javascript/javascriptSemanticValueTypes.js"; import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js"; import { retainSemanticGraphNode, @@ -155,7 +157,7 @@ export const semanticPropertySlot = ( context: SemanticFlowProjectionContext, objectBindingId: string, path: readonly string[], - fact: JavaScriptSemanticSlot, + fact: JavaScriptSemanticProperty, ) => retainSemanticGraphNode(context.state, context.file, { kind: "property-slot", diff --git a/src/artifacts/ArtifactProviderMetadata.ts b/src/artifacts/ArtifactProviderMetadata.ts index 3e350508e..aa36eeaf4 100644 --- a/src/artifacts/ArtifactProviderMetadata.ts +++ b/src/artifacts/ArtifactProviderMetadata.ts @@ -9,8 +9,6 @@ import { ARTIFACT_ANALYSIS_OPERATIONS } from "../contracts/artifactToolContracts export const ARTIFACT_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze( ARTIFACT_GRAPH_PROVIDER, ); -const IDENTITY = ARTIFACT_PROVIDER_IDENTITY; - /** Declare artifact coverage without constructing readers or acquiring resources. */ export const artifactCapabilities = ( platform: NodeJS.Platform = process.platform, @@ -18,7 +16,7 @@ export const artifactCapabilities = ( Object.freeze( ARTIFACT_ANALYSIS_OPERATIONS.map((operation) => { const common = { - provider: IDENTITY, + provider: ARTIFACT_PROVIDER_IDENTITY, operation, effects: Object.freeze({ mutatesArtifact: false, diff --git a/src/artifacts/DirectoryArtifactReader.ts b/src/artifacts/DirectoryArtifactReader.ts index 1446294b8..673a84d37 100644 --- a/src/artifacts/DirectoryArtifactReader.ts +++ b/src/artifacts/DirectoryArtifactReader.ts @@ -3,7 +3,7 @@ import { lstat, open, opendir, realpath } from "node:fs/promises"; import { isAbsolute, join, relative, sep } from "node:path"; import type { Readable } from "node:stream"; -import { compareCodePoints } from "../domain/canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { ArtifactReaderFailure, @@ -49,7 +49,7 @@ export class DirectoryArtifactReader implements ArtifactReader { await withDirectoryIoContext("inspect directory", directory, () => assertContainedDirectory(root, directory), ); - children.sort(compareCodePoints); + children.sort(compareUnicodeCodePoints); const directories: string[] = []; for (const name of children) { abortIfNeeded(signal); diff --git a/src/artifacts/extraction/ArtifactExtraction.ts b/src/artifacts/extraction/ArtifactExtraction.ts index dcd933e7e..b87beb09a 100644 --- a/src/artifacts/extraction/ArtifactExtraction.ts +++ b/src/artifacts/extraction/ArtifactExtraction.ts @@ -1,7 +1,7 @@ import { lstat, realpath } from "node:fs/promises"; -import { compareCodePoints } from "../../domain/canonicalOrdering.js"; -import { canonicalDigest } from "../../domain/comparisonSemantics.js"; +import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; +import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; import { AsarArtifactReader } from "../AsarArtifactReader.js"; import { ArtifactPathRegistry, @@ -178,7 +178,7 @@ const materializeSelection = async ({ await reader.close(); readerClosed = true; extracted.sort((left, right) => - compareCodePoints(left.relative_path, right.relative_path), + compareUnicodeCodePoints(left.relative_path, right.relative_path), ); const result = createExtractionResult( input, @@ -245,15 +245,15 @@ const createExtractionResult = ( source_manifest_id: inventory.manifest.manifest_id, selected_occurrence_ids: selected .map(({ occurrence }) => occurrence.occurrence_id) - .sort(compareCodePoints), - files_sha256: canonicalDigest(extracted, "Artifact"), + .sort(compareUnicodeCodePoints), + files_sha256: digestCanonicalValue(extracted, "Artifact"), output_root_alias: "$OUTPUT_ROOT" as const, }; return artifactExtractionResultSchema.parse({ manifest: inventory.manifest, extraction_manifest: { ...extractionSemantic, - extraction_id: `aex_${canonicalDigest(extractionSemantic, "Artifact")}`, + extraction_id: `aex_${digestCanonicalValue(extractionSemantic, "Artifact")}`, }, output_root: input.outputRoot, artifacts: extracted, diff --git a/src/artifacts/inventory/ArtifactGraphConstruction.ts b/src/artifacts/inventory/ArtifactGraphConstruction.ts index fd3b0dad3..55d391017 100644 --- a/src/artifacts/inventory/ArtifactGraphConstruction.ts +++ b/src/artifacts/inventory/ArtifactGraphConstruction.ts @@ -1,5 +1,5 @@ -import { compareCodePoints } from "../../domain/canonicalOrdering.js"; -import { canonicalDigest } from "../../domain/comparisonSemantics.js"; +import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; +import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; import { artifactEdgeId, artifactIdForContent, @@ -52,7 +52,7 @@ export const createOccurrence = ( path: string, parent: string | null, ): MutableOccurrence => ({ - occurrence_id: `occ_${canonicalDigest({ path, kind: entry.kind }, "Artifact")}`, + occurrence_id: `occ_${digestCanonicalValue({ path, kind: entry.kind }, "Artifact")}`, artifact_id: null, parent_occurrence_id: parent, logical_path: path, @@ -80,8 +80,6 @@ export const createOccurrence = ( }); /** Compare exact child names independently of host locale and traversal order. */ -export const compareDirectoryChildNames = compareCodePoints; - export const materializeDirectoryNodes = ( occurrences: MutableOccurrence[], nodes: Map, @@ -107,9 +105,9 @@ export const materializeDirectoryNodes = ( artifact_id, entry_kind, })) - .sort((left, right) => compareDirectoryChildNames(left.name, right.name)); + .sort((left, right) => compareUnicodeCodePoints(left.name, right.name)); const node = createArtifactNode({ - sha256: canonicalDigest({ kind: "directory", children }, "Artifact"), + sha256: digestCanonicalValue({ kind: "directory", children }, "Artifact"), size: 0, format: "directory", contentState: "virtual", @@ -131,7 +129,7 @@ export const createRootNode = (input: { createArtifactNode({ sha256: input.digest?.sha256 ?? - canonicalDigest( + digestCanonicalValue( { kind: "directory-root", children: input.occurrences @@ -140,7 +138,7 @@ export const createRootNode = (input: { artifact_id, })) .sort((left, right) => - compareDirectoryChildNames(left.logical_path, right.logical_path), + compareUnicodeCodePoints(left.logical_path, right.logical_path), ), }, "Artifact", diff --git a/src/artifacts/inventory/scanCanonical.ts b/src/artifacts/inventory/scanCanonical.ts index 8bf1c1a35..da046f18b 100644 --- a/src/artifacts/inventory/scanCanonical.ts +++ b/src/artifacts/inventory/scanCanonical.ts @@ -1,5 +1,5 @@ import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js"; -import { compareCodePoints } from "../../domain/canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import { createReadStream } from "node:fs"; import { lstat } from "node:fs/promises"; @@ -253,18 +253,20 @@ const verifyRootDigest = async ( const sortNodes = (nodes: ArtifactNode[]): ArtifactNode[] => nodes.sort((left, right) => - compareCodePoints(left.artifact_id, right.artifact_id), + compareUnicodeCodePoints(left.artifact_id, right.artifact_id), ); const sortOccurrences = ( occurrences: MutableOccurrence[], ): MutableOccurrence[] => occurrences.sort((left, right) => - compareCodePoints(left.logical_path, right.logical_path), + compareUnicodeCodePoints(left.logical_path, right.logical_path), ); const sortEdges = (edges: T[]): T[] => - edges.sort((left, right) => compareCodePoints(left.edge_id, right.edge_id)); + edges.sort((left, right) => + compareUnicodeCodePoints(left.edge_id, right.edge_id), + ); const buildManifest = ({ rootNode, diff --git a/src/artifacts/javascript/JavaScriptArtifactFiles.ts b/src/artifacts/javascript/JavaScriptArtifactFiles.ts index 1e5a4e5c9..a726fd88a 100644 --- a/src/artifacts/javascript/JavaScriptArtifactFiles.ts +++ b/src/artifacts/javascript/JavaScriptArtifactFiles.ts @@ -2,7 +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 { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import { AsarArtifactReader } from "../AsarArtifactReader.js"; import { ArtifactPathRegistry, @@ -84,13 +84,13 @@ export const readJavaScriptArtifactFiles = async ( }; await visitReader(reader, "", snapshot.manifest.root_sha256, context); const files = context.files.sort((left, right) => - compareCodePoints(left.path, right.path), + compareUnicodeCodePoints(left.path, right.path), ); assertExpectedFilesWereVisited(expected, files); return { files, containers: context.containers.sort((left, right) => - compareCodePoints(left.path, right.path), + compareUnicodeCodePoints(left.path, right.path), ), text_bytes_read: context.textBytes, invalid_utf8_files: context.invalidUtf8, diff --git a/src/browser/CdpBrowserProvider.ts b/src/browser/CdpBrowserProvider.ts index a527b2abf..a97ef7358 100644 --- a/src/browser/CdpBrowserProvider.ts +++ b/src/browser/CdpBrowserProvider.ts @@ -67,13 +67,12 @@ import { captureCdpScreenshot } from "./CdpScreenshot.js"; import { comparePngScreenshots } from "./PngVisualDiff.js"; import { CDP_BROWSER_PROVIDER_IDENTITY } from "./providerIdentities.js"; -const IDENTITY = CDP_BROWSER_PROVIDER_IDENTITY; const CLEANUP_DOMAINS = ["Network", "Debugger", "Runtime", "Page"] as const; /** Passive browser observation through a selected user-owned CDP endpoint. */ export class CdpBrowserProvider implements BrowserObservationPort { identity(): ProviderIdentity { - return IDENTITY; + return CDP_BROWSER_PROVIDER_IDENTITY; } async listTargets( @@ -440,4 +439,6 @@ const providerError = ( ? new BrowserObservationError(operation, cause.reason, { cause }) : cause instanceof AnalysisError ? cause - : new ProviderAdapterError(IDENTITY.id, operation, { cause }); + : new ProviderAdapterError(CDP_BROWSER_PROVIDER_IDENTITY.id, operation, { + cause, + }); diff --git a/src/browser/CdpCaptureStorageFingerprints.ts b/src/browser/CdpCaptureStorageFingerprints.ts index 476d75763..02a52b3be 100644 --- a/src/browser/CdpCaptureStorageFingerprints.ts +++ b/src/browser/CdpCaptureStorageFingerprints.ts @@ -4,7 +4,7 @@ import canonicalize from "canonicalize"; import type { InspectWebPageInput } from "../domain/browserObservation.js"; import type { WebPageInspection } from "../domain/browserObservationSchemas.js"; -import { compareCodePoints } from "../domain/canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { CdpConnection } from "./CdpConnection.js"; import { optionalCdpCommand } from "./CdpOptionalCommand.js"; import { @@ -85,7 +85,7 @@ export const captureStorageFingerprints = async ({ await addIndexedDb(run, origin, indexedDbNames); await addCaches(run, caches); state.items.sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( `${left.scope}:${left.identity_sha256}`, `${right.scope}:${right.identity_sha256}`, ), @@ -447,7 +447,7 @@ const stableValue = (value: unknown): unknown => { return Object.fromEntries( Object.entries(record) .filter(([key]) => key !== "objectId" && key !== "customPreview") - .sort(([left], [right]) => compareCodePoints(left, right)) + .sort(([left], [right]) => compareUnicodeCodePoints(left, right)) .map(([key, item]) => [key, stableValue(item)]), ); }; diff --git a/src/browser/CdpElectronInspection.ts b/src/browser/CdpElectronInspection.ts index 646ddf233..ca49edabd 100644 --- a/src/browser/CdpElectronInspection.ts +++ b/src/browser/CdpElectronInspection.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "../domain/comparisonSemantics.js"; +import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import type { ProgressReporter } from "../application/ProgressReporter.js"; import type { ElectronPageInspection, @@ -289,7 +289,7 @@ const captureResources = async ( ? null : Math.max(0, numberValue(resource.contentSize) ?? 0), }; - const resourceKey = `electron_resource_${canonicalDigest(item, "CDP capture")}`; + const resourceKey = `electron_resource_${digestCanonicalValue(item, "CDP capture")}`; if (seen.has(resourceKey)) continue; seen.add(resourceKey); resources.push({ diff --git a/src/browser/CdpElectronProvider.ts b/src/browser/CdpElectronProvider.ts index 39a88eb4f..8191a8ec2 100644 --- a/src/browser/CdpElectronProvider.ts +++ b/src/browser/CdpElectronProvider.ts @@ -30,12 +30,10 @@ import { inspectCdpElectronPage } from "./CdpElectronInspection.js"; import { authorizedElectronFile } from "./ElectronFileScope.js"; import { CDP_ELECTRON_PROVIDER_IDENTITY } from "./providerIdentities.js"; -const IDENTITY = CDP_ELECTRON_PROVIDER_IDENTITY; - /** Passive Electron provider for local file pages exposed by loopback CDP. */ export class CdpElectronProvider implements ElectronObservationPort { identity(): ProviderIdentity { - return IDENTITY; + return CDP_ELECTRON_PROVIDER_IDENTITY; } async listTargets( @@ -169,4 +167,6 @@ const providerError = ( ? new BrowserObservationError(operation, cause.reason, { cause }) : cause instanceof AnalysisError ? cause - : new ProviderAdapterError(IDENTITY.id, operation, { cause }); + : new ProviderAdapterError(CDP_ELECTRON_PROVIDER_IDENTITY.id, operation, { + cause, + }); diff --git a/src/browser/CdpElectronScripts.ts b/src/browser/CdpElectronScripts.ts index 2bf470af6..850cfd5f5 100644 --- a/src/browser/CdpElectronScripts.ts +++ b/src/browser/CdpElectronScripts.ts @@ -2,7 +2,7 @@ import type { ElectronPageInspection, InspectElectronPageInput, } from "../domain/javascript/electronObservation.js"; -import { canonicalDigest } from "../domain/comparisonSemantics.js"; +import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import { createWebTextArtifact } from "../domain/webContentArtifact.js"; import type { CdpConnection } from "./CdpConnection.js"; import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; @@ -49,7 +49,7 @@ export const captureElectronScripts = async ( is_module: script.isModule, language: script.language, }; - const scriptKey = `electron_script_${canonicalDigest(identity, "CDP capture")}`; + const scriptKey = `electron_script_${digestCanonicalValue(identity, "CDP capture")}`; if (seen.has(scriptKey)) continue; seen.add(scriptKey); total += 1; diff --git a/src/cli/utilityCommands.ts b/src/cli/utilityCommands.ts index 2aeef4228..9b8696bcd 100644 --- a/src/cli/utilityCommands.ts +++ b/src/cli/utilityCommands.ts @@ -22,10 +22,7 @@ export const registerUtilityCommands = ( cli: CliInstance, logger: Logger, environment: Readonly>, - analysis: Pick< - DirectAnalysis, - "runProviderAnalysis" | "runProviderStatus" | "runCapabilityStatus" - >, + analysis: Pick, ): void => { const { runProviderAnalysis } = analysis; registerCapabilityCommands(cli, logger, analysis); @@ -129,10 +126,7 @@ const invalidNativeUiScenarioInput = ( const registerCapabilityCommands = ( cli: CliInstance, logger: Logger, - { - runProviderStatus, - runCapabilityStatus, - }: Pick, + { runSessionStatus }: Pick, ): void => { for (const command of [ CLI_COMMANDS.capabilities, @@ -143,12 +137,7 @@ const registerCapabilityCommands = ( command === "capabilities" ? "List provider capabilities and side effects" : "List configured analysis providers", - run: () => - logCliCommand(logger, command, () => - command === CLI_COMMANDS.providers - ? runProviderStatus(logger) - : runCapabilityStatus(logger), - ), + run: () => logCliCommand(logger, command, () => runSessionStatus(logger)), }); } }; diff --git a/src/composition/directAnalysis.ts b/src/composition/directAnalysis.ts index 3a6b0d212..a5c6706ef 100644 --- a/src/composition/directAnalysis.ts +++ b/src/composition/directAnalysis.ts @@ -5,10 +5,7 @@ import { runProviderAnalysis as executeProviderAnalysis, runManagedProviderExecution as executeManagedProvider, } from "../application/DirectAnalysis.js"; -import { - runCapabilityStatus as executeCapabilityStatus, - runProviderStatus as executeProviderStatus, -} from "../application/DirectAnalysisStatus.js"; +import { runSessionStatus as executeSessionStatus } from "../application/DirectAnalysisStatus.js"; import type { DirectAnalysisDependencies } from "../application/DirectAnalysisDependencies.js"; import { createBinarySession, createManagedBinarySession } from "./binary.js"; @@ -26,8 +23,7 @@ export const createDirectAnalysis = ( return { runDirectAnalysis: executeDirectAnalysis.bind(undefined, dependencies), runProviderAnalysis: executeProviderAnalysis.bind(undefined, dependencies), - runProviderStatus: executeProviderStatus.bind(undefined, dependencies), - runCapabilityStatus: executeCapabilityStatus.bind(undefined, dependencies), + runSessionStatus: executeSessionStatus.bind(undefined, dependencies), }; }; diff --git a/src/contracts/artifactComparisonExample.ts b/src/contracts/artifactComparisonExample.ts index 48438d562..b0f4ac767 100644 --- a/src/contracts/artifactComparisonExample.ts +++ b/src/contracts/artifactComparisonExample.ts @@ -1,11 +1,11 @@ -import { canonicalDigest } from "../domain/comparisonSemantics.js"; +import { digestCanonicalValue } from "../domain/canonicalDigest.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_${canonicalDigest({ sha256: sha }, "Artifact example")}`; - const occurrenceId = `occ_${canonicalDigest({ root: artifactId }, "Artifact example")}`; + const artifactId = `art_${digestCanonicalValue({ sha256: sha }, "Artifact example")}`; + const occurrenceId = `occ_${digestCanonicalValue({ root: artifactId }, "Artifact example")}`; const nodes = [ { artifact_id: artifactId, @@ -36,7 +36,7 @@ const inventory = (digit: string) => { limitations: [], }, ]; - const graphSha256 = canonicalDigest( + const graphSha256 = digestCanonicalValue( { nodes, occurrences, @@ -47,7 +47,7 @@ const inventory = (digit: string) => { ); return jsonValueSchema.parse({ manifest: { - manifest_id: `agm_${canonicalDigest( + manifest_id: `agm_${digestCanonicalValue( { root_artifact_id: artifactId, graph_sha256: graphSha256, diff --git a/src/contracts/javascript/javascriptRuntimeReconciliationExample.ts b/src/contracts/javascript/javascriptRuntimeReconciliationExample.ts index 6f7f3bd0f..127e13799 100644 --- a/src/contracts/javascript/javascriptRuntimeReconciliationExample.ts +++ b/src/contracts/javascript/javascriptRuntimeReconciliationExample.ts @@ -110,7 +110,8 @@ const semanticGraph = createJavaScriptSemanticGraph({ limitations: ["Semantic relations are unavailable in this minimal example."], }); -const staticEvidence = createEvidence( +/** Minimal valid static application Evidence for graph workflow examples. */ +export const JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE = createEvidence( { path: inputPath, sha256: applicationSha256, format: "directory" }, { id: "rea-javascript-application", @@ -171,9 +172,6 @@ const staticEvidence = createEvidence( }, ); -/** Minimal valid static application Evidence for graph workflow examples. */ -export const JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE = staticEvidence; - const runtimeEvidence = createEvidence( undefined, { @@ -238,7 +236,7 @@ const runtimeEvidence = createEvidence( /** Compact valid Evidence pair used by the public reconciliation contract. */ export const JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE = { - static_layers: [{ analysis: staticEvidence }], + static_layers: [{ analysis: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE }], runtime_observations: [runtimeEvidence], }; diff --git a/src/contracts/managed/managedToolContracts.ts b/src/contracts/managed/managedToolContracts.ts index 8b46881b2..2ff8d36bb 100644 --- a/src/contracts/managed/managedToolContracts.ts +++ b/src/contracts/managed/managedToolContracts.ts @@ -1,18 +1,16 @@ import { z } from "zod"; +import { isAbsolute } from "node:path"; import type { ToolContract } from "../toolContractTypes.js"; import { managedOutputSchemas } from "../toolOutputSchemaGroups.js"; import { toolContractMetadata } from "../toolEffects.js"; import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js"; -import { - isAbsoluteLocalPath, - localPathStringSchema, -} from "../../domain/localPath.js"; +import { localPathStringSchema } from "../../domain/localPath.js"; /** Exact caller boundary for execution-free PE/CLI triage and identity. */ export const managedTargetInputSchema = z.strictObject({ path: localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "path must be an absolute local filesystem path (for example /tmp/Example.dll or C:\\Example.dll)", }) diff --git a/src/contracts/mobileApplicationGraphExamples.ts b/src/contracts/mobileApplicationGraphExamples.ts index 299777b4e..cb946d495 100644 --- a/src/contracts/mobileApplicationGraphExamples.ts +++ b/src/contracts/mobileApplicationGraphExamples.ts @@ -1,11 +1,11 @@ -import { canonicalDigest } from "../domain/comparisonSemantics.js"; +import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import { createEvidence } from "../domain/evidence.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; const inventory = (format: "apk" | "ipa" | "directory", digit: string) => { const sha = digit.repeat(64); - const artifactId = `art_${canonicalDigest({ sha256: sha }, "Artifact inventory")}`; - const occurrenceId = `occ_${canonicalDigest( + const artifactId = `art_${digestCanonicalValue({ sha256: sha }, "Artifact inventory")}`; + const occurrenceId = `occ_${digestCanonicalValue( { root: artifactId }, "Artifact inventory", )}`; @@ -39,7 +39,7 @@ const inventory = (format: "apk" | "ipa" | "directory", digit: string) => { limitations: [], }, ]; - const graphSha256 = canonicalDigest( + const graphSha256 = digestCanonicalValue( { nodes, occurrences, @@ -50,7 +50,7 @@ const inventory = (format: "apk" | "ipa" | "directory", digit: string) => { ); return jsonValueSchema.parse({ manifest: { - manifest_id: `agm_${canonicalDigest( + manifest_id: `agm_${digestCanonicalValue( { root_artifact_id: artifactId, graph_sha256: graphSha256, diff --git a/src/contracts/sessionLifecycleInputs.ts b/src/contracts/sessionLifecycleInputs.ts index a230c9a4f..ca11b03a0 100644 --- a/src/contracts/sessionLifecycleInputs.ts +++ b/src/contracts/sessionLifecycleInputs.ts @@ -1,14 +1,12 @@ +import { isAbsolute } from "node:path"; import { executableFormatHintSchema } from "../domain/dosCom.js"; -import { - isAbsoluteLocalPath, - localPathStringSchema, -} from "../domain/localPath.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { z } from "zod"; import { analysisProviderSelectorSchema } from "./providerSelection.js"; /** Caller-supplied analysis snapshot path; must name its destination absolutely. */ const snapshotPathSchema = localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "snapshot_path must be an absolute local filesystem path (for example /tmp/rea/analysis.json or C:\\rea\\analysis.json)", }) @@ -18,7 +16,7 @@ const snapshotPathSchema = localPathStringSchema /** Caller-supplied analysis target path; must name the target absolutely. */ const binaryTargetPathSchema = localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "path must be an absolute local filesystem path (for example /tmp/fixture.bin or C:\\analysis\\fixture.bin)", }) diff --git a/src/contracts/sessionStatusContract.ts b/src/contracts/sessionStatusContract.ts index 7b509b39a..68385a728 100644 --- a/src/contracts/sessionStatusContract.ts +++ b/src/contracts/sessionStatusContract.ts @@ -1,9 +1,7 @@ import { z } from "zod"; +import { isAbsolute } from "node:path"; -import { - isAbsoluteLocalPath, - localPathStringSchema, -} from "../domain/localPath.js"; +import { localPathStringSchema } from "../domain/localPath.js"; /** Optional expectations used to compare the live session with its caller. */ export const binarySessionInputSchema = z.strictObject({ @@ -13,7 +11,7 @@ export const binarySessionInputSchema = z.strictObject({ .regex(/^[a-f0-9]{64}$/u) .optional(), expected_server_path: localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "expected_server_path must be an absolute local filesystem path (for example /opt/rea/dist/main.js)", }) diff --git a/src/contracts/sessionToolContracts.ts b/src/contracts/sessionToolContracts.ts index 5436d445e..6c610b984 100644 --- a/src/contracts/sessionToolContracts.ts +++ b/src/contracts/sessionToolContracts.ts @@ -1,9 +1,7 @@ import { z } from "zod"; -import { - isAbsoluteLocalPath, - localPathStringSchema, -} from "../domain/localPath.js"; +import { isAbsolute } from "node:path"; +import { localPathStringSchema } from "../domain/localPath.js"; import { artifactComparisonInputSchema } from "../domain/artifactComparison.js"; import { bundleComparisonInputSchema } from "../domain/bundleComparison.js"; import { callPathInputSchema } from "../domain/callPathSchemas.js"; @@ -57,7 +55,7 @@ const session = < /** Session-owned Evidence bundle export options. */ export const exportEvidenceBundleInputSchema = z.strictObject({ path: localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "path must be an absolute local filesystem path (for example /tmp/rea/evidence.json or C:\\rea\\evidence.json)", }) diff --git a/src/contracts/sessionToolSchemas.ts b/src/contracts/sessionToolSchemas.ts index cac69149d..191acdb63 100644 --- a/src/contracts/sessionToolSchemas.ts +++ b/src/contracts/sessionToolSchemas.ts @@ -1,9 +1,7 @@ import { z } from "zod"; +import { isAbsolute } from "node:path"; -import { - isAbsoluteLocalPath, - localPathStringSchema, -} from "../domain/localPath.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { processTraceSpecificationSchema } from "../domain/process/processTraceSpecification.js"; import { evidenceSchema } from "../domain/evidence.js"; @@ -26,7 +24,7 @@ export const addressContextInputSchema = z.strictObject({ /** Session-owned Evidence bundle import options. */ export const importEvidenceBundleInputSchema = z.strictObject({ path: localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "path must be an absolute local filesystem path (for example /tmp/evidence.json or C:\\rea\\evidence.json)", }) diff --git a/src/domain/apple/appleApplication.test.ts b/src/domain/apple/appleApplication.test.ts index 8e7b9453b..5c846611a 100644 --- a/src/domain/apple/appleApplication.test.ts +++ b/src/domain/apple/appleApplication.test.ts @@ -3,8 +3,8 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { projectAppleApplication } from "./appleApplication.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; -import { canonicalDigest } from "../comparisonSemantics.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; import { createEvidence, type Evidence } from "../evidence.js"; import { jsonValueSchema } from "../jsonValue.js"; @@ -20,7 +20,7 @@ interface FixtureEntry { const sha = (text: string): string => createHash("sha256").update(text).digest("hex"); const artifactId = (sha256: string): string => - `art_${canonicalDigest({ sha256 }, "Artifact inventory")}`; + `art_${digestCanonicalValue({ sha256 }, "Artifact inventory")}`; /** Build one complete, content-addressed inventory page for pure projection tests. */ const inventoryEvidence = ( @@ -57,8 +57,8 @@ const inventoryEvidence = ( ) => ({ occurrence_id: path === "." - ? `occ_${canonicalDigest({ root: rootId }, "Artifact inventory")}` - : `occ_${canonicalDigest( + ? `occ_${digestCanonicalValue({ root: rootId }, "Artifact inventory")}` + : `occ_${digestCanonicalValue( { root_artifact_id: rootId, logical_path: path, entry_kind: kind }, "Artifact inventory", )}`, @@ -97,9 +97,9 @@ const inventoryEvidence = ( left.artifact_id.localeCompare(right.artifact_id), ); const sortedOccurrences = occurrences.sort((left, right) => - compareCodePoints(left.logical_path, right.logical_path), + compareUnicodeCodePoints(left.logical_path, right.logical_path), ); - const graphSha256 = canonicalDigest( + const graphSha256 = digestCanonicalValue( { nodes: sortedNodes, occurrences: sortedOccurrences, @@ -122,7 +122,7 @@ const inventoryEvidence = ( authority: "shipped-artifact", result: jsonValueSchema.parse({ manifest: { - manifest_id: `agm_${canonicalDigest( + manifest_id: `agm_${digestCanonicalValue( { root_artifact_id: rootId, graph_sha256: graphSha256 }, "Artifact inventory", )}`, diff --git a/src/domain/artifactComparison.ts b/src/domain/artifactComparison.ts index f02379fa6..3a1101182 100644 --- a/src/domain/artifactComparison.ts +++ b/src/domain/artifactComparison.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "./canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; import { z } from "zod"; import { @@ -147,7 +147,7 @@ export const compareArtifacts = ( right_manifest_id: right.inventory.manifest.manifest_id, summary, changes, - limitations: [...new Set(limitations)].sort(compareCodePoints), + limitations: [...new Set(limitations)].sort(compareUnicodeCodePoints), }); }; @@ -171,7 +171,7 @@ const compareOccurrences = ( const rightRelations = relationsByPath(right); const paths = [ ...new Set([...leftByPath.keys(), ...rightByPath.keys()]), - ].sort(compareCodePoints); + ].sort(compareUnicodeCodePoints); const output: ArtifactChange[] = []; for (const path of paths) { const leftOccurrence = leftByPath.get(path); @@ -370,7 +370,7 @@ const relationsByPath = ( } for (const values of output.values()) values.sort((left, right) => - compareCodePoints(canonicalJson(left), canonicalJson(right)), + compareUnicodeCodePoints(canonicalJson(left), canonicalJson(right)), ); return output; }; diff --git a/src/domain/artifactIdentity.ts b/src/domain/artifactIdentity.ts index f97cc419b..ad773de33 100644 --- a/src/domain/artifactIdentity.ts +++ b/src/domain/artifactIdentity.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "./comparisonSemantics.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; import type { ArtifactEdge, ArtifactNode, @@ -8,7 +8,7 @@ import type { /** Content-address an artifact independently of where it was observed. */ export const artifactIdForContent = (sha256: string): string => - `art_${canonicalDigest({ sha256 }, "Artifact")}`; + `art_${digestCanonicalValue({ sha256 }, "Artifact")}`; /** Identify an occurrence by its root and exact logical location. */ export const occurrenceIdForLocation = (input: { @@ -17,8 +17,8 @@ export const occurrenceIdForLocation = (input: { readonly entryKind: ArtifactOccurrence["entry_kind"]; }): string => input.logicalPath === "." - ? `occ_${canonicalDigest({ root: input.rootArtifactId }, "Artifact")}` - : `occ_${canonicalDigest( + ? `occ_${digestCanonicalValue({ root: input.rootArtifactId }, "Artifact")}` + : `occ_${digestCanonicalValue( { root_artifact_id: input.rootArtifactId, logical_path: input.logicalPath, @@ -34,7 +34,7 @@ export const artifactGraphDigest = (input: { readonly edges: readonly ArtifactEdge[]; readonly contradictions: readonly IntegrityContradiction[]; }): string => - canonicalDigest( + digestCanonicalValue( { nodes: input.nodes, occurrences: input.occurrences, @@ -49,7 +49,7 @@ export const artifactManifestId = ( rootArtifactId: string, graphSha256: string, ): string => - `agm_${canonicalDigest( + `agm_${digestCanonicalValue( { root_artifact_id: rootArtifactId, graph_sha256: graphSha256 }, "Artifact", )}`; @@ -61,7 +61,7 @@ export const artifactContradictionId = (input: { readonly declaredSha256: string; readonly observedSha256: string; }): string => - `ic_${canonicalDigest( + `ic_${digestCanonicalValue( { root_artifact_id: input.rootArtifactId, logical_path: input.logicalPath, @@ -81,4 +81,4 @@ export const artifactEdgeId = ( | "occurrence_id" | "logical_path" >, -): string => `edge_${canonicalDigest(semantic, "Artifact")}`; +): string => `edge_${digestCanonicalValue(semantic, "Artifact")}`; diff --git a/src/domain/artifactInspection.ts b/src/domain/artifactInspection.ts index 2c9238659..f4c197b9c 100644 --- a/src/domain/artifactInspection.ts +++ b/src/domain/artifactInspection.ts @@ -4,7 +4,7 @@ import { artifactInventoryResultSchema, type ArtifactInventoryResult, } from "./artifactGraph.js"; -import { canonicalDigest } from "./comparisonSemantics.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; import { uniqueSorted } from "./canonicalOrdering.js"; import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js"; import { jsonValueSchema } from "./jsonValue.js"; @@ -170,7 +170,7 @@ export const createArtifactInspection = ( }, substeps: [ { - substep_id: `ais_${canonicalDigest( + substep_id: `ais_${digestCanonicalValue( { operation: "inventory_artifact", evidence_id: inventoryEvidence.evidence_id, @@ -211,7 +211,7 @@ export const createArtifactInspection = ( }; return artifactInspectionResultSchema.parse({ ...semantic, - inspection_id: `ai_${canonicalDigest(semantic, "Artifact inspection")}`, + inspection_id: `ai_${digestCanonicalValue(semantic, "Artifact inspection")}`, }); }; @@ -258,7 +258,7 @@ const observation = ( evidence_id: evidenceId, }; return { - observation_id: `aio_${canonicalDigest(semantic, "Artifact inspection")}`, + observation_id: `aio_${digestCanonicalValue(semantic, "Artifact inspection")}`, ...semantic, }; }; @@ -277,7 +277,7 @@ const allRelationships = ( evidence_id: evidenceId, }; return { - relationship_id: `air_${canonicalDigest(semantic, "Artifact inspection")}`, + relationship_id: `air_${digestCanonicalValue(semantic, "Artifact inspection")}`, ...semantic, }; }); @@ -305,7 +305,7 @@ const allHypotheses = ( "Format classification proposes a bounded follow-up; it is not semantic proof.", }; return { - hypothesis_id: `aih_${canonicalDigest(semantic, "Artifact inspection")}`, + hypothesis_id: `aih_${digestCanonicalValue(semantic, "Artifact inspection")}`, ...semantic, }; }); @@ -324,7 +324,7 @@ const contradictions = ( evidence_id: evidenceId, }; return { - contradiction_id: `aic_${canonicalDigest(semantic, "Artifact inspection")}`, + contradiction_id: `aic_${digestCanonicalValue(semantic, "Artifact inspection")}`, ...semantic, }; }); @@ -418,7 +418,7 @@ const branch = ( evidence_id: evidenceId, }; return { - branch_id: `aib_${canonicalDigest(semantic, "Artifact inspection")}`, + branch_id: `aib_${digestCanonicalValue(semantic, "Artifact inspection")}`, ...semantic, }; }; diff --git a/src/domain/artifactInventoryEvidence.ts b/src/domain/artifactInventoryEvidence.ts index d24d8266f..a1c74794a 100644 --- a/src/domain/artifactInventoryEvidence.ts +++ b/src/domain/artifactInventoryEvidence.ts @@ -5,7 +5,7 @@ import { type ArtifactOccurrence, type IntegrityContradiction, } from "./artifactGraph.js"; -import { compareCodePoints } from "./canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; import { canonicalJson } from "./comparisonSemantics.js"; import { artifactContradictionId, @@ -163,13 +163,13 @@ const orderedInventory = ( }, ): InventorySet => { const nodes = [...values.nodes.values()].sort((left, right) => - compareCodePoints(left.artifact_id, right.artifact_id), + compareUnicodeCodePoints(left.artifact_id, right.artifact_id), ); const occurrences = [...values.occurrences.values()].sort((left, right) => - compareCodePoints(left.logical_path, right.logical_path), + compareUnicodeCodePoints(left.logical_path, right.logical_path), ); const edges = [...values.edges.values()].sort((left, right) => - compareCodePoints(left.edge_id, right.edge_id), + compareUnicodeCodePoints(left.edge_id, right.edge_id), ); return { manifest: first.manifest, @@ -179,7 +179,7 @@ const orderedInventory = ( integrityContradictions: first.integrity_contradictions, limitations: [ ...new Set(pages.flatMap(({ inventory }) => inventory.limitations)), - ].sort(compareCodePoints), + ].sort(compareUnicodeCodePoints), complete: nodes.length === first.manifest.node_count && occurrences.length === first.manifest.occurrence_count && diff --git a/src/domain/browserScenarioNormalization.ts b/src/domain/browserScenarioNormalization.ts index 08c889ab9..6af5fb2ac 100644 --- a/src/domain/browserScenarioNormalization.ts +++ b/src/domain/browserScenarioNormalization.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "./comparisonSemantics.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; import type { BrowserScenarioArtifactKind, @@ -30,7 +30,7 @@ export const commitBrowserScenarioNormalization = ( const commitment = { built_in_rules: [...BUILT_IN_RULES], rules }; return { ...commitment, - sha256: canonicalDigest(commitment, "Browser scenario"), + sha256: digestCanonicalValue(commitment, "Browser scenario"), }; }; @@ -40,7 +40,7 @@ export const digestNormalizedScenarioValue = ( artifact: BrowserScenarioArtifactKind, rules: readonly BrowserScenarioNormalizationRule[], ): string => - canonicalDigest(normalize(value, artifact, rules), "Browser scenario"); + digestCanonicalValue(normalize(value, artifact, rules), "Browser scenario"); const normalize = ( value: unknown, diff --git a/src/domain/browserScenarioValues.ts b/src/domain/browserScenarioValues.ts index dd47a0081..544d4c222 100644 --- a/src/domain/browserScenarioValues.ts +++ b/src/domain/browserScenarioValues.ts @@ -1,8 +1,9 @@ import { z } from "zod"; +import { isAbsolute } from "node:path"; import { IDENTIFIER_PATTERN } from "./stringPatterns.js"; -import { isAbsoluteLocalPath, localPathStringSchema } from "./localPath.js"; +import { localPathStringSchema } from "./localPath.js"; import { browserNetworkContentSelectionSchema } from "./browserNetworkEvidence.js"; import { @@ -12,13 +13,10 @@ import { export const scenarioIdentifierSchema = z.string().regex(IDENTIFIER_PATTERN); -const browserExecutablePathSchema = localPathStringSchema.refine( - isAbsoluteLocalPath, - { - message: - "executable_path must be an absolute local filesystem path (for example /opt/chromium/chrome or C:\\chromium\\chrome.exe)", - }, -); +const browserExecutablePathSchema = localPathStringSchema.refine(isAbsolute, { + message: + "executable_path must be an absolute local filesystem path (for example /opt/chromium/chrome or C:\\chromium\\chrome.exe)", +}); const browserScenarioBaseUrlSchema = z .string() diff --git a/src/domain/callPath.ts b/src/domain/callPath.ts index 68df8db68..bc833828f 100644 --- a/src/domain/callPath.ts +++ b/src/domain/callPath.ts @@ -157,9 +157,9 @@ const parseSnapshots = ( const snapshots = new Map(); for (const group of groups) { const snapshot = parseFunctionEvidence(group); - const address = normalizeAddress(snapshot.dossier.procedure.address); + const address = parseCallPathAddress(snapshot.dossier.procedure.address); for (const callee of snapshot.dossier.callees) - normalizeAddress(callee.address); + parseCallPathAddress(callee.address); if (snapshots.has(address)) throw new TypeError(`Duplicate function Evidence for ${address}`); snapshots.set(address, snapshot); @@ -196,7 +196,7 @@ const createGraph = ( for (const [address, snapshot] of snapshots) { graph.mergeNode(address); for (const callee of snapshot.dossier.callees) { - const calleeAddress = normalizeAddress(callee.address); + const calleeAddress = parseCallPathAddress(callee.address); graph.mergeNode(calleeAddress); graph.mergeDirectedEdge(address, calleeAddress); } @@ -371,5 +371,3 @@ const uniqueEvidence = (snapshots: Iterable): string[] => [ ...new Set([...snapshots].flatMap((snapshot) => snapshotLinks(snapshot))), ].sort((left, right) => left.localeCompare(right)); - -const normalizeAddress = (input: string): string => parseCallPathAddress(input); diff --git a/src/domain/canonicalOrdering.ts b/src/domain/canonicalOrdering.ts index 0675c081e..9ab153d34 100644 --- a/src/domain/canonicalOrdering.ts +++ b/src/domain/canonicalOrdering.ts @@ -1,20 +1,6 @@ import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; -/** - * 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. - * - * Ordering by code point differs from ordering by UTF-16 code unit for astral - * code points (above U+FFFF), whose lead surrogate is numerically below the - * BMP tail U+E000-U+FFFF. - */ -export const compareCodePoints = (left: string, right: string): number => - compareUnicodeCodePoints(left, right); - /** Deduplicate and sort strings with canonical code-point ordering. */ export const uniqueSorted = ( values: readonly Value[], -): Value[] => [...new Set(values)].sort(compareCodePoints); +): Value[] => [...new Set(values)].sort(compareUnicodeCodePoints); diff --git a/src/domain/comparisonSemantics.ts b/src/domain/comparisonSemantics.ts index 9c005d124..f697e6a86 100644 --- a/src/domain/comparisonSemantics.ts +++ b/src/domain/comparisonSemantics.ts @@ -1,7 +1,5 @@ import canonicalize from "canonicalize"; -import { digestCanonicalValue } from "./canonicalDigest.js"; - /** Whether an inventory can support a claim that an item is absent. */ export const absenceClaimable = (coverage: { readonly status: string; @@ -12,14 +10,6 @@ export const absenceClaimable = (coverage: { coverage.truncated !== true && (coverage.omitted_count === undefined || coverage.omitted_count === 0); -/** SHA-256 of canonical JSON for stable, provider-neutral comparison IDs. */ -export const canonicalDigest = ( - value: unknown, - context = "Comparison", -): string => { - return digestCanonicalValue(value, context); -}; - /** Canonical JSON used for deterministic tie breaking and stable digests. */ export const canonicalJson = ( value: unknown, diff --git a/src/domain/firmware/firmwareAnalysis.ts b/src/domain/firmware/firmwareAnalysis.ts index abea685e7..00f6675d0 100644 --- a/src/domain/firmware/firmwareAnalysis.ts +++ b/src/domain/firmware/firmwareAnalysis.ts @@ -1,13 +1,14 @@ import { z } from "zod"; +import { isAbsolute } from "node:path"; import { digestSchema } from "../digests.js"; import { jsonValueSchema } from "../jsonValue.js"; -import { isAbsoluteLocalPath, localPathStringSchema } from "../localPath.js"; +import { localPathStringSchema } from "../localPath.js"; const bytes = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER); const range = z.strictObject({ offset: bytes, length: bytes.positive() }); const target = { path: localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "path must be an absolute local filesystem path (for example /tmp/firmware.bin or C:\\firmware\\firmware.bin)", }) @@ -22,7 +23,7 @@ export const firmwareInputSchemas = { extract_firmware: z.strictObject({ ...target, output_directory: localPathStringSchema - .refine(isAbsoluteLocalPath, { + .refine(isAbsolute, { message: "output_directory must be an absolute local filesystem path", }) .describe("Absolute, absent output directory; parent must exist"), diff --git a/src/domain/functionComparison.test.ts b/src/domain/functionComparison.test.ts index f7fbc258a..665635c6b 100644 --- a/src/domain/functionComparison.test.ts +++ b/src/domain/functionComparison.test.ts @@ -5,8 +5,9 @@ import { enhancedInputSchemas } from "../contracts/enhancedInputs.js"; import { functionComparisonResultSchema } from "./functionComparisonSchemas.js"; import { createEvidence, type Evidence } from "./evidence.js"; import { functionDossierSchema } from "./hopperValues.js"; -import { canonicalDigest, canonicalJson } from "./comparisonSemantics.js"; -import { compareCodePoints } from "./canonicalOrdering.js"; +import { canonicalJson } from "./comparisonSemantics.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; +import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; import { jsonValueSchema } from "./jsonValue.js"; import { compareFunctions } from "./functionComparison.js"; @@ -228,7 +229,7 @@ describe("function collection collation ties", () => { const canonicalProjection = callees .map(({ name }) => ({ direction: "out", name })) .sort((left, right) => - compareCodePoints(canonicalJson(left), canonicalJson(right)), + compareUnicodeCodePoints(canonicalJson(left), canonicalJson(right)), ); const result = compareFunctions( observe("b", calling(callees)), @@ -239,8 +240,8 @@ describe("function collection collation ties", () => { ); expect(calls).toMatchObject({ status: "unchanged", - left_digest: canonicalDigest(canonicalProjection), - right_digest: canonicalDigest(canonicalProjection), + left_digest: digestCanonicalValue(canonicalProjection), + right_digest: digestCanonicalValue(canonicalProjection), }); }); }); diff --git a/src/domain/functionComparisonNormalization.ts b/src/domain/functionComparisonNormalization.ts index ea7b1defc..5bf43a972 100644 --- a/src/domain/functionComparisonNormalization.ts +++ b/src/domain/functionComparisonNormalization.ts @@ -1,5 +1,5 @@ import { canonicalJson } from "./comparisonSemantics.js"; -import { compareCodePoints } from "./canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; import type { FunctionSnapshot } from "./functionDossierEvidence.js"; import type { FunctionComparisonResult } from "./functionComparisonSchemas.js"; @@ -179,7 +179,7 @@ export const sorted = (values: readonly unknown[]): readonly unknown[] => value, json: canonicalJson(value, "Function normalization"), })) - .sort((left, right) => compareCodePoints(left.json, right.json)) + .sort((left, right) => compareUnicodeCodePoints(left.json, right.json)) .map(({ value }) => value); export const isAutoName = (name: string): boolean => diff --git a/src/domain/functionComparisonResults.ts b/src/domain/functionComparisonResults.ts index b605b947f..95465c3e7 100644 --- a/src/domain/functionComparisonResults.ts +++ b/src/domain/functionComparisonResults.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "./comparisonSemantics.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; import type { DimensionName, FunctionComparisonResult, @@ -87,4 +87,4 @@ export const summarize = (dimensions: readonly FunctionDimension[]) => ({ }); const digest = (value: unknown): string => - canonicalDigest(value, "Function comparison"); + digestCanonicalValue(value, "Function comparison"); diff --git a/src/domain/javascript/electronActiveObservation.ts b/src/domain/javascript/electronActiveObservation.ts index 0b4de6073..9f2e90b03 100644 --- a/src/domain/javascript/electronActiveObservation.ts +++ b/src/domain/javascript/electronActiveObservation.ts @@ -1,8 +1,9 @@ import { z } from "zod"; +import { isAbsolute } from "node:path"; -import { isAbsoluteLocalPath, localPathStringSchema } from "../localPath.js"; +import { localPathStringSchema } from "../localPath.js"; -const pathInputSchema = localPathStringSchema.refine(isAbsoluteLocalPath, { +const pathInputSchema = localPathStringSchema.refine(isAbsolute, { message: "Electron executable, application, and root paths must be absolute local filesystem paths (for example /Applications/Electron.app/Contents/MacOS/Electron)", }); diff --git a/src/domain/javascript/electronStaticAnalysisBrowser.ts b/src/domain/javascript/electronStaticAnalysisBrowser.ts index 465fcf683..f29e7cf8f 100644 --- a/src/domain/javascript/electronStaticAnalysisBrowser.ts +++ b/src/domain/javascript/electronStaticAnalysisBrowser.ts @@ -15,7 +15,7 @@ import { electronStaticValue, objectProperty, } from "./electronStaticAnalysisValues.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js"; import { argumentNode, @@ -150,7 +150,9 @@ const collectWebPreferences = ( if (value.status === "dynamic") unknown += 1; preferences.push({ name, value }); } - preferences.sort((left, right) => compareCodePoints(left.name, right.name)); + preferences.sort((left, right) => + compareUnicodeCodePoints(left.name, right.name), + ); return { status: "object-literal", preferences, diff --git a/src/domain/javascript/electronStaticAnalysisNative.ts b/src/domain/javascript/electronStaticAnalysisNative.ts index 541645c32..1aef90f1a 100644 --- a/src/domain/javascript/electronStaticAnalysisNative.ts +++ b/src/domain/javascript/electronStaticAnalysisNative.ts @@ -1,6 +1,6 @@ import * as t from "@babel/types"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { stripQueryAndFragment } from "../artifactPathSyntax.js"; import type { ElectronNativeAddonBindingFinding } from "./electronStaticAnalysisTypes.js"; import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js"; @@ -137,7 +137,7 @@ const addBinding = (input: NativeBindingInput): void => { const { context, node, specifier, kind } = input; const unique = [ ...new Set(input.members.filter((member) => member !== "")), - ].sort(compareCodePoints); + ].sort(compareUnicodeCodePoints); const members = unique.length === 0 ? ["*"] : unique; addLocatedFinding(context, { collection: context.accumulator.nativeAddonBindings, diff --git a/src/domain/javascript/electronStaticAnalysisValues.ts b/src/domain/javascript/electronStaticAnalysisValues.ts index 0311009f9..796c5a397 100644 --- a/src/domain/javascript/electronStaticAnalysisValues.ts +++ b/src/domain/javascript/electronStaticAnalysisValues.ts @@ -1,6 +1,6 @@ import * as t from "@babel/types"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { ElectronStaticValue } from "./electronStaticAnalysisTypes.js"; import { readExactJavaScriptLiteral, @@ -105,7 +105,7 @@ export const collectContextBridgeMembers = ( unknown: 0, }; collectMembersAt(node, "", 0, state); - const members = [...new Set(state.members)].sort(compareCodePoints); + const members = [...new Set(state.members)].sort(compareUnicodeCodePoints); return { status: "object-literal", members, diff --git a/src/domain/javascript/javascriptApplicationChangeGraph.ts b/src/domain/javascript/javascriptApplicationChangeGraph.ts index e21fb8b6c..ce0e136b3 100644 --- a/src/domain/javascript/javascriptApplicationChangeGraph.ts +++ b/src/domain/javascript/javascriptApplicationChangeGraph.ts @@ -9,7 +9,8 @@ import type { ApplicationNode, } from "./javascriptApplicationGraphSchemas.js"; import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { ApplicationVersionComparisonItem } from "./javascriptApplicationVersionComparisonSchemas.js"; interface ChangeGraphInput { @@ -218,7 +219,7 @@ const sourceCoverageLimits = (input: ChangeGraphInput) => (limit) => [`${limit.name}\0${limit.value}\0${limit.unit}`, limit], ), ).values(), - ].sort((left, right) => compareCodePoints(left.name, right.name)); + ].sort((left, right) => compareUnicodeCodePoints(left.name, right.name)); const combineOmittedCounts = (left: number | null, right: number | null) => left === null || right === null ? null : left + right; @@ -238,7 +239,7 @@ const mergeNodes = (nodes: readonly ApplicationNode[]): ApplicationNode[] => { .map((observation) => [observation.observation_id, observation]), ).values(), ].sort((left, right) => - compareCodePoints(left.observation_id, right.observation_id), + compareUnicodeCodePoints(left.observation_id, right.observation_id), ); return createJavaScriptApplicationNode({ kind: first.kind, @@ -250,11 +251,11 @@ const mergeNodes = (nodes: readonly ApplicationNode[]): ApplicationNode[] => { }); }); return merged.sort((left, right) => - compareCodePoints(left.node_id, right.node_id), + compareUnicodeCodePoints(left.node_id, right.node_id), ); }; 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), + (left, right) => compareUnicodeCodePoints(left.edge_id, right.edge_id), ); diff --git a/src/domain/javascript/javascriptApplicationGraph.ts b/src/domain/javascript/javascriptApplicationGraph.ts index 56c39dfb6..ba122e1f0 100644 --- a/src/domain/javascript/javascriptApplicationGraph.ts +++ b/src/domain/javascript/javascriptApplicationGraph.ts @@ -1,6 +1,8 @@ import { z } from "zod"; -import { canonicalDigest, canonicalJson } from "../comparisonSemantics.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { canonicalJson } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { freezeOwnedJsonSnapshotSteps } from "../immutableJson.js"; import type { @@ -37,7 +39,7 @@ const normalizeLimits = ( limits: ApplicationGraphEvidence["coverage"]["limits"], ): ApplicationGraphEvidence["coverage"]["limits"] => [...new Map(limits.map((limit) => [limitKey(limit), limit])).values()].sort( - (left, right) => compareCodePoints(limitKey(left), limitKey(right)), + (left, right) => compareUnicodeCodePoints(limitKey(left), limitKey(right)), ); const normalizeEvidence = ( @@ -65,7 +67,7 @@ const nodeSemantic = (node: Pick) => ({ }); const nodeId = (node: Pick): string => - `jag_node_${canonicalDigest(nodeSemantic(node), "JavaScript Application Graph")}`; + `jag_node_${digestCanonicalValue(nodeSemantic(node), "JavaScript Application Graph")}`; const observationSemantic = ( nodeIdentifier: string, @@ -82,14 +84,14 @@ const observationId = ( "observation_id" >, ): string => - `jag_observation_${canonicalDigest( + `jag_observation_${digestCanonicalValue( observationSemantic(nodeIdentifier, observation), )}`; type EdgeSemantic = Omit; const edgeId = (edge: EdgeSemantic): string => - `jag_edge_${canonicalDigest(edge, "JavaScript Application Graph")}`; + `jag_edge_${digestCanonicalValue(edge, "JavaScript Application Graph")}`; const sortedUniqueIssue = ( values: readonly string[], @@ -98,7 +100,9 @@ const sortedUniqueIssue = ( context: z.RefinementCtx, ): void => { for (let index = 1; index < values.length; index += 1) { - if (compareCodePoints(values[index - 1] ?? "", values[index] ?? "") < 0) + if ( + compareUnicodeCodePoints(values[index - 1] ?? "", values[index] ?? "") < 0 + ) continue; context.addIssue({ code: "custom", @@ -394,7 +398,7 @@ const checkGraphInvariants = ( const { graph_id: identifier, ...semantic } = graph; if ( identifier !== - `jag_${canonicalDigest(semantic, "JavaScript Application Graph")}` + `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}` ) context.addIssue({ code: "custom", @@ -432,7 +436,7 @@ export const createJavaScriptApplicationNode = ( }); }) .sort((left, right) => - compareCodePoints(left.observation_id, right.observation_id), + compareUnicodeCodePoints(left.observation_id, right.observation_id), ); return applicationNodeSchema.parse({ node_id: identifier, @@ -467,10 +471,10 @@ export const createJavaScriptApplicationGraph = ( ...parsed, root_node_ids: uniqueSorted(parsed.root_node_ids), nodes: [...parsed.nodes].sort((left, right) => - compareCodePoints(left.node_id, right.node_id), + compareUnicodeCodePoints(left.node_id, right.node_id), ), edges: [...parsed.edges].sort((left, right) => - compareCodePoints(left.edge_id, right.edge_id), + compareUnicodeCodePoints(left.edge_id, right.edge_id), ), coverage: { ...parsed.coverage, @@ -480,7 +484,7 @@ export const createJavaScriptApplicationGraph = ( }; return javascriptApplicationGraphSchema.parse({ ...semantic, - graph_id: `jag_${canonicalDigest(semantic, "JavaScript Application Graph")}`, + graph_id: `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}`, }); }; @@ -517,7 +521,7 @@ export const parseJavaScriptApplicationGraph = ( export const computeJavaScriptApplicationGraphSha256 = ( input: unknown, ): string => - canonicalDigest( + digestCanonicalValue( parseJavaScriptApplicationGraph(input), "JavaScript Application Graph", ); diff --git a/src/domain/javascript/javascriptApplicationVersionComparison.ts b/src/domain/javascript/javascriptApplicationVersionComparison.ts index dd340c298..50f82c335 100644 --- a/src/domain/javascript/javascriptApplicationVersionComparison.ts +++ b/src/domain/javascript/javascriptApplicationVersionComparison.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import type { Evidence } from "../evidence.js"; import { buildJavaScriptApplicationChangeGraph } from "./javascriptApplicationChangeGraph.js"; @@ -78,7 +78,7 @@ export const compareJavaScriptApplicationVersions = ( }; return applicationVersionComparisonResultSchema.parse({ ...semantic, - comparison_id: `javc_${canonicalDigest(semantic, "Application version comparison")}`, + comparison_id: `javc_${digestCanonicalValue(semantic, "Application version comparison")}`, }); }; diff --git a/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts b/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts index 49488de51..e30c74205 100644 --- a/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts +++ b/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts @@ -39,7 +39,6 @@ export const compareApplicationVersionsInputSchema = z } }); -const emptyCandidateNodesSchema = emptyArraySchema; const candidateNodesSchema = z.tuple([nodeIdSchema]).rest(nodeIdSchema); const exactMatchSchema = z.strictObject({ status: z.literal("matched"), @@ -49,22 +48,22 @@ const exactMatchSchema = z.strictObject({ "exact-module-source-digest", ]), confidence: z.literal("exact"), - candidate_left_node_ids: emptyCandidateNodesSchema, - candidate_right_node_ids: emptyCandidateNodesSchema, + candidate_left_node_ids: emptyArraySchema, + candidate_right_node_ids: emptyArraySchema, }); const sourceMapMatchSchema = z.strictObject({ status: z.literal("matched"), basis: z.literal("source-map-identity"), confidence: z.literal("high"), - candidate_left_node_ids: emptyCandidateNodesSchema, - candidate_right_node_ids: emptyCandidateNodesSchema, + candidate_left_node_ids: emptyArraySchema, + candidate_right_node_ids: emptyArraySchema, }); const inferredMatchSchema = z.strictObject({ status: z.literal("matched"), basis: z.enum(["structural-fingerprint", "semantic-key"]), confidence: z.literal("medium"), - candidate_left_node_ids: emptyCandidateNodesSchema, - candidate_right_node_ids: emptyCandidateNodesSchema, + candidate_left_node_ids: emptyArraySchema, + candidate_right_node_ids: emptyArraySchema, }); const matchedItemMatchSchema = z.union([ exactMatchSchema, @@ -75,14 +74,14 @@ const unmatchedItemMatchSchema = z.strictObject({ status: z.literal("unmatched"), basis: z.literal("none"), confidence: z.literal("unknown"), - candidate_left_node_ids: emptyCandidateNodesSchema, - candidate_right_node_ids: emptyCandidateNodesSchema, + candidate_left_node_ids: emptyArraySchema, + candidate_right_node_ids: emptyArraySchema, }); const leftAmbiguousMatchSchema = z.strictObject({ status: z.literal("ambiguous"), basis: z.literal("none"), confidence: z.literal("unknown"), - candidate_left_node_ids: emptyCandidateNodesSchema, + candidate_left_node_ids: emptyArraySchema, candidate_right_node_ids: candidateNodesSchema, }); const rightAmbiguousMatchSchema = z.strictObject({ @@ -90,7 +89,7 @@ const rightAmbiguousMatchSchema = z.strictObject({ basis: z.literal("none"), confidence: z.literal("unknown"), candidate_left_node_ids: candidateNodesSchema, - candidate_right_node_ids: emptyCandidateNodesSchema, + candidate_right_node_ids: emptyArraySchema, }); const comparisonItemContextShape = { diff --git a/src/domain/javascript/javascriptApplicationVersionItems.ts b/src/domain/javascript/javascriptApplicationVersionItems.ts index a7784c195..7e931904f 100644 --- a/src/domain/javascript/javascriptApplicationVersionItems.ts +++ b/src/domain/javascript/javascriptApplicationVersionItems.ts @@ -1,9 +1,6 @@ -import { - absenceClaimable, - canonicalDigest, - canonicalJson, -} from "../comparisonSemantics.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { absenceClaimable, canonicalJson } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { Evidence } from "../evidence.js"; import type { ApplicationEdge, @@ -96,7 +93,9 @@ export const classifyJavaScriptApplicationVersions = ( ...matching.pairs.map((pair) => matchedItem(pair, fullContext)), ...leftItems, ...rightItems, - ].sort((left, right) => compareCodePoints(left.item_id, right.item_id)), + ].sort((left, right) => + compareUnicodeCodePoints(left.item_id, right.item_id), + ), }; }; @@ -326,7 +325,7 @@ const locations = (node: ApplicationNode) => evidence.location.available ? evidence.location.value : evidence.location, ) .sort((left, right) => - compareCodePoints(canonicalJson(left), canonicalJson(right)), + compareUnicodeCodePoints(canonicalJson(left), canonicalJson(right)), ); const properties = (node: ApplicationNode) => @@ -337,7 +336,7 @@ const properties = (node: ApplicationNode) => source_map_reference, })) .sort((left, right) => - compareCodePoints(canonicalJson(left), canonicalJson(right)), + compareUnicodeCodePoints(canonicalJson(left), canonicalJson(right)), ); const relationshipSignature = ( @@ -371,7 +370,7 @@ const relationshipSignature = ( ]; return []; }); - return canonicalJson(signatures.sort(compareCodePoints)); + return canonicalJson(signatures.sort(compareUnicodeCodePoints)); }; interface EdgeSignatureContext { @@ -400,7 +399,7 @@ const itemEvidenceLinks = ( ...nativeLinks(left, context.leftNativeEvidence), ...nativeLinks(right, context.rightNativeEvidence), ]), - ].sort(compareCodePoints); + ].sort(compareUnicodeCodePoints); const nativeLinks = ( node: ApplicationNode | undefined, @@ -445,7 +444,7 @@ const itemWithId = ( semantic: Semantic, ): Semantic & { readonly item_id: string } => ({ ...semantic, - item_id: `javc_item_${canonicalDigest(semantic, "Application version item")}`, + item_id: `javc_item_${digestCanonicalValue(semantic, "Application version item")}`, }); const stringProperty = (node: ApplicationNode, key: string): string | null => { diff --git a/src/domain/javascript/javascriptApplicationVersionKeys.ts b/src/domain/javascript/javascriptApplicationVersionKeys.ts index e9536f3e0..134acd657 100644 --- a/src/domain/javascript/javascriptApplicationVersionKeys.ts +++ b/src/domain/javascript/javascriptApplicationVersionKeys.ts @@ -1,5 +1,5 @@ import { canonicalJson } from "../comparisonSemantics.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js"; import type { JsonValue } from "../jsonValue.js"; @@ -85,7 +85,7 @@ export const matchJavaScriptApplicationVersions = ( const rightIds = new Set(remainingRight.map(({ node_id: id }) => id)); return { pairs: pairs.sort((a, b) => - compareCodePoints( + compareUnicodeCodePoints( `${a.left.node_id}\0${a.right.node_id}`, `${b.left.node_id}\0${b.right.node_id}`, ), @@ -109,7 +109,7 @@ interface TierContext { const applyTier = (context: TierContext): void => { const leftByKey = nodesByKey(context.left.values(), context.tier.key); const rightByKey = nodesByKey(context.right.values(), context.tier.key); - for (const key of [...leftByKey.keys()].sort(compareCodePoints)) { + for (const key of [...leftByKey.keys()].sort(compareUnicodeCodePoints)) { const leftGroup = leftByKey.get(key) ?? []; const rightGroup = rightByKey.get(key) ?? []; if (rightGroup.length === 0) continue; @@ -174,7 +174,7 @@ const filteredCandidates = ( [...candidates.entries()].flatMap(([nodeId, values]) => { const filtered = [...values] .filter((value) => retained.has(value)) - .sort(compareCodePoints); + .sort(compareUnicodeCodePoints); return filtered.length === 0 ? [] : [[nodeId, filtered] as const]; }), ); @@ -309,10 +309,10 @@ const stringList = (value: unknown): string[] => Array.isArray(value) ? value .filter((item): item is string => typeof item === "string") - .sort(compareCodePoints) + .sort(compareUnicodeCodePoints) : []; const sortedNodes = (nodes: Iterable): ApplicationNode[] => [...nodes].sort((left, right) => - compareCodePoints(left.node_id, right.node_id), + compareUnicodeCodePoints(left.node_id, right.node_id), ); diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index fd12f6435..8447de0eb 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -4,7 +4,7 @@ import * as t from "@babel/types"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { propertyName } from "./javascriptAstValues.js"; import { stringValue } from "./javascriptStaticAnalysisHelpers.js"; @@ -37,7 +37,7 @@ export const collectJavaScriptExports = (node: t.Node): StaticExports => { }, }); return { - values: [...exports].sort(compareCodePoints), + values: [...exports].sort(compareUnicodeCodePoints), }; }; diff --git a/src/domain/javascript/javascriptExportShapeComparison.ts b/src/domain/javascript/javascriptExportShapeComparison.ts index 63cb22dd0..8fb493839 100644 --- a/src/domain/javascript/javascriptExportShapeComparison.ts +++ b/src/domain/javascript/javascriptExportShapeComparison.ts @@ -1,4 +1,5 @@ -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { canonicalExportShapeValue, digestExportShapeValue, @@ -107,7 +108,7 @@ const sortChanges = ( })) .sort( (left, right) => - compareCodePoints(left.sortKey, right.sortKey) || + compareUnicodeCodePoints(left.sortKey, right.sortKey) || left.index - right.index, ) .map(({ change }) => change); diff --git a/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts b/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts index 9e0671d2b..808066c4d 100644 --- a/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts +++ b/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts @@ -7,7 +7,6 @@ const evidenceIdSchema = prefixedDigestSchema("ev"); const graphIdSchema = prefixedDigestSchema("jag"); const nodeIdSchema = prefixedDigestSchema("jag_node"); const textSchema = z.string().min(1); -const selectorTextSchema = textSchema; const semanticPrimitiveSchema = z.union([ z.string(), z.number().finite(), @@ -59,10 +58,10 @@ export const compareJavaScriptExportShapesInputSchema = z .strictObject({ left: evidenceSchema, right: evidenceSchema, - left_module_path: selectorTextSchema, - left_export_name: selectorTextSchema, - right_module_path: selectorTextSchema, - right_export_name: selectorTextSchema, + left_module_path: textSchema, + left_export_name: textSchema, + right_module_path: textSchema, + right_export_name: textSchema, }) .superRefine((input, context) => { if (input.left.evidence_id === input.right.evidence_id) @@ -151,8 +150,8 @@ const projectedReturnShapeSchema = z export const projectedExportReturnShapesSchema = z .strictObject({ semantic_role: z.literal("export-return-shapes"), - module_path: selectorTextSchema, - exported_name: selectorTextSchema, + module_path: textSchema, + exported_name: textSchema, callable_id: textSchema, callable_kind: z.enum(["function", "class", "method"]), static_return_shapes: z.array(projectedReturnShapeSchema), @@ -179,8 +178,8 @@ export const projectedExportReturnShapesSchema = z const exportCandidateSchema = z.strictObject({ node_id: nodeIdSchema, - module_path: selectorTextSchema, - export_name: selectorTextSchema, + module_path: textSchema, + export_name: textSchema, matches_requested_module: z.boolean(), matches_requested_export: z.boolean(), }); @@ -188,8 +187,8 @@ const exportCandidateSchema = z.strictObject({ const selectorResultShape = { evidence_id: evidenceIdSchema, graph_id: graphIdSchema, - requested_module_path: selectorTextSchema, - requested_export_name: selectorTextSchema, + requested_module_path: textSchema, + requested_export_name: textSchema, candidates: z.array(exportCandidateSchema), omitted_candidates: z.number().int().min(0), }; @@ -238,7 +237,8 @@ const propertyPresenceSchema = z.enum([ "unknown-coverage", ]); -const comparisonChangeSchema = z.strictObject({ +/** One change row; presence is required even when values stay unknown. */ +export const javaScriptExportShapeComparisonChangeSchema = z.strictObject({ change_id: prefixedDigestSchema("jesc_change"), status: z.enum(["added", "removed", "changed", "unknown"]), path: jsonPointerSchema, @@ -255,10 +255,6 @@ const comparisonChangeSchema = z.strictObject({ limitations: z.array(textSchema), }); -/** One change row; presence is required even when values stay unknown. */ -export const javaScriptExportShapeComparisonChangeSchema = - comparisonChangeSchema; - const inventoryPropertyCoverageSchema = z.strictObject({ path: jsonPointerSchema, status: z.enum(["complete", "partial"]), @@ -286,7 +282,7 @@ export const javaScriptExportShapeComparisonResultSchema = z.strictObject({ changed: z.number().int().min(0), unknown: z.number().int().min(0), }), - changes: z.array(comparisonChangeSchema), + changes: z.array(javaScriptExportShapeComparisonChangeSchema), coverage: z.strictObject({ status: z.enum(["complete-within-inputs", "partial", "truncated"]), left_graph_status: z.enum([ @@ -326,7 +322,7 @@ export type JavaScriptExportShapeComparisonResult = z.output< typeof javaScriptExportShapeComparisonResultSchema >; export type JavaScriptExportShapeComparisonChange = z.output< - typeof comparisonChangeSchema + typeof javaScriptExportShapeComparisonChangeSchema >; export type JavaScriptExportShapePropertyInventory = z.output< typeof propertyInventorySchema diff --git a/src/domain/javascript/javascriptExportShapeSelection.ts b/src/domain/javascript/javascriptExportShapeSelection.ts index 22510f617..afc6862e7 100644 --- a/src/domain/javascript/javascriptExportShapeSelection.ts +++ b/src/domain/javascript/javascriptExportShapeSelection.ts @@ -5,7 +5,7 @@ import { type JavaScriptExportShapeComparisonResult, type ProjectedExportReturnShapes, } from "./javascriptExportShapeComparisonSchemas.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; type Shape = ProjectedExportReturnShapes["static_return_shapes"][number]; type SelectorResult = JavaScriptExportShapeComparisonResult["left"]; @@ -196,7 +196,7 @@ const exportCandidates = ( } } return [...candidates.values()].sort((left, right) => - compareCodePoints(candidateKey(left), candidateKey(right)), + compareUnicodeCodePoints(candidateKey(left), candidateKey(right)), ); }; diff --git a/src/domain/javascript/javascriptExportShapeVariants.ts b/src/domain/javascript/javascriptExportShapeVariants.ts index e9420daed..bedf57cbe 100644 --- a/src/domain/javascript/javascriptExportShapeVariants.ts +++ b/src/domain/javascript/javascriptExportShapeVariants.ts @@ -7,7 +7,8 @@ import type { JavaScriptExportShapeComparisonResult, JavaScriptExportShapePropertyInventory, } from "./javascriptExportShapeComparisonSchemas.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { SelectedJavaScriptExport, RetainedJavaScriptExportShapes, @@ -252,7 +253,7 @@ const compareDiscriminants = ( left: Discriminant, right: Discriminant, ): number => - compareCodePoints( + compareUnicodeCodePoints( canonicalExportShapeValue(left), canonicalExportShapeValue(right), ); diff --git a/src/domain/javascript/javascriptFeatureSeed.ts b/src/domain/javascript/javascriptFeatureSeed.ts index 25d34add6..534285068 100644 --- a/src/domain/javascript/javascriptFeatureSeed.ts +++ b/src/domain/javascript/javascriptFeatureSeed.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js"; import { featureSeedMatchMode, @@ -30,7 +30,7 @@ export const findApplicationFeatureSeeds = ( nodes .flatMap((node) => seedMatch(node, seed)) .sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( `${left.node_id}\0${left.basis}\0${left.field}`, `${right.node_id}\0${right.basis}\0${right.field}`, ), @@ -230,7 +230,9 @@ const propertyFields = ( continue; } if (current !== null && typeof current === "object") { - for (const key of Object.keys(current).sort(compareCodePoints).reverse()) + for (const key of Object.keys(current) + .sort(compareUnicodeCodePoints) + .reverse()) pending.push({ current: current[key] ?? null, path: `${path}.${key}`, diff --git a/src/domain/javascript/javascriptFeatureTrace.ts b/src/domain/javascript/javascriptFeatureTrace.ts index be492f943..e535080a9 100644 --- a/src/domain/javascript/javascriptFeatureTrace.ts +++ b/src/domain/javascript/javascriptFeatureTrace.ts @@ -1,5 +1,6 @@ -import { canonicalDigest } from "../comparisonSemantics.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { Evidence } from "../evidence.js"; import { createJavaScriptApplicationGraph, @@ -86,7 +87,7 @@ export const traceApplicationFeature = ( }; return applicationFeatureTraceResultSchema.parse({ ...semantic, - trace_id: `jatr_${canonicalDigest(semantic, "Feature trace")}`, + trace_id: `jatr_${digestCanonicalValue(semantic, "Feature trace")}`, }); }; @@ -126,7 +127,7 @@ const noMatchResult = ( }; return applicationFeatureTraceResultSchema.parse({ ...semantic, - trace_id: `jatr_${canonicalDigest(semantic, "Feature trace")}`, + trace_id: `jatr_${digestCanonicalValue(semantic, "Feature trace")}`, }); }; @@ -170,7 +171,9 @@ const terminalPaths = ( ) .map((node) => pathTo(node, traversal.predecessors, edgeById)) .filter((path): path is NonNullable => path !== null) - .sort((left, right) => compareCodePoints(left.path_id, right.path_id)); + .sort((left, right) => + compareUnicodeCodePoints(left.path_id, right.path_id), + ); }; const pathTo = ( @@ -207,7 +210,7 @@ const pathTo = ( }; return { ...semantic, - path_id: `jatp_${canonicalDigest(semantic, "Feature trace")}`, + path_id: `jatp_${digestCanonicalValue(semantic, "Feature trace")}`, }; }; diff --git a/src/domain/javascript/javascriptFeatureTraversal.ts b/src/domain/javascript/javascriptFeatureTraversal.ts index 2f7b7c891..748314ba4 100644 --- a/src/domain/javascript/javascriptFeatureTraversal.ts +++ b/src/domain/javascript/javascriptFeatureTraversal.ts @@ -3,7 +3,7 @@ import type { ApplicationNode, } from "./javascriptApplicationGraphSchemas.js"; import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; interface AdjacencyEntry { readonly edge: ApplicationEdge; @@ -53,9 +53,11 @@ export const traverseApplicationFeature = ( nodes: [...visited] .map((nodeId) => nodeById.get(nodeId)) .filter((node): node is ApplicationNode => node !== undefined) - .sort((left, right) => compareCodePoints(left.node_id, right.node_id)), + .sort((left, right) => + compareUnicodeCodePoints(left.node_id, right.node_id), + ), edges: [...retainedEdges.values()].sort((left, right) => - compareCodePoints(left.edge_id, right.edge_id), + compareUnicodeCodePoints(left.edge_id, right.edge_id), ), predecessors, }; @@ -80,7 +82,7 @@ const buildAdjacency = ( } for (const entries of adjacency.values()) entries.sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( `${left.edge.edge_id}\0${left.nextNodeId}`, `${right.edge.edge_id}\0${right.nextNodeId}`, ), diff --git a/src/domain/javascript/javascriptNativeHandoff.ts b/src/domain/javascript/javascriptNativeHandoff.ts index 69051254e..ec197695f 100644 --- a/src/domain/javascript/javascriptNativeHandoff.ts +++ b/src/domain/javascript/javascriptNativeHandoff.ts @@ -1,4 +1,4 @@ -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { Evidence } from "../evidence.js"; import { type ApplicationEdge, @@ -38,7 +38,7 @@ export const buildJavaScriptNativeHandoffs = ( providers: uniqueProviders(linked), evidence_ids: linked .map(({ evidence_id: id }) => id) - .sort(compareCodePoints), + .sort(compareUnicodeCodePoints), recommended_tools: [ "open_binary" as const, "binary_overview" as const, @@ -50,7 +50,7 @@ export const buildJavaScriptNativeHandoffs = ( ]; }) .sort((left, right) => - compareCodePoints(left.native_node_id, right.native_node_id), + compareUnicodeCodePoints(left.native_node_id, right.native_node_id), ); }; @@ -95,7 +95,9 @@ const nativeExportNodes = ( return [...new Set(ids)] .map((id) => nodeById.get(id)) .filter((node): node is ApplicationNode => node?.kind === "native-export") - .sort((left, right) => compareCodePoints(left.node_id, right.node_id)); + .sort((left, right) => + compareUnicodeCodePoints(left.node_id, right.node_id), + ); }; const requestedExports = (nodes: readonly ApplicationNode[]): string[] => { @@ -106,7 +108,7 @@ const requestedExports = (nodes: readonly ApplicationNode[]): string[] => { ...stringArray(observation.properties.members), ]), ); - return [...new Set(values)].sort(compareCodePoints); + return [...new Set(values)].sort(compareUnicodeCodePoints); }; const stringArray = (value: unknown): string[] => @@ -125,7 +127,7 @@ const uniqueProviders = ( ]), ).values(), ].sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( `${left.id}\0${left.name}\0${left.version ?? ""}`, `${right.id}\0${right.name}\0${right.version ?? ""}`, ), diff --git a/src/domain/javascript/javascriptRuntimeReconciliationActive.ts b/src/domain/javascript/javascriptRuntimeReconciliationActive.ts index 3dbd073c6..32ef65086 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationActive.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationActive.ts @@ -1,6 +1,6 @@ import { isAbsolute } from "node:path"; -import { canonicalDigest } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; import { isPathWithinRoot } from "../localPath.js"; import { z } from "zod"; @@ -44,7 +44,7 @@ export const parseActiveElectronCapture = ( kind: "electron-active", evidence, inspection: normalizeInspection(result), - captureSha256: canonicalDigest(result, "Runtime reconciliation"), + captureSha256: digestCanonicalValue(result, "Runtime reconciliation"), scriptsCompleteWithinScope: false, }; }; @@ -83,7 +83,7 @@ const normalizeInspection = ( readonly completeness: BrowserCompleteness; } => ({ target: { - target_id: `electron-active:${canonicalDigest(result.application.application_path, "Runtime reconciliation").slice(0, 32)}`, + target_id: `electron-active:${digestCanonicalValue(result.application.application_path, "Runtime reconciliation").slice(0, 32)}`, type: "electron-application", title: result.application.application_path, attached: true, diff --git a/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts b/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts index 1eb941ba7..7e7067804 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts @@ -11,7 +11,8 @@ import type { ParsedRuntimeCapture, ParsedStaticLayer, } from "./javascriptRuntimeReconciliationParsing.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { RuntimeProjection } from "./javascriptRuntimeReconciliationRuntime.js"; interface ReconciledGraphInput { @@ -93,12 +94,14 @@ const boundGraphContent = ( const allRoots = preferredRootIds(input); const rootNodeIds = allRoots; const sortedNodes = [...nodes].sort((left, right) => - compareCodePoints(left.node_id, right.node_id), + compareUnicodeCodePoints(left.node_id, right.node_id), ); const retainedNodes = sortedNodes; const retainedIds = new Set(retainedNodes.map(({ node_id: id }) => id)); const retainedEdges = [...edges] - .sort((left, right) => compareCodePoints(left.edge_id, right.edge_id)) + .sort((left, right) => + compareUnicodeCodePoints(left.edge_id, right.edge_id), + ) .filter( ({ source_node_id: source, target_node_id: target }) => retainedIds.has(source) && retainedIds.has(target), @@ -231,7 +234,7 @@ const mergeNodes = (nodes: readonly ApplicationNode[]): ApplicationNode[] => { .map((observation) => [observation.observation_id, observation]), ).values(), ].sort((left, right) => - compareCodePoints(left.observation_id, right.observation_id), + compareUnicodeCodePoints(left.observation_id, right.observation_id), ); return createJavaScriptApplicationNode({ kind: first.kind, diff --git a/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts b/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts index b94aa0955..96bbe2e59 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationMatching.ts @@ -1,9 +1,9 @@ import { createJavaScriptApplicationEdge } from "./javascriptApplicationGraph.js"; import type { ApplicationEdge } from "./javascriptApplicationGraphSchemas.js"; import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js"; -import { canonicalDigest } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { JavaScriptRuntimeReconciliationItem } from "./javascriptRuntimeReconciliationSchemas.js"; import type { RuntimeReconciliationEntity } from "./javascriptRuntimeReconciliationRuntime.js"; import { @@ -29,7 +29,7 @@ export const reconcileRuntimeEntities = (input: { reconcileEntity(entity, index, input.layers), ); const retained = evaluated.sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( left.item.reconciliation_id, right.item.reconciliation_id, ), @@ -177,7 +177,7 @@ const candidatesWithLocation = ( if (!located.has(key)) located.set(key, { candidate, mapping }); } return [...located.values()].sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( candidateKey(left.candidate), candidateKey(right.candidate), ), @@ -331,7 +331,7 @@ const createItem = ( candidate_static_nodes: candidateReferences.items, } satisfies Omit; return { - reconciliation_id: `jrr_item_${canonicalDigest(semantic, "Runtime reconciliation")}`, + reconciliation_id: `jrr_item_${digestCanonicalValue(semantic, "Runtime reconciliation")}`, ...semantic, }; }; diff --git a/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts index a339f7db6..4c4310341 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts @@ -22,8 +22,8 @@ import { type JavaScriptRuntimeObservation, type JavaScriptRuntimeTargetLocation, } from "./javascriptRuntimeObservation.js"; -import { canonicalDigest } from "../comparisonSemantics.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { parseEvidence, type Evidence } from "../evidence.js"; import { javascriptApplicationAnalysisResultSchema, @@ -140,7 +140,9 @@ export const parseStaticLayers = ( ): ParsedStaticLayer[] => layers .map((layer) => parseStaticLayer(layer)) - .sort((left, right) => compareCodePoints(left.layerId, right.layerId)); + .sort((left, right) => + compareUnicodeCodePoints(left.layerId, right.layerId), + ); /** Parse only supported passive web/Electron inspection Evidence. */ export const parseRuntimeCaptures = ( @@ -149,7 +151,10 @@ export const parseRuntimeCaptures = ( observations .map((observation) => parseRuntimeCapture(observation)) .sort((left, right) => - compareCodePoints(left.evidence.evidence_id, right.evidence.evidence_id), + compareUnicodeCodePoints( + left.evidence.evidence_id, + right.evidence.evidence_id, + ), ); const parseStaticLayer = (layer: StaticLayerInput): ParsedStaticLayer => { @@ -176,7 +181,7 @@ const parseStaticLayer = (layer: StaticLayerInput): ParsedStaticLayer => { "JavaScript application Evidence subject disagrees with its result", ); return { - layerId: `jrl_${canonicalDigest( + layerId: `jrl_${digestCanonicalValue( { role: layer.role, evidence_id: evidence.evidence_id, @@ -219,7 +224,7 @@ const parseRuntimeCapture = (input: Evidence): ParsedRuntimeCapture => { kind: "browser", evidence, inspection, - captureSha256: canonicalDigest(inspection, "Runtime reconciliation"), + captureSha256: digestCanonicalValue(inspection, "Runtime reconciliation"), scriptsCompleteWithinScope: scriptsComplete(inspection.completeness), }; } @@ -247,7 +252,7 @@ const parseRuntimeCapture = (input: Evidence): ParsedRuntimeCapture => { kind: "electron", evidence, inspection, - captureSha256: canonicalDigest(inspection, "Runtime reconciliation"), + captureSha256: digestCanonicalValue(inspection, "Runtime reconciliation"), scriptsCompleteWithinScope: scriptsComplete(inspection.completeness), }; } @@ -270,7 +275,7 @@ const parseRuntimeCapture = (input: Evidence): ParsedRuntimeCapture => { kind: "v8-inspector", evidence, inspection, - captureSha256: canonicalDigest(result, "Runtime reconciliation"), + captureSha256: digestCanonicalValue(result, "Runtime reconciliation"), scriptsCompleteWithinScope: false, }; } diff --git a/src/domain/javascript/javascriptRuntimeReconciliationResult.ts b/src/domain/javascript/javascriptRuntimeReconciliationResult.ts index e39166d81..a814deae1 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationResult.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationResult.ts @@ -1,5 +1,5 @@ import { type JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; -import { canonicalDigest } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import type { StaticLoadStateProjection } from "./javascriptRuntimeLoadState.js"; import type { RuntimeMatchingProjection } from "./javascriptRuntimeReconciliationMatching.js"; @@ -49,7 +49,7 @@ export const createJavaScriptRuntimeReconciliationResult = ( limitations: reconciliationLimitations(completion), }; return javascriptRuntimeReconciliationResultSchema.parse({ - reconciliation_id: `jrr_${canonicalDigest(semantic, "Runtime reconciliation")}`, + reconciliation_id: `jrr_${digestCanonicalValue(semantic, "Runtime reconciliation")}`, ...semantic, }); }; diff --git a/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts b/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts index 38f606b4d..e60ecb784 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { evidenceSchema } from "../evidence.js"; import { javascriptRuntimeUnresolvedLocationSchema } from "./javascriptRuntimeObservation.js"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; @@ -85,7 +85,7 @@ function normalizeRuntimeMappings( mappings.map((mapping) => [runtimeMappingKey(mapping), mapping]), ).values(), ].sort((left, right) => - compareCodePoints(runtimeMappingKey(left), runtimeMappingKey(right)), + compareUnicodeCodePoints(runtimeMappingKey(left), runtimeMappingKey(right)), ); } diff --git a/src/domain/javascript/javascriptRuntimeStaticCandidates.ts b/src/domain/javascript/javascriptRuntimeStaticCandidates.ts index 641f8c1e4..64a47d050 100644 --- a/src/domain/javascript/javascriptRuntimeStaticCandidates.ts +++ b/src/domain/javascript/javascriptRuntimeStaticCandidates.ts @@ -2,7 +2,8 @@ import { posix, relative, win32 } from "node:path"; import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js"; import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { ParsedStaticLayer } from "./javascriptRuntimeReconciliationParsing.js"; import type { RuntimeReconciliationEntity } from "./javascriptRuntimeReconciliationRuntime.js"; @@ -39,8 +40,8 @@ export const collectStaticRuntimeCandidates = ( .flatMap((layer) => candidatesForLayer(layer)) .sort( (left, right) => - compareCodePoints(left.layer.layerId, right.layer.layerId) || - compareCodePoints(left.node.node_id, right.node.node_id), + compareUnicodeCodePoints(left.layer.layerId, right.layer.layerId) || + compareUnicodeCodePoints(left.node.node_id, right.node.node_id), ); /** Translate one already-authorized runtime location into artifact paths. */ @@ -284,7 +285,7 @@ const activeArtifactPrefixes = ( ) ) prefixes.add(mapping.artifact_prefix); - return [...prefixes].sort(compareCodePoints); + return [...prefixes].sort(compareUnicodeCodePoints); }; const pathWithinPrefix = (path: string, prefix: string): boolean => @@ -315,6 +316,6 @@ const uniqueMappedPaths = ( ).values(), ].sort( (left, right) => - compareCodePoints(left.path, right.path) || - compareCodePoints(left.basis, right.basis), + compareUnicodeCodePoints(left.path, right.path) || + compareUnicodeCodePoints(left.basis, right.basis), ); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts index 880aae06c..0fd5aa194 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.calls.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; -import { semanticBinding } from "./javascriptSemanticIr.js"; import { + bindingById, onlyCallable, topLevelBinding, } from "./javascriptSemanticAnalysis.fixture.js"; @@ -39,7 +39,7 @@ describe("JavaScript semantic analysis: calls 1", () => { .filter(({ callSiteId }) => callSiteId === call.callSiteId) .map(({ argumentIndex, parameterBindingId }) => ({ argumentIndex, - parameter: semanticBinding(ir, parameterBindingId)?.name, + parameter: bindingById(ir, parameterBindingId)?.name, })), ).toEqual([ { argumentIndex: 0, parameter: "value" }, @@ -309,7 +309,7 @@ describe("JavaScript semantic rest parameter flow", () => { expect( ir.argumentFlows.map(({ argumentIndex, parameterBindingId }) => [ argumentIndex, - semanticBinding(ir, parameterBindingId)?.name, + bindingById(ir, parameterBindingId)?.name, ]), ).toEqual(expected); }, diff --git a/src/domain/javascript/javascriptSemanticAnalysis.fixture.ts b/src/domain/javascript/javascriptSemanticAnalysis.fixture.ts index 4d58866b6..92be0da04 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.fixture.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.fixture.ts @@ -17,6 +17,12 @@ export const bindingsNamed = ( ): JavaScriptSemanticBinding[] => ir.bindings.filter(({ name: candidate }) => candidate === name); +export const bindingById = ( + ir: JavaScriptSemanticIr, + bindingId: string, +): JavaScriptSemanticBinding | undefined => + ir.bindings.find(({ bindingId: candidate }) => candidate === bindingId); + export const onlyBinding = ( ir: JavaScriptSemanticIr, name: string, diff --git a/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts index c9fb833ab..ba0644056 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts @@ -1,12 +1,9 @@ import { describe, expect, it } from "vitest"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; -import { - semanticBinding, - semanticReferenceAt, -} from "./javascriptSemanticIr.js"; import { programScope, + bindingById, bindingsNamed, onlyBinding, topLevelBinding, @@ -88,12 +85,12 @@ describe("JavaScript semantic analysis: structure 1", () => { ); if (shadowReference === undefined || shadowReference.bindingId === null) throw new Error("Missing shadow reference"); - expect(semanticBinding(ir, shadowReference.bindingId)).toEqual(shadow); + expect(bindingById(ir, shadowReference.bindingId)).toEqual(shadow); expect( - semanticReferenceAt( - ir, - shadowReference.location.start.line, - shadowReference.location.start.column, + ir.references.find( + ({ location }) => + location.start.line === shadowReference.location.start.line && + location.start.column === shadowReference.location.start.column, ), ).toEqual(shadowReference); }); diff --git a/src/domain/javascript/javascriptSemanticCallResolution.ts b/src/domain/javascript/javascriptSemanticCallResolution.ts index 26b270ab1..246472301 100644 --- a/src/domain/javascript/javascriptSemanticCallResolution.ts +++ b/src/domain/javascript/javascriptSemanticCallResolution.ts @@ -4,7 +4,7 @@ import type { JavaScriptSemanticArgumentFlow, JavaScriptSemanticCallable, } from "./javascriptSemanticIr.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; import { resolveSemanticBindingState, @@ -182,7 +182,9 @@ export const parameterBindings = ( parameterAcceptsArgument(callable, location, index, state), ), ) - .sort((left, right) => compareCodePoints(left.bindingId, right.bindingId)); + .sort((left, right) => + compareUnicodeCodePoints(left.bindingId, right.bindingId), + ); }; const parameterAcceptsArgument = ( @@ -216,4 +218,4 @@ const rangeContains = ( outer.end.column >= inner.end.column)); const uniqueCallableIds = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareCodePoints); + [...new Set(values)].sort(compareUnicodeCodePoints); diff --git a/src/domain/javascript/javascriptSemanticChildProcesses.ts b/src/domain/javascript/javascriptSemanticChildProcesses.ts index 2ce7c2fe5..fe7b5e802 100644 --- a/src/domain/javascript/javascriptSemanticChildProcesses.ts +++ b/src/domain/javascript/javascriptSemanticChildProcesses.ts @@ -14,7 +14,7 @@ import { type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; import { @@ -219,7 +219,7 @@ const childInteraction = ( processBindingId: binding?.bindingId ?? null, linkedProcessIds: linked .map((candidate) => spawnId(candidate)) - .sort(compareCodePoints), + .sort(compareUnicodeCodePoints), eventName: kind === "listener" ? eventName : null, signalName: kind === "signal" diff --git a/src/domain/javascript/javascriptSemanticCoverage.ts b/src/domain/javascript/javascriptSemanticCoverage.ts index 1f92406e1..c40edc03a 100644 --- a/src/domain/javascript/javascriptSemanticCoverage.ts +++ b/src/domain/javascript/javascriptSemanticCoverage.ts @@ -1,4 +1,10 @@ import type { JavaScriptSemanticResourceLimit } from "./javascriptSemanticValueTypes.js"; +import type { ApplicationCoverage } from "./javascriptApplicationEvidenceSchemas.js"; +import { + SEMANTIC_EXPRESSION_DEPTH_LIMIT, + SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT, + SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT, +} from "./javascriptSemanticResourceLimits.js"; /** Exact retention state for one callable's direct returns. */ export type JavaScriptSemanticReturnCoverage = { @@ -40,6 +46,33 @@ export const semanticCoverageResourceLimits = ( coverage: JavaScriptSemanticCoverage, ): readonly JavaScriptSemanticResourceLimit[] => coverage.resourceLimits ?? []; +const semanticResourceBounds: Record< + JavaScriptSemanticResourceLimit, + Pick +> = { + "primitive-candidates": { + value: SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT, + unit: "items", + }, + "primitive-bytes": { + value: SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT, + unit: "bytes", + }, + "expression-depth": { + value: SEMANTIC_EXPRESSION_DEPTH_LIMIT, + unit: "depth", + }, +}; + +/** Map semantic evaluator bounds to their caller-visible application coverage. */ +export const semanticResourceLimitCoverage = ( + resourceLimits: readonly JavaScriptSemanticResourceLimit[], +): ApplicationCoverage["limits"] => + resourceLimits.map((resourceLimit) => ({ + name: `javascript_semantic_${resourceLimit.replaceAll("-", "_")}`, + ...semanticResourceBounds[resourceLimit], + })); + /** Classify one callable's direct-return coverage. */ export const semanticReturnCoverage = ( retainedCount: number, diff --git a/src/domain/javascript/javascriptSemanticDataEffects.ts b/src/domain/javascript/javascriptSemanticDataEffects.ts index e355d3ddb..83afd13d7 100644 --- a/src/domain/javascript/javascriptSemanticDataEffects.ts +++ b/src/domain/javascript/javascriptSemanticDataEffects.ts @@ -8,7 +8,7 @@ import type { JavaScriptSemanticConfigurationOperation, JavaScriptSemanticRequestOperation, } from "./javascriptSemanticIr.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { semanticStaticPropertyKey, semanticStaticPropertyName, @@ -301,7 +301,7 @@ const collectResponseConsumers = ( ), linkedRequestIds: linked .map(({ operation }) => operation.requestId) - .sort(compareCodePoints), + .sort(compareUnicodeCodePoints), endpoint: null, fields: [], resolution: diff --git a/src/domain/javascript/javascriptSemanticFingerprints.ts b/src/domain/javascript/javascriptSemanticFingerprints.ts index 2e3336741..3f7309029 100644 --- a/src/domain/javascript/javascriptSemanticFingerprints.ts +++ b/src/domain/javascript/javascriptSemanticFingerprints.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { JavaScriptSemanticCallAnalysis } from "./javascriptSemanticCalls.js"; import type { JavaScriptSemanticCallable, @@ -100,7 +100,7 @@ export const collectJavaScriptSemanticFingerprints = ( ), ), literalSetSha256: digest( - [...new Set(syntax.literals)].sort(compareCodePoints), + [...new Set(syntax.literals)].sort(compareUnicodeCodePoints), ), effects: [ ...(isAsyncCallable(node) ? (["async"] as const) : []), diff --git a/src/domain/javascript/javascriptSemanticGraph.ts b/src/domain/javascript/javascriptSemanticGraph.ts index a515b2685..032a2802c 100644 --- a/src/domain/javascript/javascriptSemanticGraph.ts +++ b/src/domain/javascript/javascriptSemanticGraph.ts @@ -1,7 +1,9 @@ import { z } from "zod"; -import { canonicalDigest, canonicalJson } from "../comparisonSemantics.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { canonicalJson } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { canonicalJsonDigestSteps } from "../canonicalJsonDigestSteps.js"; import { freezeOwnedJsonSnapshotSteps } from "../immutableJson.js"; import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js"; @@ -30,7 +32,7 @@ const normalizeEvidence = ( coverage: { ...evidence.coverage, limits: [...evidence.coverage.limits].sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( canonicalJson(left, "JavaScript semantic graph"), canonicalJson(right, "JavaScript semantic graph"), ), @@ -44,7 +46,7 @@ const normalizeEvidence = ( export const javaScriptSemanticNodeId = ( node: Pick, ): string => - `jsrg_node_${canonicalDigest({ kind: node.kind, identity: node.identity }, "JavaScript semantic graph")}`; + `jsrg_node_${digestCanonicalValue({ kind: node.kind, identity: node.identity }, "JavaScript semantic graph")}`; /** Normalize one semantic entity and derive its artifact-version identifier. */ export const createJavaScriptSemanticGraphNode = ( @@ -81,7 +83,7 @@ export const createJavaScriptSemanticGraphRelation = ( }; return javaScriptSemanticRelationSchema.parse({ ...semantic, - relation_id: `jsrg_relation_${canonicalDigest(semantic, "JavaScript semantic graph")}`, + relation_id: `jsrg_relation_${digestCanonicalValue(semantic, "JavaScript semantic graph")}`, }); }; @@ -96,7 +98,7 @@ export const createJavaScriptSemanticGraphUnknown = (input: unknown) => { }; return javaScriptSemanticUnknownSchema.parse({ ...semantic, - unknown_id: `jsrg_unknown_${canonicalDigest(semantic, "JavaScript semantic graph")}`, + unknown_id: `jsrg_unknown_${digestCanonicalValue(semantic, "JavaScript semantic graph")}`, }); }; @@ -112,14 +114,14 @@ export const createJavaScriptSemanticFingerprint = (input: unknown) => { limitations: uniqueSorted(parsed.limitations), evidence: normalizeEvidence(parsed.evidence), }; - const fingerprintSha256 = canonicalDigest( + const fingerprintSha256 = digestCanonicalValue( semantic.components, "JavaScript semantic graph", ); return javaScriptSemanticFingerprintSchema.parse({ ...semantic, fingerprint_sha256: fingerprintSha256, - fingerprint_id: `jsrg_fingerprint_${canonicalDigest( + fingerprint_id: `jsrg_fingerprint_${digestCanonicalValue( { function_node_id: semantic.function_node_id, algorithm: semantic.algorithm, @@ -149,7 +151,9 @@ const sortedUniqueIssue = ( context: GraphIssueReporter, ): void => { for (let index = 1; index < values.length; index += 1) { - if (compareCodePoints(values[index - 1] ?? "", values[index] ?? "") < 0) + if ( + compareUnicodeCodePoints(values[index - 1] ?? "", values[index] ?? "") < 0 + ) continue; context.addIssue({ code: "custom", @@ -326,7 +330,8 @@ function* checkRelationsSteps( const { relation_id: identifier, ...semantic } = relation; if ( identifier !== - "jsrg_relation_" + canonicalDigest(semantic, "JavaScript semantic graph") + "jsrg_relation_" + + digestCanonicalValue(semantic, "JavaScript semantic graph") ) context.addIssue({ code: "custom", @@ -370,7 +375,8 @@ function* checkUnknownsSteps( const { unknown_id: identifier, ...semantic } = unknown; if ( identifier !== - "jsrg_unknown_" + canonicalDigest(semantic, "JavaScript semantic graph") + "jsrg_unknown_" + + digestCanonicalValue(semantic, "JavaScript semantic graph") ) context.addIssue({ code: "custom", @@ -439,7 +445,7 @@ function* checkFingerprintsSteps( }); if ( fingerprint.fingerprint_sha256 !== - canonicalDigest(fingerprint.components, "JavaScript semantic graph") + digestCanonicalValue(fingerprint.components, "JavaScript semantic graph") ) context.addIssue({ code: "custom", @@ -448,7 +454,7 @@ function* checkFingerprintsSteps( }); const expectedIdentifier = "jsrg_fingerprint_" + - canonicalDigest( + digestCanonicalValue( { function_node_id: fingerprint.function_node_id, algorithm: fingerprint.algorithm, @@ -514,7 +520,7 @@ const checkGraph = (graph: GraphRecord, context: GraphIssueReporter): void => { const { graph_id: identifier, ...semantic } = graph; if ( identifier !== - "jsrg_" + canonicalDigest(semantic, "JavaScript semantic graph") + "jsrg_" + digestCanonicalValue(semantic, "JavaScript semantic graph") ) context.addIssue({ code: "custom", @@ -538,21 +544,21 @@ const normalizeGraphInput = ( ...parsed, root_node_ids: uniqueSorted(parsed.root_node_ids), nodes: [...parsed.nodes].sort((left, right) => - compareCodePoints(left.node_id, right.node_id), + compareUnicodeCodePoints(left.node_id, right.node_id), ), relations: [...parsed.relations].sort((left, right) => - compareCodePoints(left.relation_id, right.relation_id), + compareUnicodeCodePoints(left.relation_id, right.relation_id), ), fingerprints: [...parsed.fingerprints].sort((left, right) => - compareCodePoints(left.fingerprint_id, right.fingerprint_id), + compareUnicodeCodePoints(left.fingerprint_id, right.fingerprint_id), ), unknowns: [...parsed.unknowns].sort((left, right) => - compareCodePoints(left.unknown_id, right.unknown_id), + compareUnicodeCodePoints(left.unknown_id, right.unknown_id), ), coverage: { ...parsed.coverage, limits: [...parsed.coverage.limits].sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( canonicalJson(left, "JavaScript semantic graph"), canonicalJson(right, "JavaScript semantic graph"), ), @@ -562,7 +568,9 @@ const normalizeGraphInput = ( ...family, unknown_ids: uniqueSorted(family.unknown_ids), })) - .sort((left, right) => compareCodePoints(left.family, right.family)), + .sort((left, right) => + compareUnicodeCodePoints(left.family, right.family), + ), }, limitations: uniqueSorted(parsed.limitations), }); @@ -579,7 +587,7 @@ export const createJavaScriptSemanticGraph = ( // public schema still verifies arbitrary records and their commitments. const record: GraphRecord = { ...semantic, - graph_id: `jsrg_${canonicalDigest(semantic, "JavaScript semantic graph")}`, + graph_id: `jsrg_${digestCanonicalValue(semantic, "JavaScript semantic graph")}`, }; const issues: SemanticGraphIssue[] = []; checkGraphContent(record, { diff --git a/src/domain/javascript/javascriptSemanticIr.ts b/src/domain/javascript/javascriptSemanticIr.ts index 16a137a19..0e547f155 100644 --- a/src/domain/javascript/javascriptSemanticIr.ts +++ b/src/domain/javascript/javascriptSemanticIr.ts @@ -435,24 +435,6 @@ export interface JavaScriptSemanticIr { readonly limitations: readonly string[]; } -/** Find the innermost resolved reference at one source coordinate. */ -export const semanticReferenceAt = ( - ir: JavaScriptSemanticIr, - line: number, - column: number, -): JavaScriptSemanticReference | undefined => - ir.references.find( - ({ location }) => - location.start.line === line && location.start.column === column, - ); - -/** Read one binding by its deterministic semantic identifier. */ -export const semanticBinding = ( - ir: JavaScriptSemanticIr, - bindingId: string, -): JavaScriptSemanticBinding | undefined => - ir.bindings.find(({ bindingId: candidate }) => candidate === bindingId); - /** Fail-closed result when Babel cannot produce an inert syntax tree. */ export const failedJavaScriptSemanticIr = (): JavaScriptSemanticIr => ({ schema: "JavaScriptSemanticIR", diff --git a/src/domain/javascript/javascriptSemanticPrimitives.ts b/src/domain/javascript/javascriptSemanticPrimitives.ts index 0223403d0..29cff687b 100644 --- a/src/domain/javascript/javascriptSemanticPrimitives.ts +++ b/src/domain/javascript/javascriptSemanticPrimitives.ts @@ -2,7 +2,7 @@ import type { JavaScriptSemanticPrimitive, JavaScriptSemanticValue, } from "./javascriptSemanticValueTypes.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { semanticPrimitiveKey } from "./javascriptSemanticProvenance.js"; import { SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT, @@ -35,7 +35,7 @@ export const semanticPrimitiveSet = ( return semanticResourceLimitUnknown("primitive-bytes"); const unique = [...uniqueValues] .map((value) => ({ key: semanticPrimitiveKey(value), value })) - .sort((left, right) => compareCodePoints(left.key, right.key)) + .sort((left, right) => compareUnicodeCodePoints(left.key, right.key)) .map(({ value }) => value); const only = unique[0]; return unique.length === 1 && only !== undefined diff --git a/src/domain/javascript/javascriptSemanticProjection.ts b/src/domain/javascript/javascriptSemanticProjection.ts index 1642a0a41..53881d632 100644 --- a/src/domain/javascript/javascriptSemanticProjection.ts +++ b/src/domain/javascript/javascriptSemanticProjection.ts @@ -14,7 +14,7 @@ import { type JavaScriptSemanticScopeState, } from "./javascriptSemanticState.js"; import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { evaluateSemanticBinding, @@ -160,7 +160,7 @@ export const immutableSemanticScopes = ( bindingsComplete: scope.bindingsComplete, bindingIds: [...scope.bindings.values()] .map(({ bindingId }) => bindingId) - .sort(compareCodePoints), + .sort(compareUnicodeCodePoints), })); /** Evaluate and freeze bindings into deterministic IR order. */ @@ -178,7 +178,9 @@ export const immutableSemanticBindings = ( value: evaluateSemanticBinding(binding, state), provenance: evaluateSemanticProvenance(binding, state), })) - .sort((left, right) => compareCodePoints(left.bindingId, right.bindingId)); + .sort((left, right) => + compareUnicodeCodePoints(left.bindingId, right.bindingId), + ); const callableKind = ( node: t.Node, diff --git a/src/domain/javascript/javascriptSemanticPromises.ts b/src/domain/javascript/javascriptSemanticPromises.ts index 96eca56b2..d956d68f7 100644 --- a/src/domain/javascript/javascriptSemanticPromises.ts +++ b/src/domain/javascript/javascriptSemanticPromises.ts @@ -11,7 +11,7 @@ import { semanticResolutionBlocked, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; import { @@ -87,7 +87,7 @@ export const collectJavaScriptSemanticPromises = ( ownerCallableId: candidate.ownerCallableId, ...ownership, sourcePromiseIds: [...new Set(sources.promiseIds)].sort( - compareCodePoints, + compareUnicodeCodePoints, ), sourceResolution: sources.status, }; diff --git a/src/domain/javascript/javascriptSemanticProvenance.ts b/src/domain/javascript/javascriptSemanticProvenance.ts index 69a4a29a8..218c465b5 100644 --- a/src/domain/javascript/javascriptSemanticProvenance.ts +++ b/src/domain/javascript/javascriptSemanticProvenance.ts @@ -3,7 +3,7 @@ import type { JavaScriptModuleOrigin, } from "./javascriptSemanticIr.js"; import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; /** Construct exact local provenance without inventing module origins. */ export const semanticLocalProvenance = (): JavaScriptBindingProvenance => ({ @@ -44,7 +44,7 @@ export const uniqueSemanticOrigins = ( origins.map((origin) => [semanticOriginKey(origin), origin]), ).values(), ].sort((left, right) => - compareCodePoints(semanticOriginKey(left), semanticOriginKey(right)), + compareUnicodeCodePoints(semanticOriginKey(left), semanticOriginKey(right)), ); const semanticOriginKey = (origin: JavaScriptModuleOrigin): string => diff --git a/src/domain/javascript/javascriptSemanticQuery.ts b/src/domain/javascript/javascriptSemanticQuery.ts index 4b1c58c46..e793ccede 100644 --- a/src/domain/javascript/javascriptSemanticQuery.ts +++ b/src/domain/javascript/javascriptSemanticQuery.ts @@ -3,7 +3,7 @@ import type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation, } from "./javascriptSemanticGraphSchemas.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js"; import { isJavaScriptSemanticOwnershipRelation as ownershipRelation } from "./javascriptSemanticQueryRelations.js"; import { @@ -147,7 +147,7 @@ const buildAdjacency = ( } for (const entries of adjacency.values()) entries.sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( `${left.relation.relation_id}\0${left.nextNodeId}`, `${right.relation.relation_id}\0${right.nextNodeId}`, ), diff --git a/src/domain/javascript/javascriptSemanticQueryIdentity.ts b/src/domain/javascript/javascriptSemanticQueryIdentity.ts index 3876fe0a8..ba9aaf94d 100644 --- a/src/domain/javascript/javascriptSemanticQueryIdentity.ts +++ b/src/domain/javascript/javascriptSemanticQueryIdentity.ts @@ -4,7 +4,8 @@ import canonicalize from "canonicalize"; import type { JavaScriptSemanticGraph } from "./javascriptSemanticGraph.js"; import type { JavaScriptSemanticGraphNode } from "./javascriptSemanticGraphSchemas.js"; -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { JavaScriptSemanticQueryInput } from "./javascriptSemanticQuerySchemas.js"; /** Canonical JSON used by semantic query commitments and ordering. */ @@ -66,7 +67,7 @@ export const resolveJavaScriptSemanticQuerySeeds = ( return graph.nodes .filter((node) => seedMatches(node, input, fingerprintFunctions)) .map(({ node_id }) => node_id) - .sort(compareCodePoints); + .sort(compareUnicodeCodePoints); }; const seedMatches = ( diff --git a/src/domain/javascript/javascriptSemanticReturns.ts b/src/domain/javascript/javascriptSemanticReturns.ts index 119ec6bd8..5a1a3f415 100644 --- a/src/domain/javascript/javascriptSemanticReturns.ts +++ b/src/domain/javascript/javascriptSemanticReturns.ts @@ -15,7 +15,7 @@ import type { ProjectedPropertyCoverage, ProjectedReturnField, } from "./javascriptExportShapeComparisonSchemas.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; import { resolveSemanticBindingState, @@ -204,9 +204,9 @@ export const flattenSemanticReturnValue = ( const fields: ProjectedReturnField[] = []; const propertyCoverage: ProjectedPropertyCoverage[] = []; flattenValue(value, "", fields, propertyCoverage); - fields.sort((left, right) => compareCodePoints(left.path, right.path)); + fields.sort((left, right) => compareUnicodeCodePoints(left.path, right.path)); propertyCoverage.sort((left, right) => - compareCodePoints(left.path, right.path), + compareUnicodeCodePoints(left.path, right.path), ); return { fields, propertyCoverage }; }; diff --git a/src/domain/javascript/javascriptSemanticSlots.ts b/src/domain/javascript/javascriptSemanticSlots.ts index 61b61041d..4150231e4 100644 --- a/src/domain/javascript/javascriptSemanticSlots.ts +++ b/src/domain/javascript/javascriptSemanticSlots.ts @@ -3,12 +3,9 @@ import type { JavaScriptSemanticValue, } from "./javascriptSemanticValueTypes.js"; -/** A container's own slot, using the value lattice's canonical presence model. */ -export type JavaScriptSemanticSlot = JavaScriptSemanticProperty; - /** Static container inventory; partial siblings do not weaken retained slots. */ export interface JavaScriptSemanticContainer { - readonly slots: readonly JavaScriptSemanticSlot[]; + readonly slots: readonly JavaScriptSemanticProperty[]; readonly coverage: | { readonly status: "complete"; readonly omitted: 0 } | { readonly status: "partial"; readonly omitted: number | null }; @@ -48,9 +45,9 @@ export function semanticContainer( export const semanticSlotAtPath = ( value: JavaScriptSemanticValue, path: readonly string[], -): JavaScriptSemanticSlot => { +): JavaScriptSemanticProperty => { const leafName = path.at(-1) ?? ""; - let current: JavaScriptSemanticSlot = { + let current: JavaScriptSemanticProperty = { name: "", presence: "present", value, diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 8fac48e8e..791d3c388 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -16,7 +16,7 @@ import { type JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { readExactJavaScriptLiteral, semanticStaticPropertyKey, @@ -300,7 +300,7 @@ const evaluateObject = ( }); } const properties = [...propertiesByName.values()].sort((left, right) => - compareCodePoints(left.name, right.name), + compareUnicodeCodePoints(left.name, right.name), ); return unknownProperties ? { diff --git a/src/domain/javascript/javascriptStaticAnalysisBundler.ts b/src/domain/javascript/javascriptStaticAnalysisBundler.ts index e82096f01..f11901c8c 100644 --- a/src/domain/javascript/javascriptStaticAnalysisBundler.ts +++ b/src/domain/javascript/javascriptStaticAnalysisBundler.ts @@ -6,7 +6,7 @@ import { collectJavaScriptExports, fingerprintJavaScriptAst, } from "./javascriptAstFingerprint.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { addFindingOnce } from "./javascriptStaticAnalysisFindings.js"; import { argumentValue, @@ -66,7 +66,7 @@ export const inspectBundlerRegistration = ( unknown_async_chunk_keys: runtimeValue.unknownAsyncChunkKeys + recovered.unknownAsyncChunkKeys, modules: recovered.modules.sort((left, right) => - compareCodePoints(left.module_key, right.module_key), + compareUnicodeCodePoints(left.module_key, right.module_key), ), location: range(call), }; @@ -107,7 +107,7 @@ export const inspectEsbuildWrapper = ( async_chunk_keys: recovered.asyncChunkKeys, unknown_async_chunk_keys: recovered.unknownAsyncChunkKeys, modules: recovered.modules.sort((left, right) => - compareCodePoints(left.module_key, right.module_key), + compareUnicodeCodePoints(left.module_key, right.module_key), ), location: range(call), }; @@ -225,7 +225,7 @@ const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => { asyncChunkKeys: [], unknownAsyncChunkKeys: 0, }; - const entries = collectBundlerEntryModuleKeys(node, requireName); + const entries = collectBundlerCallArgumentValues(node, requireName); const asyncChunks = collectBundlerAsyncChunkKeys(node, requireName); return { requireName, @@ -236,11 +236,6 @@ const runtimeMetadata = (node: t.Node | null | undefined): RuntimeMetadata => { }; }; -const collectBundlerEntryModuleKeys = ( - factory: BundlerFunction, - requireName: string, -): StaticValues => collectBundlerCallArgumentValues(factory, requireName); - const collectBundlerAsyncChunkKeys = ( factory: BundlerFunction, requireName: string | null, @@ -277,7 +272,7 @@ const isBundlerFunction = ( t.isObjectMethod(node); const uniqueValues = (values: readonly string[], unknown = 0): StaticValues => { - const unique = [...new Set(values)].sort(compareCodePoints); + const unique = [...new Set(values)].sort(compareUnicodeCodePoints); return { values: unique, unknown, diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index 28cd6e624..ed1cd8e21 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -15,7 +15,7 @@ import { readExactJavaScriptLiteral, semanticStaticPropertyName, } from "./javascriptAstValues.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; /** Explicit result for source text that Babel cannot parse. */ export const failedJavaScriptStaticAnalysis = (): JavaScriptStaticAnalysis => ({ @@ -156,17 +156,13 @@ export const staticArrayValues = ( const value = argumentValue(element); return value === undefined ? [] : [value]; }); - const values = [...new Set(staticValues)].sort(compareCodePoints); + const values = [...new Set(staticValues)].sort(compareUnicodeCodePoints); return { values, unknown: array.elements.length - staticValues.length, }; }; -/** Resolve a path composed only from inert literal syntax. */ -export const staticPath = (node: t.Node): string | undefined => - staticPathAt(node); - /** Classify whether inert path syntax is a module specifier or file expression. */ export const staticPathResolutionContext = ( node: t.Node, @@ -175,13 +171,14 @@ export const staticPathResolutionContext = ( ? "filesystem-expression" : "module-specifier"; -const staticPathAt = (node: t.Node): string | undefined => { +/** Resolve a path composed only from inert literal syntax. */ +export const staticPath = (node: t.Node): string | undefined => { if (t.isStringLiteral(node)) return node.value; if (t.isTemplateLiteral(node) && node.expressions.length === 0) return stringValue(node); if (t.isBinaryExpression(node, { operator: "+" })) { - const left = staticPathAt(node.left); - const right = staticPathAt(node.right); + const left = staticPath(node.left); + const right = staticPath(node.right); return left === undefined || right === undefined ? undefined : `${left}${right}`; @@ -215,7 +212,7 @@ const staticCallPath = ( const parts: string[] = []; for (const argument of node.arguments) { if (isDirectoryIdentity(argumentNode(argument))) continue; - const value = t.isNode(argument) ? staticPathAt(argument) : undefined; + const value = t.isNode(argument) ? staticPath(argument) : undefined; if (value === undefined) return undefined; parts.push(value); } @@ -228,7 +225,7 @@ const staticFileUrlPath = ( if (argument === undefined) return undefined; return isSourceFileUrl(argument) ? sourceRelativeFileUrlPath(argument) - : staticPathAt(argument); + : staticPath(argument); }; /** Project a known source-relative URL through the actual file URL decoder. */ @@ -284,7 +281,7 @@ const staticResolvedPath = ( parts.push(null); continue; } - const value = t.isNode(argument) ? staticPathAt(argument) : undefined; + const value = t.isNode(argument) ? staticPath(argument) : undefined; // Validate every argument, including those before the last anchor. if (value === undefined) return undefined; parts.push(value); @@ -684,7 +681,7 @@ export const sortedUnique = ( key: (value: Value) => string, ): Value[] => [...new Map(values.map((value) => [key(value), value])).values()].sort( - (left, right) => compareCodePoints(key(left), key(right)), + (left, right) => compareUnicodeCodePoints(key(left), key(right)), ); /** Hash exact UTF-8 source text. */ diff --git a/src/domain/javascript/sourceToBundleComparison.test.ts b/src/domain/javascript/sourceToBundleComparison.test.ts index 5e1f4d9dd..f6179d901 100644 --- a/src/domain/javascript/sourceToBundleComparison.test.ts +++ b/src/domain/javascript/sourceToBundleComparison.test.ts @@ -6,7 +6,7 @@ import { createJavaScriptApplicationNode, } from "./javascriptApplicationGraph.js"; import { createHistoricalSourceGraph } from "../referenceSourceGraph.js"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { compareSourceToBundle } from "./sourceToBundleComparison.js"; import { SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS, @@ -365,7 +365,7 @@ const historicalGraph = ( content_state: "hashed" as const, limitations: [], })), - ].sort((left, right) => compareCodePoints(left.path, right.path)), + ].sort((left, right) => compareUnicodeCodePoints(left.path, right.path)), relationships: [], parse_failures: [], exclusions: [], diff --git a/src/domain/javascript/sourceToBundleComparison.ts b/src/domain/javascript/sourceToBundleComparison.ts index 6a8294b19..ac0007cb4 100644 --- a/src/domain/javascript/sourceToBundleComparison.ts +++ b/src/domain/javascript/sourceToBundleComparison.ts @@ -1,5 +1,7 @@ -import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js"; -import { absenceClaimable, canonicalDigest } from "../comparisonSemantics.js"; +import { uniqueSorted } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; +import { absenceClaimable } from "../comparisonSemantics.js"; +import { digestCanonicalValue } from "../canonicalDigest.js"; import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; import type { HistoricalSourceGraph } from "../referenceSourceGraph.js"; @@ -84,7 +86,7 @@ export const compareSourceToBundle = ( }; return sourceToBundleComparisonResultSchema.parse({ ...semanticResult, - comparison_id: `stbc_${canonicalDigest(semanticResult, "Source-to-bundle comparison")}`, + comparison_id: `stbc_${digestCanonicalValue(semanticResult, "Source-to-bundle comparison")}`, }); }; @@ -316,7 +318,7 @@ const mappingId = ( input: SourceToBundleComparisonInput, item: Omit, ): string => - `stbc_item_${canonicalDigest( + `stbc_item_${digestCanonicalValue( { reference_root_sha256: input.reference.root_sha256, application_graph_id: input.application.graph.graph_id, @@ -382,4 +384,4 @@ const compareCandidates = ( right: SourceToBundleCandidate, ): number => right.score - left.score || - compareCodePoints(left.current_node_id, right.current_node_id); + compareUnicodeCodePoints(left.current_node_id, right.current_node_id); diff --git a/src/domain/javascript/sourceToBundleSignals.ts b/src/domain/javascript/sourceToBundleSignals.ts index fedbdd80a..719062200 100644 --- a/src/domain/javascript/sourceToBundleSignals.ts +++ b/src/domain/javascript/sourceToBundleSignals.ts @@ -1,6 +1,6 @@ import { posix } from "node:path"; -import { compareCodePoints } from "../canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js"; import type { HistoricalSourceGraph } from "../referenceSourceGraph.js"; import { @@ -70,7 +70,9 @@ export const sourceBearingNodes = ( ): ApplicationNode[] => nodes .filter(({ kind }) => RELEVANT_NODE_KINDS.has(kind)) - .sort((left, right) => compareCodePoints(left.node_id, right.node_id)); + .sort((left, right) => + compareUnicodeCodePoints(left.node_id, right.node_id), + ); /** Keep historical source files in stable path order. */ export const historicalSourceFiles = ( @@ -81,7 +83,7 @@ export const historicalSourceFiles = ( (entry): entry is SourceFile => entry.kind === "file" && entry.classifications.includes("source"), ) - .sort((left, right) => compareCodePoints(left.path, right.path)); + .sort((left, right) => compareUnicodeCodePoints(left.path, right.path)); /** Build deterministic candidate indices without assigning fuzzy matches. */ export const buildSourceToBundleCandidateIndex = ( @@ -130,7 +132,7 @@ export const candidateIdsForSource = ( const output: string[] = []; for (const group of groups) { if (group === undefined) continue; - for (const nodeId of [...group].sort(compareCodePoints)) { + for (const nodeId of [...group].sort(compareUnicodeCodePoints)) { if (seen.has(nodeId)) continue; seen.add(nodeId); output.push(nodeId); @@ -256,7 +258,7 @@ const signal = ( kind, weight: signalWeight(kind), source_value: sourceValue, - current_values: [...new Set(currentValues)].sort(compareCodePoints), + current_values: [...new Set(currentValues)].sort(compareUnicodeCodePoints), }); const candidateConfidence = ( @@ -325,7 +327,7 @@ const uniquePaths = (paths: readonly CurrentPath[]): CurrentPath[] => ]), ).values(), ].sort((left, right) => - compareCodePoints( + compareUnicodeCodePoints( `${left.kind}\0${left.value}`, `${right.kind}\0${right.value}`, ), diff --git a/src/domain/jsonShape.ts b/src/domain/jsonShape.ts index 10e40a12b..2defacd58 100644 --- a/src/domain/jsonShape.ts +++ b/src/domain/jsonShape.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { compareCodePoints } from "./canonicalOrdering.js"; +import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; const jsonValueTypeSchema = z.enum([ "object", @@ -93,7 +93,7 @@ export const inferJsonShape = (text: string): JsonShape | null => { .sort( (left, right) => left.path.localeCompare(right.path) || - compareCodePoints(left.path, right.path), + compareUnicodeCodePoints(left.path, right.path), ), }); }; diff --git a/src/domain/localPath.ts b/src/domain/localPath.ts index 6de151e5d..3226c2e44 100644 --- a/src/domain/localPath.ts +++ b/src/domain/localPath.ts @@ -7,10 +7,6 @@ export const localPathStringSchema = z .min(1) .regex(/^[^\u0000]*$/u, "Local filesystem paths cannot contain NUL"); -/** True when the value is an absolute filesystem path on the host platform. */ -export const isAbsoluteLocalPath = (value: string): boolean => - isAbsolute(value); - /** Test lexical containment on the host platform; callers must resolve symlinks first. */ export const isPathWithinRoot = (root: string, path: string): boolean => { const value = relative(root, path); diff --git a/src/domain/managed/managedMemberComparisonMatch.ts b/src/domain/managed/managedMemberComparisonMatch.ts index 229263c4f..2c9e0cf04 100644 --- a/src/domain/managed/managedMemberComparisonMatch.ts +++ b/src/domain/managed/managedMemberComparisonMatch.ts @@ -50,14 +50,14 @@ const keyMethod = (item: Method): Keyed => ({ item, exactKey: item.body.status === "present" && item.body.normalized_il_sha256 !== null - ? stableKey([ + ? sha256([ "method-exact", item.signature.raw_sha256, item.body.normalized_il_sha256, ]) : null, // The raw blob is the exact signature; decoding only feeds shape keys. - signatureKey: stableKey([ + signatureKey: sha256([ "method-signature", item.declaring_type, item.name, @@ -65,7 +65,7 @@ const keyMethod = (item: Method): Keyed => ({ ]), structuralKey: item.signature.parse_status === "decoded" && item.body.status === "present" - ? stableKey([ + ? sha256([ "method-structural", item.signature.kind, item.signature.calling_convention, @@ -98,17 +98,17 @@ const keyField = (item: Field): Keyed => ({ item, exactKey: item.signature.parse_status === "decoded" - ? stableKey(["field-exact", item.signature.raw_sha256]) + ? sha256(["field-exact", item.signature.raw_sha256]) : null, // Only an undecoded field reaches this round: a decoded field with the // same raw signature already met its counterpart in the exact round. - signatureKey: stableKey([ + signatureKey: sha256([ "field-signature", item.declaring_type, item.name, item.signature.raw_sha256, ]), - structuralKey: stableKey([ + structuralKey: sha256([ "field-structural", item.signature.kind, item.signature.field_type, @@ -116,8 +116,6 @@ const keyField = (item: Field): Keyed => ({ ]), }); -const stableKey = (value: JsonValue): string => sha256(value); - const matchMethods = ( left: readonly Keyed[], right: readonly Keyed[], diff --git a/src/domain/referenceSourceGraph.ts b/src/domain/referenceSourceGraph.ts index 436df0729..1e8c3d782 100644 --- a/src/domain/referenceSourceGraph.ts +++ b/src/domain/referenceSourceGraph.ts @@ -2,7 +2,8 @@ import { posix } from "node:path"; import { z } from "zod"; -import { canonicalDigest, canonicalJson } from "./comparisonSemantics.js"; +import { canonicalJson } from "./comparisonSemantics.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; import { digestSchema } from "./../domain/digests.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; @@ -473,7 +474,7 @@ const computeRootSha256 = ( entries: readonly z.infer[], exclusions: readonly z.infer[], ): string => - canonicalDigest( + digestCanonicalValue( rootCommitment(entries, exclusions), "Reference source graph", ); @@ -496,7 +497,10 @@ export const parseHistoricalSourceGraph = ( /** Compute a deterministic graph commitment containing no absolute root path. */ export const computeHistoricalSourceGraphSha256 = (input: unknown): string => - canonicalDigest(parseHistoricalSourceGraph(input), "Reference source graph"); + digestCanonicalValue( + parseHistoricalSourceGraph(input), + "Reference source graph", + ); /** Build a deterministic, relocation-independent manifest for a source graph. */ export const createHistoricalSourceManifest = ( @@ -514,7 +518,7 @@ export const createHistoricalSourceManifest = ( }; return historicalSourceManifestBaseSchema.parse({ ...semantic, - manifest_id: `hsm_${canonicalDigest(semantic, "Reference source graph")}`, + manifest_id: `hsm_${digestCanonicalValue(semantic, "Reference source graph")}`, }); }; @@ -525,7 +529,8 @@ export const parseHistoricalSourceManifest = ( const manifest = historicalSourceManifestBaseSchema.parse(input); const { manifest_id: manifestId, ...semantic } = manifest; if ( - `hsm_${canonicalDigest(semantic, "Reference source graph")}` !== manifestId + `hsm_${digestCanonicalValue(semantic, "Reference source graph")}` !== + manifestId ) throw new TypeError("Historical source manifest identifier does not match"); return manifest; diff --git a/src/domain/referenceSourceImportParsing.ts b/src/domain/referenceSourceImportParsing.ts index 972a3157b..70097297a 100644 --- a/src/domain/referenceSourceImportParsing.ts +++ b/src/domain/referenceSourceImportParsing.ts @@ -201,9 +201,6 @@ const isRequireCallee = (callee: Node | null | undefined): boolean => { return false; }; -const isImportCallee = (callee: Node | null | undefined): boolean => - isImport(callee); - const collectModuleExpressions = ( node: unknown, targets: Array, @@ -272,7 +269,7 @@ const extractRequireAndDynamicImports = ( continue; } - if (isImportCallee(call.callee) && isModuleExpression(first)) { + if (isImport(call.callee) && isModuleExpression(first)) { const result = moduleSpecifierFromExpression(first); if (result !== undefined) { appendRelationship(relationships, { diff --git a/src/domain/webCaptureDiff.ts b/src/domain/webCaptureDiff.ts index 6f666f163..c0ac4698a 100644 --- a/src/domain/webCaptureDiff.ts +++ b/src/domain/webCaptureDiff.ts @@ -1,4 +1,4 @@ -import { canonicalDigest } from "./comparisonSemantics.js"; +import { digestCanonicalValue } from "./canonicalDigest.js"; import type { WebPageInspection } from "./browserObservationSchemas.js"; import { webCaptureDiffSchema, @@ -106,7 +106,7 @@ const accessibilityDimension = ( return compareDimension( singleton( "accessibility_tree", - digest( + digestCanonicalValue( accessibilityProjection( before.accessibility, textComparable, @@ -116,7 +116,7 @@ const accessibilityDimension = ( ), singleton( "accessibility_tree", - digest( + digestCanonicalValue( accessibilityProjection( after.accessibility, textComparable, @@ -167,16 +167,15 @@ const compareWebCaptureDimensions = ( ): WebCaptureDiff["dimensions"] => { const before = input.before.inspection; const after = input.after.inspection; - const dimension = compareDimension; return { - dom_structure: dimension( - singleton("document", digest(domProjection(before))), - singleton("document", digest(domProjection(after))), + dom_structure: compareDimension( + singleton("document", digestCanonicalValue(domProjection(before))), + singleton("document", digestCanonicalValue(domProjection(after))), sectionsComplete(before, ["frames", "dom"]) && sectionsComplete(after, ["frames", "dom"]), "DOM or frame capture was incomplete in at least one observation.", ), - scripts: dimension( + scripts: compareDimension( keyed( before.scripts.items.map(scriptProjection), (item) => item.script_key, @@ -189,28 +188,28 @@ const compareWebCaptureDimensions = ( sectionsComplete(after, ["scripts"]), "Script inventory was incomplete in at least one observation.", ), - resources: dimension( + resources: compareDimension( keyed(before.resources, (item) => item.resource_key), keyed(after.resources, (item) => item.resource_key), sectionsComplete(before, ["resources"]) && sectionsComplete(after, ["resources"]), "Resource inventory was incomplete in at least one observation.", ), - network: dimension( + network: compareDimension( networkMap(before), networkMap(after), sectionsComplete(before, ["network_requests"]) && sectionsComplete(after, ["network_requests"]), "Network capture is attach-window limited or incomplete.", ), - metadata: dimension( - singleton("metadata", digest(metadataProjection(before))), - singleton("metadata", digest(metadataProjection(after))), + metadata: compareDimension( + singleton("metadata", digestCanonicalValue(metadataProjection(before))), + singleton("metadata", digestCanonicalValue(metadataProjection(after))), sectionsComplete(before, ["metadata"]) && sectionsComplete(after, ["metadata"]), "Safe metadata capture was incomplete in at least one observation.", ), - webmcp: dimension( + webmcp: compareDimension( webMcpMap(input.before.webmcp), webMcpMap(input.after.webmcp), webMcpComplete(input.before.webmcp) && webMcpComplete(input.after.webmcp), @@ -225,14 +224,16 @@ const keyed = ( values: readonly T[], identity: (value: T) => string, ): ReadonlyMap => - new Map(values.map((value) => [identity(value), digest(value)])); + new Map( + values.map((value) => [identity(value), digestCanonicalValue(value)]), + ); const networkMap = ( inspection: WebPageInspection, ): ReadonlyMap => { const grouped = new Map(); for (const request of inspection.network.requests) { - const identity = `net_${digest({ + const identity = `net_${digestCanonicalValue({ method: request.method, url: request.url, resource_type: request.resource_type, @@ -257,7 +258,9 @@ const networkMap = ( return new Map( [...grouped].map(([identity, values]) => [ identity, - digest(values.map((value) => digest(value)).sort()), + digestCanonicalValue( + values.map((value) => digestCanonicalValue(value)).sort(), + ), ]), ); }; @@ -317,11 +320,13 @@ const scriptProjection = ( const metadataProjection = (inspection: WebPageInspection) => ({ responses: inspection.metadata.responses .map(({ request_id: _requestId, ...response }) => response) - .map((value) => digest(value)) + .map((value) => digestCanonicalValue(value)) + .sort(), + dom_urls: inspection.metadata.dom_urls + .map((value) => digestCanonicalValue(value)) .sort(), - dom_urls: inspection.metadata.dom_urls.map((value) => digest(value)).sort(), agent_hints: inspection.metadata.agent_hints - .map((value) => digest(value)) + .map((value) => digestCanonicalValue(value)) .sort(), excluded_dom_urls: inspection.metadata.excluded_dom_urls, headers_allowlisted: inspection.metadata.headers_allowlisted, @@ -330,8 +335,6 @@ const metadataProjection = (inspection: WebPageInspection) => ({ const singleton = (key: string, value: string): ReadonlyMap => new Map([[key, value]]); -const digest = (value: unknown): string => canonicalDigest(value); - const accessibilityComparable = ( inspection: WebPageInspection, ): { @@ -399,7 +402,7 @@ const storageMap = ( const map = new Map([ [ "storage:summary", - digest({ + digestCanonicalValue({ origin: storage.origin, values_redacted: storage.values_redacted, ...(includeUsage @@ -414,7 +417,7 @@ const storageMap = ( if (!includeKeys) return map; const add = (kind: string, keys: readonly string[]) => { for (const key of keys) { - map.set(`storage:${kind}:${key}`, digest(key)); + map.set(`storage:${kind}:${key}`, digestCanonicalValue(key)); } }; add("local_storage", storage.local_storage_keys); @@ -424,7 +427,10 @@ const storageMap = ( for (const fingerprint of storage.content_fingerprints) { const identity = `${fingerprint.scope}:${fingerprint.identity_sha256}`; if (!fingerprintIdentities.has(identity)) continue; - map.set(`storage:content:${identity}`, digest(fingerprint.value_sha256)); + map.set( + `storage:content:${identity}`, + digestCanonicalValue(fingerprint.value_sha256), + ); } return map; }; diff --git a/src/dotnet/ManagedStaticProviderMetadata.ts b/src/dotnet/ManagedStaticProviderMetadata.ts index 4495cdd59..04b412828 100644 --- a/src/dotnet/ManagedStaticProviderMetadata.ts +++ b/src/dotnet/ManagedStaticProviderMetadata.ts @@ -12,14 +12,12 @@ import { export const MANAGED_STATIC_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze( MANAGED_STATIC_PROVIDER, ); -const IDENTITY = MANAGED_STATIC_PROVIDER_IDENTITY; - /** Declare managed metadata coverage without opening or loading an assembly. */ export const managedStaticCapabilities = (): readonly CapabilityDescriptor[] => Object.freeze( MANAGED_TOOL_CONTRACTS.map((contract) => Object.freeze({ - provider: IDENTITY, + provider: MANAGED_STATIC_PROVIDER_IDENTITY, operation: contract.name, available: true as const, reason: null, diff --git a/src/main/transport.ts b/src/main/transport.ts index c4a921b91..729b5d6d1 100644 --- a/src/main/transport.ts +++ b/src/main/transport.ts @@ -14,15 +14,12 @@ import { MCP_CONNECTION_START_FAILED, } from "./messages.js"; -/** Optional adapters whose absence must not prevent the core MCP server. */ -export type OptionalProviders = OptionalProviderLoadResult; - interface ServerContext { readonly environment: Readonly; readonly delivery: ToolResultDelivery; readonly logger: Logger; readonly serverLogger: Logger; - readonly loadOptionalProviders: () => Promise; + readonly loadOptionalProviders: () => Promise; } export const startMcpTransport = async ( @@ -38,7 +35,7 @@ export const startMcpTransport = async ( | { readonly ok: false } > => { const { serverLogger } = serverContext; - let optionalProviders: OptionalProviders = {}; + let optionalProviders: OptionalProviderLoadResult = {}; try { optionalProviders = await serverContext.loadOptionalProviders(); } catch (cause: unknown) { diff --git a/src/main/types.ts b/src/main/types.ts index fa8de8ba9..8b46e25f4 100644 --- a/src/main/types.ts +++ b/src/main/types.ts @@ -1,3 +1,5 @@ +import type { OptionalProviderLoadResult } from "../application/OptionalObservationProviders.js"; + export interface RuntimeDependencies { readonly env: NodeJS.ProcessEnv; readonly serve: typeof import("@modelcontextprotocol/server/stdio").serveStdio; @@ -5,7 +7,5 @@ export interface RuntimeDependencies { readonly setExitCode: (code: number) => void; readonly registerShutdown: (handler: () => void) => () => void; readonly createServer?: typeof import("../server/createServer.js").createServer; - readonly loadOptionalProviders?: () => Promise< - import("./transport.js").OptionalProviders - >; + readonly loadOptionalProviders?: () => Promise; } diff --git a/src/native/NativeMacOSProviderMetadata.ts b/src/native/NativeMacOSProviderMetadata.ts index 3f7c24dbd..18e7a100d 100644 --- a/src/native/NativeMacOSProviderMetadata.ts +++ b/src/native/NativeMacOSProviderMetadata.ts @@ -10,8 +10,6 @@ export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ name: "macOS native inspection utilities", version: null, }); -const IDENTITY = NATIVE_MACOS_PROVIDER_IDENTITY; - /** Operations whose target process can show UI, use the network and write files. */ const OWNED_PROCESS_OPERATIONS: ReadonlySet = new Set([ "capture_native_ui_scenario", @@ -36,7 +34,7 @@ export const nativeMacOSCapabilities = ( reason: "Native macOS utilities require macOS.", } as const); return Object.freeze({ - provider: IDENTITY, + provider: NATIVE_MACOS_PROVIDER_IDENTITY, operation: contract.name, ...availability, effects: Object.freeze({ diff --git a/src/reference/ReferenceSourceReaderEntries.ts b/src/reference/ReferenceSourceReaderEntries.ts index 2b60f20c2..36ba8952b 100644 --- a/src/reference/ReferenceSourceReaderEntries.ts +++ b/src/reference/ReferenceSourceReaderEntries.ts @@ -1,3 +1,4 @@ +import type { BigIntStats } from "node:fs"; import { readdir, readlink, realpath } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; @@ -18,7 +19,6 @@ import { bigLstat, } from "./ReferenceSourceReaderValidate.js"; import { - type BigStats, type PendingDirectory, type ReferenceSourceEntry, type ReferenceSourceResult, @@ -119,7 +119,7 @@ const readDirectoryNames = async ( const readMetadata = async ( path: string, ): Promise< - | { readonly ok: true; readonly value: BigStats } + | { readonly ok: true; readonly value: BigIntStats } | { readonly ok: false; readonly message: string } > => { try { @@ -193,7 +193,7 @@ const processFileEntry = async ( state: TraversalState, absolute: string, path: string, - metadata: BigStats, + metadata: BigIntStats, ): Promise => { const result = await readStableFile({ root: state.root, diff --git a/src/reference/ReferenceSourceReaderFile.ts b/src/reference/ReferenceSourceReaderFile.ts index 2e0845af8..b9c483e5d 100644 --- a/src/reference/ReferenceSourceReaderFile.ts +++ b/src/reference/ReferenceSourceReaderFile.ts @@ -1,3 +1,4 @@ +import type { BigIntStats } from "node:fs"; import { constants } from "node:fs"; import { open, type FileHandle } from "node:fs/promises"; import { dirname } from "node:path"; @@ -13,7 +14,6 @@ import { validateDirectory, } from "./ReferenceSourceReaderValidate.js"; import { - type BigStats, type ReferenceSourceEntry, type StableFileRequest, } from "./ReferenceSourceReaderTypes.js"; @@ -23,7 +23,7 @@ const READ_CHUNK_BYTES = 64 * 1024; type PreparedFileRead = | { readonly status: "ready"; - readonly before: BigStats; + readonly before: BigIntStats; } | { readonly status: "failed"; readonly entry: ReferenceSourceEntry }; @@ -33,12 +33,12 @@ type FileContentsRead = type FinalizeFileReadRequest = { readonly root: string; - readonly rootIdentity: BigStats; + readonly rootIdentity: BigIntStats; readonly absolute: string; readonly path: string; readonly handle: FileHandle; - readonly before: BigStats; - readonly parentBefore: { readonly ok: true; readonly stats: BigStats }; + readonly before: BigIntStats; + readonly parentBefore: { readonly ok: true; readonly stats: BigIntStats }; readonly chunks: Buffer[]; readonly total: number; readonly signal?: AbortSignal; diff --git a/src/reference/ReferenceSourceReaderTypes.ts b/src/reference/ReferenceSourceReaderTypes.ts index 92c517c49..9ea1a0c5e 100644 --- a/src/reference/ReferenceSourceReaderTypes.ts +++ b/src/reference/ReferenceSourceReaderTypes.ts @@ -57,15 +57,13 @@ export interface ReferenceSourceReaderError { readonly message: string; } -export type BigStats = BigIntStats; - export type PendingDirectory = { readonly path: string; }; export type TraversalState = { readonly root: string; - readonly rootIdentity: BigStats; + readonly rootIdentity: BigIntStats; readonly signal?: AbortSignal; readonly shouldExclude?: (path: string) => boolean; readonly entries: ReferenceSourceEntry[]; @@ -75,10 +73,10 @@ export type TraversalState = { export type StableFileRequest = { readonly root: string; - readonly rootIdentity: BigStats; + readonly rootIdentity: BigIntStats; readonly absolute: string; readonly path: string; - readonly expected: BigStats; + readonly expected: BigIntStats; readonly signal?: AbortSignal; }; diff --git a/src/reference/ReferenceSourceReaderValidate.ts b/src/reference/ReferenceSourceReaderValidate.ts index 01a258a28..a941f674b 100644 --- a/src/reference/ReferenceSourceReaderValidate.ts +++ b/src/reference/ReferenceSourceReaderValidate.ts @@ -1,3 +1,4 @@ +import type { BigIntStats } from "node:fs"; import { constants } from "node:fs"; import { lstat, realpath } from "node:fs/promises"; @@ -9,15 +10,14 @@ import { } from "./ReferenceSourceReaderErrors.js"; import { isPathWithinRoot } from "../domain/localPath.js"; import { - type BigStats, type ReferenceSourceFailureCode, type ReferenceSourceResult, } from "./ReferenceSourceReaderTypes.js"; -export const bigLstat = (path: string): Promise => +export const bigLstat = (path: string): Promise => lstat(path, { bigint: true }); -export const sameFile = (left: BigStats, right: BigStats): boolean => +export const sameFile = (left: BigIntStats, right: BigIntStats): boolean => left.dev === right.dev && left.ino === right.ino && left.size === right.size && @@ -32,11 +32,11 @@ export const noFollowOpenSupported = (): boolean => export const validateDirectory = async ( root: string, - rootIdentity: BigStats, + rootIdentity: BigIntStats, path: string, signal?: AbortSignal, ): Promise< - | { readonly ok: true; readonly stats: BigStats } + | { readonly ok: true; readonly stats: BigIntStats } | { readonly ok: false; readonly code: ReferenceSourceFailureCode; @@ -94,7 +94,7 @@ export const prepareRoot = async ( ): Promise< ReferenceSourceResult<{ readonly canonicalRoot: string; - readonly rootIdentity: BigStats; + readonly rootIdentity: BigIntStats; }> > => { if (isAborted(signal)) return err(cancelled()); diff --git a/src/server/createServer.ts b/src/server/createServer.ts index a56e51c07..b0d95ecbb 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -66,7 +66,7 @@ import { registerNativeTools } from "./registerNativeTools.js"; import { registerOfficialTools } from "./registerOfficialTools.js"; import { registerGuidedPrompts } from "./registerPrompts.js"; import { registerSessionTools } from "./registerSessionTools.js"; -import type { SessionAvailability } from "./sessionAvailabilityPolicy.js"; +import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js"; const TARGET_FREE_INSTRUCTIONS = @@ -94,7 +94,7 @@ export interface CreateServerOptions { readonly electronObservation?: ElectronObservationPort; readonly electronActiveObservation?: ElectronActiveObservationPort; readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort; - readonly availabilityPolicy?: () => SessionAvailability; + readonly availabilityPolicy?: () => AvailabilityPolicy; readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures; } diff --git a/src/server/registerSessionStatusTool.ts b/src/server/registerSessionStatusTool.ts index 5bc5eb4c4..c20170647 100644 --- a/src/server/registerSessionStatusTool.ts +++ b/src/server/registerSessionStatusTool.ts @@ -9,7 +9,7 @@ import type { ClientFeatureAvailability } from "../contracts/toolOutputSchemaPri import { jsonObjectSchema } from "../domain/jsonValue.js"; import { createServerIdentity } from "../serverIdentity.js"; import { mcpClientMetadata } from "./mcpClientMetadata.js"; -import type { SessionAvailability } from "./sessionAvailabilityPolicy.js"; +import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { err } from "../domain/result.js"; @@ -21,7 +21,7 @@ export interface SessionStatusToolOptions { readonly session: BinarySessionPort; readonly contract: ReturnType>; readonly startedAt: string; - readonly availabilityPolicy: () => SessionAvailability; + readonly availabilityPolicy: () => AvailabilityPolicy; readonly androidAnalysisAvailability: ( signal: AbortSignal, ) => Promise; diff --git a/src/server/registerSessionTools.ts b/src/server/registerSessionTools.ts index 9472c55ac..d4dc0b48a 100644 --- a/src/server/registerSessionTools.ts +++ b/src/server/registerSessionTools.ts @@ -29,10 +29,8 @@ import { registerUnknownTools, } from "./registerSessionRecordTools.js"; import { registerSessionStatusTool } from "./registerSessionStatusTool.js"; -import { - sessionAvailabilityPolicy, - type SessionAvailability, -} from "./sessionAvailabilityPolicy.js"; +import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js"; +import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; @@ -140,7 +138,7 @@ export interface LifecycleToolRegistration { readonly closeContract: ReturnType>; readonly statusContract: ReturnType>; readonly startedAt: string; - readonly availabilityPolicy: () => SessionAvailability; + readonly availabilityPolicy: () => AvailabilityPolicy; readonly androidAnalysisAvailability: ( signal: AbortSignal, ) => Promise; @@ -218,7 +216,7 @@ const registerOpenLifecycleTool = ({ /** Register MCP-only target lifecycle operations on a long-lived session. */ export interface SessionToolOptions { readonly startedAt?: string; - readonly availabilityPolicy?: () => SessionAvailability; + readonly availabilityPolicy?: () => AvailabilityPolicy; readonly androidAnalysisAvailability?: ( signal: AbortSignal, ) => Promise; diff --git a/src/server/sessionAvailabilityPolicy.ts b/src/server/sessionAvailabilityPolicy.ts index 66259f4a8..14996091d 100644 --- a/src/server/sessionAvailabilityPolicy.ts +++ b/src/server/sessionAvailabilityPolicy.ts @@ -2,14 +2,12 @@ import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js"; import { platform } from "node:process"; -export type SessionAvailability = AvailabilityPolicy; - export interface SessionAvailabilityDefaults { readonly optionalProviderLoadFailures?: | OptionalProviderLoadFailures | undefined; readonly optionalFeatures?: Pick< - SessionAvailability, + AvailabilityPolicy, | "evmInterfaceEnabled" | "browserObservationEnabled" | "browserScenarioEnabled" @@ -27,9 +25,9 @@ export interface SessionAvailabilityDefaults { /** Select configured availability reporting or the target-free defaults. */ export const sessionAvailabilityPolicy = ( - configured: (() => SessionAvailability) | undefined, + configured: (() => AvailabilityPolicy) | undefined, defaults: SessionAvailabilityDefaults, -): (() => SessionAvailability) => { +): (() => AvailabilityPolicy) => { const policy = configured ?? (() => ({ diff --git a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts index b23e5edb2..5d54cc154 100644 --- a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts +++ b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts @@ -4,8 +4,8 @@ import { join } from "node:path"; import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { describe, expect, it } from "vitest"; -import { compareCodePoints } from "../../../src/domain/canonicalOrdering.js"; -import { canonicalDigest } from "../../../src/domain/comparisonSemantics.js"; +import { compareUnicodeCodePoints } from "../../../src/domain/unicodeCodePointOrder.js"; +import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js"; import { compareArtifacts } from "../../../src/domain/artifactComparison.js"; import type { ArtifactInventoryResult } from "../../../src/domain/artifactGraph.js"; @@ -54,9 +54,9 @@ const directoryIdForChildren = ( inventory: ArtifactInventoryResult, paths: readonly string[], ) => - `art_${canonicalDigest( + `art_${digestCanonicalValue( { - sha256: canonicalDigest( + sha256: digestCanonicalValue( { kind: "directory", children: paths @@ -69,7 +69,7 @@ const directoryIdForChildren = ( }; }) .sort((left, right) => - compareCodePoints(String(left.name), String(right.name)), + compareUnicodeCodePoints(String(left.name), String(right.name)), ), }, "Artifact", diff --git a/tests/boundary/process/cliEnvironmentSeam.test.ts b/tests/boundary/process/cliEnvironmentSeam.test.ts index f3304bcdc..fd1f03421 100644 --- a/tests/boundary/process/cliEnvironmentSeam.test.ts +++ b/tests/boundary/process/cliEnvironmentSeam.test.ts @@ -70,7 +70,7 @@ describe("the CLI takes its environment as an input", () => { // environment rather than whatever the process happens to carry. const result = await createDirectAnalysis({ REA_LOG_LEVEL: "not-a-level", - }).runCapabilityStatus(); + }).runSessionStatus(); expect(result).toMatchObject({ error: expect.anything() }); }); }); diff --git a/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts b/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts index b934e3e36..a75db7b0a 100644 --- a/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts +++ b/tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts @@ -144,6 +144,26 @@ it("reports semantic value resource limits in application graph coverage", async }), ]), }); + const sourceModuleLimits = graph.nodes + .filter(({ kind }) => kind === "javascript-module") + .flatMap(({ observations }) => observations) + .filter( + ({ evidence }) => + evidence.extractor.operation === "recover-source-module", + ) + .flatMap(({ evidence }) => evidence.coverage.limits); + expect(sourceModuleLimits).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "javascript_semantic_primitive_candidates", + unit: "items", + }), + expect.objectContaining({ + name: "javascript_semantic_primitive_bytes", + unit: "bytes", + }), + ]), + ); expect(graph.limitations).toContain( "Primitive candidate budget exceeded (maximum 256 alternatives).", );