generated from coulomb/repo-seed
Replace the ad-hoc coordination-domain spine with the Repo Classification Standard: 14 market domains, classification columns on managed_repos, and workplans anchored by repo_id (topic_id optional). - Add Alembic migration d8e9f0a1b2c3 with data backfill and workstream→workplan rename - Add api/classification.py validation and register-from-classification tooling - Expose workplan-first REST/MCP surface with legacy workstream aliases - Add C-24 consistency rule and legacy domain frontmatter mapping - Update dashboard repos page with category/capability/stake filters - Update orientation docs; mark STATE-WP-0065 finished
153 lines
5.5 KiB
Python
153 lines
5.5 KiB
Python
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.task_status import CANONICAL_TASK_STATUSES
|
|
from api.workplan_status import CLOSED_WORKPLAN_STATUSES, normalize_workplan_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_WORKPLAN_STATUSES = {"proposed", "ready", "active", "backlog"}
|
|
TASK_STATUSES = set(CANONICAL_TASK_STATUSES)
|
|
|
|
|
|
def classify_workplan_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_workplan_status(current_status)
|
|
target = normalize_workplan_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_WORKPLAN_STATUSES and current not in CLOSED_WORKPLAN_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",
|
|
)
|
|
|
|
|
|
classify_workstream_status_change = classify_workplan_status_change
|
|
|
|
|
|
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 == "wait" and not (blocking_reason or "").strip():
|
|
return StateChangeClassification(
|
|
ReconciliationClass.HUMAN_CONFIRMATION,
|
|
"waiting tasks should explain the wait condition",
|
|
"capture the wait reason 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",
|
|
)
|