refactor(domain): reuse canonical ordering and digest owners

This commit is contained in:
morluto
2026-10-22 14:04:28 +08:00
parent c537c52c96
commit 51be954c4b
6 changed files with 37 additions and 55 deletions
@@ -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 = <Value>(
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")}`;
+5 -9
View File
@@ -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<
+12
View File
@@ -5,6 +5,18 @@ export const uniqueSorted = <Value extends string>(
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 = <Value>(
values: readonly Value[],
key: (value: Value) => string,
): Value[] => {
const byKey = new Map<string, Value>();
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;
@@ -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,
),
@@ -829,15 +829,6 @@ export const registrationKey = (
]),
]);
/** Sort values by a deterministic unique semantic key. */
export const sortedUnique = <Value>(
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");
+5 -6
View File
@@ -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[],