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_surfaceroutes its destructive removal through ONE:137 rmtree+:139 unlinkcarry NO raw literal after routing;:142 root.rmdir()- Return value +
remove_agents/_remove_orphaned_agent_dirsmessaging are - Mixed (manifest+user) dir ⇒
_prove_dirreturns 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
syncleaves.kittify/command-skills-manifest.jsonpinned release/hash --jsonoutput 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-commitdoes NOT bear the spec-kitty install_commit_guardmust 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
.mdextension; 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_commitcommits 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.