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"]

View file

@ -0,0 +1,201 @@
"""Unit tests for C-32 (work-record registration: intake/decision kinds,
CUST-WP-0061-T02) detection and file write-back only. The live
create-in-DB path is covered by the fix_repo integration path, proven
manually against a real repo/API (see CUST-WP-0061 progress notes); these
tests stay offline and use a synthetic registry fixture, matching the
conventions in test_work_record_check.py.
"""
from __future__ import annotations
import sys
import textwrap
from pathlib import Path
import pytest
import yaml
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from consistency_check import ( # noqa: E402
ConsistencyReport,
_check_work_record_registration,
_classify_work_record_kind,
_inject_yaml_block_field,
_load_work_record_kind_registry,
)
REGISTRY_YAML = textwrap.dedent(
"""
version: "0.1"
kinds:
- kind: intake
id_patterns: ['^[A-Z]+-IN-[0-9]{4}$']
legacy_patterns:
- pattern: '^AWQ-[0-9]{3}$'
source: binky AutopilotWorkQueue
- kind: decision
id_patterns: ['^[A-Z]+-DEC-[0-9]{4}-[0-9]{3}$']
legacy_patterns:
- pattern: '^DEC-[0-9]{4}-[0-9]{3}$'
source: binky DecisionQueue
- kind: engagement
id_patterns: ['^[A-Z]+-ENG-[0-9]{4}-[0-9]{3}$']
legacy_patterns:
- pattern: '^OH-[0-9]{4}-[0-9]{3}$'
source: binky OfficeHourQueue
- kind: workplan
id_patterns: ['^[A-Z]+-WP-[0-9]{4}$']
"""
)
@pytest.fixture
def registry_file(tmp_path, monkeypatch):
reg = tmp_path / "work-record-types.yaml"
reg.write_text(REGISTRY_YAML, encoding="utf-8")
monkeypatch.setenv("WORK_RECORD_REGISTRY", str(reg))
return reg
class TestClassifyWorkRecordKind:
def test_classifies_intake(self, registry_file):
kinds = _load_work_record_kind_registry()
assert _classify_work_record_kind("BINKY-IN-0001", kinds) == "intake"
def test_classifies_legacy_awq(self, registry_file):
kinds = _load_work_record_kind_registry()
assert _classify_work_record_kind("AWQ-010", kinds) == "intake"
def test_classifies_decision(self, registry_file):
kinds = _load_work_record_kind_registry()
assert _classify_work_record_kind("DEC-2026-004", kinds) == "decision"
def test_unregistered_returns_none(self, registry_file):
kinds = _load_work_record_kind_registry()
assert _classify_work_record_kind("FOO-QX-001", kinds) is None
class TestCheckWorkRecordRegistration:
def _report(self, repo_dir: Path) -> ConsistencyReport:
report = ConsistencyReport(repo_slug="test", repo_path=str(repo_dir))
_check_work_record_registration(repo_dir, report)
return report
def test_intake_without_hub_id_is_flagged_fixable(self, tmp_path, registry_file):
(tmp_path / "queue.md").write_text(
"```yaml\nid: BINKY-IN-0001\ntitle: needs registration\n```\n",
encoding="utf-8",
)
report = self._report(tmp_path)
assert len(report.issues) == 1
issue = report.issues[0]
assert issue.check_id == "C-32"
assert issue.fixable is True
assert issue._fix_context["kind"] == "intake"
assert issue._fix_context["rid"] == "BINKY-IN-0001"
def test_intake_with_hub_id_is_skipped(self, tmp_path, registry_file):
(tmp_path / "queue.md").write_text(
'```yaml\nid: BINKY-IN-0001\ntitle: already linked\n'
'state_hub_intake_id: "019f8000-0000-7000-8000-000000000000"\n```\n',
encoding="utf-8",
)
report = self._report(tmp_path)
assert report.issues == []
def test_decision_without_hub_id_is_flagged_fixable(self, tmp_path, registry_file):
(tmp_path / "DecisionQueue.md").write_text(
"```yaml\nid: DEC-2026-005\ntitle: a new decision\n```\n",
encoding="utf-8",
)
report = self._report(tmp_path)
assert len(report.issues) == 1
assert report.issues[0]._fix_context["kind"] == "decision"
def test_engagement_without_hub_id_is_info_not_fixable(self, tmp_path, registry_file):
(tmp_path / "OfficeHourQueue.md").write_text(
"```yaml\nid: OH-2026-001\ntitle: bank call\n```\n",
encoding="utf-8",
)
report = self._report(tmp_path)
assert len(report.issues) == 1
assert report.issues[0].severity == "INFO"
assert report.issues[0].fixable is False
def test_workplan_kind_is_untouched_by_c32(self, tmp_path, registry_file):
"""Workplans keep their own C-06 registration path; C-32 must not
double-register them."""
(tmp_path / "workplans" / "CUST-WP-0099-x.md").parent.mkdir(parents=True)
(tmp_path / "workplans" / "CUST-WP-0099-x.md").write_text(
"---\nid: CUST-WP-0099\n---\n```yaml\nid: CUST-WP-0099\ntitle: x\n```\n",
encoding="utf-8",
)
report = self._report(tmp_path)
assert report.issues == []
def test_template_placeholder_ignored(self, tmp_path, registry_file):
(tmp_path / "AGENTS.md").write_text(
"```yaml\nid: BINKY-IN-NNNN\ntitle: template\n```\n", encoding="utf-8"
)
report = self._report(tmp_path)
assert report.issues == []
def test_no_op_when_registry_unavailable(self, tmp_path, monkeypatch):
monkeypatch.setenv("WORK_RECORD_REGISTRY", str(tmp_path / "missing.yaml"))
monkeypatch.setattr(Path, "home", lambda: tmp_path / "nohome")
(tmp_path / "queue.md").write_text(
"```yaml\nid: BINKY-IN-0001\ntitle: x\n```\n", encoding="utf-8"
)
report = self._report(tmp_path)
assert report.issues == []
class TestInjectYamlBlockField:
def test_injects_field_into_matching_block(self, tmp_path):
md = tmp_path / "queue.md"
md.write_text(
"```yaml\nid: BINKY-IN-0001\ntitle: needs registration\n```\n",
encoding="utf-8",
)
changed = _inject_yaml_block_field(
md, "state_hub_intake_id", "019f8000-0000-7000-8000-000000000000", "BINKY-IN-0001"
)
assert changed is True
text = md.read_text()
assert 'state_hub_intake_id: "019f8000-0000-7000-8000-000000000000"' in text
# round-trips as valid yaml
block = text.split("```yaml\n", 1)[1].rsplit("```", 1)[0]
parsed = yaml.safe_load(block)
assert parsed["state_hub_intake_id"] == "019f8000-0000-7000-8000-000000000000"
def test_does_not_touch_other_blocks(self, tmp_path):
md = tmp_path / "queue.md"
md.write_text(
"```yaml\nid: BINKY-IN-0001\ntitle: first\n```\n"
"```yaml\nid: BINKY-IN-0002\ntitle: second\n```\n",
encoding="utf-8",
)
_inject_yaml_block_field(md, "state_hub_intake_id", "abc-123", "BINKY-IN-0001")
text = md.read_text()
assert "abc-123" in text
second_block = text.split("BINKY-IN-0002")[1]
assert "state_hub_intake_id" not in second_block
def test_no_op_when_id_not_found(self, tmp_path):
md = tmp_path / "queue.md"
original = "```yaml\nid: BINKY-IN-0001\ntitle: x\n```\n"
md.write_text(original, encoding="utf-8")
changed = _inject_yaml_block_field(md, "state_hub_intake_id", "abc-123", "BINKY-IN-9999")
assert changed is False
assert md.read_text() == original
def test_idempotent_when_field_already_set(self, tmp_path):
md = tmp_path / "queue.md"
md.write_text(
'```yaml\nid: BINKY-IN-0001\nstate_hub_intake_id: "already-there"\n```\n',
encoding="utf-8",
)
changed = _inject_yaml_block_field(md, "state_hub_intake_id", "new-value", "BINKY-IN-0001")
assert changed is False
assert "already-there" in md.read_text()
assert "new-value" not in md.read_text()