From 51be954c4b04bf3f5cf015202dd7d0123bfbb30d Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:04:28 +0800 Subject: [PATCH] refactor(domain): reuse canonical ordering and digest owners --- .../ReconstructionObligationCandidates.ts | 42 ++++++------------- src/domain/apple/appleApplication.ts | 14 +++---- src/domain/canonicalOrdering.ts | 12 ++++++ .../javascript/javascriptStaticAnalysis.ts | 4 +- .../javascriptStaticAnalysisHelpers.ts | 9 ---- src/domain/residualUnknown.ts | 11 +++-- 6 files changed, 37 insertions(+), 55 deletions(-) diff --git a/src/application/ReconstructionObligationCandidates.ts b/src/application/ReconstructionObligationCandidates.ts index 392499a754..6b72ab2b2e 100644 --- a/src/application/ReconstructionObligationCandidates.ts +++ b/src/application/ReconstructionObligationCandidates.ts @@ -1,6 +1,5 @@ -import { createHash } from "node:crypto"; - -import canonicalize from "canonicalize"; +import { uniqueSorted, uniqueSortedBy } from "../domain/canonicalOrdering.js"; +import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import type { Evidence } from "../domain/evidence.js"; import { @@ -407,12 +406,12 @@ const addCandidate = ( } candidates.set(candidate.obligation_id, { ...current, - authority_references: uniqueBy( + authority_references: uniqueSortedBy( [...current.authority_references, ...candidate.authority_references], (reference) => `${reference.evidence_id}:${reference.state}:${reference.location}`, ), - observed_cases: uniqueBy( + observed_cases: uniqueSortedBy( [...current.observed_cases, ...candidate.observed_cases], (item) => `${item.kind}:${item.evidence_id}:${item.location}`, ), @@ -421,14 +420,14 @@ const addCandidate = ( ...candidate.authority_references, ]), // Each observation keeps its own unresolved authority and next steps. - unavailable_authority: uniqueBy( - [...current.unavailable_authority, ...candidate.unavailable_authority], - (item) => item, - ), - required_next_evidence: uniqueBy( - [...current.required_next_evidence, ...candidate.required_next_evidence], - (item) => item, - ), + unavailable_authority: uniqueSorted([ + ...current.unavailable_authority, + ...candidate.unavailable_authority, + ]), + required_next_evidence: uniqueSorted([ + ...current.required_next_evidence, + ...candidate.required_next_evidence, + ]), // A generated ID digests its derivation key, so two generated candidates // with one ID are the same claim seen by several records. Only another // reviewed definition can introduce a competing definition. @@ -448,20 +447,5 @@ const sourceState = ( return "unknown"; }; -const uniqueBy = ( - values: readonly Value[], - key: (value: Value) => string, -): Value[] => - [...new Map(values.map((value) => [key(value), value])).values()].sort( - (left, right) => compareUnicodeCodePoints(key(left), key(right)), - ); - const obligationId = (key: string): string => - `obl_${digest({ schema: "ReconstructionObligation", key })}`; - -const digest = (value: unknown): string => { - const encoded = canonicalize(value); - if (encoded === undefined) - throw new TypeError("Reconstruction obligation key is not canonical JSON"); - return createHash("sha256").update(encoded).digest("hex"); -}; + `obl_${digestCanonicalValue({ schema: "ReconstructionObligation", key }, "Reconstruction obligation key")}`; diff --git a/src/domain/apple/appleApplication.ts b/src/domain/apple/appleApplication.ts index fc70e7070e..20ccc0db51 100644 --- a/src/domain/apple/appleApplication.ts +++ b/src/domain/apple/appleApplication.ts @@ -34,6 +34,7 @@ import { unavailableComponentSchema, type ProjectedInventoryEntry, } from "../mobileApplicationGraphProjection.js"; +import { uniqueSortedBy } from "../canonicalOrdering.js"; import { compareUnicodeCodePoints as compare } from "../unicodeCodePointOrder.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); @@ -145,10 +146,10 @@ export const projectAppleApplication = ( const runtimeFamilies = identifyRuntimeFamilies(all); const bridgeProjection = identifyBridgeCandidates( classified.javascript, - deduplicateComponents([ - ...classified.executables, - ...classified.native_libraries, - ]), + uniqueSortedBy( + [...classified.executables, ...classified.native_libraries], + (component) => component.path, + ), roots, ); const uninventoriedImage = @@ -304,11 +305,6 @@ const classifyComponents = ( }; }; -const deduplicateComponents = (values: readonly Component[]): Component[] => - [...new Map(values.map((value) => [value.path, value])).values()].sort( - (left, right) => compare(left.path, right.path), - ); - const identifyRuntimeFamilies = (all: readonly Component[]) => { const families = new Set< z.infer< diff --git a/src/domain/canonicalOrdering.ts b/src/domain/canonicalOrdering.ts index 8e97efd0dc..5b4f59124c 100644 --- a/src/domain/canonicalOrdering.ts +++ b/src/domain/canonicalOrdering.ts @@ -5,6 +5,18 @@ export const uniqueSorted = ( values: readonly Value[], ): Value[] => [...new Set(values)].sort(compareUnicodeCodePoints); +/** Retain the last value for each exact key, deriving keys once per input. */ +export const uniqueSortedBy = ( + values: readonly Value[], + key: (value: Value) => string, +): Value[] => { + const byKey = new Map(); + for (const value of values) byKey.set(key(value), value); + return [...byKey] + .sort(([left], [right]) => compareUnicodeCodePoints(left, right)) + .map(([, value]) => value); +}; + /** Lexical ordering for identifiers and digests already validated as ASCII. */ export const compareAsciiStrings = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0; diff --git a/src/domain/javascript/javascriptStaticAnalysis.ts b/src/domain/javascript/javascriptStaticAnalysis.ts index 0c84220e0b..e6c5cd79fe 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.ts @@ -1,3 +1,4 @@ +import { uniqueSortedBy } from "../canonicalOrdering.js"; import * as t from "@babel/types"; import { RETURNED_REFERENCE_WIDENING_LIMITATION } from "./javascriptSemanticCoverage.js"; import { collectElectronWrappedCallPaths } from "./electronWrappedCallPaths.js"; @@ -20,7 +21,6 @@ import { detectVendors, failedJavaScriptStaticAnalysis, registrationKey, - sortedUnique, stringValue, } from "./javascriptStaticAnalysisHelpers.js"; import { @@ -218,7 +218,7 @@ const finalizeStaticAnalysis = ( accumulator.storage, accumulator.moduleRangeIndex, ), - bundler_registrations: sortedUnique( + bundler_registrations: uniqueSortedBy( accumulator.registrations, registrationKey, ), diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index 8a5b2fab17..7a28037d0f 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -829,15 +829,6 @@ export const registrationKey = ( ]), ]); -/** Sort values by a deterministic unique semantic key. */ -export const sortedUnique = ( - values: readonly Value[], - key: (value: Value) => string, -): Value[] => - [...new Map(values.map((value) => [key(value), value])).values()].sort( - (left, right) => compareUnicodeCodePoints(key(left), key(right)), - ); - /** Hash exact UTF-8 source text. */ export const sha256Text = (value: string): string => createHash("sha256").update(value).digest("hex"); diff --git a/src/domain/residualUnknown.ts b/src/domain/residualUnknown.ts index 94e9087391..cfa45ae10d 100644 --- a/src/domain/residualUnknown.ts +++ b/src/domain/residualUnknown.ts @@ -1,3 +1,4 @@ +import { uniqueSorted } from "./canonicalOrdering.js"; import { z } from "zod"; import { digestCanonicalValue } from "./canonicalDigest.js"; import { @@ -51,7 +52,7 @@ const comparisonConclusionScopeSchema = z }) .strict() .superRefine(({ dimensions }, context) => { - if (!sameOrderedValues(dimensions, sortedUnique(dimensions))) + if (!sameOrderedValues(dimensions, uniqueSorted(dimensions))) context.addIssue({ code: "custom", message: "Comparison dimensions must be sorted and unique", @@ -339,7 +340,7 @@ export const updateResidualUnknown = ( : { comparison_conclusion_scope: current.comparison_conclusion_scope }), ...projectRevisionDetails(input), resolution: input.resolution, - mutation_evidence_ids: sortedUnique([ + mutation_evidence_ids: uniqueSorted([ ...current.mutation_evidence_ids, mutationEvidenceId, ]), @@ -367,8 +368,8 @@ type RevisionDetailsInput = Pick< const projectRevisionDetails = (input: RevisionDetailsInput) => ({ severity: input.severity, - supporting_evidence_ids: sortedUnique(input.supporting_evidence_ids), - contradicting_evidence_ids: sortedUnique(input.contradicting_evidence_ids), + supporting_evidence_ids: uniqueSorted(input.supporting_evidence_ids), + contradicting_evidence_ids: uniqueSorted(input.contradicting_evidence_ids), required_authority: input.required_authority, required_confidence: input.required_confidence, required_environment: input.required_environment, @@ -385,8 +386,6 @@ const computeRevisionDigest = ( return digestCanonicalValue(semantic, "Unknown identity"); }; -const sortedUnique = (values: readonly string[]): string[] => - [...new Set(values)].sort(compareUnicodeCodePoints); const sameOrderedValues = ( left: readonly string[], right: readonly string[],