C-31 work-record sidetrack detector (CUST-WP-0060-T05)
Warn on YAML-block ids matching no kind in the canon work-record type registry (~/the-custodian/canon/standards/work-record-types.yaml, override via WORK_RECORD_REGISTRY). Detection only; registration of non-workplan kinds is a later stage. Skips template placeholders and repos without the registry available. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
814595c317
commit
59770145fd
1 changed files with 80 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ Checks:
|
|||
C-28 inbox-stale-unread WARN No Unread inbox messages older than INBOX_STALE_DAYS
|
||||
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)
|
||||
|
||||
Usage:
|
||||
python scripts/consistency_check.py --repo SLUG [--fix] [--no-writeback] [--json] [--api-base URL]
|
||||
|
|
@ -772,6 +773,82 @@ def _infer_slug_from_path(api_base: str, path: str) -> "tuple[str, str] | None":
|
|||
return None
|
||||
|
||||
|
||||
_WORK_RECORD_FENCE_RE = re.compile(r"```(?:yaml|task)\n(.*?)```", re.S)
|
||||
_WORK_RECORD_ID_LINE_RE = re.compile(r"^id:\s*[\"']?([A-Za-z0-9_-]+)[\"']?\s*$", re.M)
|
||||
_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.
|
||||
|
||||
Location: $WORK_RECORD_REGISTRY, else ~/the-custodian/canon/standards/
|
||||
work-record-types.yaml. Returns None (check skipped) when unavailable.
|
||||
"""
|
||||
if not _HAS_YAML:
|
||||
return None
|
||||
candidates = []
|
||||
env = os.environ.get("WORK_RECORD_REGISTRY")
|
||||
if env:
|
||||
candidates.append(Path(env))
|
||||
candidates.append(
|
||||
Path.home() / "the-custodian/canon/standards/work-record-types.yaml")
|
||||
for path in candidates:
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
reg = _yaml.safe_load(path.read_text())
|
||||
patterns = []
|
||||
for kind in reg.get("kinds", []):
|
||||
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
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _check_unregistered_work_records(repo_dir: Path, report: "ConsistencyReport") -> None:
|
||||
"""C-31: warn on YAML-block ids matching no registered work-record kind.
|
||||
|
||||
Detection only (CUST-WP-0060-T05); registration of non-workplan kinds is
|
||||
a later stage. An unregistered id scheme is a coordination sidetrack that
|
||||
will need manual reintegration — surface it the day it appears.
|
||||
"""
|
||||
patterns = _load_work_record_patterns()
|
||||
if patterns is None:
|
||||
return
|
||||
seen: set[str] = set()
|
||||
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 body in _WORK_RECORD_FENCE_RE.findall(text):
|
||||
for rid in _WORK_RECORD_ID_LINE_RE.findall(body):
|
||||
if rid in seen or "NNN" in rid or rid.endswith("-TNN"):
|
||||
continue
|
||||
if not _WORK_RECORD_ID_LIKE_RE.match(rid):
|
||||
continue
|
||||
if any(p.match(rid) for p in patterns):
|
||||
continue
|
||||
seen.add(rid)
|
||||
report.add(
|
||||
severity="WARN",
|
||||
check_id="C-31",
|
||||
message=(
|
||||
f"[{md.relative_to(repo_dir)}] id '{rid}' matches no "
|
||||
f"kind in the canon work-record type registry — "
|
||||
f"unregistered species become sidetracks needing "
|
||||
f"manual reintegration (work-record-types_v0.1)"
|
||||
),
|
||||
fixable=False,
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
|
@ -835,6 +912,9 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||
fixable=False,
|
||||
)
|
||||
|
||||
# C-31: work-record sidetrack detector (canon work-record-types registry)
|
||||
_check_unregistered_work_records(repo_dir, report)
|
||||
|
||||
# C-01: workplans/ directory missing
|
||||
if not workplans_dir.is_dir():
|
||||
report.add(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue