From d3c5d68d084a35d0cffffbf8a61d6990aef5bf21 Mon Sep 17 00:00:00 2001 From: Timothy Kompanchenko Date: Sat, 18 Apr 2026 08:17:20 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(spec):=20v0.7.5=20=E2=80=94=20signatur?= =?UTF-8?q?es=20schema=20alignment=20+=20composition-pack=20file=20require?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the spec and its normative grammar with what the current sealer and relay already produce and consume. Also closes the composition-pack validation hole. Signatures schema (kp-signatures.schema.json): - Regenerated from ARCHIVE.md §4. Required: algorithm, pack_hash, files, sealed_at, sealed_by. Optional: pack_id, pack_name, pack_version, parent, signature. - Removed pre-v0.7.3 fields (signed_at, signing_key, and flat-string signature) that no current sealer writes. - Root closed with additionalProperties: false. pack_id documented (ARCHIVE.md §4): - Added to the field types table and required/optional table. - Prose policy: OPTIONAL in schema for compatibility with archives sealed before the field was documented; new archives SHOULD include it. pack_name and pack_version also documented as optional self-describing receipts. Composition-pack file requirements (SPEC.md §2): - New subsection specifying that packs with composition.yaml MAY omit evidence.md. claims.md remains REQUIRED; its content is intentionally minimal per COMPOSITION.md §3 (context about the composition itself). Conformance runner (conformance/run.py): - validate_pack() now detects composition packs and skips the evidence.md presence check for them; evidence-ID extraction tolerates a missing evidence.md. - signatures.yaml is validated against kp-signatures.schema.json when present. Previously the runner was silent on signatures. Fixtures: - composition.kpack (valid) — minimum-shape composition pack. - maximal.kpack/signatures.yaml — exercises the refreshed schema with the optional parent block and self-describing receipt fields. Co-Authored-By: Claude --- .../valid/composition.kpack/PACK.yaml | 4 + .../valid/composition.kpack/claims.md | 23 +++++ .../valid/composition.kpack/composition.yaml | 25 +++++ .../valid/maximal.kpack/signatures.yaml | 14 +++ conformance/grammar/kp-signatures.schema.json | 91 ++++++++++++------- conformance/run.py | 41 ++++++++- spec/ARCHIVE.md | 14 +++ spec/CHANGELOG.md | 21 +++++ spec/SPEC.md | 14 +++ 9 files changed, 210 insertions(+), 37 deletions(-) create mode 100644 conformance/fixtures/valid/composition.kpack/PACK.yaml create mode 100644 conformance/fixtures/valid/composition.kpack/claims.md create mode 100644 conformance/fixtures/valid/composition.kpack/composition.yaml create mode 100644 conformance/fixtures/valid/maximal.kpack/signatures.yaml diff --git a/conformance/fixtures/valid/composition.kpack/PACK.yaml b/conformance/fixtures/valid/composition.kpack/PACK.yaml new file mode 100644 index 0000000..f0b0d25 --- /dev/null +++ b/conformance/fixtures/valid/composition.kpack/PACK.yaml @@ -0,0 +1,4 @@ +name: composition-fixture +version: 2026.04.18 +domain: test/conformance +author: KP Conformance Suite diff --git a/conformance/fixtures/valid/composition.kpack/claims.md b/conformance/fixtures/valid/composition.kpack/claims.md new file mode 100644 index 0000000..eb77ed1 --- /dev/null +++ b/conformance/fixtures/valid/composition.kpack/claims.md @@ -0,0 +1,23 @@ + +--- +pack: composition-fixture | v: 2026.04.18 | domain: test/conformance +confidence: simple | normalized +--- + +# Composition Fixture + +> Composition pack fixture. References other packs via `composition.yaml` — see `minimal-fixture` and `typical-fixture`. Evidence lives in those referenced packs, not here. This pack carries composition-context narrative only. + +## Composition Context + +This fixture exercises the composition-pack path in the conformance runner. It has `composition.yaml` present, no `evidence.md`, and only narrative content in `claims.md` — no dense claim bullets to cite evidence for. + +A composition pack SHOULD contain claim bullets when its composition context warrants them (e.g., a meeting pack with claims about the meeting itself). This fixture intentionally omits them to prove the minimum-shape case. diff --git a/conformance/fixtures/valid/composition.kpack/composition.yaml b/conformance/fixtures/valid/composition.kpack/composition.yaml new file mode 100644 index 0000000..a2502d2 --- /dev/null +++ b/conformance/fixtures/valid/composition.kpack/composition.yaml @@ -0,0 +1,25 @@ +type: meeting-prep + +meeting: + date: 2026-04-18T10:00:00-04:00 + duration: 30m + participants: + - Alice + - Bob + context: > + Conformance test fixture — demonstrates a valid composition pack + structure with no evidence.md. All knowledge lives in referenced + packs; this pack only declares the composition. + +agenda: + - topic: Minimal pack coverage + reference: minimal-fixture + talking_points: + - Confirm frontmatter parsing + - Walk through a single-claim pack + +pre_load: + - minimal-fixture + +on_demand: + - typical-fixture diff --git a/conformance/fixtures/valid/maximal.kpack/signatures.yaml b/conformance/fixtures/valid/maximal.kpack/signatures.yaml new file mode 100644 index 0000000..5248396 --- /dev/null +++ b/conformance/fixtures/valid/maximal.kpack/signatures.yaml @@ -0,0 +1,14 @@ +algorithm: SHA-256 +pack_id: "0b3c1f9a-4d7e-4a1c-9f2b-3e4a5b6c7d8e" +pack_name: maximal-fixture +pack_version: "2026.04.04-rev2" +pack_hash: "0000000000000000000000000000000000000000000000000000000000000000" +files: + PACK.yaml: "1111111111111111111111111111111111111111111111111111111111111111" + claims.md: "2222222222222222222222222222222222222222222222222222222222222222" + evidence.md: "3333333333333333333333333333333333333333333333333333333333333333" +sealed_at: "2026-04-18T10:00:00Z" +sealed_by: capture-client +parent: + version: "2026.04.04-rev1" + pack_hash: "4444444444444444444444444444444444444444444444444444444444444444" diff --git a/conformance/grammar/kp-signatures.schema.json b/conformance/grammar/kp-signatures.schema.json index f1aa575..3d530d6 100644 --- a/conformance/grammar/kp-signatures.schema.json +++ b/conformance/grammar/kp-signatures.schema.json @@ -2,65 +2,88 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://kp.dev/schemas/kp-signatures-v1.json", "title": "KP:1 signatures.yaml Schema", - "description": "JSON Schema for the signatures.yaml integrity manifest in a KP:1 knowledge pack. Provides cryptographic hashes and optional signing for integrity verification.", + "description": "JSON Schema for the signatures.yaml integrity manifest in a KP:1 knowledge pack. Aligned with ARCHIVE.md §4 (v0.7.3+).", "type": "object", - "required": ["algorithm", "files"], + "required": ["algorithm", "pack_hash", "files", "sealed_at", "sealed_by"], "additionalProperties": false, "properties": { "algorithm": { "type": "string", - "enum": ["sha-256", "sha-384", "sha-512"], - "description": "Hash algorithm used for all file digests." + "enum": ["SHA-256"], + "description": "Hash algorithm. Currently only SHA-256 (future-proofed for upgrades)." }, - "signed_at": { + "pack_id": { "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp of when the hashes were computed and optionally signed." + "description": "Stable pack identity across versions. OPTIONAL in the schema for backward compatibility with pre-documentation archives; new archives SHOULD include it." + }, + "pack_name": { + "type": "string", + "description": "Optional self-describing pack name (equal to PACK.yaml name). Enables receipts and relay indexing without a second file read." + }, + "pack_version": { + "type": "string", + "description": "Optional self-describing pack version (equal to PACK.yaml version). Enables receipts without a second file read." + }, + "pack_hash": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Pack content hash computed per ARCHIVE.md §3 (lowercase hex SHA-256 digest, 64 characters)." }, "files": { "type": "object", "additionalProperties": { "type": "string", - "pattern": "^[a-f0-9]+$", - "description": "Hex-encoded content hash for the file." + "pattern": "^[a-f0-9]{64}$" }, "minProperties": 1, - "description": "Map of file paths (relative to pack root) to their hex-encoded content hashes. At minimum should include PACK.yaml and claims.md." + "description": "Map of NFC-normalized relative file paths (with / separator) to their lowercase hex SHA-256 digests. MUST NOT include signatures.yaml itself." }, - "signing_key": { + "sealed_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 UTC timestamp of sealing (e.g., 2026-04-12T14:30:00Z)." + }, + "sealed_by": { + "type": "string", + "description": "Identifier of the sealing system (e.g., capture-client, analysis-service)." + }, + "parent": { "type": "object", - "required": ["type", "public_key"], + "required": ["version", "pack_hash"], "additionalProperties": false, "properties": { - "type": { + "version": { "type": "string", - "enum": ["ed25519", "ecdsa-p256", "rsa-4096"], - "description": "Key algorithm." + "description": "Parent pack's version string from its PACK.yaml." }, - "public_key": { + "pack_hash": { "type": "string", - "description": "Base64-encoded public key." - }, - "key_id": { - "type": "string", - "description": "Optional key identifier for key management systems." + "pattern": "^[a-f0-9]{64}$", + "description": "Parent pack's pack_hash (lowercase hex)." } }, - "description": "Signing key reference. Required when a signature is present." + "description": "Parent version reference. Absent for v1 of a pack; present for v2 and later." }, "signature": { - "type": "string", - "description": "Base64-encoded cryptographic signature over the canonical hash manifest." - } - }, - "allOf": [ - { - "if": { - "required": ["signature"] + "type": "object", + "required": ["method", "value", "key_id"], + "additionalProperties": false, + "properties": { + "method": { + "type": "string", + "enum": ["ed25519", "hmac-sha256", "rsa-pss-sha256"], + "description": "Signing method identifier." + }, + "value": { + "type": "string", + "description": "Base64-encoded (standard, padded) signature over the signing payload defined in ARCHIVE.md §4.2." + }, + "key_id": { + "type": "string", + "description": "Opaque key identifier for locating the verification key." + } }, - "then": { - "required": ["signing_key", "signed_at"] - } + "description": "Optional digital signature. When present, all three sub-fields are REQUIRED." } - ] + } } diff --git a/conformance/run.py b/conformance/run.py index 720df06..7d756a7 100644 --- a/conformance/run.py +++ b/conformance/run.py @@ -39,6 +39,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent SCHEMA_PATH = SCRIPT_DIR / "grammar" / "kp-pack.schema.json" +SIGNATURES_SCHEMA_PATH = SCRIPT_DIR / "grammar" / "kp-signatures.schema.json" FIXTURES_DIR = SCRIPT_DIR / "fixtures" EXAMPLES_DIR = SCRIPT_DIR.parent / "examples" @@ -48,6 +49,7 @@ "verbose-claims.kpack", "mixed-syntax.kpack", "maximal.kpack", + "composition.kpack", ] INVALID_ORDER = [ "no-rosetta.kpack", @@ -79,6 +81,7 @@ } _schema_cache = None +_signatures_schema_cache = None def schema(): @@ -89,6 +92,14 @@ def schema(): return _schema_cache +def signatures_schema(): + global _signatures_schema_cache + if _signatures_schema_cache is None: + with open(SIGNATURES_SCHEMA_PATH) as f: + _signatures_schema_cache = json.load(f) + return _signatures_schema_cache + + def _stringify_dates(obj): """Convert datetime.date objects from PyYAML back to ISO strings.""" if isinstance(obj, (datetime.date, datetime.datetime)): @@ -135,7 +146,17 @@ def validate_pack(pack_dir: Path) -> list[Err]: pack_yaml_path = pack_dir / "PACK.yaml" claims_path = pack_dir / "claims.md" evidence_path = pack_dir / "evidence.md" - for p in (pack_yaml_path, claims_path, evidence_path): + composition_path = pack_dir / "composition.yaml" + signatures_path = pack_dir / "signatures.yaml" + + # Composition packs MAY omit evidence.md (SPEC.md §2, + # "Composition-pack File Requirements"). claims.md remains required. + is_composition = composition_path.exists() + required_files = [pack_yaml_path, claims_path] + if not is_composition: + required_files.append(evidence_path) + + for p in required_files: if not p.exists(): errs.append(Err("parse", f"missing {p.name}")) if errs: @@ -152,6 +173,18 @@ def validate_pack(pack_dir: Path) -> list[Err]: except jsonschema.ValidationError as e: errs.append(Err("schema", e.message)) + # ── signatures.yaml: validate against its schema (if present) ── + if signatures_path.exists(): + try: + sigs = _stringify_dates(yaml.safe_load(signatures_path.read_text())) + except yaml.YAMLError as e: + errs.append(Err("signatures", f"signatures.yaml parse error: {e}")) + else: + try: + jsonschema.validate(sigs, signatures_schema()) + except jsonschema.ValidationError as e: + errs.append(Err("signatures", f"signatures.yaml: {e.message}")) + # ── claims.md: Rosetta header ── text = claims_path.read_text() rosetta = re.match(r"