Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions conformance/fixtures/valid/composition.kpack/PACK.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: composition-fixture
version: 2026.04.18
domain: test/conformance
author: KP Conformance Suite
23 changes: 23 additions & 0 deletions conformance/fixtures/valid/composition.kpack/claims.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- KP:1 — Knowledge Pack Format
Claims: - [ID] assertion {confidence|type|evidence|date|depth|nature} context
Positions 5-6 optional. Verbose named-field syntax also accepted.
Types: o=observed r=reported c=computed i=inferred
Depth: assumed | investigated | exhaustive (optional, position 5)
Nature: judgment | prediction | meta (optional, position 6; omitted=factual)
Relations: →supports ⊗contradicts ⊗!error ⊗~tension ←requires ~refines ⊘supersedes ↔see_also
Files: evidence.md=sources history.md=past entities.md=graph composition.yaml=references
-->
---
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.
25 changes: 25 additions & 0 deletions conformance/fixtures/valid/composition.kpack/composition.yaml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions conformance/fixtures/valid/maximal.kpack/signatures.yaml
Original file line number Diff line number Diff line change
@@ -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"
112 changes: 79 additions & 33 deletions conformance/grammar/kp-signatures.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,111 @@
"$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}$"
},
"propertyNames": {
"not": { "const": "signatures.yaml" }
},
"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 the root-level signatures.yaml itself (enforced via propertyNames)."
},
"sealed_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 UTC timestamp of sealing (e.g., 2026-04-12T14:30:00Z)."
},
"signing_key": {
"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."
"pattern": "^[a-f0-9]{64}$",
"description": "Parent pack's pack_hash (lowercase hex)."
},
"key_id": {
"type": "string",
"description": "Optional key identifier for key management systems."
"merge_parents": {
"type": "array",
"items": {
"type": "object",
"required": ["version", "pack_hash"],
"additionalProperties": false,
"properties": {
"version": {
"type": "string",
"description": "Additional parent's version string."
},
"pack_hash": {
"type": "string",
"pattern": "^[a-f0-9]{64}$",
"description": "Additional parent's pack_hash (lowercase hex)."
}
}
},
"description": "Additional parent references for branch-merge lineage. OPTIONAL; absent in single-parent archives."
}
},
"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."
}
]
}
}
48 changes: 45 additions & 3 deletions conformance/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -48,6 +49,7 @@
"verbose-claims.kpack",
"mixed-syntax.kpack",
"maximal.kpack",
"composition.kpack",
]
INVALID_ORDER = [
"no-rosetta.kpack",
Expand Down Expand Up @@ -79,6 +81,7 @@
}

_schema_cache = None
_signatures_schema_cache = None


def schema():
Expand All @@ -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)):
Expand Down Expand Up @@ -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:
Expand All @@ -152,6 +173,25 @@ 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:
if sigs is None:
errs.append(Err("signatures", "signatures.yaml is empty"))
else:
try:
jsonschema.validate(
sigs,
signatures_schema(),
format_checker=jsonschema.FormatChecker(),
)
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"<!-- KP:(\d+)\s", text)
Expand Down Expand Up @@ -187,8 +227,10 @@ def validate_pack(pack_dir: Path) -> list[Err]:
if isinstance(pc, dict) and "scale" in pc and pc["scale"] != fm_scale:
errs.append(Err("SC-10", f"scale '{fm_scale}' != PACK.yaml '{pc['scale']}'"))

# ── evidence.md: extract defined evidence IDs ──
ev_ids = set(re.findall(r"^## (E\d+)", evidence_path.read_text(), re.MULTILINE))
# ── evidence.md: extract defined evidence IDs (empty set if absent) ──
ev_ids: set[str] = set()
if evidence_path.exists():
ev_ids = set(re.findall(r"^## (E\d+)", evidence_path.read_text(), re.MULTILINE))

# ── claims.md: parse claim blocks ──
# A claim block starts with `- [C###]` or `- **[C###]**` and includes
Expand Down
16 changes: 16 additions & 0 deletions spec/ARCHIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ This section defines the schema for `signatures.yaml`, resolving SPEC.md §2's "
# Hash algorithm (currently only SHA-256; future-proofed for upgrades)
algorithm: SHA-256

# Stable pack identity across versions (OPTIONAL for legacy archives;
# SHOULD be present in new archives — see "Required fields" below)
pack_id: "0b3c1f9a-4d7e-4a1c-9f2b-3e4a5b6c7d8e"

# Self-describing receipts (OPTIONAL; mirrors PACK.yaml name/version)
pack_name: "solar-energy-market"
pack_version: "2026.03.18"

# Pack hash — computed per §3
pack_hash: "a1b2c3d4e5f6..."

Expand Down Expand Up @@ -196,12 +204,16 @@ signature:
| Field | Type | Format |
|-------|------|--------|
| `algorithm` | string | Algorithm identifier. Currently `SHA-256`. |
| `pack_id` | string | Stable identity of the pack across versions. A UUID is RECOMMENDED. Two sealed archives MUST share the same `pack_id` if and only if they represent versions of the same logical pack. |
| `pack_name` | string | Self-describing copy of `PACK.yaml.name`. Enables receipts and indexing without loading the manifest. |
| `pack_version` | string | Self-describing copy of `PACK.yaml.version`. Same rationale as `pack_name`. |
| `pack_hash` | string | Lowercase hex-encoded SHA-256 digest (64 characters). |
| `files` | map\<string, string\> | Keys: NFC-normalized relative paths with `/` separator. Values: lowercase hex SHA-256 digests. MUST NOT include `signatures.yaml`. |
| `sealed_at` | string | ISO 8601 UTC timestamp (e.g., `2026-04-12T14:30:00Z`). |
| `sealed_by` | string | Identifier of the sealing system (e.g., `capture-client`, `analysis-service`). |
| `parent.version` | string | Parent pack's version string from its PACK.yaml. |
| `parent.pack_hash` | string | Parent pack's `pack_hash` (lowercase hex). |
| `parent.merge_parents` | array\<object\> | OPTIONAL. Additional parent references for branch-merge lineage. Each entry has the same shape as `parent` itself (`version` + `pack_hash`). Absent in single-parent archives. |
| `signature.method` | string | Signing method identifier: `ed25519`, `hmac-sha256`, or `rsa-pss-sha256`. |
| `signature.value` | string | Base64-encoded (standard, padded) signature over the signing payload. |
| `signature.key_id` | string | Opaque key identifier for locating the verification key. |
Expand All @@ -215,9 +227,13 @@ signature:
| `files` | Always | |
| `sealed_at` | Always | |
| `sealed_by` | Always | |
| `pack_id` | New archives | SHOULD be present in archives sealed against v0.7.5 or later. Schema accepts absence for backwards compatibility with archives sealed before `pack_id` was documented. Readers MUST tolerate its absence. |
| `pack_name` | Never | OPTIONAL; aids self-describing receipts. |
| `pack_version` | Never | OPTIONAL; aids self-describing receipts. |
| `parent` | When version > 1 | Absent for the first version of a pack |
| `parent.version` | When `parent` is present | Both sub-fields are REQUIRED when the `parent` block exists |
| `parent.pack_hash` | When `parent` is present | |
| `parent.merge_parents` | Never | OPTIONAL; populated only when a version results from merging two or more lineages |
| `signature` | When signing is configured | Optional; depends on deployment |
| `signature.method` | When `signature` is present | All three sub-fields are REQUIRED when the `signature` block exists |
| `signature.value` | When `signature` is present | |
Expand Down
22 changes: 22 additions & 0 deletions spec/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@

---

## v0.7.5 — 2026-04-18

**Signatures schema alignment + composition-pack file requirements.**

### Added
- **`pack_id` in `signatures.yaml`** — Stable pack identity across versions, emitted by current sealers and used by relays. Documented in ARCHIVE.md §4 with a prose MAY/SHOULD policy for backwards compatibility (schema accepts absence; new archives SHOULD include it). `pack_name` and `pack_version` also documented as optional self-describing receipts.
- **`parent.merge_parents`** — Optional array inside the `parent` block for branch-merge lineage. Each entry has the same `version`/`pack_hash` shape as `parent` itself. Documents a latent capability declared in the sealer implementation so the refreshed schema does not reject valid merge-lineage archives.
- **Composition-pack file requirements** — New subsection in SPEC.md §2 listing the file-presence rules for composition packs: `composition.yaml` REQUIRED, `claims.md` REQUIRED (intentionally minimal, about the composition context only), `evidence.md` OPTIONAL. Cross-references `spec/COMPOSITION.md` for semantics.
- **Signatures schema validation in the conformance runner** — `conformance/run.py` now validates any `signatures.yaml` it finds against `kp-signatures.schema.json`, with `jsonschema.FormatChecker` enabled so declared formats (`date-time`) are actually enforced. Empty-file and YAML parse errors are surfaced with friendly messages. Previously the runner was silent on signatures.
- **Fixture: `composition.kpack`** — Valid-pack fixture demonstrating the composition-pack minimum shape (composition.yaml present, no evidence.md, prose-only claims.md).
- **Fixture: `signatures.yaml` on `maximal.kpack`** — Exercises the signatures schema with a populated `parent` block and the optional `pack_name`/`pack_version` fields.

### Changed
- **`kp-signatures.schema.json`** — Regenerated to match ARCHIVE.md §4 (v0.7.3+ shape). Required fields are now `algorithm`, `pack_hash`, `files`, `sealed_at`, `sealed_by`. Optional: `pack_id`, `pack_name`, `pack_version`, `parent` (with `version` + `pack_hash` + optional `merge_parents`), `signature` (with `method` + `value` + `key_id`). Previous pre-v0.7.3 fields (`signed_at`, `signing_key`, `signature` as a flat string) removed. Schema root closed with `additionalProperties: false`, mirroring the manifest-schema pattern. The `files` map now enforces "MUST NOT include signatures.yaml itself" via `propertyNames`, not prose alone.
- **`conformance/run.py`** — `validate_pack()` detects composition packs (those with `composition.yaml`) and skips the `evidence.md` presence check for them. Evidence-ID extraction now tolerates a missing `evidence.md` (empty set rather than exception). `signatures.yaml` is validated against the new schema when present, with `FormatChecker` enabled so `format: date-time` on `sealed_at` is actually enforced.

### Fixed
- **Schema-vs-impl drift** — The prior `kp-signatures.schema.json` required fields (`signed_at`, `signing_key`, `signature` as a flat string) that neither the sealer (kp-forge) nor the relay (kp-packs) have produced or consumed since v0.7.3. Any current sealed archive would have failed schema validation. Aligned.
- **Composition-pack validation hole** — Packs with `composition.yaml` and no `evidence.md` (valid per `COMPOSITION.md` design) were being rejected by the conformance runner for the missing-file reason. No longer.

---

## v0.7.4 — 2026-04-16

**Manifest extension lane — standardizes where experiments belong without widening the core schema.**
Expand Down
Loading