Index workplan flavor and omit residuals from default views.
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:
parent
ea11451e9c
commit
ddc3338541
18 changed files with 852 additions and 25 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue