/*
RFC-0212: the claim annotation surface. A per-record provenance sidecar
`.claims.yaml` is a map of field path → annotation, turning a bare field
into a CKL claim (RFC-0211) without touching the editable record body. Closed Zod
schema so `content.claim.validate` can shape-check every sidecar.
Moved from @warpgogol/business/schemas to @warpgogol/werkstatt-shared/schemas as part of RFC-0470
(legacy business layer deletion). This is Content Knowledge Lifecycle (CKL)
infrastructure, not business-layer code.
- Do not resolve field paths, evaluate freshness, hash derivations, or read the filesystem — the kernel validators do that.
- Do not wrap or validate the record value itself; a claim only annotates.
- RFC-0212: initial claim annotation + record-claims schemas.
- RFC-0470: moved from @warpgogol/business/schemas/claims.ts to @warpgogol/werkstatt-shared/schemas/claims.ts.
*/
import { z } from "zod";
/** ISO 8601 calendar date, e.g. 2026-06-20. */
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
/** ISO 8601 duration with at least one component, e.g. P3M, P1Y, P2W, PT12H. */
const ISO8601_DURATION = /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$/;
const isoDate = z.string().regex(ISO_DATE, "must be an ISO 8601 date (YYYY-MM-DD)");
/**
* One field's claim annotation (RFC-0211 claim, minus the value which stays in the
* record body). `.strict()` so a typo'd key is caught as a schema violation
* (CKL-CLAIM-01) instead of silently ignored.
*/
export const claimAnnotationSchema = z
.object({
claimClass: z
.enum(["general", "legal", "price", "comparative-commercial"])
.optional()
.default("general"),
provenance: z.enum(["external", "derived", "asserted", "generated"]),
/** ISO date the value was last verified / asserted. */
asOf: isoDate,
/** ISO date after which the claim is stale. */
validUntil: isoDate.optional(),
/** ISO 8601 recurring review cadence, e.g. "P3M". */
reviewEvery: z
.string()
.regex(ISO8601_DURATION, "must be an ISO 8601 duration (e.g. P3M)")
.refine((v) => v !== "P" && v !== "PT", "duration must have at least one component")
.optional(),
/** → source descriptor id (RFC-0214) when provenance = external. */
sourceRef: z.string().min(1).optional(),
/** Canonical source subject string (RFC-0215) when provenance = derived. */
derivedFrom: z.string().min(1).optional(),
/** Normalized hash of the source value at derivation time (RFC-0215). */
sourceHash: z.string().min(1).optional(),
comparedEntity: z
.object({
name: z.string().min(1),
kind: z.enum(["vendor", "marketplace", "platform", "competitor-category"]),
url: z.string().url().optional(),
})
.optional(),
claimKind: z
.enum(["third-party-price", "capability", "ownership", "export", "support", "risk", "other"])
.optional(),
statement: z.string().min(1).optional(),
value: z
.object({
amount: z.number().optional(),
min: z.number().optional(),
max: z.number().optional(),
currency: z.string().min(1).optional(),
unit: z.string().min(1).optional(),
})
.optional(),
publicDisclosure: z
.object({
label: z.string().min(1),
showStandDate: z.literal(true),
showSourceLabel: z.boolean(),
})
.optional(),
criticality: z.enum(["advisory", "important", "blocking"]).optional(),
/** Responsible agent/human handle — routing (RFC-0216). */
owner: z.string().min(1).optional(),
confidence: z.enum(["high", "medium", "low"]).optional(),
})
.strict()
.superRefine((claim, ctx) => {
if (claim.claimClass !== "comparative-commercial") return;
const required: Array = [
"comparedEntity",
"claimKind",
"statement",
"sourceRef",
"reviewEvery",
"publicDisclosure",
"criticality",
];
for (const key of required) {
if (claim[key] === undefined) {
ctx.addIssue({
code: "custom",
path: [key],
message: "required for comparative-commercial claims",
});
}
}
if (claim.provenance !== "external") {
ctx.addIssue({
code: "custom",
path: ["provenance"],
message: "comparative-commercial claims must use external provenance",
});
}
if (claim.criticality !== "blocking") {
ctx.addIssue({
code: "custom",
path: ["criticality"],
message: "comparative-commercial claims must be blocking",
});
}
if (claim.claimKind === "third-party-price") {
if (!claim.value?.currency)
ctx.addIssue({
code: "custom",
path: ["value", "currency"],
message: "third-party-price claims require value.currency",
});
if (!claim.value?.unit)
ctx.addIssue({
code: "custom",
path: ["value", "unit"],
message: "third-party-price claims require value.unit",
});
}
});
export type ClaimAnnotation = z.infer;
/** The full `.claims.yaml` sidecar: fieldPath → annotation. */
export const recordClaimsSchema = z.record(z.string().min(1), claimAnnotationSchema);
export type RecordClaims = z.infer;