diff --git a/src/domain/canonicalOrdering.ts b/src/domain/canonicalOrdering.ts index 9ab153d346..8e97efd0dc 100644 --- a/src/domain/canonicalOrdering.ts +++ b/src/domain/canonicalOrdering.ts @@ -4,3 +4,14 @@ import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js"; export const uniqueSorted = ( values: readonly Value[], ): Value[] => [...new Set(values)].sort(compareUnicodeCodePoints); + +/** 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; + +/** Whether validated ASCII strings are already ordered without duplicates. */ +export const isSortedUniqueAscii = (values: readonly string[]): boolean => + values.every( + (value, index) => + index === 0 || compareAsciiStrings(values[index - 1] ?? "", value) < 0, + ); diff --git a/src/domain/completionLedgerGeneration.ts b/src/domain/completionLedgerGeneration.ts index f356b94100..c52da3f455 100644 --- a/src/domain/completionLedgerGeneration.ts +++ b/src/domain/completionLedgerGeneration.ts @@ -4,6 +4,10 @@ import canonicalize from "canonicalize"; import { z } from "zod"; import { IDENTIFIER_PATTERN } from "./stringPatterns.js"; +import { + compareAsciiStrings, + isSortedUniqueAscii, +} from "./canonicalOrdering.js"; import { createEvidenceCompletionLedger, @@ -121,15 +125,6 @@ const completionManifestObjectSchema = z.strictObject({ }); type CompletionManifestValue = z.infer; -const compareIdentifiers = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; - -const isSortedUnique = (values: readonly string[]): boolean => - values.every( - (value, index) => - index === 0 || compareIdentifiers(values[index - 1] ?? "", value) < 0, - ); - const canonicalJson = (value: unknown): string => { const serialized = canonicalize(value); if (serialized === undefined) @@ -148,20 +143,22 @@ const manifestIssues = ( manifest: CompletionManifestValue, context: z.RefinementCtx, ): void => { - if (!isSortedUnique(manifest.claims.map(({ claim_id }) => claim_id))) + if (!isSortedUniqueAscii(manifest.claims.map(({ claim_id }) => claim_id))) context.addIssue({ code: "custom", path: ["claims"], message: "Manifest claims must be sorted by unique claim ID", }); - if (!isSortedUnique(manifest.skill_digests.map(({ skill_id }) => skill_id))) + if ( + !isSortedUniqueAscii(manifest.skill_digests.map(({ skill_id }) => skill_id)) + ) context.addIssue({ code: "custom", path: ["skill_digests"], message: "Skill digests must be sorted by unique skill ID", }); for (const [index, claim] of manifest.claims.entries()) - if (!isSortedUnique(claim.artifact_sha256s)) + if (!isSortedUniqueAscii(claim.artifact_sha256s)) context.addIssue({ code: "custom", path: ["claims", index, "artifact_sha256s"], @@ -194,10 +191,10 @@ const canonicalClaims = ( claims .map((claim) => ({ ...claim, - artifact_sha256s: [...claim.artifact_sha256s].sort(compareIdentifiers), - evidence_ids: [...claim.evidence_ids].sort(compareIdentifiers), + artifact_sha256s: [...claim.artifact_sha256s].sort(compareAsciiStrings), + evidence_ids: [...claim.evidence_ids].sort(compareAsciiStrings), })) - .sort((left, right) => compareIdentifiers(left.claim_id, right.claim_id)); + .sort((left, right) => compareAsciiStrings(left.claim_id, right.claim_id)); /** Generate canonical manifest and ledger artifacts from live verifier output. */ export const createCompletionLedgerArtifacts = ( @@ -206,18 +203,18 @@ export const createCompletionLedgerArtifacts = ( ): CompletionLedgerArtifacts => { const report = completionVerifierReportSchema.parse(input); const claims = canonicalClaims(report.claims); - if (!isSortedUnique(claims.map(({ claim_id }) => claim_id))) + if (!isSortedUniqueAscii(claims.map(({ claim_id }) => claim_id))) throw new TypeError("Completion verifier claim IDs must be unique"); for (const claim of claims) { - if (!isSortedUnique(claim.artifact_sha256s)) + if (!isSortedUniqueAscii(claim.artifact_sha256s)) throw new TypeError("Artifact digests must be unique per claim"); - if (!isSortedUnique(claim.evidence_ids)) + if (!isSortedUniqueAscii(claim.evidence_ids)) throw new TypeError("Evidence IDs must be unique per claim"); } const skills = skillDigests .map((skill) => skillDigestSchema.parse(skill)) - .sort((left, right) => compareIdentifiers(left.skill_id, right.skill_id)); - if (!isSortedUnique(skills.map(({ skill_id }) => skill_id))) + .sort((left, right) => compareAsciiStrings(left.skill_id, right.skill_id)); + if (!isSortedUniqueAscii(skills.map(({ skill_id }) => skill_id))) throw new TypeError("Completion skill IDs must be unique"); const projection = { verifier: report.verifier, diff --git a/src/domain/evidenceCompletionLedger.ts b/src/domain/evidenceCompletionLedger.ts index bf3e0f9e98..f725983a97 100644 --- a/src/domain/evidenceCompletionLedger.ts +++ b/src/domain/evidenceCompletionLedger.ts @@ -5,6 +5,10 @@ import { z } from "zod"; import { IDENTIFIER_PATTERN } from "./stringPatterns.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; +import { + compareAsciiStrings, + isSortedUniqueAscii, +} from "./canonicalOrdering.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const claimIdSchema = z.string().regex(IDENTIFIER_PATTERN); @@ -57,15 +61,6 @@ type EvidenceCompletionLedgerValue = z.infer< typeof completionLedgerObjectSchema >; -const compareIdentifiers = (left: string, right: string): number => - left < right ? -1 : left > right ? 1 : 0; - -const isSortedUnique = (values: readonly string[]): boolean => - values.every( - (value, index) => - index === 0 || compareIdentifiers(values[index - 1] ?? "", value) < 0, - ); - const summarize = ( records: readonly EvidenceCompletionRecord[], ): EvidenceCompletionLedger["summary"] => { @@ -104,14 +99,14 @@ const ledgerIssues = ( ledger: EvidenceCompletionLedgerValue, context: z.RefinementCtx, ): void => { - if (!isSortedUnique(ledger.records.map((record) => record.claim_id))) + if (!isSortedUniqueAscii(ledger.records.map((record) => record.claim_id))) context.addIssue({ code: "custom", path: ["records"], message: "Completion records must be sorted by unique claim ID", }); for (const [index, record] of ledger.records.entries()) - if (!isSortedUnique(record.evidence_ids)) + if (!isSortedUniqueAscii(record.evidence_ids)) context.addIssue({ code: "custom", path: ["records", index, "evidence_ids"], @@ -149,13 +144,13 @@ export const createEvidenceCompletionLedger = ( .map((record) => evidenceCompletionRecordSchema.parse(record)) .map((record) => ({ ...record, - evidence_ids: [...record.evidence_ids].sort(compareIdentifiers), + evidence_ids: [...record.evidence_ids].sort(compareAsciiStrings), })) - .sort((left, right) => compareIdentifiers(left.claim_id, right.claim_id)); - if (!isSortedUnique(records.map((record) => record.claim_id))) + .sort((left, right) => compareAsciiStrings(left.claim_id, right.claim_id)); + if (!isSortedUniqueAscii(records.map((record) => record.claim_id))) throw new TypeError("Completion claim IDs must be unique"); for (const record of records) - if (!isSortedUnique(record.evidence_ids)) + if (!isSortedUniqueAscii(record.evidence_ids)) throw new TypeError("Completion Evidence IDs must be unique per claim"); return evidenceCompletionLedgerSchema.parse({ ledger_id: computeLedgerId(records),