Index workplan flavor and omit residuals from default views.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 32s

STATE-WP-0092: persist flavor from files, treat depends_on as the C-20
canonical edge, exclude flavor=residual from summary/next_steps/deps
unless include_residuals is set. Live primary still needs the alembic
revision applied.

Assistant: grok
Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
This commit is contained in:
tegwick 2026-09-14 15:28:58 +02:00
parent ea11451e9c
commit ddc3338541
18 changed files with 852 additions and 25 deletions

View file

@ -39,6 +39,9 @@ Checks:
C-33 work-record-index-stale WARN Yes WORK-RECORDS.md missing or stale generated per-repo index (CUST-WP-0061-T04)
C-34 quality-dor-ready WARN No status=ready without quality_dor DoR-Ok (STATE-WP-0077 soft)
C-35 repo-manager-conformance WARN No Repo Manager flavor/standards contract reports findings
C-36 work-record-flavor-unknown WARN No flavor is set but not in the closed list
C-37 residual-provenance-missing WARN No flavor: residual without origin/origin_ref
C-38 task-flavor-drift WARN Yes task flavor differs between file and DB (file wins)
(finished¬DoD-Ok is listed by `statehub quality-debt`, not per-file C-warn avoids historical flood)
Usage:
@ -118,6 +121,13 @@ from api.task_status import ( # noqa: E402
TERMINAL_TASK_STATUSES,
normalize_task_status,
)
from api.work_record_flavor import ( # noqa: E402
FLAVOR_PROMOTION_REASONS,
WORK_RECORD_FLAVORS,
is_residual_flavor,
normalize_flavor,
normalize_promotion_reason,
)
_WORK_RECORD_NAMESPACE_UUID = uuid.UUID("a4058507-5c4a-5a00-ab06-fffa4fb46009")
@ -509,6 +519,36 @@ def _as_list(value: Any) -> list[str]:
return [str(value).strip().strip('"')]
_TASK_RECORD_ID_RE = re.compile(r"-T\d{2,3}$")
def _dedupe_preserve(items: list[str]) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for item in items:
if item in seen:
continue
seen.add(item)
out.append(item)
return out
def _frontmatter_depends_on_workplans(meta: dict) -> list[str]:
"""Canonical `depends_on` plus legacy `depends_on_workplans` alias."""
combined = _as_list(meta.get("depends_on")) + _as_list(meta.get("depends_on_workplans"))
return _dedupe_preserve(
[item for item in combined if item and not _TASK_RECORD_ID_RE.search(item)]
)
def _frontmatter_depends_on_tasks(meta: dict) -> list[str]:
from_alias = _as_list(meta.get("depends_on_tasks"))
from_depends_on = [
item for item in _as_list(meta.get("depends_on")) if _TASK_RECORD_ID_RE.search(item)
]
return _dedupe_preserve(from_alias + from_depends_on)
def _as_int_or_none(value: Any) -> int | None:
if value in (None, "", "~", "null", "None", "none"):
return None
@ -1664,6 +1704,86 @@ def check_repo(
_fix_context={"ws_id": ws_id, "field": "planning_order", "value": planning_order},
)
file_flavor = normalize_flavor(meta.get("flavor"))
if file_flavor is not None and file_flavor not in WORK_RECORD_FLAVORS:
report.add(
severity="WARN",
check_id="C-36",
message=(
f"Unknown work-record flavor {file_flavor!r} in '{ws.get('slug')}'; "
f"expected one of {', '.join(WORK_RECORD_FLAVORS)}"
),
file_path=fname,
db_id=ws_id,
file_value=file_flavor,
fixable=False,
)
file_flavor = None
db_flavor = normalize_flavor(ws.get("flavor"))
if file_flavor != db_flavor:
report.add(
severity="WARN", check_id="C-19",
message=(
f"Flavor drift in '{ws.get('slug')}': "
f"file={file_flavor!r} db={db_flavor!r} (file wins)"
),
file_path=fname,
db_id=ws_id,
file_value=file_flavor,
db_value=db_flavor,
fixable=True,
_fix_context={"ws_id": ws_id, "field": "flavor", "value": file_flavor},
)
file_reason = normalize_promotion_reason(meta.get("flavor_promotion_reason"))
if file_reason is not None and file_reason not in FLAVOR_PROMOTION_REASONS:
report.add(
severity="WARN",
check_id="C-36",
message=(
f"Unknown flavor_promotion_reason {file_reason!r} in '{ws.get('slug')}'; "
f"expected one of {', '.join(FLAVOR_PROMOTION_REASONS)}"
),
file_path=fname,
db_id=ws_id,
file_value=file_reason,
fixable=False,
)
file_reason = None
db_reason = normalize_promotion_reason(ws.get("flavor_promotion_reason"))
if file_reason != db_reason:
report.add(
severity="WARN", check_id="C-19",
message=(
f"Flavor promotion reason drift in '{ws.get('slug')}': "
f"file={file_reason!r} db={db_reason!r} (file wins)"
),
file_path=fname,
db_id=ws_id,
file_value=file_reason,
db_value=db_reason,
fixable=True,
_fix_context={
"ws_id": ws_id,
"field": "flavor_promotion_reason",
"value": file_reason,
},
)
if is_residual_flavor(file_flavor):
origin = str(meta.get("origin") or "").strip()
origin_ref = str(meta.get("origin_ref") or "").strip()
if not origin and not origin_ref:
report.add(
severity="WARN",
check_id="C-37",
message=(
f"Residual workplan '{ws.get('slug')}' has no origin/origin_ref "
"provenance (not invented)"
),
file_path=fname,
db_id=ws_id,
fixable=False,
)
# C-10, C-11, C-12: task-level checks
db_task_by_id: dict[str, dict] = {}
if isinstance(db_tasks, list):
@ -1688,7 +1808,7 @@ def check_repo(
if dep.get("to_task_id"):
existing_dep_keys.add(("task", dep["to_task_id"], rel))
for target_wp_id in _as_list(meta.get("depends_on_workplans")):
for target_wp_id in _frontmatter_depends_on_workplans(meta):
target_ws_id = workplan_id_to_ws_id.get(target_wp_id)
if not target_ws_id:
report.add(
@ -1717,7 +1837,7 @@ def check_repo(
},
)
for target_task_id in _as_list(meta.get("depends_on_tasks")):
for target_task_id in _frontmatter_depends_on_tasks(meta):
target_sh_id = task_file_id_to_sh_id.get(target_task_id)
if not target_sh_id:
report.add(
@ -1849,6 +1969,36 @@ def check_repo(
fixable=True,
_fix_context={"task_id": t_sh_id, "description": file_description},
)
file_task_flavor = normalize_flavor(task.get("flavor"))
if file_task_flavor is not None and file_task_flavor not in WORK_RECORD_FLAVORS:
report.add(
severity="WARN",
check_id="C-36",
message=(
f"Unknown work-record flavor {file_task_flavor!r} on task '{t_id}'"
),
file_path=f"{fname}#{t_id}",
db_id=t_sh_id,
file_value=file_task_flavor,
fixable=False,
)
file_task_flavor = None
db_task_flavor = normalize_flavor(db_task.get("flavor"))
if file_task_flavor != db_task_flavor:
report.add(
severity="WARN",
check_id="C-38",
message=(
f"Task flavor drift '{t_id}': "
f"file={file_task_flavor!r} db={db_task_flavor!r} (file wins)"
),
file_path=f"{fname}#{t_id}",
db_id=t_sh_id,
file_value=file_task_flavor,
db_value=db_task_flavor,
fixable=True,
_fix_context={"task_id": t_sh_id, "flavor": file_task_flavor},
)
elif t_id:
# C-11: task exists in file but not linked to DB
ws_status = ws.get("status", "")
@ -3062,6 +3212,15 @@ def fix_repo(
)
continue
create_flavor = normalize_flavor(meta.get("flavor"))
if create_flavor is not None and create_flavor not in WORK_RECORD_FLAVORS:
create_flavor = None
create_reason = normalize_promotion_reason(meta.get("flavor_promotion_reason"))
if create_reason is not None and create_reason not in FLAVOR_PROMOTION_REASONS:
create_reason = None
create_from = normalize_flavor(meta.get("flavor_promoted_from"))
if create_from is not None and create_from not in WORK_RECORD_FLAVORS:
create_from = None
ws_data = _api_post(api_base, "/workplans", {
"id": desired_ws_id,
"topic_id": topic_id,
@ -3072,6 +3231,9 @@ def fix_repo(
"owner": str(meta.get("owner", "")).strip() or None,
"planning_priority": str(meta.get("planning_priority", "")).strip() or None,
"planning_order": _as_int_or_none(meta.get("planning_order")),
"flavor": create_flavor,
"flavor_promotion_reason": create_reason,
"flavor_promoted_from": create_from,
})
if ws_data is None or (isinstance(ws_data, dict) and "_error" in ws_data):
last_error = ws_data.get("_error") if isinstance(ws_data, dict) else "no response"
@ -3116,6 +3278,9 @@ def fix_repo(
if raw_task_id not in (None, "", "~", "null", "None", "none")
else _derived_work_record_uuid(t_id)
)
task_flavor = normalize_flavor(task.get("flavor"))
if task_flavor is not None and task_flavor not in WORK_RECORD_FLAVORS:
task_flavor = None
t_data = _api_post(api_base, "/tasks", {
"id": desired_task_id,
"workplan_id": new_ws_id,
@ -3124,6 +3289,7 @@ def fix_repo(
"status": t_status,
"priority": t_priority,
"assignee": task.get("assignee") or None,
"flavor": task_flavor,
})
if t_data and "_error" not in t_data:
t_db_id = t_data["id"]
@ -3349,6 +3515,19 @@ def fix_repo(
f"C-22 FAILED: task {task_id[:8]}… description update: {result['_error']}"
)
elif issue.check_id == "C-38":
task_id = ctx["task_id"]
flavor = ctx.get("flavor")
result = _api_patch(api_base, f"/tasks/{task_id}", {"flavor": flavor})
if result is not None and "_error" not in result:
report.fixes_applied.append(
f"C-38 fixed: task {task_id[:8]}… flavor → {flavor!r}"
)
elif result is not None:
report.fixes_applied.append(
f"C-38 FAILED: task {task_id[:8]}… flavor → {flavor!r}: {result['_error']}"
)
elif issue.check_id == "C-15":
# T03 — writeback: DB is ahead of file — patch file to match DB.
if no_writeback: