Classify UI state reconciliation changes

This commit is contained in:
2026-05-23 17:14:41 +02:00
parent 87d49a2573
commit 215a91e599
3 changed files with 294 additions and 2 deletions

View File

@@ -0,0 +1,148 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Any
from api.services.lifecycle import status_value
from api.workplan_status import CLOSED_WORKSTREAM_STATUSES, normalize_workstream_status
class ReconciliationClass(str, Enum):
WRITE_THROUGH = "write_through"
DEFERRED = "deferred"
HUMAN_CONFIRMATION = "human_confirmation"
@dataclass(frozen=True)
class StateChangeClassification:
reconciliation_class: ReconciliationClass
reason: str
follow_up: str
WRITE_THROUGH_WORKSTREAM_STATUSES = {"proposed", "ready", "active", "backlog"}
TASK_STATUSES = {"todo", "in_progress", "blocked", "done", "cancelled"}
def classify_workstream_status_change(
*,
current_status: Any,
target_status: Any,
file_backed: bool,
archived_file: bool = False,
tasks_terminal: bool | None = None,
) -> StateChangeClassification:
"""Classify a UI-originated workstream status transition."""
current = normalize_workstream_status(current_status)
target = normalize_workstream_status(target_status)
if not file_backed:
return StateChangeClassification(
ReconciliationClass.DEFERRED,
"workstream is not backed by a local workplan file",
"create a reconciliation record for operator follow-up",
)
if archived_file:
return StateChangeClassification(
ReconciliationClass.HUMAN_CONFIRMATION,
"workplan file is archived",
"confirm whether to restore the file or leave the DB state unchanged",
)
if current == target:
return StateChangeClassification(
ReconciliationClass.WRITE_THROUGH,
"status is unchanged",
"no file update required",
)
if target in WRITE_THROUGH_WORKSTREAM_STATUSES and current not in CLOSED_WORKSTREAM_STATUSES:
return StateChangeClassification(
ReconciliationClass.WRITE_THROUGH,
"open lifecycle transition can be represented in workplan frontmatter",
"patch workplan status and sync the DB from file",
)
if target == "finished":
if tasks_terminal is True:
return StateChangeClassification(
ReconciliationClass.WRITE_THROUGH,
"all known tasks are terminal",
"patch workplan status to finished and sync the DB from file",
)
return StateChangeClassification(
ReconciliationClass.HUMAN_CONFIRMATION,
"finishing with non-terminal or unknown task state can hide open work",
"ask for confirmation or finish/cancel remaining tasks first",
)
if target == "archived":
return StateChangeClassification(
ReconciliationClass.HUMAN_CONFIRMATION,
"archiving may move files and hide active context",
"ask for confirmation and run the archive workflow",
)
if target == "blocked":
return StateChangeClassification(
ReconciliationClass.HUMAN_CONFIRMATION,
"blocked workstreams need an explicit blocker or dependency reason",
"capture the blocker before writing status",
)
return StateChangeClassification(
ReconciliationClass.DEFERRED,
f"unsupported workstream status transition {current!r} -> {target!r}",
"create a reconciliation record for operator review",
)
def classify_task_status_change(
*,
current_status: Any,
target_status: Any,
file_backed: bool,
task_linked: bool,
archived_file: bool = False,
blocking_reason: str | None = None,
) -> StateChangeClassification:
"""Classify a UI-originated task status transition."""
current = status_value(current_status)
target = status_value(target_status)
if not file_backed:
return StateChangeClassification(
ReconciliationClass.DEFERRED,
"task belongs to a workstream without a local workplan file",
"create a reconciliation record for operator follow-up",
)
if archived_file:
return StateChangeClassification(
ReconciliationClass.HUMAN_CONFIRMATION,
"task belongs to an archived workplan file",
"confirm whether to restore or edit the archived file",
)
if not task_linked:
return StateChangeClassification(
ReconciliationClass.DEFERRED,
"task is not linked to a task block in the workplan file",
"create or repair the task block link before writing status",
)
if current == target:
return StateChangeClassification(
ReconciliationClass.WRITE_THROUGH,
"status is unchanged",
"no file update required",
)
if target == "blocked" and not (blocking_reason or "").strip():
return StateChangeClassification(
ReconciliationClass.HUMAN_CONFIRMATION,
"blocked tasks require a blocking reason",
"capture the blocker before writing status",
)
if target in TASK_STATUSES:
return StateChangeClassification(
ReconciliationClass.WRITE_THROUGH,
"task status can be represented in the workplan task block",
"patch task block status and sync the DB from file",
)
return StateChangeClassification(
ReconciliationClass.DEFERRED,
f"unsupported task status transition {current!r} -> {target!r}",
"create a reconciliation record for operator review",
)

View File

@@ -0,0 +1,125 @@
from __future__ import annotations
from api.services.reconciliation import (
ReconciliationClass,
classify_task_status_change,
classify_workstream_status_change,
)
def test_workstream_open_transition_writes_through():
result = classify_workstream_status_change(
current_status="active",
target_status="backlog",
file_backed=True,
)
assert result.reconciliation_class == ReconciliationClass.WRITE_THROUGH
assert "frontmatter" in result.reason
def test_workstream_unfiled_transition_is_deferred():
result = classify_workstream_status_change(
current_status="active",
target_status="backlog",
file_backed=False,
)
assert result.reconciliation_class == ReconciliationClass.DEFERRED
assert "not backed" in result.reason
def test_workstream_finish_requires_terminal_tasks():
result = classify_workstream_status_change(
current_status="active",
target_status="finished",
file_backed=True,
tasks_terminal=False,
)
assert result.reconciliation_class == ReconciliationClass.HUMAN_CONFIRMATION
assert "open work" in result.reason
def test_workstream_finish_with_terminal_tasks_writes_through():
result = classify_workstream_status_change(
current_status="active",
target_status="finished",
file_backed=True,
tasks_terminal=True,
)
assert result.reconciliation_class == ReconciliationClass.WRITE_THROUGH
assert "finished" in result.follow_up
def test_archived_workstream_file_requires_confirmation():
result = classify_workstream_status_change(
current_status="finished",
target_status="active",
file_backed=True,
archived_file=True,
)
assert result.reconciliation_class == ReconciliationClass.HUMAN_CONFIRMATION
assert "archived" in result.reason
def test_task_status_update_writes_through_to_task_block():
result = classify_task_status_change(
current_status="todo",
target_status="in_progress",
file_backed=True,
task_linked=True,
)
assert result.reconciliation_class == ReconciliationClass.WRITE_THROUGH
assert "task block" in result.reason
def test_task_without_file_is_deferred():
result = classify_task_status_change(
current_status="todo",
target_status="in_progress",
file_backed=False,
task_linked=True,
)
assert result.reconciliation_class == ReconciliationClass.DEFERRED
assert "without a local workplan file" in result.reason
def test_unlinked_task_is_deferred_until_link_repaired():
result = classify_task_status_change(
current_status="todo",
target_status="in_progress",
file_backed=True,
task_linked=False,
)
assert result.reconciliation_class == ReconciliationClass.DEFERRED
assert "not linked" in result.reason
def test_blocked_task_requires_blocking_reason():
result = classify_task_status_change(
current_status="todo",
target_status="blocked",
file_backed=True,
task_linked=True,
)
assert result.reconciliation_class == ReconciliationClass.HUMAN_CONFIRMATION
assert "blocking reason" in result.reason
def test_blocked_task_with_reason_writes_through():
result = classify_task_status_change(
current_status="todo",
target_status="blocked",
file_backed=True,
task_linked=True,
blocking_reason="Waiting on dependency",
)
assert result.reconciliation_class == ReconciliationClass.WRITE_THROUGH

View File

@@ -4,7 +4,7 @@ type: workplan
title: "UI State Change Reconciliation" title: "UI State Change Reconciliation"
domain: custodian domain: custodian
repo: state-hub repo: state-hub
status: proposed status: active
owner: codex owner: codex
topic_slug: custodian topic_slug: custodian
planning_priority: high planning_priority: high
@@ -33,11 +33,26 @@ for other workstream and task state transitions.
This workplan defines the reconciliation path between UI actions, DB state, This workplan defines the reconciliation path between UI actions, DB state,
repo files, and scheduled cleanup. repo files, and scheduled cleanup.
## Reconciliation Classes
| Class | Meaning | Default follow-up |
|-------|---------|-------------------|
| `write_through` | The change can be represented directly in the workplan file. | Patch the file first, then sync the DB from file state. |
| `deferred` | The change cannot safely touch the file immediately. | Create a visible reconciliation record or task. |
| `human_confirmation` | The change may hide work, require a blocker reason, or move archived context. | Ask for confirmation or gather missing intent before writing. |
## Initial Transition Classification
| Target | Safe write-through | Deferred | Human confirmation |
|--------|--------------------|----------|--------------------|
| Workstream status | Open, file-backed transitions among `proposed`, `ready`, `active`, and `backlog`; `finished` only when known tasks are terminal. | Non-file-backed workstreams, unsupported statuses, unavailable file links. | Archived files, `blocked` without explicit blocker/dependency context, `finished` with open/unknown tasks, `archived`. |
| Task status | Linked task blocks in active workplan files for `todo`, `in_progress`, `done`, `cancelled`, and `blocked` with a blocking reason. | Non-file-backed workstreams or DB tasks without a linked task block. | Archived files and `blocked` task changes without a blocking reason. |
## T01 - Classify UI State Changes ## T01 - Classify UI State Changes
```task ```task
id: STATE-WP-0048-T01 id: STATE-WP-0048-T01
status: todo status: done
priority: high priority: high
state_hub_task_id: "0332be96-ebc7-4a7b-97c6-bbe6ae3a66ac" state_hub_task_id: "0332be96-ebc7-4a7b-97c6-bbe6ae3a66ac"
``` ```
@@ -49,6 +64,10 @@ confirmation.
Done when each workstream/task transition has a reconciliation class and a Done when each workstream/task transition has a reconciliation class and a
reason. reason.
Result 2026-05-23: added the initial classification matrix and
`api.services.reconciliation` helpers for workstream/task status changes. Tests
pin the write-through, deferred, and human-confirmation decisions.
## T02 - Add Reconciliation API Contract ## T02 - Add Reconciliation API Contract
```task ```task