Data Model: Upgrade reports one outcome

UpgradeOutcomeKind (string enum)

applied · no_op · drift_unresolved · failed

UpgradeFailureReason (string enum)

ValueHolds when
migration_failedresult.success is false
activation_erroractivation_errors is non-empty
worktree_failureworktree_failures is non-empty
commit_recovery_failedthe churn commit failed to restore the operator's staging
surface_repair_faileda repair was not applied
preview_incompletea dry-run preview could not be completed
surface_driftdrifted_paths is non-empty

Order above is the order of reasons.

SurfaceRepairReport (frozen)

FieldTypeMeaning
drifted_pathstuple[Path, ...]managed files preserved pending consent
failedboola repair was not applied
preview_incompletebooldry-run preview could not be completed
failure_messagestuple[str, ...]one reason per non-applied repair, synthesised when the repair gave no error diagnostic

Returned by the finalizer's run_surface_repair callable.

UpgradeOutcome (existing, extended)

Field / memberChange
result, manual_review_paths, worktree_failures, activation_errors, repair, committed, exit_codeunchanged
surface_drift_failedremoved
had_migrations: boolnew
drifted_paths: list[Path]new
surface_repair_failed: bool, preview_incomplete: boolnew
warnings() -> list[str]new; result.warnings plus the mission-state repair message when repair.failed
reasons -> tuple[UpgradeFailureReason, ...]new, derived
kind -> UpgradeOutcomeKindnew, derived
effective_success -> boolnow not reasons (same truth table as before)
errors() -> list[str]new; ordered: result.errors, activation_errors, worktree_failures (de-duplicated), repair-failure message when no diagnostic names it, drift message when drifted_paths
status -> strnew; success / up_to_date / failed
closing_line() -> strnew
derive_exit_code()unchanged; still the only site

Invariants

1. exit_code == 0 iff kind in {applied, no_op}. 2. status == "failed" iff exit_code != 0. 3. errors() is non-empty whenever exit_code != 0. 4. The drift message appears in errors() iff len(drifted_paths) >= 1, and its count equals len(drifted_paths). 5. A repair (mission-state) outcome never contributes a reason.