Data Model: User-content preservation for mutating flows

This mission is behavioural (guarding mutating flows), not schema-additive. The "entities" are the decision types the flows reason over. No new persisted schema is introduced except the overwrite backup sidecar naming convention.

Entities / value objects

MutatingFlow

A Spec Kitty command that writes, deletes, or rewrites files or git state.

manifest), implement (hook install), intake (+--auto), validate-encoding --fix, agent tasks finalize-tasks, safe_commit/upgrade.

user-authored content.

  • Members in scope: agent config remove, agent config sync (orphan sweep +
  • Invariant (FR-014): never reports success while destroying, losing, or corrupting

UserAuthoredAsset

A file/hook/brief the operator created that Spec Kitty did not deploy and cannot prove it owns.

.git/hooks/pre-commit, a hand-written .kittify/mission-brief.md, committed spec *.md, a repository-pinned command-skills-manifest.json, staged git hunks.

  • Examples: .claude/commands/*.md, other agents' managed subdirs, a foreign

OwnershipProof (reused, tool_surface.operations.OwnershipProof)

Content-based evidence that authorizes removal/replacement. Kinds:

  • manifest: manifest entry + content_hash == sha256(current bytes) + copy delivery.
  • signature: hook body bears # Generated by spec-kitty..
  • canonical: bytes match a known canonical/version-marker.
  • Never name- or directory-based (C-003).

PreservationVerdict (reused, asset_preservation.OwnershipVerdict)

The guard's decision for one path: owned (removed) | preserved-in-place | archived-then-removed. Carries diagnostic (the operator-facing warning) and (when archived) backup_path.

verdict.owned; a preserved verdict never renders as "Removed".

  • New usage rule (FR-015): the caller's success/΅"Removed" messaging is driven by

OverwriteBackup (new)

A byte-exact sidecar copy of a foreign file taken before an overwrite.

existing backup), symlink-aware (captures os.readlink, does not deref).

  • Naming: <path>.<timestamp> in-place sidecar (e.g. .git/hooks/pre-commit.2026-09-22T17-58-26Z).
  • Producer: new asset_preservation.backup.backup_before_overwrite(path) -> Path.
  • Properties: byte-identical, mode+mtime preserved, O_EXCL (never clobbers an
  • Surfaced: the producing flow prints the returned path.

EffectiveDependencyGraph

The WP dependency graph actually persisted to frontmatter after preservation — the graph agent tasks finalize-tasks must validate (not the tasks.md-parsed graph).

self-refs, unknown-WP refs). On failure: exit non-zero, seed no status.

  • Validation: detect_dependency_cycles + validate_dependencies (cycles,

SafeCommitOutcome

The typed result of safe_commit.

every caller surfaces both fields; upgrade must not flatten it to a generic skip warning with exit 0.

  • Success: commit created via a temporary index; operator index/worktree untouched.
  • Failure (residual path): SafeCommitRecoveryFailed(orphan_stash_ref, commit_sha) —

State transition — removal decision (per candidate path)

exists? ── no ──► skip
   │ yes
   ▼
prover.prove(path) ── proof ──► REMOVE (guard performs it) ──► prune manifest ──► render "Removed …" (verdict.owned=True)
   │ None (unprovable)
   ▼
backup_parent given? ── no ──► PRESERVE in place ──► render "Preserved …; not package-owned" (verdict.owned=False, exit 0)
   │ yes
   ▼
ARCHIVE verbatim ──► remove original ──► render "Preserved …; archived to <backup>"

State transition — overwrite decision (hook / intake)

target pre-exists & foreign?
   │ no ──► write as today
   ▼ yes
flow is workflow-internal (implement hook)? ──► backup_before_overwrite() ──► replace ──► surface backup path (exit 0)
flow is user-driven (intake)? ──────────────► refuse, require --force (exit non-zero), brief byte-identical