Contracts
bundle-compatibility-api.schema.yaml
Bundle Compatibility API contract
Module: src/doctrine/versioning.py (new file)
Describes the public interface of the compatibility registry.
title: BundleCompatibilityAPI description: | Public interface of src/doctrine/versioning.py. Consumed by: charter subcommands (reader check) and spec-kitty upgrade (migration).
constants: CURRENT_BUNDLE_SCHEMA_VERSION: type: integer value: 2 description: "The bundle schema version Phase 7 synthesis writes."
MIN_READABLE_BUNDLE_SCHEMA: type: integer value: 1 description: "Oldest bundle version this CLI can process (with migration)."
MAX_READABLE_BUNDLE_SCHEMA: type: integer value: 2 description: "Newest bundle version this CLI can read natively."
enums: BundleCompatibilityStatus: values: COMPATIBLE: "Bundle version matches supported range. Proceed normally." NEEDS_MIGRATION: "Bundle version < current; registered migration exists. Block + run spec-kitty upgrade." INCOMPATIBLE_OLD: "Bundle version < min; no migration registered. Block + contact support." INCOMPATIBLE_NEW: "Bundle version > max. Block + upgrade CLI." MISSING_VERSION: "bundle_schema_version absent from metadata.yaml. Treated as v1 (NEEDS_MIGRATION path)."
dataclasses: BundleCompatibilityResult: fields: status: BundleCompatibilityStatus bundle_version: "int | None" supported_min: int supported_max: int message: "str # human-readable, includes remediation instruction" exit_code: "int # 0 = COMPATIBLE, 1 = all other statuses" properties: is_compatible: "bool # status == COMPATIBLE" needs_migration: "bool # status in (NEEDS_MIGRATION, MISSING_VERSION)"
functions: check_bundle_compatibility: signature: "(bundle_version: int | None) -> BundleCompatibilityResult" description: | Pure function. No filesystem I/O. Called by charter subcommands after reading bundle_schema_version from metadata.yaml.
- bundle_version=None → MISSING_VERSION (treated as v1, NEEDS_MIGRATION path)
- bundle_version==2 (current) → COMPATIBLE
- 1 <= bundle_version < 2 → NEEDS_MIGRATION (migration registered)
- bundle_version > 2 → INCOMPATIBLE_NEW
- bundle_version < 1 → INCOMPATIBLE_OLD
get_bundle_schema_version: signature: "(charter_dir: Path) -> int | None" description: | Read bundle_schema_version integer from <charter_dir>/metadata.yaml. Returns None if file absent or field missing. Pure filesystem read; no validation side-effects.
run_migration: signature: "(from_version: int, bundle_root: Path, dry_run: bool = False) -> MigrationResult" description: | Apply the registered migration from from_version to from_version+1 (and chain if multiple versions to bridge). Idempotent: running twice produces same result. Returns MigrationResult with changes_made list and any errors. Raises KeyError if no migration registered for from_version. Called exclusively by spec-kitty upgrade migration step.
registered_migrations:
to_version: 2 function: migrate_v1_to_v2 description: | Upgrades v1 sidecars (Phase 3 baseline) to v2 (Phase 7 hardened). Adds sentinel values for fields that were not captured in v1: synthesizer_version: "(pre-phase7-migration)" synthesis_run_id: "(pre-phase7-migration)" produced_at: file mtime in ISO 8601 UTC, or "(pre-phase7-migration)" source_input_ids: copy of existing source_urns corpus_snapshot_id: existing value or "(none)" if was null Upgrades manifest schema_version from "1" to "2" and adds synthesizer_version: "(pre-phase7-migration)" manifest_hash: computed from upgraded manifest content Stamps bundle_schema_version: 2 in metadata.yaml.
- from_version: 1
provenance-entry-v2.schema.yaml
ProvenanceEntry v2 YAML sidecar contract
Storage: .kittify/charter/provenance/<kind>-<slug>.yaml
Phase 7 additions: synthesizer_version, source_input_ids, produced_at,
corpus_snapshot_id promoted to required, synthesis_run_id added.
schema_version bumped from "1" to "2".
$schema: "https://json-schema.org/draft/2020-12" title: ProvenanceEntry description: Per-artifact provenance sidecar (v2, Phase 7 hardened) type: object
required:
- schema_version
- artifact_urn
- artifact_kind
- artifact_slug
- artifact_content_hash
- inputs_hash
- adapter_id
- adapter_version
- synthesizer_version
- source_input_ids
- generated_at
- produced_at
- corpus_snapshot_id
- synthesis_run_id
properties: schema_version: type: string const: "2" description: "Sidecar format version. Must be '2' for Phase 7 sidecars."
artifact_urn: type: string description: "Fully-qualified URN identifying the artifact."
artifact_kind: type: string enum: ["directive", "tactic", "styleguide"]
artifact_slug: type: string description: "Human-readable kebab-case slug of the artifact."
artifact_content_hash: type: string minLength: 1 description: "BLAKE3-256 or SHA-256 hex digest of canonical_yaml(body) bytes."
inputs_hash: type: string minLength: 1 description: "Full hex digest of the normalized SynthesisRequest."
adapter_id: type: string minLength: 1 description: "Identifier of the synthesis adapter that generated this artifact."
adapter_version: type: string minLength: 1 description: "Version of the synthesis adapter."
synthesizer_version: type: string minLength: 1 description: "spec-kitty-cli version string at synthesis time (e.g. '3.2.0a5'). '(pre-phase7-migration)' is the sentinel for migrated v1 sidecars."
source_section: type: ["string", "null"] description: "Charter section that was the primary source. At least one of source_section or source_urns must be non-empty (existing invariant)."
source_urns: type: array items: type: string description: "List of source URNs. At least one of source_section or source_urns must be non-empty."
source_input_ids: type: array items: type: string description: "Ordered list of source input identifiers used by the adapter. Phase 7 initializes this from source_urns."
generated_at: type: string description: "ISO 8601 UTC timestamp when the adapter generated the artifact content." pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"
produced_at: type: string description: "ISO 8601 UTC timestamp when the provenance sidecar was written to disk." pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"
corpus_snapshot_id: type: string minLength: 1 description: "ID of the source corpus snapshot. '(none)' when synthesis ran without a corpus snapshot."
synthesis_run_id: type: string minLength: 1 description: "run_id ULID from StagingDir, linking this sidecar to the SynthesisManifest. '(pre-phase7-migration)' is the sentinel for migrated v1 sidecars."
evidence_bundle_hash: type: ["string", "null"] description: "SHA-256 hex digest of the serialized EvidenceBundle. null when no evidence bundle was used."
adapter_notes: type: ["string", "null"] description: "Free-form notes from the adapter."
allOf:
if: properties: source_section: type: "null" then: properties: source_urns: minItems: 1
- description: "At least one of source_section or source_urns must be non-empty."
additionalProperties: false
synthesis-manifest-v2.schema.yaml
SynthesisManifest v2 contract
Storage: .kittify/charter/synthesis-manifest.yaml
Phase 7 additions: synthesizer_version, manifest_hash.
schema_version bumped from "1" to "2".
$schema: "https://json-schema.org/draft/2020-12" title: SynthesisManifest description: Top-of-bundle commit marker (v2, Phase 7 hardened) type: object
required:
- schema_version
- created_at
- run_id
- adapter_id
- adapter_version
- synthesizer_version
- manifest_hash
- artifacts
properties: schema_version: type: string const: "2" description: "Manifest format version. Must be '2' for Phase 7 manifests."
mission_id: type: ["string", "null"] description: "Optional ULID of the Spec Kitty mission that triggered synthesis."
created_at: type: string description: "ISO 8601 UTC timestamp when this manifest was created." pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"
run_id: type: string minLength: 1 description: "ULID identifying this synthesis run. Matches the StagingDir.run_id."
adapter_id: type: string description: "Primary adapter id. Empty string for mixed-identity runs."
adapter_version: type: string description: "Primary adapter version. Empty string for mixed-identity runs."
synthesizer_version: type: string minLength: 1 description: "spec-kitty-cli version string at synthesis time."
manifest_hash: type: string minLength: 64 maxLength: 64 description: | SHA-256 hex digest of canonical_yaml(all manifest fields EXCEPT manifest_hash itself). Validation: strip manifest_hash key, re-serialize, re-hash, compare.
artifacts: type: array items: $ref: "#/$defs/ManifestArtifactEntry" description: "One entry per committed artifact, in deterministic order."
built_in_only: type: boolean default: false description: | True when synthesis legitimately produced no project DRG. Readers treat this as authoritative built-in-only state and ignore stale project graph files.
$defs: ManifestArtifactEntry: type: object required:
properties: kind: type: string enum: ["directive", "tactic", "styleguide"] slug: type: string path: type: string description: "Repo-relative path to the artifact YAML under .kittify/doctrine/." provenance_path: type: string description: "Repo-relative path to the provenance sidecar under .kittify/charter/." content_hash: type: string description: "SHA-256 hex digest of the artifact YAML bytes." additionalProperties: false
- kind
- slug
- path
- provenance_path
- content_hash
additionalProperties: false