Data Model: Canonical Baseline and Repository Boundary

Mission: 081-canonical-baseline-and-repository-boundary Date: 2026-04-10

Identity Layer — Before and After

Current Model (pre-081)

All identity fields are bundled under ProjectIdentity and stored in the project: config section. The naming implies SaaS project scope, but every value is locally minted and repository/build-scoped. The same project_uuid value doubles as both a "project identity" label and the required namespace scope key for body sync, queue dedup, and upstream contract validation.

ProjectIdentity
├── project_uuid   : UUID4    # locally minted, actually repository identity
│                              # also used as required namespace key for body sync
├── project_slug   : str      # derived from git remote/dir, actually repo display name
├── node_id        : str      # 12-char hex machine ID
├── repo_slug      : str?     # optional owner/repo Git provider reference
└── build_id       : str      # UUID4, per checkout/worktree

Config representation (.kittify/config.yaml):

project:
  uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  slug: "spec-kitty"
  node_id: "abcdef012345"
  build_id: "f1e2d3c4-b5a6-7890-1234-567890abcdef"
  # repo_slug: "Priivacy-ai/spec-kitty"  # optional

Wire protocol (event envelope):

{
  "project_uuid": "a1b2c3d4-...",
  "project_slug": "spec-kitty",
  "build_id": "f1e2d3c4-...",
  "node_id": "abcdef012345",
  "repo_slug": "Priivacy-ai/spec-kitty"
}

Namespace/dedup (body sync):

NamespaceRef(
    project_uuid="a1b2c3d4-...",   # required, non-empty
    mission_slug="081-...",
    target_branch="main",
    mission_type="software-dev",
    manifest_version="1",
)

Canonical Model (post-081)

Identity is split into scoped layers. RepositoryIdentity holds the local stable identity. ProjectBinding holds the optional SaaS collaboration identity. Each field name reflects its actual scope. repository_uuid replaces project_uuid as the required namespace key for all local operations.

RepositoryIdentity
├── repository_uuid    : UUID4    # stable local identity (was project_uuid)
│                                  # required namespace key for body sync/dedup
├── repository_label   : str      # human-readable display name (was project_slug)
├── node_id            : str      # 12-char hex machine ID (unchanged)
├── repo_slug          : str?     # optional owner/repo Git provider reference (unchanged)
└── build_id           : str      # UUID4, per checkout/worktree (unchanged)

ProjectBinding (optional, absent until SaaS binding)
├── project_uuid       : UUID4    # SaaS-assigned collaboration identity
└── bound_at           : datetime # when binding was established

Config representation (.kittify/config.yaml):

repository:
  repository_uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  repository_label: "spec-kitty"
  node_id: "abcdef012345"
  build_id: "f1e2d3c4-b5a6-7890-1234-567890abcdef"
  # repo_slug: "Priivacy-ai/spec-kitty"  # optional, unchanged meaning

# project:                # absent until SaaS binding
#   project_uuid: "..."   # assigned by SaaS, not locally minted
#   bound_at: "..."       # ISO timestamp of binding

Wire protocol (event envelope, post-migration):

{
  "repository_uuid": "a1b2c3d4-...",
  "repository_label": "spec-kitty",
  "repo_slug": "Priivacy-ai/spec-kitty",
  "build_id": "f1e2d3c4-...",
  "node_id": "abcdef012345",
  "project_uuid": null
}

Namespace/dedup (body sync, post-migration):

NamespaceRef(
    repository_uuid="a1b2c3d4-...",   # required, non-empty (was project_uuid)
    mission_slug="081-...",
    target_branch="main",
    mission_type="software-dev",
    manifest_version="1",
)

Field Mapping

Current NameCurrent SectionCanonical NameCanonical SectionMigrationSemantic Change
uuidproject:repository_uuidrepository:Copy value, rename key and sectionName only; value and scope unchanged
slugproject:repository_labelrepository:Copy value, rename key and sectionName only; value and derivation unchanged
node_idproject:node_idrepository:Move to new section, no key renameNone
build_idproject:build_idrepository:Move to new section, no key renameNone
repo_slugproject:repo_slugrepository:Move to new section, no key renameNone; retains owner/repo meaning
(new)project_uuidproject:New field, absent until SaaS bindingNew; SaaS-assigned only
(new)bound_atproject:New field, absent until SaaS bindingNew

Class Rename Mapping

CurrentCanonicalNotes
ProjectIdentityRepositoryIdentityPrimary identity class
generate_project_uuid()generate_repository_uuid()UUID4 minting function
derive_project_slug()derive_repository_label()Git remote/dir name derivation
backfill_project_uuid()backfill_repository_uuid()Legacy migration function
ensure_identity()ensure_identity()Name is scope-neutral, no change needed
load_identity()load_identity()Name is scope-neutral, no change needed

Wire Protocol Field Mapping

Both UUID and label fields require dual-write during migration:

Current Wire FieldCanonical Wire FieldDual-Write RequiredNotes
project_uuidrepository_uuidYesSame value; both sent during transition
project_slugrepository_labelYesSame value; both sent during transition
repo_slugrepo_slugNoUnchanged name and meaning
build_idbuild_idNoUnchanged
node_idnode_idNoUnchanged
(absent)project_uuid (when bound)N/ANew field, only present after SaaS binding

Namespace/Dedup Key Migration

ComponentCurrent Key FieldCanonical Key FieldMigration
NamespaceRef dataclassproject_uuid (required non-empty)repository_uuid (required non-empty)Rename field; same value flows through
body_upload_queue SQLiteproject_uuid TEXT NOT NULLrepository_uuid TEXT NOT NULLSchema migration; existing rows carry same value
UNIQUE constraintproject_uuid, mission_slug, ...repository_uuid, mission_slug, ...Schema migration
Queue coalescence keysproject_uuid scopes deduprepository_uuid scopes dedupUpdate key lists
upstream_contract.jsonproject_uuid in required_fieldsrepository_uuid in required_fieldsConfig file update

Key invariant: The UUID value itself does not change. Only the field name changes. Existing offline queue entries remain valid because the value stored in the column is the same — the column is just renamed.

Function Rename Mapping (Path Resolution)

CurrentCanonicalCall SitesNotes
locate_project_root()locate_repository_root()36 across 20 filesTwo implementations exist (paths.py, project_resolver.py); consolidate first
get_project_root_or_exit()get_repository_root_or_exit()11 across 8 filesThin wrapper; rename follows

Variable Standardization

Result variables from path resolution are currently inconsistent:

Current Names (mixed)Canonical Name
project_rootrepo_root
project_dirrepo_root
main_reporepo_root
resolved_rootrepo_root
detected_rootrepo_root
repo_rootrepo_root (already correct)
rootrepo_root (when holding repository root)

State Transitions

The ProjectBinding entity has a simple lifecycle:

[absent] --bind--> [bound]
[bound]  --unbind--> [absent]
  • absent: No SaaS project claims this repository. project: section is absent from config.yaml. All CLI operations work normally. repository_uuid is the namespace key.
  • bound: A SaaS project has been assigned. project: section appears in config.yaml with project_uuid and bound_at. CLI operations continue to work identically; the project_uuid is included in wire protocol payloads alongside repository_uuid.

No intermediate states. Binding is atomic (either the SaaS assigned a UUID or it didn't).

Consumer Impact Summary

ConsumerFields UsedMigration Complexity
sync/project_identity.pyAll fieldsHigh — core class rename + all field renames
sync/emitter.pyproject_uuid, project_slug, build_id, node_id, repo_slugHigh — event envelope field renames (wire protocol dual-write for UUID and label)
sync/namespace.pyproject_uuid (required namespace key)High — rename required field + update all callers
sync/queue.pyproject_uuid (SQLite schema, coalescence keys)High — schema migration + key rename
sync/client.pybuild_idLow — field name unchanged
cli/commands/tracker.pyAll fieldsMedium — project_identity dict construction
tracker/saas_service.pyproject_identity dictMedium — payload rename
tracker/saas_client.pyproject_identity dictMedium — HTTP payload rename
context/resolver.pyproject_uuidLow — single read site; rename to repository_uuid
dossier/drift_detector.pyproject_uuid, node_idLow — baseline key rename
migration/backfill_identity.pyproject_uuidLow — function rename
core/paths.pyN/A (function names only)Medium — 36 call sites
cli/helpers.pyN/A (function names only)Low — wrapper rename + 11 call sites
core/upstream_contract.jsonproject_uuid in required_fieldsLow — config file update