generated from coulomb/repo-seed
Complete workplan state model cleanup
This commit is contained in:
@@ -12,7 +12,7 @@ Checks:
|
||||
C-05 workstream-title-drift WARN Yes File title != DB title (file wins)
|
||||
C-06 workstream-unlinked WARN Yes Workplan has no state_hub_workstream_id
|
||||
C-07 orphan-db-active FAIL No Active DB workstream, no backing file
|
||||
C-08 orphan-db-completed INFO No Completed/archived DB workstream, no file
|
||||
C-08 orphan-db-closed INFO No Finished/archived DB workstream, no file
|
||||
C-09 workstream-repo-mismatch FAIL Yes DB workstream repo_id != file location
|
||||
C-10 task-status-drift WARN Yes Task status differs between file and DB
|
||||
C-11 task-unlinked WARN Yes Task block has no state_hub_task_id
|
||||
@@ -51,6 +51,20 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
from api.workplan_status import ( # noqa: E402
|
||||
CANONICAL_WORKSTREAM_STATUSES,
|
||||
CLOSED_WORKSTREAM_STATUSES,
|
||||
LEGACY_WORKSTREAM_STATUS_ALIASES,
|
||||
OPEN_WORKSTREAM_STATUSES,
|
||||
SUPPORTED_WORKSTREAM_STATUSES,
|
||||
normalize_workstream_status as _normalize_workstream_status,
|
||||
ready_review_status,
|
||||
)
|
||||
|
||||
try:
|
||||
import yaml as _yaml
|
||||
_HAS_YAML = True
|
||||
@@ -71,19 +85,15 @@ except ImportError:
|
||||
_TASK_BLOCK_RE = re.compile(r"```task\s*\n(.*?)\n```", re.DOTALL)
|
||||
_HEADING_RE = re.compile(r"^#{1,4}\s+(.+?)$", re.MULTILINE)
|
||||
_ARCHIVED_WP_RE = re.compile(r"^\d{6}-(.+\.md)$")
|
||||
VALID_WP_STATUSES = {"active", "completed", "archived"}
|
||||
VALID_WP_STATUSES = set(CANONICAL_WORKSTREAM_STATUSES)
|
||||
SUPPORTED_WP_STATUSES = set(SUPPORTED_WORKSTREAM_STATUSES)
|
||||
VALID_TASK_STATUSES = {"todo", "in_progress", "blocked", "done", "cancelled"}
|
||||
VALID_TASK_PRIORITIES = {"low", "medium", "high", "critical"}
|
||||
VALID_DEP_RELATIONSHIPS = {"blocks", "starts_after", "informs", "soft_dependency"}
|
||||
DEFAULT_REMOTE_ALL_MAX_SECONDS = int(os.environ.get("CONSISTENCY_REMOTE_ALL_MAX_SECONDS", "300"))
|
||||
|
||||
# Workplan files use task-style vocabulary ("done"); the DB workstream API uses
|
||||
# "completed". This map translates file values to DB values before comparison
|
||||
# and before PATCHing, so "done" vs "completed" is never flagged as C-04 drift.
|
||||
FILE_TO_DB_WORKSTREAM_STATUS: dict[str, str] = {
|
||||
"done": "completed",
|
||||
"todo": "active", # workplan not yet started → active workstream in DB
|
||||
}
|
||||
# Legacy file/API aliases translated before comparison and PATCHing.
|
||||
FILE_TO_DB_WORKSTREAM_STATUS: dict[str, str] = dict(LEGACY_WORKSTREAM_STATUS_ALIASES)
|
||||
|
||||
# Ordinal ranking for task statuses used by the no-regress rule (T01/C-15).
|
||||
# blocked and in_progress share rank 1 — both are "in flight".
|
||||
@@ -96,9 +106,9 @@ STATUS_ORDER: dict[str, int] = {
|
||||
}
|
||||
|
||||
|
||||
def normalise_workstream_status(status: str) -> str:
|
||||
def normalise_workstream_status(status: str, *, has_started: bool | None = None) -> str:
|
||||
"""Translate a workplan file status value to its DB-canonical equivalent."""
|
||||
return FILE_TO_DB_WORKSTREAM_STATUS.get(status, status)
|
||||
return _normalize_workstream_status(status, has_started=has_started)
|
||||
|
||||
|
||||
def canonical_workplan_filename(path: Path) -> str:
|
||||
@@ -593,10 +603,11 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
||||
file_title = str(meta.get("title", "")).strip()
|
||||
file_domain = str(meta.get("domain", "")).strip()
|
||||
|
||||
if archived_file and normalise_workstream_status(file_status) == "active":
|
||||
normalised_file_status = normalise_workstream_status(file_status)
|
||||
if archived_file and normalised_file_status not in CLOSED_WORKSTREAM_STATUSES:
|
||||
report.add(
|
||||
severity="FAIL", check_id="C-18",
|
||||
message="Archived workplan file has active/todo status",
|
||||
message="Archived workplan file has an open or planning status",
|
||||
file_path=fname,
|
||||
file_value=file_status,
|
||||
fixable=False,
|
||||
@@ -652,10 +663,10 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
||||
# Continue to check drift even with mismatched repo
|
||||
|
||||
# C-04: status drift — normalise file value before comparing so that
|
||||
# "done" (file) vs "completed" (DB) is not treated as drift.
|
||||
# legacy file/API aliases are not treated as drift.
|
||||
db_status = ws.get("status", "")
|
||||
normalised_file_status = normalise_workstream_status(file_status)
|
||||
if file_status and db_status and normalised_file_status != db_status:
|
||||
normalised_db_status = normalise_workstream_status(db_status)
|
||||
if file_status and db_status and normalised_file_status != normalised_db_status:
|
||||
report.add(
|
||||
severity="WARN", check_id="C-04",
|
||||
message=(
|
||||
@@ -674,6 +685,28 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
||||
},
|
||||
)
|
||||
|
||||
if normalised_file_status == "ready":
|
||||
review = ready_review_status(
|
||||
repo_dir,
|
||||
meta.get("reviewed_against_commit"),
|
||||
meta.get("context_paths"),
|
||||
)
|
||||
if review.needs_review:
|
||||
detail = f"Ready workplan may be stale: {review.reason}"
|
||||
if review.changed_paths:
|
||||
preview = ", ".join(review.changed_paths[:5])
|
||||
extra = "" if len(review.changed_paths) <= 5 else ", ..."
|
||||
detail = f"{detail}; changed paths: {preview}{extra}"
|
||||
report.add(
|
||||
severity="WARN",
|
||||
check_id="C-21",
|
||||
message=detail,
|
||||
file_path=fname,
|
||||
file_value=file_status,
|
||||
db_value="needs_review",
|
||||
fixable=False,
|
||||
)
|
||||
|
||||
# C-05: title drift
|
||||
db_title = ws.get("title", "")
|
||||
if file_title and db_title and file_title != db_title:
|
||||
@@ -888,7 +921,7 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
||||
# C-12: DB tasks with no file backing
|
||||
if isinstance(db_tasks, list):
|
||||
ws_status = ws.get("status", "")
|
||||
ws_finished = ws_status in ("completed", "archived")
|
||||
ws_finished = normalise_workstream_status(ws_status) in CLOSED_WORKSTREAM_STATUSES
|
||||
for db_t in db_tasks:
|
||||
if db_t["id"] not in file_task_sh_ids:
|
||||
db_t_status = db_t.get("status", "")
|
||||
@@ -912,7 +945,7 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
||||
|
||||
# C-13: all DB tasks done but workstream still active — worker forgot to close
|
||||
db_status = ws.get("status", "")
|
||||
if db_status == "active" and isinstance(db_tasks, list) and db_tasks:
|
||||
if normalise_workstream_status(db_status) == "active" and isinstance(db_tasks, list) and db_tasks:
|
||||
non_terminal = [
|
||||
t for t in db_tasks
|
||||
if t.get("status") not in ("done", "cancelled")
|
||||
@@ -932,7 +965,7 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
||||
_fix_context={
|
||||
"ws_id": ws_id,
|
||||
"field": "status",
|
||||
"value": "completed",
|
||||
"value": "finished",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -963,26 +996,27 @@ def _check_orphan_db(
|
||||
for ws in all_ws:
|
||||
ws_id = ws["id"]
|
||||
ws_status = ws.get("status", "")
|
||||
if ws_status == "active" and ws_id in active_file_ws_ids:
|
||||
normalised_status = normalise_workstream_status(ws_status)
|
||||
if normalised_status not in CLOSED_WORKSTREAM_STATUSES and ws_id in active_file_ws_ids:
|
||||
continue
|
||||
if ws_status in ("completed", "archived") and ws_id in file_ws_ids:
|
||||
if normalised_status in CLOSED_WORKSTREAM_STATUSES and ws_id in file_ws_ids:
|
||||
continue
|
||||
ws_slug = ws.get("slug", "")
|
||||
if ws_status == "active":
|
||||
if normalised_status not in CLOSED_WORKSTREAM_STATUSES:
|
||||
report.add(
|
||||
severity="FAIL", check_id="C-07",
|
||||
message=(
|
||||
f"Active DB workstream '{ws_slug}' (id={ws_id[:8]}…) "
|
||||
f"Non-closed DB workstream '{ws_slug}' (id={ws_id[:8]}…) "
|
||||
f"has no backing workplan file — ADR-001 violation"
|
||||
),
|
||||
db_id=ws_id,
|
||||
fixable=False,
|
||||
)
|
||||
elif ws_status in ("completed", "archived"):
|
||||
elif normalised_status in CLOSED_WORKSTREAM_STATUSES:
|
||||
report.add(
|
||||
severity="INFO", check_id="C-08",
|
||||
message=(
|
||||
f"Completed/archived DB workstream '{ws_slug}' "
|
||||
f"Closed DB workstream '{ws_slug}' "
|
||||
f"(id={ws_id[:8]}…, status={ws_status}) has no backing workplan file"
|
||||
),
|
||||
db_id=ws_id,
|
||||
@@ -1019,9 +1053,11 @@ def _check_ghost_duplicates(
|
||||
topic_ids.add(ws["topic_id"])
|
||||
|
||||
for topic_id in topic_ids:
|
||||
topic_ws = _api_get(api_base, "/workstreams", {"topic_id": topic_id, "status": "active"})
|
||||
if not isinstance(topic_ws, list):
|
||||
continue
|
||||
topic_ws: list[dict] = []
|
||||
for status in OPEN_WORKSTREAM_STATUSES:
|
||||
status_rows = _api_get(api_base, "/workstreams", {"topic_id": topic_id, "status": status})
|
||||
if isinstance(status_rows, list):
|
||||
topic_ws.extend(status_rows)
|
||||
for ws in topic_ws:
|
||||
ws_id = ws["id"]
|
||||
if ws_id in file_ws_ids:
|
||||
@@ -1166,9 +1202,13 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
|
||||
domain_slug: str = ""
|
||||
|
||||
# Resolve domain slug: prefer active workstreams, fall back to any workstream
|
||||
# so that a fully-completed repo doesn't degrade to "(unknown)".
|
||||
workstreams = _api_get(api_base, "/workstreams", {"repo_id": repo_id, "status": "active"}) or []
|
||||
_ws_for_domain = workstreams if (isinstance(workstreams, list) and workstreams) else []
|
||||
# so that a fully-finished repo doesn't degrade to "(unknown)".
|
||||
workstreams: list[dict] = []
|
||||
for status in OPEN_WORKSTREAM_STATUSES:
|
||||
rows = _api_get(api_base, "/workstreams", {"repo_id": repo_id, "status": status}) or []
|
||||
if isinstance(rows, list):
|
||||
workstreams.extend(rows)
|
||||
_ws_for_domain = workstreams if workstreams else []
|
||||
if not _ws_for_domain:
|
||||
all_ws = _api_get(api_base, "/workstreams", {"repo_id": repo_id}) or []
|
||||
_ws_for_domain = all_ws if isinstance(all_ws, list) else []
|
||||
@@ -1379,7 +1419,8 @@ def fix_repo(
|
||||
wp_id = str(meta.get("id", "")).strip()
|
||||
title = str(meta.get("title", "")).strip()
|
||||
status = str(meta.get("status", "active")).strip()
|
||||
if status not in ("active", "completed", "archived"):
|
||||
status = normalise_workstream_status(status)
|
||||
if status not in VALID_WP_STATUSES:
|
||||
status = "active"
|
||||
|
||||
# Find topic_id for this domain
|
||||
@@ -1500,7 +1541,7 @@ def fix_repo(
|
||||
t_id = str(task.get("id", "")).strip()
|
||||
# Skip creating tasks for finished workstreams — the workstream is
|
||||
# done/archived so unlinked tasks are stale file artefacts, not gaps.
|
||||
if ws_status in ("completed", "archived"):
|
||||
if normalise_workstream_status(ws_status) in CLOSED_WORKSTREAM_STATUSES:
|
||||
report.fixes_applied.append(
|
||||
f"C-11 skipped: task '{t_id}' in {ws_status} workstream — not created"
|
||||
)
|
||||
@@ -1596,7 +1637,7 @@ def fix_repo(
|
||||
|
||||
|
||||
# Check IDs that are known-background noise in multi-machine setups:
|
||||
# C-08 = completed/archived DB workstream with no file (pre-ADR-001 legacy)
|
||||
# C-08 = finished/archived DB workstream with no file (pre-ADR-001 legacy)
|
||||
# These alone do not warrant a pull+fix cycle.
|
||||
_BACKGROUND_CHECKS: frozenset[str] = frozenset({"C-08"})
|
||||
|
||||
@@ -1707,7 +1748,7 @@ def archive_closed_workplans(
|
||||
) -> list[str]:
|
||||
"""Move closed root workplans into workplans/archived/ with YYMMDD prefix.
|
||||
|
||||
Only root-level files whose frontmatter status normalises to completed or
|
||||
Only root-level files whose frontmatter status normalises to finished or
|
||||
archived are moved. Files with any open task blocks are left in place.
|
||||
"""
|
||||
repo_dir = Path(repo_path)
|
||||
@@ -1732,7 +1773,7 @@ def archive_closed_workplans(
|
||||
if wanted not in {str(meta.get("id", "")), wp_file.stem, wp_file.name}:
|
||||
continue
|
||||
status = normalise_workstream_status(str(meta.get("status", "")).strip())
|
||||
if status not in ("completed", "archived"):
|
||||
if status not in CLOSED_WORKSTREAM_STATUSES:
|
||||
continue
|
||||
tasks = get_tasks_from_workplan(meta, body)
|
||||
open_tasks = [
|
||||
|
||||
Reference in New Issue
Block a user