Tasks: Preserve user assets in migrate --force and git-source fetch

Mission: asset-preservation-migrate-fetch-01M3E857 | Branch: spec/asset-preservation-migrate-fetch Spec: spec.md · Plan: plan.md · Contract: contracts/preservation-contract.md · Research: research.md

Design authority: research.md F1–F10 (post-plan adversarial pass, dispositioned). All fixes are ATDD red-first, reuse the asset_preservation primitives, and add no CLI version bump.

Subtask Index (reference table — completion is event-sourced via mark-status)

IDDescriptionWPParallel
T001Red-first: customised template survives execute_migration(dry_run=False), exit 0WP01
T002Thread package-counterpart bytes into execute_migration (via _find_package_counterpart)WP01
T003Route IDENTICAL+SUPERSEDED removal through guard_destructive_removal(CanonicalContentProver(canonical=…), backup_parent=None, dry_run); drop raw unlinkWP01
T004Keep classify_asset/--dry-run reporting honest (differing → customised/preserved)WP01
T005Re-pin deletion tests in test_migrate_integration.py (incl. #285 version-skew) to corrected contractWP01
T006Re-pin test_e2e_runtime_integration.py SUPERSEDED+IDENTICAL pins; run test_global_runtime_convergence_unit.pyWP01
T007Red-first regressions in test_sources.py: pre-existing-pack, checkout-fail, empty-dir permitted, dirty-update, committed-ahead, ref-advance, tag/SHA non-regressionWP02
T008_first_install: clone-to-temp + snapshot move-aside promote; refuse on exists-&-non-empty (permit empty); rmtree only the tempWP02
T009_update: ref-type resolution (origin/<ref> branch / bare tag-SHA) + ahead & dirty checks + git-native backup or fail-closed refuse before resetWP02
T010Re-pin test_sources.py:188-191 call-order; verify :214/:264WP02
T011Extend FS-op gate _module_set() to migrate.py + git_source.pyWP03
T012Add migrate.py to _ROUTED_MODULES; keep git_source.py scanned-not-routed + never-allowlist guardWP03
T013Allowlist migrate.py:239 empty rmdir + git_source.py temp rmtree; re-pin git-argv reset allowlist entry + fix rationaleWP03
T014Prove both gates fail-able (add/remove a raw literal)WP03
T015CHANGELOG entry (no version bump)WP03

WP01 ∥ WP02 (disjoint write scopes). WP03 depends on WP01 AND WP02.


WP01 — migrate preserves customised assets (#4961)

default; only byte-identical defaults are removed. FR-001, FR-002.

byte-match canonical prover (backup_parent=None) → honest dry-run labels → re-pin the deletion tests (this file + test_e2e_runtime_integration.py).

CanonicalContentProver(canonical=<counterpart bytes>) (F5). #285 version-skew becomes preserve (intended).

  • Goal: migrate --force/--dry-run never delete a .kittify/ asset that differs from its package
  • Priority: P1 (headline data-loss). Independent test: customise a shipped template → it survives.
  • Subtasks: T001, T002, T003, T004, T005, T006
  • Sketch: red-first regression → thread counterpart bytes → route removal through the guard with a
  • Dependencies: none. Prompt: tasks/WP01-migrate-preserve-customised-assets.md
  • Risks: NFR-004 regression if the prover can't prove identical defaults — mitigated by

WP02 — git-source fetch preserves hand-authored packs (#4960, #4989)

silently discards local pack edits; a configured ref advances. FR-003, FR-004.

(permit empty) → _update ref-type resolution + ahead/dirty git-native backup-or-refuse → re-pin call-order test.

(F7) — both handled explicitly. Cross-platform swap onto existing dir (F8).

  • Goal: charter/doctrine fetch never rmtrees a pre-existing pack on a failed clone and never
  • Priority: P1. Independent test: seed a pack, fail the clone → pack survives; dirty/ahead update → preserved.
  • Subtasks: T007, T008, T009, T010
  • Sketch: red-first regressions → _first_install clone-to-temp + move-aside + refuse-on-nonempty
  • Dependencies: none. Prompt: tasks/WP02-git-source-preserve-packs.md
  • Risks: blanket origin/<ref> regresses tag/SHA packs (F6); worktree archive loses committed history

WP03 — close the destructive-op class + document (FR-005)

fail-able; CHANGELOG documents the fix. FR-005.

+ never-allowlist guard → allowlist the guarded literals + re-pin the git-argv reset entry → prove fail-able → CHANGELOG.

  • Goal: the destructive-op / mutation-ownership arch gate scans both product modules and stays
  • Priority: P2 (regression guard). Independent test: adding a raw literal to either module reds the gate.
  • Subtasks: T011, T012, T013, T014, T015
  • Sketch: extend _module_set() → add migrate.py to _ROUTED_MODULES, git_source.py scanned-not-routed
  • Dependencies: WP01, WP02 (the gate can only pass once both fixes exist).
  • Prompt: tasks/WP03-close-destructive-op-class.md
  • Risks: line-pinned allowlist entry drift (F4/F9); _ROUTED_MODULES completeness test.