C-32: fix-consistency registration for intake/decision work records (CUST-WP-0061-T02)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

Extends fix-consistency to scan any file for kind: intake / kind: decision
YAML blocks (not just workplans/, per canon: any file is a potential
work-record source), create the corresponding hub entity when missing a
state_hub_intake_id / state_hub_decision_id, and write the id back into
the source block -- same write-back pattern as C-06 for workplans.
kind: engagement is reported INFO (deferred, not fixable): no hub entity
exists for it yet, a separate stage-3 follow-on.

- _load_work_record_kind_registry(): kind-aware registry loader, factored
  out so C-31's existing flat _load_work_record_patterns() builds on it
  without duplication (verified: C-31's 16 tests still pass unmodified)
- _check_work_record_registration(): detection, wired into check_repo
  right after C-31
- _inject_yaml_block_field(): write-back helper for  fenced
  blocks, mirroring _inject_task_id_into_block's pattern for
  blocks
- fix_repo C-32 dispatch: creates the intake (scoped to repo_id) or
  decision (scoped to resolved topic_id, reusing C-06's domain->topic
  resolution) via the REST API, then writes the id back
- tests/test_work_record_registration.py: 15 tests (classification,
  detection incl. engagement-deferred and workplan-kind-exclusion,
  injection incl. idempotence and non-interference with sibling blocks)

Live-verified end to end against a real registered repo (binky-control,
not just synthetic fixtures): a real fix-consistency run found and
registered 3 genuinely open, previously-unlinked intake items
(AWQ-002/003/006) sitting in AutopilotWorkQueue.md, and correctly
deferred 5 real OH- engagement items as INFO. No regressions: full
consistency_check + consistency_sweep suite (128 tests) and C-31's own
suite (16 tests) still green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-21 00:48:10 +02:00
parent 88ba666c95
commit aade470f4a
2 changed files with 405 additions and 6 deletions

View file

@ -34,6 +34,7 @@ Checks:
C-29 inbox-work-unpromoted WARN No Stale unread message looks like a multi-step work request without a workplan
C-30 scope-current-state-stale WARN No SCOPE.md Current State contradicts live workplan statuses
C-31 work-record-unregistered WARN No YAML-block id matches no kind in the canon work-record type registry (sidetrack detector, CUST-WP-0060)
C-32 work-record-not-indexed WARN Yes kind: intake/decision YAML block has no hub id not indexed in DB (registration, CUST-WP-0061-T02)
Usage:
python scripts/consistency_check.py --repo SLUG [--fix] [--no-writeback] [--json] [--api-base URL]
@ -779,11 +780,14 @@ _WORK_RECORD_ID_LIKE_RE = re.compile(r"^[A-Z][A-Z0-9]*(-[A-Z0-9]+)+$")
_WORK_RECORD_SKIP_DIRS = frozenset({".git", "node_modules", ".venv", "history"})
def _load_work_record_patterns() -> list[re.Pattern] | None:
"""Load id patterns from the canon work-record type registry.
def _load_work_record_kind_registry() -> list[dict] | None:
"""Load [{kind, patterns}] from the canon work-record type registry.
Location: $WORK_RECORD_REGISTRY, else ~/the-custodian/canon/standards/
work-record-types.yaml. Returns None (check skipped) when unavailable.
work-record-types.yaml. Returns None (checks skipped) when unavailable.
Kind-aware: shared by C-31 (flat sidetrack detection) and C-32
(kind-aware registration needs to know intake vs. decision vs.
engagement to pick the right hub entity).
"""
if not _HAS_YAML:
return None
@ -798,17 +802,28 @@ def _load_work_record_patterns() -> list[re.Pattern] | None:
continue
try:
reg = _yaml.safe_load(path.read_text())
patterns = []
kinds = []
for kind in reg.get("kinds", []):
patterns += [re.compile(p) for p in kind.get("id_patterns", [])]
patterns = [re.compile(p) for p in kind.get("id_patterns", [])]
patterns += [re.compile(lp["pattern"])
for lp in kind.get("legacy_patterns", [])]
return patterns
kinds.append({"kind": kind["kind"], "patterns": patterns})
return kinds
except Exception:
return None
return None
def _load_work_record_patterns() -> list[re.Pattern] | None:
"""Flat id-pattern list across all kinds — used by C-31's sidetrack
detector, which only needs to know "is this id registered at all",
not which kind it belongs to."""
kinds = _load_work_record_kind_registry()
if kinds is None:
return None
return [p for k in kinds for p in k["patterns"]]
def _check_unregistered_work_records(repo_dir: Path, report: "ConsistencyReport") -> None:
"""C-31: warn on YAML-block ids matching no registered work-record kind.
@ -849,6 +864,115 @@ def _check_unregistered_work_records(repo_dir: Path, report: "ConsistencyReport"
)
_YAML_ONLY_FENCE_RE = re.compile(r"```yaml\n(.*?)\n```", re.S)
_WORK_RECORD_ID_FIELD_BY_KIND = {
"intake": "state_hub_intake_id",
"decision": "state_hub_decision_id",
"engagement": "state_hub_engagement_id",
}
_WORK_RECORD_CREATE_ENDPOINT_BY_KIND = {
"intake": "/intakes",
"decision": "/decisions",
}
def _classify_work_record_kind(record_id: str, kind_registry: list[dict]) -> str | None:
for k in kind_registry:
if any(p.match(record_id) for p in k["patterns"]):
return k["kind"]
return None
def _inject_yaml_block_field(
file_path: Path, field_name: str, field_value: str, match_id: str
) -> bool:
"""Inject a field into the ```yaml``` block whose id == match_id.
Mirrors _inject_task_id_into_block, adapted for fenced yaml blocks
(intake/decision/engagement records live as ```yaml, not ```task)."""
text = file_path.read_text(encoding="utf-8")
def _replace(m: re.Match) -> str:
block_content = m.group(1)
meta = _parse_yaml_block(block_content.strip())
if not isinstance(meta, dict) or str(meta.get("id", "")) != match_id:
return m.group(0)
existing_val = meta.get(field_name)
if existing_val is not None and str(existing_val).strip() not in ("", "~", "null", "None", "none"):
return m.group(0)
new_content = re.sub(
rf"^{re.escape(field_name)}:.*$",
f'{field_name}: "{field_value}"',
block_content,
flags=re.MULTILINE,
)
if new_content == block_content:
new_content = block_content.rstrip() + f"\n{field_name}: \"{field_value}\""
return f"```yaml\n{new_content}\n```"
new_text = _YAML_ONLY_FENCE_RE.sub(_replace, text)
if new_text != text:
file_path.write_text(new_text, encoding="utf-8")
return True
return False
def _check_work_record_registration(repo_dir: Path, report: "ConsistencyReport") -> None:
"""C-32: register kind: intake / kind: decision YAML blocks found
anywhere in the repo against the hub (CUST-WP-0061-T02, work-record
stage 3) writes state_hub_intake_id / state_hub_decision_id back into
the source block, same write-back pattern as C-06 for workplans.
kind: engagement has no hub entity yet (deferred; reported INFO, not
fixable) building one is a separate stage-3 follow-on, not this task.
"""
kind_registry = _load_work_record_kind_registry()
if kind_registry is None:
return
for md in sorted(repo_dir.rglob("*.md")):
if any(part in _WORK_RECORD_SKIP_DIRS for part in md.parts):
continue
try:
text = md.read_text(errors="replace")
except OSError:
continue
for block in _YAML_ONLY_FENCE_RE.findall(text):
meta = _parse_yaml_block(block.strip())
if not isinstance(meta, dict) or meta.get("_parse_error"):
continue
rid = str(meta.get("id", "")).strip()
if not rid or "NNN" in rid:
continue
kind = _classify_work_record_kind(rid, kind_registry)
if kind not in _WORK_RECORD_ID_FIELD_BY_KIND:
continue
id_field = _WORK_RECORD_ID_FIELD_BY_KIND[kind]
existing = str(meta.get(id_field, "")).strip().strip('"')
if existing and existing not in ("~", "null", "None", "none"):
continue
rel = md.relative_to(repo_dir)
if kind not in _WORK_RECORD_CREATE_ENDPOINT_BY_KIND:
report.add(
severity="INFO", check_id="C-32",
message=(
f"[{rel}] engagement '{rid}' has no hub entity yet — "
f"registration deferred (CUST-WP-0061 stage-3 seed)"
),
fixable=False,
)
continue
report.add(
severity="WARN", check_id="C-32",
message=f"[{rel}] {kind} '{rid}' has no {id_field} — not indexed in DB",
file_path=str(rel),
file_value=rid,
fixable=True,
_fix_context={"md_path": md, "kind": kind, "rid": rid, "meta": meta},
)
def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = None) -> ConsistencyReport:
"""Run all consistency checks for a registered repo."""
repo = _api_get(api_base, f"/repos/{repo_slug}", return_error=True)
@ -915,6 +1039,9 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
# C-31: work-record sidetrack detector (canon work-record-types registry)
_check_unregistered_work_records(repo_dir, report)
# C-32: intake/decision work-record registration (CUST-WP-0061-T02)
_check_work_record_registration(repo_dir, report)
# C-01: workplans/ directory missing
if not workplans_dir.is_dir():
report.add(
@ -2588,6 +2715,77 @@ def fix_repo(
f" ! task {t_id} not created: {t_data.get('_error', t_data)}"
)
elif issue.check_id == "C-32":
md_path = ctx["md_path"]
kind = ctx["kind"]
rid = ctx["rid"]
meta = ctx["meta"]
id_field = _WORK_RECORD_ID_FIELD_BY_KIND[kind]
title = str(meta.get("title") or rid)
repo_record = _api_get(api_base, f"/repos/{repo_slug}")
if not repo_record:
report.fixes_applied.append(
f"C-32 SKIP {rid}: could not look up repo '{repo_slug}'"
)
continue
created = None
if kind == "intake":
payload = {
"title": title,
"repo_id": repo_record.get("id"),
"description": meta.get("description"),
"lane": meta.get("lane", "green"),
"origin": meta.get("origin"),
"origin_ref": meta.get("origin_ref"),
"source_repo_path": str(md_path.relative_to(Path(report.repo_path))),
}
created = _api_post(api_base, "/intakes", payload)
elif kind == "decision":
repo_market_domain = str(repo_record.get("domain_slug") or "").strip()
topic_domain = resolve_topic_domain_slug(
repo_market_domain, repo_market_domain=repo_market_domain or None
)
topics = _api_get(api_base, "/topics")
topic_id = None
if isinstance(topics, list):
for t in topics:
if t.get("domain_slug") == topic_domain:
topic_id = t["id"]
break
if topic_id is None:
report.fixes_applied.append(
f"C-32 SKIP {rid}: no topic found for domain '{topic_domain}'"
)
continue
payload = {
"title": title,
"topic_id": topic_id,
"decision_type": meta.get("decision_type", "pending"),
"description": meta.get("description"),
"rationale": meta.get("agent_recommendation") or meta.get("rationale"),
}
created = _api_post(api_base, "/decisions", payload)
if not created or "_error" in created:
report.fixes_applied.append(
f"C-32 FAIL {rid}: could not create {kind} in DB: "
f"{(created or {}).get('_error', 'no response')}"
)
continue
new_id = created["id"]
if _inject_yaml_block_field(md_path, id_field, new_id, rid):
report.fixes_applied.append(
f"C-32 fixed: created {kind} {new_id[:8]}… for {rid}, "
f"wrote {id_field} to {md_path.name}"
)
else:
report.fixes_applied.append(
f"C-32 WARN {rid}: created {kind} {new_id[:8]}… but "
f"failed to write {id_field} back to {md_path.name}"
)
elif issue.check_id == "C-09":
ws_id = ctx["ws_id"]
correct_repo_id = ctx["correct_repo_id"]