diff --git a/SCHEMA_INDEX.md b/SCHEMA_INDEX.md index 8b2e4f2..67fbe74 100644 --- a/SCHEMA_INDEX.md +++ b/SCHEMA_INDEX.md @@ -4,8 +4,8 @@ | Directory | Form | When to use | |-----------|------|-------------| -| [`schema/`](./schema/) | **Unified** — one file per spec version validates the whole document. Files: `klickd-v1.json`, `klickd-v2.json`, `klickd-v3.4.schema.json`. | Single-pass validators, schema registries, third-party tooling. | -| [`schemas/`](./schemas/) | **Split** — separate envelope and payload schemas for v3. Files: `klickd-envelope-v3.schema.json`, `klickd-payload-v3.schema.json`. | Secure decoders that validate the envelope *before* decryption and the payload *after*. | +| [`schema/`](./schema/) | **Unified** — one file per spec version validates the whole document. Files: `klickd-v1.json`, `klickd-v2.json`, `klickd-v3.4.schema.json`, `klickd-v4-preview.schema.json` (preview), `klickd-v4.schema.json` (GA strict candidate). | Single-pass validators, schema registries, third-party tooling. | +| [`schemas/`](./schemas/) | **Split** — separate envelope and payload schemas. Files: `klickd-envelope-v3.schema.json`, `klickd-payload-v3.schema.json`, `klickd-payload-v4-preview.schema.json` (preview), `klickd-payload-v4.schema.json` (GA strict candidate). | Secure decoders that validate the envelope *before* decryption and the payload *after*. | Both directories are **normative for v3.x (production, current and recommended)** and are kept in sync. The split form (`schemas/`) is the canonical pre-/post-decrypt boundary; the unified form (`schema/`) is the convenience form for single-shot validation. @@ -30,7 +30,27 @@ In addition to the normative v3 schemas, the repository ships **permissive previ **These schemas are PERMISSIVE.** They use `additionalProperties: true` and only declare top-level hooks for the preview fields. They are intended to **accept and preserve** draft v4 structures, not to perform strict validation. They MUST NOT be used to reject a file that is otherwise a valid v3.5.1 file. - **Normative for production?** No. Production validation MUST use `schema/klickd-v3.4.schema.json` or the split v3 schemas under `schemas/`. -- **Strict v4 validation?** Not in this preview. A future PR will introduce strict v4 schemas; until then, the preview schemas are deliberately loose. +- **Strict v4 validation?** See the **v4 GA strict schemas** section below. - **Unknown fields?** A v4-preview reader MUST preserve unknown fields verbatim when round-tripping. See SPEC.md §33.7. See [SPEC.md §33](./SPEC.md) and the RFCs under [`docs/rfcs/`](./docs/rfcs/) for the design source. + +--- + +## v4 GA strict schemas (P0-2 — coexist with preview, do NOT supersede it) + +To unblock the GA track described in [`docs/roadmap/ROAD-TO-V4-GA.md` §P0-2](./docs/roadmap/ROAD-TO-V4-GA.md), the repository now also ships **strict v4 schemas**. The preview schemas remain in place unchanged: both pairs coexist until the preview sunset is announced separately. + +| File | Form | Status | When to use | +|------|------|--------|-------------| +| [`schemas/klickd-payload-v4.schema.json`](./schemas/klickd-payload-v4.schema.json) | **Split — payload** | **GA strict candidate (normative target)** | Strict validation of a `.klickd` v4 decrypted payload: `verification_gates` (v1 core enum), `human_veto_policy`, `claim_sources` (v1), `media_profile` (RFC-001 v1, `version`+`entries[]`, hash strict), `migration` (RFC-004 v1 frozen fields), plus carry-over v3 surface. RFC-002 v2-additive fields (`reversibility`, `blast_radius`, `contract_tests`, `success_criteria`, `verification_artifacts`) remain permissive while Draft. | +| [`schema/klickd-v4.schema.json`](./schema/klickd-v4.schema.json) | **Unified** | **GA strict candidate (normative target)** | Strict validation of a full v4 document. Encrypted files MUST carry `kdf` + `cipher` + `ciphertext` (envelope-v3 contract unchanged per §33.10 #2). | + +**Coexistence rules:** + +- Preview schemas (`*-v4-preview.schema.json`) remain authoritative for **preview** files (`preview: "v4.0.0-preview.1"`). They MUST continue to accept all preview vectors. +- Strict schemas (`klickd-payload-v4.schema.json`, `klickd-v4.schema.json`) are the **GA strict target**. They accept both `payload_schema_version: "4.0"` (GA) and the legacy preview value `"4.0.0-preview.1"` so the 5 persona examples and preview vectors round-trip against them. +- **Top-level `additionalProperties: true` is preserved.** SPEC.md §33.7 (unknown-field preservation) is unconditional and not relaxed by the strict schemas. +- **No SDK is bumped.** No npm / PyPI / Zenodo release is triggered. No git tag is created. + +**Validation:** see [`scripts/validate_v4_schemas.py`](./scripts/validate_v4_schemas.py) for the canonical local validation runner. diff --git a/docs/roadmap/ROAD-TO-V4-GA.md b/docs/roadmap/ROAD-TO-V4-GA.md index 5342832..651912e 100644 --- a/docs/roadmap/ROAD-TO-V4-GA.md +++ b/docs/roadmap/ROAD-TO-V4-GA.md @@ -106,6 +106,19 @@ Chaque entrée précise : *Objet → Livrables → Critères de sortie (Definiti - validation passe sur tous les vectors v4 stricts ; - les vectors preview restent acceptés via le schéma preview (deux schémas coexistent jusqu’au sunset). - **Dépendances :** P0-1. +- **Statut (2026-05-24) :** **GA candidate landed (docs/schema only, no SDK/release).** Le PR P0-2 introduit + [`schemas/klickd-payload-v4.schema.json`](../../schemas/klickd-payload-v4.schema.json) et + [`schema/klickd-v4.schema.json`](../../schema/klickd-v4.schema.json) (strict sur les sections RFC-001 v1 + `Accepted` et RFC-002 v1 core `Accepted` ; permissif sur les sections RFC-002 v2-additive et RFC-004 encore + en `Draft`). `additionalProperties: true` est conservé **au niveau racine** pour respecter le contrat de + préservation des champs inconnus de [SPEC.md §33.7](../../SPEC.md). Les 5 personas de + [`examples/v4/personas/`](../../examples/v4/personas/) et tous les `expected_payload` de + [`tests/vectors_v40_preview.json`](../../tests/vectors_v40_preview.json) valident contre le schéma strict. + Les schémas preview restent en place inchangés (coexistence, pas de remplacement). **Aucun bump de version + SDK, aucun tag, aucune release npm/PyPI/Zenodo.** Le runner local est + [`scripts/validate_v4_schemas.py`](../../scripts/validate_v4_schemas.py). Les vectors v4 *stricts* + (P0-6) restent à produire avant de pouvoir cocher la DoD complète ; à ce stade la DoD est validée sur le + corpus preview existant et les 5 personas. #### P0-3 — SDK Python `klickd` 4.0.0 diff --git a/examples/v4/personas/README.md b/examples/v4/personas/README.md index ea999f2..5eb57e9 100644 --- a/examples/v4/personas/README.md +++ b/examples/v4/personas/README.md @@ -123,10 +123,12 @@ forte sur les sections preview). - [x] Passphrase de test publique documentée (`klickd-example-only`), sans jamais figurer comme contenu chiffré. - [x] Validation contre le schéma preview permissif (cf. §5 ci-dessous). -- [ ] Validation stricte v4 ([P0-2 / P0-6](../../../docs/roadmap/ROAD-TO-V4-GA.md)) : - **différée**. Le schéma strict v4 et les vectors stricts v4 n'existent - pas encore — la DoD complète sera atteignable une fois P0-2/P0-6 mergés, - conformément à l'ordre des dépendances inscrit dans ROAD-TO-V4-GA. +- [x] Validation stricte v4 contre [`schemas/klickd-payload-v4.schema.json`](../../../schemas/klickd-payload-v4.schema.json) + (P0-2 GA candidate, livré séparément). Les 5 personas valident sans erreur — + voir [`scripts/validate_v4_schemas.py`](../../../scripts/validate_v4_schemas.py). +- [ ] Validation contre les vectors v4 *stricts* + ([P0-6](../../../docs/roadmap/ROAD-TO-V4-GA.md)) : + **différée**. Les vectors v4 stricts n'existent pas encore — track P0-6. ### Reproduire la validation locale diff --git a/schema/README.md b/schema/README.md index d731650..9f1734d 100644 --- a/schema/README.md +++ b/schema/README.md @@ -8,6 +8,7 @@ This directory contains **unified** JSON Schemas — each file validates a compl | `klickd-v2.json` | v2.x | PBKDF2-SHA256 + 4-field AAD. | | `klickd-v3.4.schema.json` | v3.4 (current) | Argon2id + RFC 8785 JCS AAD + `kdf`/`cipher` blocks. Forward-compatible with v3.5 fields. | | `klickd-v4-preview.schema.json` | **v4.0.0-preview.1 (PREVIEW, non-normative, NOT GA)** | Permissive (`additionalProperties: true`) acceptance schema for draft v4 documents with top-level hooks for `media_profile` / `verification_gates` / `human_veto_policy` / `claim_sources` / `verification_artifacts` / `migration` / `context_cost` / `profile_kind`. See SPEC.md §33. | +| `klickd-v4.schema.json` | **v4 GA strict candidate (P0-2)** | Strict unified schema. Encrypted v4 files MUST carry `kdf` + `cipher` + `ciphertext` (envelope-v3 contract unchanged per §33.10 #2). Inline payload fields cross-reference `../schemas/klickd-payload-v4.schema.json`. Top-level `additionalProperties: true` preserved (SPEC.md §33.7). Coexists with the preview schema — does NOT supersede it. | **Use these files** when a single-schema validation is sufficient (CI tooling, third-party integrations, schema registries). diff --git a/schema/klickd-v4.schema.json b/schema/klickd-v4.schema.json new file mode 100644 index 0000000..824557c --- /dev/null +++ b/schema/klickd-v4.schema.json @@ -0,0 +1,129 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://klickd.app/schema/v4/klickd.schema.json", + "title": "klickd v4 — Unified (Strict GA candidate, normative)", + "description": "Strict unified JSON Schema for a .klickd v4 document (envelope + optional inline payload). This schema is the GA strict equivalent of the permissive klickd-v4-preview.schema.json. It validates the envelope shape strictly (mirroring the v3 envelope contract — Argon2id + AES-256-GCM is unchanged in v4 per SPEC.md §33.10 #2) and, for unencrypted files, validates the inline payload against the strict v4 payload schema (klickd-payload-v4.schema.json). Top-level additionalProperties is TRUE to preserve unknown fields verbatim (SPEC.md §33.7 forward-compatibility invariant). Does NOT supersede the permissive preview schema (klickd-v4-preview.schema.json) — both coexist.", + "type": "object", + "additionalProperties": true, + "required": ["klickd_version", "created_at", "encrypted"], + "properties": { + "klickd_version": { + "type": "string", + "description": "Wire / envelope version. Strict v4 GA producers SHOULD emit '4.0'. v3.x values are also accepted because the v3 envelope contract is unchanged (§33.10 #2) and a v3.x file remains readable by a v4 reader.", + "pattern": "^(3|4)\\.\\d+(\\.[0-9A-Za-z-.]+)?$" + }, + "preview": { + "type": "string", + "description": "OPTIONAL preview marker. Absent on GA strict files. If present, this file was written against a preview iteration (e.g. 'v4.0.0-preview.1')." + }, + "created_at": { + "type": "string", + "description": "RFC 3339 UTC timestamp of file creation. Z-suffix only, no fractional seconds (matches v3 envelope strict contract).", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" + }, + "encrypted": { + "type": "boolean" + }, + "domain": { + "type": "string", + "minLength": 1, + "description": "Semantic category. Registered values follow v3 (education, work, finance, legal, creative, health, research, robotics, gaming, creator). Custom non-empty strings are permitted." + }, + "profile_kind": { + "type": "string", + "description": "Top-level discriminator for the profile shape. Reserved canonical values: 'learner', 'agent', 'team', 'robot', 'creator'." + }, + "domain_schema_version": { + "type": "string", + "pattern": "^([a-z][a-z0-9_-]*-\\d+\\.\\d+|\\d+\\.\\d+(\\.\\d+)?)$" + }, + "kdf": { + "type": "object", + "description": "Structured KDF block — same contract as envelope-v3 §15.", + "required": ["name", "params", "salt"], + "additionalProperties": false, + "properties": { + "name": {"type": "string", "enum": ["argon2id", "pbkdf2-sha256"]}, + "params": { + "oneOf": [ + { + "type": "object", + "required": ["m", "t", "p"], + "additionalProperties": false, + "properties": { + "m": {"type": "integer", "minimum": 65536}, + "t": {"type": "integer", "minimum": 1}, + "p": {"type": "integer", "minimum": 1} + } + }, + { + "type": "object", + "required": ["iterations"], + "additionalProperties": false, + "properties": { + "iterations": {"type": "integer", "minimum": 600000} + } + } + ] + }, + "salt": {"type": "string"} + } + }, + "cipher": { + "type": "object", + "description": "Structured cipher block — same contract as envelope-v3 §16. v4 does not introduce a new cipher.", + "required": ["name", "iv"], + "additionalProperties": false, + "properties": { + "name": {"type": "string", "const": "AES-256-GCM"}, + "iv": {"type": "string"} + } + }, + "ciphertext": { + "type": "string", + "description": "AES-256-GCM ciphertext (base64 padded). Required when encrypted=true." + }, + "iv": { + "type": "string", + "description": "Legacy top-level IV mirror, retained for v3.0 round-trip parity. New producers SHOULD place IV inside the structured cipher block." + }, + "kdf_salt": { + "type": "string", + "description": "Legacy top-level salt mirror, retained for v3.0 round-trip parity." + }, + "payload_schema_version": { + "type": "string", + "description": "Payload schema version when the payload is inline (encrypted=false). Strict v4 GA value: '4.0'. Preview '4.0.0-preview.1' accepted for round-trip.", + "enum": ["4.0", "4.0.0-preview.1"] + }, + "media_profile": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/media_profile"}, + "verification_gates": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/verification_gates"}, + "human_veto_policy": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/human_veto_policy"}, + "claim_sources": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/claim_sources"}, + "verification_artifacts": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/verification_artifacts"}, + "contract_tests": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/contract_tests"}, + "success_criteria": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/success_criteria"}, + "reversibility": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/reversibility"}, + "blast_radius": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/blast_radius"}, + "risk_thresholds": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/risk_thresholds"}, + "preflight_checks": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/preflight_checks"}, + "error_journal": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/error_journal"}, + "migration": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/migration"}, + "context_cost": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/context_cost"}, + "gaming_profile": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/gaming_profile"}, + "deprecated_fields": {"$ref": "https://klickd.app/schemas/v4/klickd-payload.schema.json#/properties/deprecated_fields"} + }, + "allOf": [ + { + "if": { + "type": "object", + "properties": {"encrypted": {"const": true}}, + "required": ["encrypted"] + }, + "then": { + "required": ["kdf", "cipher", "ciphertext"], + "description": "Encrypted v4 file MUST carry kdf + cipher + ciphertext (envelope-v3 contract retained)." + } + } + ] +} diff --git a/schemas/README.md b/schemas/README.md index 7d4b316..f637f9f 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -7,6 +7,7 @@ This directory contains the **split** JSON Schemas for `.klickd` v3: | `klickd-envelope-v3.schema.json` | Outer JSON envelope (encrypted). Validates `klickd_version`, `kdf`, `cipher`, `ciphertext`, AAD-relevant fields. | | `klickd-payload-v3.schema.json` | Inner decrypted payload. Validates `identity`, `agent_instructions`, `user_preferences`, `context`, `knowledge`, `memory`, etc. | | `klickd-payload-v4-preview.schema.json` | **v4.0.0-preview.1 payload (PREVIEW, non-normative, NOT GA).** Permissive (`additionalProperties: true`) acceptance schema for draft v4 payloads with top-level hooks for `media_profile` / `verification_gates` / `human_veto_policy` / `claim_sources` / `verification_artifacts` / `migration` / `context_cost` / `profile_kind`. See SPEC.md §33. | +| `klickd-payload-v4.schema.json` | **v4 GA strict candidate (P0-2).** Strict payload schema: `verification_gates` (v1 enum), `human_veto_policy`, `claim_sources` (v1), `media_profile` (RFC-001 v1, `version`+`entries[]`, BLAKE3 hash strict), `migration` (RFC-004 v1 frozen fields). RFC-002 v2-additive fields (`reversibility`, `blast_radius`, `contract_tests`, `success_criteria`, `verification_artifacts`) remain permissive while Draft. Top-level `additionalProperties: true` preserved (SPEC.md §33.7). Coexists with the preview schema — does NOT supersede it. | **Use this split form** when you need to validate the envelope *before* decryption and the payload *after* decryption as two independent steps (typical of secure decoders that fail-fast on the envelope). diff --git a/schemas/klickd-payload-v4.schema.json b/schemas/klickd-payload-v4.schema.json new file mode 100644 index 0000000..89b2f42 --- /dev/null +++ b/schemas/klickd-payload-v4.schema.json @@ -0,0 +1,354 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://klickd.app/schemas/v4/klickd-payload.schema.json", + "title": "klickd Payload v4 (Strict GA candidate, normative)", + "description": "Strict JSON Schema for the .klickd v4 inner payload. This schema is the GA strict equivalent of the permissive klickd-payload-v4-preview.schema.json. Strict blocks correspond to RFC-001 (media_profile v1, Accepted) and RFC-002 v1 core (verification_gates, human_veto_policy, claim_sources v1, Accepted). Sections that remain Draft (RFC-002 v2-additive: contract_tests, success_criteria, reversibility, blast_radius, verification_artifacts, error_journal.rule_created; RFC-004 migration runtime) are accepted but left permissive so they can be tightened by a future strict schema bump without breaking GA writers. Top-level additionalProperties is TRUE to preserve unknown fields verbatim (SPEC.md §33.7 forward-compatibility invariant). Strict shape on frozen sub-objects uses additionalProperties: false locally. Does NOT supersede the permissive preview schema (klickd-payload-v4-preview.schema.json) — both coexist until the preview sunset announced separately.", + "type": "object", + "required": [ + "payload_schema_version" + ], + "additionalProperties": true, + "properties": { + "payload_schema_version": { + "type": "string", + "description": "Payload schema version. For GA strict files, the canonical value is '4.0'. The preview value '4.0.0-preview.1' is also accepted so a preview file already in the wild round-trips against the strict schema without forcing a rewrite. A future strict bump (4.1, etc.) MAY tighten this enum.", + "enum": ["4.0", "4.0.0-preview.1"] + }, + "domain_schema_version": { + "type": "string", + "description": "Domain-specific schema version. Two accepted forms: (a) v3-style '{domain}-{major}.{minor}' (e.g. 'education-1.2'); (b) bare semver-ish '{major}.{minor}(.{patch})?' for domain-agnostic profiles (e.g. '1.0.0').", + "pattern": "^([a-z][a-z0-9_-]*-\\d+\\.\\d+|\\d+\\.\\d+(\\.\\d+)?)$" + }, + "preview": { + "type": "string", + "description": "OPTIONAL preview marker. Present only on files written against a preview iteration (e.g. 'v4.0.0-preview.1'). Absent on GA strict files." + }, + "profile_kind": { + "type": "string", + "description": "Top-level discriminator for the profile shape. Reserved canonical values: 'learner', 'agent', 'team', 'robot', 'creator'. Custom strings are permitted (extension space). v3.x is implicitly 'learner'." + }, + "injection_target": { + "type": "string", + "enum": ["system_prompt", "user_message", "both"], + "description": "Where to inject the .klickd context (carried over from v3.x §20.2)." + }, + "identity": { + "type": "object", + "description": "Persistent user identity attributes. Carried over from v3 SPEC §8; same strictness as v3 payload schema. All sub-fields are OPTIONAL.", + "additionalProperties": true, + "properties": { + "name": {"type": "string", "maxLength": 256}, + "display_name": {"type": "string", "maxLength": 256, "description": "v4 GA preferred user-facing alias. Either 'name' or 'display_name' may be used; producers SHOULD pick one."}, + "language": {"type": "string", "description": "BCP 47 language tag."}, + "timezone": {"type": "string", "description": "IANA time zone identifier."}, + "communication_style": {"type": "string"} + } + }, + "companion_identity": { + "type": "object", + "description": "Optional companion / co-agent persona. Same surface as v3.4 §27. Permissive.", + "additionalProperties": true + }, + "context": { + "type": "object", + "description": "Operational session state. Carried over from v3 SPEC §9. Permissive (unevaluatedProperties policy of v3 retained).", + "additionalProperties": true + }, + "knowledge": { + "type": "object", + "description": "Structured knowledge state. Carried over from v3 SPEC §10. Permissive.", + "additionalProperties": true + }, + "memory": { + "type": "array", + "maxItems": 1000, + "description": "Structured conversation/event log. Same v3 §11 constraints retained.", + "items": {"type": "object", "additionalProperties": true} + }, + "learning_goal": { + "type": "object", + "description": "User's stated learning / delivery goal. Same surface as v3 §27. Permissive.", + "additionalProperties": true, + "properties": { + "type": {"type": "string"}, + "deadline": {"type": "string"}, + "stakes": {"type": "string", "enum": ["low", "medium", "high"]} + } + }, + "user_preferences": { + "oneOf": [ + {"type": "string", "maxLength": 32768}, + {"type": "object", "additionalProperties": true} + ], + "description": "Canonical type is string (v3.5 §22.6). Object form retained for backward compatibility." + }, + "agent_instructions": { + "type": "string", + "maxLength": 32768, + "description": "System-prompt injection string. Same v3 §7.2 contract." + }, + "onboarding_trigger": { + "type": "string", + "description": "Optional onboarding trigger (v3.4 §29b). Common values: 'on_new_agent', 'manual'." + }, + "ethics": { + "type": "object", + "description": "Ethical guard surface (v3.x). Permissive in this schema; safety / locked fields are governed by docs/spec/DEPRECATION_POLICY_V4.md §9.", + "additionalProperties": true + }, + "media_profile": { + "description": "RFC-001 v1 (Accepted): portable, hash-referenced media context. Strict on the v1 frozen surface (versioned + entries[]); a permissive 'summary' form (preview-era) is also accepted for round-trip compatibility. New modalities / fields land via additive RFC bumps.", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["version", "entries"], + "properties": { + "version": {"type": "integer", "const": 1, "description": "media_profile schema version. v1 per RFC-001."}, + "entries": { + "type": "array", + "maxItems": 256, + "items": {"$ref": "#/$defs/mediaProfileEntry"} + } + } + }, + { + "type": "object", + "description": "Permissive summary form (preview-era). Strict producers SHOULD migrate to the versioned + entries[] form. Kept for round-trip parity with vectors_v40_preview.json.", + "additionalProperties": true, + "not": {"required": ["version", "entries"]} + }, + { + "type": "null" + } + ] + }, + "verification_gates": { + "description": "RFC-002 v1 core (Accepted): user's preferred friction profile per action class. Two accepted shapes: (a) structured form (recommended) with version + gates[]; (b) flat map {action_class: level} for compactness (used by §33.5 minimal example and preview readers).", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["version", "gates"], + "properties": { + "version": {"type": "integer", "const": 1}, + "user_default": {"$ref": "#/$defs/gateLevel"}, + "gates": { + "type": "array", + "maxItems": 256, + "items": {"$ref": "#/$defs/gateEntry"} + } + } + }, + { + "type": "object", + "description": "Flat map form: {action_class: gate_level}. Each value MUST be one of the five canonical levels.", + "additionalProperties": {"$ref": "#/$defs/gateLevel"} + } + ] + }, + "human_veto_policy": { + "description": "RFC-002 v1 (Accepted): standing rules about when a human MUST be in the loop. Overrides any lower gate.", + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "applies_to": { + "type": "array", + "items": {"type": "string"}, + "maxItems": 64, + "description": "List of action classes for which a human veto is required." + }, + "second_party": { + "type": ["string", "null"], + "description": "Optional named second party who may unlock. null means owner-only." + }, + "min_level": { + "$ref": "#/$defs/gateLevel", + "description": "Floor gate level. The policy MUST NOT resolve below this level for any action class in 'applies_to'. RFC-002 v1 §4 #6 — human veto is sacred." + }, + "rationale": {"type": "string", "maxLength": 1024} + } + }, + "claim_sources": { + "description": "RFC-002 v1 (Accepted) + v2-additive (Draft). Strict on v1 fields ('prefer', 'require_citation_for'); 'records' and extended fields remain permissive (v2 still Draft).", + "type": ["object", "null"], + "additionalProperties": true, + "properties": { + "prefer": { + "type": "array", + "items": {"type": "string"}, + "maxItems": 64, + "description": "Ordered preference list. Each entry is a free-form source identifier; common values: 'user_supplied', 'tool:web_search', 'tool:doi_resolver'." + }, + "require_citation_for": { + "type": "array", + "items": {"type": "string"}, + "maxItems": 64 + }, + "records": { + "type": "array", + "description": "v2-additive (Draft). Permissive until promoted.", + "items": {"type": "object", "additionalProperties": true} + } + } + }, + "risk_thresholds": { + "description": "RFC-002 v1 (Accepted): numeric / categorical knobs. Permissive on inner shape — extension space.", + "type": ["object", "null"], + "additionalProperties": true + }, + "preflight_checks": { + "description": "RFC-002 v1 (Accepted): small named checks to run before acting. Each entry is an object with at minimum 'name'.", + "type": ["array", "null"], + "items": {"type": ["object", "string"], "additionalProperties": true} + }, + "error_journal": { + "description": "RFC-002 v1 (Accepted): append-only lessons learned. Permissive on entry shape ('rule_created' remains Draft in v2-additive).", + "type": ["array", "null"], + "items": {"type": "object", "additionalProperties": true} + }, + "verification_artifacts": { + "description": "RFC-002 §8b.8 (Draft, v2-additive). Permissive: pointer ledger, not a payload sink. Accepted as null or array.", + "type": ["array", "null"], + "items": {"type": "object", "additionalProperties": true} + }, + "contract_tests": { + "description": "RFC-002 v2-additive (Draft). Permissive.", + "type": ["array", "null"], + "items": {"type": "object", "additionalProperties": true} + }, + "success_criteria": { + "description": "RFC-002 v2-additive (Draft). Permissive.", + "type": ["object", "array", "null"], + "additionalProperties": true + }, + "reversibility": { + "description": "RFC-002 v2-additive (Draft). Permissive — typical shape is {action_class: enum} but enums are not yet frozen.", + "type": ["object", "null"], + "additionalProperties": true + }, + "blast_radius": { + "description": "RFC-002 v2-additive (Draft). Permissive.", + "type": ["object", "null"], + "additionalProperties": true + }, + "migration": { + "description": "RFC-004 (Draft): optional migration metadata block. Audit-only — no migration tooling ships with the strict schema. Strict on v1 frozen fields; extension fields permitted.", + "type": ["object", "null"], + "additionalProperties": true, + "properties": { + "source_version": {"type": "string"}, + "migrated_at": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$" + }, + "migration_report_ref": {"type": "string"}, + "backup_ref": {"type": "string"} + } + }, + "context_cost": { + "description": "Research/benchmark track (benchmarks/context_cost/RFC.md). No spec semantics depend on this field. Permissive.", + "type": ["object", "null"], + "additionalProperties": true + }, + "gaming_profile": { + "description": "R4-P1-4 gaming.klickd baseline (preview, registry-based). NOT a v4 MUST. Accepted as a registry-defined optional profile; permissive structure.", + "type": ["object", "null"], + "additionalProperties": true + }, + "deprecated_fields": { + "description": "OPTIONAL informational block defined by docs/spec/DEPRECATION_POLICY_V4.md §6. Lists fields the producer has emitted as 'deprecated' status. Readers that don't understand it MUST silently ignore (§33.7).", + "type": ["array", "null"], + "items": { + "type": "object", + "additionalProperties": true, + "required": ["name"], + "properties": { + "name": {"type": "string"}, + "since": {"type": "string"}, + "replacement": {"type": ["string", "null"]}, + "removal_target": {"type": ["string", "null"]} + } + } + }, + "_example_metadata": { + "description": "Optional non-normative block used by repository examples (see examples/v4/personas/README.md). Carries 'persona', 'non_normative', 'contains_real_pii', 'contains_secrets', etc. A reader MUST NOT use this block as a trust signal.", + "type": "object", + "additionalProperties": true + } + }, + "$defs": { + "gateLevel": { + "type": "string", + "enum": ["silent", "warn", "confirm", "block", "require-owner"], + "description": "RFC-002 v1 §6 — fixed enum of five gate levels. No new levels are added in GA." + }, + "gateEntry": { + "type": "object", + "additionalProperties": false, + "required": ["action_class", "level"], + "properties": { + "id": {"type": "string", "maxLength": 128}, + "action_class": {"type": "string", "maxLength": 128}, + "level": {"$ref": "#/$defs/gateLevel"}, + "reason": {"type": "string", "maxLength": 1024} + } + }, + "mediaProfileEntry": { + "type": "object", + "additionalProperties": true, + "required": ["id", "modality", "hash"], + "description": "RFC-001 v1 entry. Strict on v1 frozen fields (id, modality, hash). Additional metadata (consent, producer, etc.) is permitted as extension surface.", + "properties": { + "id": {"type": "string", "maxLength": 128}, + "modality": { + "type": "string", + "enum": ["voice", "image", "document", "embedding"], + "description": "RFC-001 v1 §4 #3 — closed enum at v1. Custom modalities live under 'x_*' namespaces (outside this entry)." + }, + "label": {"type": "string", "maxLength": 256}, + "language": {"type": "string"}, + "uri": {"type": "string", "description": "May be file://, https://, ipfs://, cas://, or a relative path. Reader chooses how to resolve."}, + "media_type": {"type": "string"}, + "byte_size": {"type": "integer", "minimum": 0}, + "duration_ms": {"type": "integer", "minimum": 0}, + "bytes_b64": { + "type": "string", + "contentEncoding": "base64", + "description": "Inline base64 PERMITTED ONLY for entries ≤ 16 KiB total (RFC-001 v1 §4 #1). Strict enforcement of the 16 KiB cap is not expressible in JSON Schema and is enforced by reader-side logic." + }, + "hash": { + "type": "object", + "additionalProperties": false, + "required": ["algo", "value"], + "properties": { + "algo": {"type": "string", "const": "blake3", "description": "RFC-001 v1 §4 #2 — one hash, one algorithm: BLAKE3."}, + "value": {"type": "string", "description": "Hash value, encoding per RFC-001 (base64 or hex-prefixed). Strict format enforcement is reader-side."} + } + }, + "producer": { + "type": "object", + "additionalProperties": true, + "properties": { + "kind": {"type": "string"}, + "device": {"type": "string"} + } + }, + "consent": { + "type": "object", + "additionalProperties": true, + "description": "RFC-001 v1 §4 #5 — consent is per-entry, not per-file.", + "properties": { + "purposes": { + "type": "array", + "items": {"type": "string"}, + "maxItems": 64 + }, + "expires_at": {"type": "string"}, + "revocable": {"type": "boolean"} + } + } + } + } + } +} diff --git a/scripts/validate_v4_schemas.py b/scripts/validate_v4_schemas.py new file mode 100755 index 0000000..a58b8ee --- /dev/null +++ b/scripts/validate_v4_schemas.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +P0-2 strict v4 schema validation runner. + +Validates the .klickd v4 GA strict candidate schemas against: + - the 5 persona examples under examples/v4/personas/ + - examples/v4-preview/minimal.klickd (unified shape only — it carries no + payload_schema_version on purpose) + - every expected_payload of tests/vectors_v40_preview.json (payload schema) + - a fixed set of negative cases (each one MUST fail validation) + +Exits 0 on success, 1 on any unexpected error. Intended for local use and +as a candidate CI step. Does not modify any file. No SDK is bumped, no +release is triggered. See docs/roadmap/ROAD-TO-V4-GA.md §P0-2. +""" +from __future__ import annotations + +import json +import pathlib +import sys + +try: + from jsonschema import Draft202012Validator, RefResolver +except ImportError: + print("ERROR: jsonschema not installed. pip install jsonschema", file=sys.stderr) + sys.exit(2) + +REPO = pathlib.Path(__file__).resolve().parent.parent +PAYLOAD_SCHEMA_PATH = REPO / "schemas" / "klickd-payload-v4.schema.json" +UNIFIED_SCHEMA_PATH = REPO / "schema" / "klickd-v4.schema.json" + + +def load_validators() -> tuple[Draft202012Validator, Draft202012Validator]: + payload_schema = json.loads(PAYLOAD_SCHEMA_PATH.read_text()) + unified_schema = json.loads(UNIFIED_SCHEMA_PATH.read_text()) + store = { + payload_schema["$id"]: payload_schema, + unified_schema["$id"]: unified_schema, + } + resolver = RefResolver.from_schema(unified_schema, store=store) + return ( + Draft202012Validator(payload_schema), + Draft202012Validator(unified_schema, resolver=resolver), + ) + + +def report(label: str, errs: list, expect_fail: bool = False) -> int: + ok = bool(errs) if expect_fail else not errs + tag = "OK" if ok else "FAIL" + print(f"[{tag}] {label} — errors={len(errs)}") + for e in errs[:3]: + print(f" - {list(e.path)}: {e.message[:200]}") + return 0 if ok else 1 + + +def main() -> int: + payload_validator, unified_validator = load_validators() + failures = 0 + + # 1. 5 persona examples — unified + payload validation + for p in sorted((REPO / "examples" / "v4" / "personas").glob("*.klickd")): + data = json.loads(p.read_text()) + failures += report( + f"persona unified {p.name}", + list(unified_validator.iter_errors(data)), + ) + failures += report( + f"persona payload {p.name}", + list(payload_validator.iter_errors(data)), + ) + + # 2. SPEC §33.5 minimal preview example — unified validation only + # (no payload_schema_version; it's an envelope-shaped illustration). + minimal = REPO / "examples" / "v4-preview" / "minimal.klickd" + if minimal.is_file(): + failures += report( + f"unified {minimal.relative_to(REPO)}", + list(unified_validator.iter_errors(json.loads(minimal.read_text()))), + ) + + # 3. Every expected_payload in vectors_v40_preview.json — payload schema + vectors = REPO / "tests" / "vectors_v40_preview.json" + if vectors.is_file(): + vdoc = json.loads(vectors.read_text()) + for v in vdoc.get("vectors", []): + ep = v.get("expected_payload") + if not ep: + continue + failures += report( + f"vector {v['id']}", + list(payload_validator.iter_errors(ep)), + ) + + # 4. Negative cases — each MUST fail. + negatives = [ + ( + "neg: unknown gate level", + payload_validator, + { + "payload_schema_version": "4.0", + "verification_gates": { + "version": 1, + "gates": [{"action_class": "x", "level": "loud"}], + }, + }, + ), + ( + "neg: media entry missing hash", + payload_validator, + { + "payload_schema_version": "4.0", + "media_profile": { + "version": 1, + "entries": [{"id": "x", "modality": "voice"}], + }, + }, + ), + ( + "neg: unsupported payload_schema_version", + payload_validator, + {"payload_schema_version": "9.9"}, + ), + ( + "neg: encrypted=true missing kdf/cipher/ciphertext", + unified_validator, + { + "klickd_version": "4.0", + "created_at": "2026-05-24T00:00:00Z", + "encrypted": True, + }, + ), + ( + "neg: media modality not in v1 enum", + payload_validator, + { + "payload_schema_version": "4.0", + "media_profile": { + "version": 1, + "entries": [ + { + "id": "x", + "modality": "video", + "hash": {"algo": "blake3", "value": "xx"}, + } + ], + }, + }, + ), + ] + for label, validator, data in negatives: + failures += report( + label, list(validator.iter_errors(data)), expect_fail=True + ) + + if failures: + print(f"\nFAILED: {failures} validation issue(s).", file=sys.stderr) + return 1 + print("\nAll v4 strict-schema validations passed.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())