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 ProjectIdentity with is_complete == True when an on-disk project_uuid exists.
  • MUST return a not-initialized, side-effect-free identity when project_uuid is absent (see C-IR-4); read paths MUST NOT mint project_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:

FieldRule
project_slugderive_project_slug(repo_root) (already deterministic)
node_idsha256(hostname:username)[:12] (already deterministic)
build_iduuid5(NAMESPACE, f"{project_uuid}:{node_id}")changed from uuid4()
project_uuidNOT 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_uuid is absent (truly uninitialized) and a read/emit path needs identity:
  • The path MUST NOT mint+persist a random project_uuid as 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) and cli/commands/tracker.py — the explicit tracker bind path (_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_upgrade is 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 EventEmitter emit 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-analysis allowlist MUST NOT grow to include config.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)