Phase 1 Data Model: Source-aware unblock refusal message

No persisted data model changes. This mission touches an in-memory decision request and reads (never mutates) the lane state machine. The "model" here is the lane→reachability partition that shapes the message.

Entities

MoveTaskRequest (existing; read-only for this change)

Relevant fields consumed by _guard_planned_rollback:

FieldMeaningUsed to…
target_laneThe requested destination laneGate on == Lane.PLANNED (unchanged)
old_laneThe work package's current/source lane (may be an alias, e.g. doing)NEW: resolve → lane → allowed_targets() to shape the message
forceWhether --force was passedDeliberately unread by this guard (force-proofness) — unchanged
feedback_provided / feedback_exists / feedback_is_file / feedback_contentReview-feedback file factsThe four allow/deny checks (unchanged)

Lane state (existing; read-only accessor)

wp_state_for(lane).allowed_targets() -> frozenset[Lane] — the state machine's own declaration of legal outgoing edges. Source of truth for the legal-target list; the message never hard-codes a lane set.

Reachability partition (the new derived fact)

planned_reachable(old_lane) := Lane.PLANNED in wp_state_for(resolve_lane_alias(old_lane)).allowed_targets()

Source laneallowed_targets()planned reachable?Message arm
in_reviewincludes plannedyesreview-feedback (existing text)
in_progressincludes plannedyesreview-feedback (existing text)
approvedincludes plannedyesreview-feedback (existing text)
genesis{canceled, planned}yesreview-feedback (existing text)
blocked{in_progress, canceled}nolegal-targets; name --to in_progress resume
canceled{} (terminal)nolegal-targets (terminal — no targets)
done{} (terminal)nolegal-targets (terminal — no targets)
uninitialized{}nolegal-targets (no targets)

Note: the partition is computed, not enumerated in code — any future change to a lane's allowed_targets() re-partitions automatically.

Invariants

--to planned move without a valid non-empty review-feedback file returns RefuseExit1 (lane unchanged). The reachability fact selects wording only.

allowed_targets() but never calls check_transition/build_transition_plan; it cannot proceed to an emit as a side effect of computing the message.

event (no force=true rewind out of done).

  • INV-1 (force-proof): for every source lane and every force value, a
  • INV-2 (no state-machine dependency inversion): the guard reads
  • INV-3 (no resurrection event): a refused move emits no status transition