Phase 1 Data Model — Mission Completion Terminal State
No new persisted store; this mission adds fields/derivations over the existing append-only status event log. No schema migration (NFR-002).
Entities
1. Cancellation Provenance (on the canceled status event)
(status/models.py, round-tripped in to_dict/from_dict), set at the emit site (tasks_move_task.py). operator iff the reason came from a non-empty --note. None for non-cancel events (backward-compatible default). Not policy_metadata; not a reduce-time template match.
non-empty operator note). The CLI's auto-synthesized "Force move to canceled" / "move-task: …" default is synthetic and never accept-eligible.
known synthetic templates is treated as operator (NFR-002).
- Home: an event in
status.events.jsonlwithto_lane == "canceled". - Fields used/added:
reason: str | None— existing; the human note.reason_source: "operator" | "synthetic" | None— new first-class field onStatusEventactor,at,force— existing; carried into reporting.- Invariant: a cancellation is accept-eligible iff
reason_source == "operator"(a - Legacy rule: for events predating
reason_source, areasonthat does not match the
2. Reduced snapshot — cancellation slot
when lane == "canceled". All other lanes unchanged (NFR-002).
stays deterministic and its golden tests are updated in lockstep.
- Home: per-WP dict from
reduce()(status/reducer.py:166-177). - Field added:
cancellation_reason: str | None(+ itsreason_source), projected only - Invariant: the projection is derived purely from the event log (C-002); the reducer
3. Acceptable-Ending Decision (predicate)
- Home:
is_acceptable_ending(lane, *, has_provenance) -> boolinstatus_lanes.py. - Truth table:
| lane | has_provenance | acceptable ending |
|---|---|---|
approved | — | ✅ |
done | — | ✅ |
canceled | true | ✅ |
canceled | false | ❌ (structured blocker) |
| any other | — | ❌ |
merge (executor.py + policy/merge_gates.py), dependency gate (dependency_graph.py). Single source of truth.
status_lanes.py) — the one reader consumers call so reason_source == "operator" is never inlined at 3 sites (avoids a whack-a-field regression).
- Consumers: accept (
acceptance/__init__.py, replacing_ACCEPTED_READY_LANES), - Companion accessor:
has_operator_provenance(wp_snapshot) -> bool(co-located in
4. canceled_wps report entry (accept --json)
appears in blockers, not here.
- Shape (pinned, NFR-003):
{ "wp_id": str, "reason": str, "actor": str, "at": str (ISO-8601) }. - Rule: appears only for accept-eligible cancellations; a non-provenance cancellation
5. Post-Integration Trigger (FR-007 detector)
branch the forge will run", "consecutive runs", "merge-blocked-when-absent"), validated against a fixed labeled corpus. Advisory only — produces a warning record, never a block.
- Home: authoring-time check over a work package's acceptance-criteria / subtask text.
- Fields:
wp_id,matched_phrase,criterion_excerpt. - Signal: membership in an enumerable trigger-phrase set (e.g. "after merge", "on a
State transition (unchanged matrix, C-001)
The nine-lane matrix is untouched. This mission changes only how downstream consumers interpret the existing terminal canceled lane:
canceled (terminal) ──has operator provenance──▶ acceptable ending (accept ✅, merge skips WP, dep resolved)
canceled (terminal) ──synthetic reason only──▶ structured blocker (accept ❌)