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