mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(evidence): share canonical ASCII ordering rules
This commit is contained in:
1 parent
1ac7ce3f3b
commit
c465ff5a81
3 files changed
+38
-35
No files matched your search
@@ -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,
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user