From b885a160bdf45624dd8fe5bd9800707700d4fb43 Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Wed, 16 Sep 2026 15:34:45 +0200 Subject: [PATCH] feat(core): add upstream provenance registry validation --- packages/c0py-core/src/provenance/registry.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/c0py-core/src/provenance/registry.ts diff --git a/packages/c0py-core/src/provenance/registry.ts b/packages/c0py-core/src/provenance/registry.ts new file mode 100644 index 0000000..9514c98 --- /dev/null +++ b/packages/c0py-core/src/provenance/registry.ts @@ -0,0 +1,50 @@ +import type { UpstreamCapabilityRecord } from "@siax/c0py-types"; + +const COPyleft_ISOLATED = new Set(["AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later"]); + +export interface ProvenanceValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + +export function validateUpstreamCapability( + record: UpstreamCapabilityRecord, +): ProvenanceValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + const source = record.source; + + if (!record.capability.trim()) errors.push("capability is required"); + if (!record.rationale.trim()) errors.push("rationale is required"); + if (record.disposition !== "DROP" && record.tests.length === 0) { + errors.push("at least one verification test is required"); + } + + if (["CHERRY_PICK", "REFERENCE_ONLY", "REIMPLEMENT"].includes(record.disposition)) { + if (!source.repository) errors.push("source.repository is required"); + if (!source.commit) errors.push("source.commit is required"); + } + + if (record.disposition === "CHERRY_PICK") { + if (!source.sourcePaths?.length) errors.push("source.sourcePaths is required for CHERRY_PICK"); + if (!source.license) errors.push("source.license is required for CHERRY_PICK"); + } + + if (source.license && COPyleft_ISOLATED.has(source.license) && record.disposition === "CHERRY_PICK") { + errors.push("AGPL/copy-left source must remain behind an adapter/service boundary; do not cherry-pick into core"); + } + + if (source.license && COPyleft_ISOLATED.has(source.license) && record.disposition !== "ADAPTER") { + warnings.push("copyleft source should normally be classified as ADAPTER"); + } + + return { valid: errors.length === 0, errors, warnings }; +} + +export function assertUpstreamCapability(record: UpstreamCapabilityRecord): void { + const result = validateUpstreamCapability(record); + if (!result.valid) { + throw new Error(`Invalid upstream capability ${record.id}: ${result.errors.join("; ")}`); + } +}