Contracts
identity-resolution.md
Contract: Identity Resolution (read vs write boundary)
This mission has no HTTP API; its "contracts" are the behavioral guarantees of the identity-resolution functions and the call-site policy.
C-IR-1 — resolve_identity(repo_root) is side-effect-free
- MUST return a
ProjectIdentitywithis_complete == Truewhen an on-diskproject_uuidexists. - MUST return a not-initialized, side-effect-free identity when
project_uuidis absent (see C-IR-4); read paths MUST NOT mintproject_uuid. - MUST NOT write
.kittify/config.yaml(or any tracked file) under any input. - For an already-complete on-disk identity: returns it unchanged.
- For an incomplete on-disk identity with
project_uuid: fills missing fields in memory only, using the deterministic rules below.
C-IR-2 — Deterministic completion (Decision C)
When a field is missing during read-path resolution:
| Field | Rule |
|---|---|
project_slug | derive_project_slug(repo_root) (already deterministic) |
node_id | sha256(hostname:username)[:12] (already deterministic) |
build_id | uuid5(NAMESPACE, f"{project_uuid}:{node_id}") — changed from uuid4() |
project_uuid | NOT minted on read paths; must already be present (else → C-IR-4) |
Guarantee: for fixed (project_uuid, node_id), build_id is identical across calls (NFR-001).
C-IR-3 — ensure_identity(repo_root) remains the only persisting path
- MAY write
config.yaml(completes + persists). - MUST be called only at write-authorized boundaries:
init(init.py:99,863) and explicit apply/bind commands. - MUST NOT be called from read/emit/background paths after this mission.
C-IR-4 — Uninitialized checkout on a read path
- If
project_uuidis absent (truly uninitialized) and a read/emit path needs identity: - The path MUST NOT mint+persist a random
project_uuidas a side effect. - Expected resolution: the checkout passes through
init(write-authorized) first. - Acceptable interim behavior: the read command is side-effect-free and either no-ops sync or surfaces a clear "not initialized" signal. (Exact UX is an implementation choice; it MUST NOT write
config.yaml.)
Call-site policy (verifiable by grep)
ensure_identity(is retained ONLY at write-authorized boundaries:init.py(×2) andcli/commands/tracker.py— the explicittracker bindpath (_bind_saas), which is a user-initiated write boundary per AS-5 / C-IR-3.- Every read/emit/background former call site resolves via
resolve_identity(:emitter.py(_get_project_identity,_create_git_resolver),sync/routing.py,sync/events.py,sync/__init__.py,sync/dossier_pipeline.py,tracker/origin.py.
tracker-binding-report.md
Contract: Tracker binding_ref report-only on read paths
C-TB-1 — Read-like tracker ops do not persist binding_ref
For status, sync_pull, sync_push, sync_run, map_list (tracker/saas_service.py):
WHEN the server response carries a new/changed binding_ref
THEN the method MUST NOT call save_tracker_config (no .kittify/config.yaml write)
AND it surfaces the available upgrade as a result field: pending_binding_upgrade=<ref>
When the server returns no binding_ref, or it matches the stored one, the method is a no-op (unchanged behavior).
C-TB-2 — Persistence only at an explicit boundary
WHEN the operator runs an explicit `tracker bind` / apply-style command
THEN save_tracker_config persists binding_ref to .kittify/config.yaml (write-authorized)
So an intentional binding upgrade still works; it just no longer happens as a side effect of reading.
C-TB-3 — Surface shape
pending_binding_upgradeis returned on the result object/dict of the read method.- An optional, non-fatal one-line notice MAY inform the operator an upgrade is available and how to apply it. The notice MUST NOT write any tracked file.
worktree-clean-invariant.md
Contract: Worktree-Clean Invariant (INV-1)
Covered command surface
The invariant applies to every command in this set (the parametrized test enumerates it):
- status-event emission (the
EventEmitteremit path) sync status(incl.--check),sync pull,sync push,sync run- background dossier sync trigger
- lifecycle SaaS fan-out handler
tracker status,tracker map list- dashboard daemon tick
Guarantee
GIVEN a clean checkout with SPEC_KITTY_ENABLE_SAAS_SYNC=1 and auth present
AND snap = `git status --porcelain` AND cfg = (content + mtime) of .kittify/config.yaml
WHEN any covered command runs to completion (success OR handled failure)
THEN `git status --porcelain` == snap (byte-identical)
AND .kittify/config.yaml == cfg (unchanged)
Also holds when SaaS sync is disabled or unauthenticated (FR-008): no partial writes.
Non-goals / boundaries
- The invariant is enforced by REMOVING writes, never by allowlisting (C-001). The
record-analysisallowlist MUST NOT grow to includeconfig.yaml. - Write-authorized commands (
init, explicit bind/apply) are OUT of the covered set — they may persist.
Regression guard (paired contract)
GIVEN a checkout with a genuine uncommitted source edit
WHEN `record-analysis` runs
THEN it still exits non-zero with error_code DIRTY_WORKTREE (FR-007 / SC-004)
AND the allowlist used by the guard does NOT contain .kittify/config.yaml
Extensibility guard
GIVEN a NEW read/background command is added to the covered set
WHEN it violates INV-1 (dirties the tree)
THEN the parametrized test FAILS before merge (FR-006 / AS-7)