refactor(evidence): share canonical ASCII ordering rules

This commit is contained in:
morluto committed 2026-10-22 09:21:54 +08:00
1 parent 1ac7ce3f3b
commit c465ff5a81
3 files changed
+38 -35

No files matched your search

+11
View File
@@ -4,3 +4,14 @@ import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
export const uniqueSorted = <Value extends string>(
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,
);
+17 -20
View File
@@ -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<typeof completionManifestObjectSchema>;
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,
+10 -15
View File
@@ -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),