Contracts

preservation-contract.md

Contract: Mutating-flow preservation (extends ownership-guard-contract.md)

The executable contract the ATDD tests pin. This extends — does not restate — the prior ownership-boundary-preservation-01M32KEN/contracts/ownership-guard-contract.md (C1 guard surface, C3 non-vacuous census). New surfaces only.

P1 — New shared overwrite-backup primitive (asset_preservation.backup)

backup_before_overwrite(path: Path) -> Path

Invariants: 1. Returns the sidecar path <path>.<timestamp> (timestamp from the canonical clock seam), created with O_EXCL — a second call for the same path+timestamp raises rather than clobbering an existing backup. 2. The sidecar is byte-identical to path; mode and mtime preserved where the platform allows. 3. Symlink-aware: if path is a symlink, capture the link (via os.readlink) — do NOT dereference-and-copy the target silently, and do NOT raise on a broken link where a readlink would succeed. 4. Does NOT remove or modify path (the caller replaces it afterwards); no parent-directory allocation (this is an in-place sidecar, not the removal-flow .backup-<ts>/ tree). 5. Built on the existing exported write_file_verbatim core for the non-symlink case (single copy authority).

P2 — Per-flow routing contract

C-remove/sync (#4907, #2691 removal) — cli/commands/agent/config.py

guard_destructive_removal(surface, is_tree=True, prover=ManifestProver(check_command=True), backup_parent=None).

(empty-only) is the single rationalized allowlist entry.

verdict-driven: verdict.owned ⇒ "Removed"; else surface verdict.diagnostic ("Preserved …"), exit 0. No "Removed" on a preserved surface (FR-015).

  • _remove_project_agent_surface routes its destructive removal through ONE
  • :137 rmtree + :139 unlink carry NO raw literal after routing; :142 root.rmdir()
  • Return value + remove_agents / _remove_orphaned_agent_dirs messaging are
  • Mixed (manifest+user) dir ⇒ _prove_dir returns None ⇒ whole dir preserved (no partial rmtree).

C-sync-manifest (#2691 manifest) — command_installer.install / manifest_store

values byte-identical. A manifest refresh occurs only under an explicit opt-in.

non-refreshing sync).

  • A normal sync leaves .kittify/command-skills-manifest.json pinned release/hash
  • --json output enumerates every intended tracked mutation before it occurs (zero on a

C-hook (#4895) — policy/hook_installer.py (+ lanes/implement_support.py)

signature (# Generated by spec-kitty.), call backup_before_overwrite(hook_path), then install, then surface the backup path in output. A signed hook is replaced with no backup churn. No pre-existing hook ⇒ unchanged behaviour.

except Exception: return None.

  • Before installing, if an existing .git/hooks/pre-commit does NOT bear the spec-kitty
  • install_commit_guard must not swallow the backup/report step in a blanket

C-intake (#4910) — cli/commands/intake.py

and :156 (--auto) — drop the and _source_path.exists() conjunct. Brief present ⇒ refuse without --force (exit non-zero), brief byte-identical. No backup.

  • The overwrite gate keys on brief_path.exists() alone at BOTH :296 (explicit/stdin)

C-encoding (#4896) — text_sanitization.py (+ validate_encoding.py)

  • Text/binary classified by content sniff, not .md extension; binaries skipped, never rewritten.
  • Fallback decode scoped to the offending byte(s) or refused with offset(s); no whole-file cp1252 re-decode.
  • Existing line endings preserved (no universal-newline CRLF→LF).
  • "Fixed" reported ONLY when the result is a faithful repair (no non-target byte changed).

C-finalize (#4890) — cli/commands/agent/tasks_finalize.py (+ _validation.py)

detect_dependency_cycles and validate_dependencies (reuse mission_finalize's validator; do not fork a second). On any cycle/self-ref/unknown-WP: exit non-zero, seed no canonical status. On success: dependencies payload == persisted graph.

  • Validate the effective persisted graph (post frontmatter-preservation) with both

C-safecommit (#4888) — git/commit_helpers.py, upgrade/autocommit.py, upgrade.py

and git stash list are byte-identical before/after across all callers, including with an unrelated file partially staged. It still commits exactly the passed paths.

commit_sha) propagates; upgrade` renders both (no exit-0 generic skip warning that hides the stash or misstates the landed commit).

  • safe_commit commits via a temporary index / path-scoped commit; git diff --cached
  • On a genuine residual restore failure, `SafeCommitRecoveryFailed(orphan_stash_ref,

P3 — Non-vacuous census extension (tests/architectural/test_mutation_ownership_routing.py)

Extends prior contract C3 (do not weaken shrink-only / set-equality / self-mutation): 1. cli/commands/agent/config.py added to the scanned _module_set(). 2. "cli/commands/agent/config.py" added to the pinned _ROUTED_MODULES frozenset — test_pinned_routed_module_set_is_complete asserts live==pinned set-equality (route-without-pin fails AND pin-without-route fails). 3. :142 Path.rmdir (empty-only) added to the frozen, individually-rationalized, shrink-only allowlist. No other config.py op is classifier-flagged. 4. The overwrite family (#4895, #4910) stays OUT of this removal census (that widening is #4901, out of scope).

P4 — ATDD both-directions (per WP)

For each routed/fixed site the suite pins BOTH:

target IS removed (remove AND sync paths), a genuinely faithful repair IS applied, a valid graph IS finalized — so a preserve/refuse-everything fix cannot pass.

  • preserve/repair/refuse direction (RED-on-base 32...→ GREEN-on-fix) — the C-011 repro.
  • owned-delete / faithful-success anchor (green-on-base) — a genuinely package-owned

Every preserve-direction repro is RED on the mission base and GREEN on the site's final commit; capture the actual RED-on-base run output per DIRECTIVE_030.