Data Model: 3.2.0a5 Tranche 1

This tranche has minimal data-model surface — no databases, no migrations beyond what already exists, no new persisted entities. The "entities" below are the file-shaped contracts that this tranche enforces or modifies.

E1 — Project metadata file (.kittify/metadata.yaml)

Authoritative source for the compat-planner gate (src/specify_cli/compat/planner.py). Read by every spec-kitty agent ... command.

FieldTypeCardinalityOwnerInvariant
spec_kitty.versionstring (PEP 440)1ProjectMetadata dataclassMust equal CLI's bundled version after a successful spec-kitty upgrade.
spec_kitty.schema_versionint1_stamp_schema_version (raw YAML round-trip)Must equal MIN_SUPPORTED_SCHEMA == MAX_SUPPORTED_SCHEMA (currently 3) after a successful upgrade. Invariant violated today by FR-002.
spec_kitty.initialized_atISO-8601 timestamp1ProjectMetadata.saveSet once at init. Must not be overwritten by upgrade.
spec_kitty.last_upgraded_atISO-8601 timestamp1ProjectMetadata.saveUpdated on every successful upgrade run.
environment.python_versionstring1ProjectMetadata.saveRecorded at init/upgrade for diagnostics.
environment.platformstring1ProjectMetadata.saveAs above.
environment.platform_versionstring1ProjectMetadata.saveAs above.
migrations.applied[]list of migration records0..NProjectMetadata.record_migrationAppend-only ledger.

FR-002 invariant restored after fix: After spec-kitty upgrade --yes returns exit code 0, all of the following must hold simultaneously: spec_kitty.version matches the CLI version, spec_kitty.schema_version == REQUIRED_SCHEMA_VERSION, and a subsequent spec-kitty agent mission branch-context --json exits 0 (no PROJECT_MIGRATION_NEEDED).

E2 — Bulk-edit occurrence map (occurrence_map.yaml)

Required by DIRECTIVE_035 because meta.json::change_mode == "bulk_edit". Materialized at the same level as plan.md. Schema:

mission_id: <ULID>
mission_slug: <slug>
target_string: "/spec-kitty.checklist"
related_strings:
  - "checklist.md"   # template / snapshot filename
  - "spec-kitty.checklist.md"  # generated agent copy filename
generated_at: <ISO-8601>
categories:
  code_symbols: { remove: [...], keep: [...] }
  import_paths: { remove: [...], keep: [...] }
  filesystem_paths: { remove: [...], keep: [...] }
  serialized_keys: { remove: [...], keep: [...] }
  cli_commands: { remove: [...], keep: [...] }
  user_facing_strings: { remove: [...], keep: [...] }
  tests_fixtures: { remove: [...], keep: [...] }
  logs_telemetry: { remove: [...], keep: [...] }
totals:
  remove_count: 27
  keep_count: 6

Every remove entry is a {path, line, reason} triple; every keep entry is the same plus an explicit keep_reason. Implementing agents MUST produce a diff that matches the occurrence map exactly — anything extra is gated.

E3 — Diagnostic dedup state (in-process)

New module src/specify_cli/diagnostics/dedup.py (introduced by FR-009 work). Does not persist to disk.

# Module shape (illustrative, not normative):

import contextvars
from typing import Final

_REPORTED: Final[contextvars.ContextVar[set[str]]] = contextvars.ContextVar(
    "spec_kitty_diagnostics_reported",
    default=frozenset(),
)

def report_once(cause_key: str) -> bool:
    """Return True if `cause_key` has not been reported yet in this
    invocation, and record it as reported. Return False otherwise.
    Safe under asyncio (ContextVar)."""

def reset_for_invocation() -> None:
    """Reset the dedup state. Called by tests in `setup`; not called by
    production code paths (one CLI invocation == one dedup window)."""
FieldTypeCardinalityLifecycle
cause_keystringunbounded set per invocationSet on first report; never cleared inside an invocation.

The keys are short stable identifiers like "sync.unauthenticated", "auth.token_refresh_failed". The exact key schema is finalized during WP06 implementation.

E4 — Atexit success flag (in-process)

A single module-level boolean (or threading.Event) carried by src/specify_cli/diagnostics/dedup.py or a sibling shutdown_state.py:

def mark_invocation_succeeded() -> None:
    """Called by JSON-payload-emitting commands after their final write."""

def invocation_succeeded() -> bool:
    """Read by atexit handlers in sync/background.py and sync/runtime.py
    to decide whether to log shutdown warnings (when False) or skip them
    (when True)."""

Entities NOT introduced

This tranche does NOT introduce:

  • New mission types
  • New mission step contracts
  • New CLI subcommands
  • New persisted artifacts under kitty-specs/<mission>/
  • New rows in any database (no databases involved)