diff --git a/.claude/rules/session-protocol.md b/.claude/rules/session-protocol.md index 5bcebbc..d7f51bb 100644 --- a/.claude/rules/session-protocol.md +++ b/.claude/rules/session-protocol.md @@ -42,6 +42,14 @@ ls workplans/ For each file with `status: ready`, `active`, or `blocked`, note pending `wait`/`todo`/`progress` tasks. +Optional quality debt (STATE-WP-0077): +```bash +statehub quality-debt --repo-path . +``` +Prefer **DoR-Ok** before heavy implementation on ready plans; **DoC-Ok** before +confident intake promote; record with `quality_dor` / `quality_doc` / +`quality_dod` fields — see `docs/work-record-quality-gates.md`. + **Step 4 — Present brief** 1. **Active workplans** for `infotech` — title, task counts, blocking decisions diff --git a/AGENTS.md b/AGENTS.md index ae0bcd3..02fdb10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,8 +40,22 @@ curl -s "http://127.0.0.1:8000/workplans/?topic_id=cee7bedf-2b48-46ef-8601-00647 # Check inbox curl -s "http://127.0.0.1:8000/messages/?to_agent=state-hub&unread_only=true" \ | python3 -m json.tool + +# Optional: DoX quality debt (ready without DoR-Ok, finished without DoD-Ok, …) +statehub quality-debt --repo-path . ``` +### Definition quality (DoC / DoR / DoD) + +Lifecycle `status` is independent of quality badges. Prefer: + +- **DoC-Ok** on intakes before confident promote (`quality_doc` or note) +- **DoR-Ok** on workplans/tasks before heavy implementation (`quality_dor`) +- **DoD-Ok** when claiming quality-complete finish (`quality_dod`) + +Recording form and examples: `docs/work-record-quality-gates.md`. +Policies: `policies/intake-doc.md`, `work-item-dor.md`, `workstream-dod.md`. + Mark a message read: ```bash curl -s -X PATCH "http://127.0.0.1:8000/messages//read" \ diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 0d8dbb6..c29bbaa 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -45,7 +45,7 @@ | workplan | STATE-WP-0074 | finished | — | workplans/STATE-WP-0074-hub-ecosystem-boundary-alignment.md | | workplan | STATE-WP-0075 | finished | — | workplans/STATE-WP-0075-workstream-route-410-stranglers.md | | workplan | STATE-WP-0076 | finished | — | workplans/STATE-WP-0076-definition-of-ready-and-comprehension.md | -| workplan | STATE-WP-0077 | ready | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | +| workplan | STATE-WP-0077 | finished | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | | task | ADHOC-2026-06-04-T01 | done | — | workplans/ADHOC-2026-06-04.md | | task | ADHOC-2026-07-01-T01 | done | — | workplans/ADHOC-2026-07-01.md | | task | ADHOC-2026-07-01-T02 | done | — | workplans/ADHOC-2026-07-01.md | @@ -259,7 +259,8 @@ | task | STATE-WP-0076-T04 | done | — | workplans/STATE-WP-0076-definition-of-ready-and-comprehension.md | | task | STATE-WP-0076-T05 | cancel | — | workplans/STATE-WP-0076-definition-of-ready-and-comprehension.md | | task | STATE-WP-0076-T06 | cancel | — | workplans/STATE-WP-0076-definition-of-ready-and-comprehension.md | -| task | STATE-WP-0077-T01 | todo | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | -| task | STATE-WP-0077-T02 | todo | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | -| task | STATE-WP-0077-T03 | todo | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | -| task | STATE-WP-0077-T04 | todo | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | +| task | STATE-WP-0077-T01 | done | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | +| task | STATE-WP-0077-T02 | done | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | +| task | STATE-WP-0077-T03 | done | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | +| task | STATE-WP-0077-T04 | done | — | workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md | +| intake | CUST-IN-0004 | routed | — | docs/work-record-quality-gates.md | diff --git a/custodian_cli.py b/custodian_cli.py index be404bb..06f4320 100644 --- a/custodian_cli.py +++ b/custodian_cli.py @@ -422,6 +422,29 @@ def cmd_fix_consistency(args: argparse.Namespace) -> None: sys.exit(exit_code) +def cmd_quality_debt(args: argparse.Namespace) -> None: + """List DoX quality debt (STATE-WP-0077) — ready without DoR-Ok, etc.""" + script = STATE_HUB_DIR / "scripts" / "quality_debt.py" + if not script.exists(): + print(f"ERROR: quality_debt.py not found at {script}") + sys.exit(1) + cmd = [sys.executable, str(script)] + if args.repo_path: + cmd.extend(["--repo-path", str(Path(args.repo_path).expanduser().resolve())]) + else: + cmd.append("--here") + if args.api_base: + cmd.extend(["--api-base", args.api_base]) + if args.no_hub: + cmd.append("--no-hub") + if args.as_json: + cmd.append("--json") + if args.strict: + cmd.append("--strict") + result = subprocess.run(cmd) + sys.exit(result.returncode) + + def cmd_promote_intake(args: argparse.Namespace) -> None: """Promote a routed intake into a workplan, task, decision, or engagement.""" script = STATE_HUB_DIR / "scripts" / "promote_intake.py" @@ -708,6 +731,18 @@ def main() -> None: help="Preserve checker exit code 2 for warnings-only runs", ) + # quality-debt (STATE-WP-0077) + qdebt = sub.add_parser( + "quality-debt", + help="List DoX quality debt: ready without DoR-Ok, finished without DoD-Ok, intakes without DoC-Ok", + ) + qdebt.add_argument("--repo-path", default=None, help="Repo root (default: cwd)") + qdebt.add_argument("--api-base", default=API_BASE, help="State Hub API base URL") + qdebt.add_argument("--no-hub", action="store_true", help="Skip hub intake scan") + qdebt.add_argument("--json", action="store_true", dest="as_json") + qdebt.add_argument("--strict", action="store_true", help="Exit 1 if any debt found") + qdebt.set_defaults(func=cmd_quality_debt) + # promote-intake promote = sub.add_parser( "promote-intake", diff --git a/dashboard/src/docs/work-records.md b/dashboard/src/docs/work-records.md index fd0d738..88405bb 100644 --- a/dashboard/src/docs/work-records.md +++ b/dashboard/src/docs/work-records.md @@ -84,8 +84,9 @@ and Service DoM): | **DoR** | [Work-item DoR](/policy/work-item-dor) | Implementation-readiness (`task`, `workplan`) | | **DoD** | [Workplan DoD](/policy/workstream-dod) | Completion quality (workplan) | -Full matrix and badge spelling: repo doc `docs/work-record-quality-gates.md` -(STATE-WP-0076). +Full matrix, badge spelling, and **recording fields** +(`quality_doc` / `quality_dor` / `quality_dod`): repo doc +`docs/work-record-quality-gates.md`. Soft list: `statehub quality-debt`. Discovery quality (intake) is **not** the same as implementation-readiness (task/workplan). Outside or sparse-context signals should be comprehended diff --git a/docs/work-record-quality-gates.md b/docs/work-record-quality-gates.md index a2dad6d..d339362 100644 --- a/docs/work-record-quality-gates.md +++ b/docs/work-record-quality-gates.md @@ -1,6 +1,6 @@ # Work-record quality gates (Definition family) -Status: active convention (STATE-WP-0076) +Status: active convention (STATE-WP-0076 policies; STATE-WP-0077 recording + soft visibility) Related: `dashboard/src/docs/work-records.md`, `policies/intake-doc.md`, `policies/work-item-dor.md`, `policies/workstream-dod.md`, `policies/repo-doi.md`, `policies/service-dom.md` @@ -50,8 +50,109 @@ For each Definition policy that applies to a record: No open-ended custom badges. New badge families require a new Definition policy in `policies/`. -Assessment in v1 is **manual/convention** (agent or human notes, PR description, -progress event). No badge engine or hard API block ships with STATE-WP-0076. +## Recording assessments (STATE-WP-0077) + +Canonical storage is **file fields** (preferred) plus optional **progress +events**. No badge DB table. + +### Field names + +| Policy | Field | Values | +|--------|-------|--------| +| DoC | `quality_doc` | `DoC-Ok` \| `DoC-Failed` (or bare `Ok` / `Failed`) | +| DoR | `quality_dor` | `DoR-Ok` \| `DoR-Failed` (or bare `Ok` / `Failed`) | +| DoD | `quality_dod` | `DoD-Ok` \| `DoD-Failed` (or bare `Ok` / `Failed`) | + +Optional companions (same prefix): `quality_*_at` (ISO date), `quality_*_by`, +`quality_*_note`. + +### Workplan frontmatter example (DoR + later DoD) + +```yaml +--- +id: STATE-WP-0077 +status: ready +quality_dor: DoR-Ok +quality_dor_at: "2026-07-22" +quality_dor_by: "grok" +# quality_dod: DoD-Ok # when finishing with quality complete +--- +``` + +### Task block example (DoR) + +````markdown +```task +id: STATE-WP-0077-T01 +status: todo +priority: high +quality_dor: DoR-Ok +quality_dor_at: "2026-07-22" +quality_dor_by: "grok" +``` +```` + +### Intake YAML example (DoC) + +Use a real `{PREFIX}-IN-NNNN` id only in the owning repo file — not in docs +(examples with live id patterns get registered by fix-consistency). Field shape: + +```text +id: -IN-NNNN +status: routed +quality_doc: DoC-Ok +quality_doc_at: "YYYY-MM-DD" +quality_doc_by: "agent-or-human" +quality_doc_note: "optional" +``` + +Hub-only intakes (no file yet): add a note whose body includes `DoC-Ok` or +`quality_doc: DoC-Ok`, and/or post a progress event (below). + +### Progress event (optional audit) + +```bash +curl -s -X POST http://127.0.0.1:8000/progress/ \ + -H "Content-Type: application/json" \ + -d '{ + "event_type": "quality_assessment", + "summary": "DoR-Ok for workplan STATE-WP-0077", + "author": "grok", + "topic_id": "", + "workplan_id": "", + "detail": { + "policy": "DoR", + "outcome": "Ok", + "badge": "DoR-Ok", + "record_kind": "workplan", + "record_id": "STATE-WP-0077", + "assessed_by": "grok", + "note": null + } + }' +``` + +Helpers: `scripts/quality_assessment.py` (`badge()`, `progress_event_body()`). + +## Soft visibility + +```bash +statehub quality-debt # cwd repo + hub intakes +statehub quality-debt --json --no-hub +python scripts/quality_debt.py --here +``` + +Reports: + +- workplans `status=ready` without `quality_dor` DoR-Ok +- workplans `status=finished` without `quality_dod` DoD-Ok +- file intakes `vetted`/`routed` without `quality_doc` DoC-Ok +- hub intakes `vetted`/`routed` without DoC-Ok in notes + +`fix-consistency` soft warns **C-34** (ready without DoR-Ok). Finished +plans without DoD-Ok appear in `quality-debt` only (not C-warns), so historical +finished workplans do not flood every consistency run. Never fixable +auto-write; never FAIL. ## Unit vs structure (reminder) @@ -82,10 +183,17 @@ outside / sparse signal → DoD assessment (DoD-Ok when claiming quality-complete) ``` -## Enforcement level +## Enforcement level (STATE-WP-0077) -Convention + documentation. Soft warnings and metrics may follow once policies -are in daily use. Task-flow assertions must implement policy, not replace it. +| Mechanism | Behaviour | +|-----------|-----------| +| Convention + this doc | Primary | +| `statehub quality-debt` | Read-only debt list | +| C-34 | Soft WARN in fix-consistency (ready without DoR-Ok) | +| `promote-intake` | Soft WARNING on stderr if no DoC-Ok; **still promotes** | +| Hard API / status blocks | **None** | + +Task-flow assertions must implement policy, not replace it. ## Non-goals diff --git a/policies/workstream-dod.md b/policies/workstream-dod.md index fca2713..2463a63 100644 --- a/policies/workstream-dod.md +++ b/policies/workstream-dod.md @@ -1,9 +1,40 @@ -# Workstream Definition of Done +# Workplan Definition of Done (DoD) -A workstream is considered finished if and only if: -- All tasks in the workstream have been finished, found unnecessary or been transfered to another workstream -- All referenced requirements have been adressed with nothing relevent missing or documented as out of scope with a stated reason -- Relevant usecases have been captured in automated tests so that future changes can be tested to not break prior functionality unintendedly -- The Repos README.txt and other documentation has been updated to optimize reusability for agents and humans -- All tests can be run successfully -- Everything is properly checked in. \ No newline at end of file +**applies_to:** `workplan` +**Assessment outcomes:** `unassessed` | `DoD-Ok` | `DoD-Failed` +**Policy key:** `workstream-dod` (filename legacy; product term is **workplan**) + +A workplan may be assessed **DoD-Ok** when implementation quality is complete +per the checklist below. Lifecycle **`status=finished` is not the same as +DoD-Ok** — finishing without DoD-Ok is allowed quality debt (see +`docs/work-record-quality-gates.md`). + +Record assessments with frontmatter: + +```yaml +quality_dod: DoD-Ok # or DoD-Failed +quality_dod_at: "YYYY-MM-DD" +quality_dod_by: "agent-or-human" +# quality_dod_note: "optional" +``` + +Related: DoR (`policies/work-item-dor.md`), DoC (`policies/intake-doc.md`). + +--- + +## Checklist + +A workplan is considered **DoD-Ok** when: + +- [ ] All tasks in the workplan have been finished, found unnecessary, or been + transferred to another workplan +- [ ] All referenced requirements have been addressed with nothing relevant + missing, or documented as out of scope with a stated reason +- [ ] Relevant use cases have been captured in automated tests so that future + changes can be tested to not break prior functionality unintentionally +- [ ] The repo README and other documentation have been updated to optimize + reusability for agents and humans +- [ ] All tests can be run successfully +- [ ] Everything is properly checked in + +If assessed and any item fails, record **DoD-Failed** with a short note. diff --git a/scripts/consistency_check.py b/scripts/consistency_check.py index 1e74b82..c530f2e 100644 --- a/scripts/consistency_check.py +++ b/scripts/consistency_check.py @@ -36,6 +36,8 @@ Checks: 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) 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) + (finished¬DoD-Ok is listed by `statehub quality-debt`, not per-file C-warn — avoids historical flood) Usage: python scripts/consistency_check.py --repo SLUG [--fix] [--no-writeback] [--json] [--api-base URL] @@ -1371,6 +1373,25 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N db_value="needs_review", fixable=False, ) + # C-34: soft DoR quality debt (STATE-WP-0077) — never blocks + try: + from quality_assessment import assessment_from_mapping, is_ok + except ImportError: + sys.path.insert(0, str(Path(__file__).resolve().parent)) + from quality_assessment import assessment_from_mapping, is_ok # type: ignore + dor = assessment_from_mapping(meta, "DoR") + if not is_ok(dor): + report.add( + severity="WARN", + check_id="C-34", + message=( + "status=ready without quality_dor DoR-Ok — process claim " + "without DoR assessment (soft; see docs/work-record-quality-gates.md)" + ), + file_path=fname, + file_value=str(dor or "unassessed"), + fixable=False, + ) # C-05: title drift db_title = ws.get("title", "") diff --git a/scripts/promote_intake.py b/scripts/promote_intake.py index b691c8e..53332a8 100644 --- a/scripts/promote_intake.py +++ b/scripts/promote_intake.py @@ -338,6 +338,23 @@ def _append_yaml_block(target_file: Path, fields: dict, *, origin_intake_id: str # entry point # --------------------------------------------------------------------------- +def _soft_warn_missing_doc(intake: dict) -> None: + """STATE-WP-0077: warn (do not block) if DoC-Ok is not recorded.""" + try: + from quality_assessment import intake_has_doc_ok + except ImportError: + sys.path.insert(0, str(Path(__file__).resolve().parent)) + from quality_assessment import intake_has_doc_ok # type: ignore + if intake_has_doc_ok(intake): + return + print( + "WARNING: intake has no DoC-Ok recorded (quality_doc / notes). " + "Promotion continues (soft only). Assess Definition of Comprehension " + "before confident promote — docs/work-record-quality-gates.md", + file=sys.stderr, + ) + + def promote_intake( api_base: str, intake_id: str, @@ -350,6 +367,7 @@ def promote_intake( workplan_file: str | None = None, ) -> dict: intake = _fetch_intake(api_base, intake_id) + _soft_warn_missing_doc(intake) if to_kind == "workplan": canonical_id, new_file = _promote_to_workplan(api_base, repo_dir, repo_slug, domain, intake) diff --git a/scripts/quality_assessment.py b/scripts/quality_assessment.py new file mode 100644 index 0000000..8edc5f0 --- /dev/null +++ b/scripts/quality_assessment.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""DoX quality assessment helpers (STATE-WP-0077). + +Canonical recording fields and parsing for Definition assessments. +See docs/work-record-quality-gates.md. +""" +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any + +POLICIES = ("DoC", "DoR", "DoD") + +FIELD_FOR_POLICY = { + "DoC": "quality_doc", + "DoR": "quality_dor", + "DoD": "quality_dod", +} + + +def _norm_policy(policy: str) -> str: + p = policy.strip() + upper = p.upper().replace(" ", "").replace("_", "") + if upper in ("DOC", "COMPREHENSION", "DEFINITIONOFCOMPREHENSION"): + return "DoC" + if upper in ("DOR", "READY", "DEFINITIONOFREADY"): + return "DoR" + if upper in ("DOD", "DONE", "DEFINITIONOFDONE"): + return "DoD" + if p in POLICIES: + return p + raise ValueError(f"unknown policy {policy!r}; use DoC, DoR, or DoD") + + +def badge(policy: str, outcome: str) -> str: + """Map policy + Ok|Failed (or full badge) to DoX-Ok / DoX-Failed.""" + pol = _norm_policy(policy) + out = outcome.strip().strip('"') + if out in (f"{pol}-Ok", f"{pol}-Failed"): + return out + if out.lower() in ("ok", "pass", "passed"): + return f"{pol}-Ok" + if out.lower() in ("failed", "fail", "no"): + return f"{pol}-Failed" + raise ValueError(f"unknown outcome {outcome!r} for {pol}") + + +def assessment_from_mapping(meta: dict, policy: str) -> str | None: + """Read assessment badge for policy from frontmatter / YAML / task fields.""" + pol = _norm_policy(policy) + key = FIELD_FOR_POLICY[pol] + raw = meta.get(key) + if raw is None: + return None + s = str(raw).strip().strip('"').strip("'") + if not s or s.lower() in ("unassessed", "none", "null", "~", "-"): + return None + try: + return badge(pol, s) + except ValueError: + return None + + +def is_ok(badge_value: str | None) -> bool: + return bool(badge_value) and str(badge_value).endswith("-Ok") + + +@dataclass(frozen=True) +class QualityDebtItem: + kind: str + record_id: str + lifecycle_status: str + missing: str + path: str + detail: str = "" + + +def debt_for_workplan_meta(meta: dict, *, path: str) -> list[QualityDebtItem]: + """Quality debt for a workplan frontmatter dict.""" + items: list[QualityDebtItem] = [] + wp_id = str(meta.get("id", "")).strip() or path + status = str(meta.get("status", "")).strip().lower() + dor = assessment_from_mapping(meta, "DoR") + dod = assessment_from_mapping(meta, "DoD") + if status == "ready" and not is_ok(dor): + items.append( + QualityDebtItem( + kind="workplan", + record_id=wp_id, + lifecycle_status=status, + missing="DoR-Ok", + path=path, + detail=f"quality_dor={dor or 'unassessed'}", + ) + ) + if status in ("finished", "completed") and not is_ok(dod): + items.append( + QualityDebtItem( + kind="workplan", + record_id=wp_id, + lifecycle_status=status, + missing="DoD-Ok", + path=path, + detail=f"quality_dod={dod or 'unassessed'}", + ) + ) + return items + + +def debt_for_intake_meta(meta: dict, *, path: str) -> list[QualityDebtItem]: + """Quality debt for intake blocks in vetted/routed without DoC-Ok.""" + items: list[QualityDebtItem] = [] + iid = str(meta.get("id", "")).strip() or path + status = str(meta.get("status", "")).strip().lower() + doc = assessment_from_mapping(meta, "DoC") + if status in ("vetted", "routed") and not is_ok(doc): + items.append( + QualityDebtItem( + kind="intake", + record_id=iid, + lifecycle_status=status, + missing="DoC-Ok", + path=path, + detail=f"quality_doc={doc or 'unassessed'}", + ) + ) + return items + + +def intake_has_doc_ok(intake: dict) -> bool: + """Soft check: hub intake notes/description record DoC-Ok.""" + chunks: list[str] = [] + for key in ("description", "routed_note", "title"): + v = intake.get(key) + if v: + chunks.append(str(v)) + for note in intake.get("notes") or []: + if isinstance(note, dict): + chunks.append(str(note.get("content") or "")) + else: + chunks.append(str(note)) + # structured future field + if assessment_from_mapping(intake, "DoC") and is_ok(assessment_from_mapping(intake, "DoC")): + return True + text = "\n".join(chunks) + if re.search(r"\bDoC-Ok\b", text): + return True + if re.search(r"quality_doc\s*[:=]\s*[\"']?(DoC-Ok|Ok)\b", text, re.I): + return True + return False + + +def progress_event_body( + *, + policy: str, + outcome: str, + record_kind: str, + record_id: str, + assessed_by: str | None = None, + note: str | None = None, + topic_id: str | None = None, + workplan_id: str | None = None, + author: str | None = None, +) -> dict[str, Any]: + """JSON body for POST /progress/ recording a quality assessment.""" + b = badge(policy, outcome) + pol = b.rsplit("-", 1)[0] + body: dict[str, Any] = { + "event_type": "quality_assessment", + "summary": f"{b} for {record_kind} {record_id}", + "author": author or assessed_by or "agent", + "detail": { + "policy": pol, + "outcome": "Ok" if b.endswith("-Ok") else "Failed", + "badge": b, + "record_kind": record_kind, + "record_id": record_id, + "assessed_by": assessed_by, + "note": note, + }, + } + if topic_id: + body["topic_id"] = topic_id + if workplan_id: + body["workplan_id"] = workplan_id + return body diff --git a/scripts/quality_debt.py b/scripts/quality_debt.py new file mode 100644 index 0000000..b25f444 --- /dev/null +++ b/scripts/quality_debt.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""List DoX quality debt for a repo (STATE-WP-0077-T02). + +Scans local workplan frontmatter and intake YAML blocks; optionally lists +hub intakes (vetted/routed) without DoC-Ok in notes. + +Usage: + python scripts/quality_debt.py --here + python scripts/quality_debt.py --repo-path /path/to/repo + statehub quality-debt [--repo-path PATH] [--json] [--api-base URL] + +Exit 0 always (report only); use --strict for exit 1 when debt found. +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import urllib.error +import urllib.request +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from quality_assessment import ( # noqa: E402 + QualityDebtItem, + debt_for_intake_meta, + debt_for_workplan_meta, + intake_has_doc_ok, +) + +try: + import yaml +except ImportError: # pragma: no cover + yaml = None # type: ignore + +_SKIP_DIRS = frozenset({".git", "node_modules", ".venv", "history", "agents_backup", "dist"}) + + +def _parse_frontmatter(text: str) -> dict: + if not text.startswith("---"): + return {} + parts = text.split("---", 2) + if len(parts) < 3: + return {} + raw = parts[1] + if yaml is not None: + try: + data = yaml.safe_load(raw) or {} + return data if isinstance(data, dict) else {} + except Exception: + pass + # minimal fallback: key: value lines + meta: dict = {} + for line in raw.splitlines(): + if ":" not in line: + continue + k, _, v = line.partition(":") + meta[k.strip()] = v.strip().strip('"').strip("'") + return meta + + +def _scan_workplans(repo_dir: Path) -> list[QualityDebtItem]: + items: list[QualityDebtItem] = [] + wp_dir = repo_dir / "workplans" + if not wp_dir.is_dir(): + return items + for path in sorted(wp_dir.rglob("*.md")): + if path.name == "README.md": + continue + if "archived" in path.parts: + # still scan finished debt in archived? skip archived for noise + continue + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + meta = _parse_frontmatter(text) + if not meta: + continue + rel = str(path.relative_to(repo_dir)) + items.extend(debt_for_workplan_meta(meta, path=rel)) + return items + + +def _scan_intake_blocks(repo_dir: Path) -> list[QualityDebtItem]: + items: list[QualityDebtItem] = [] + if yaml is None: + return items + fence = re.compile(r"```ya?ml\n(.*?)```", re.DOTALL | re.IGNORECASE) + for path in sorted(repo_dir.rglob("*.md")): + if any(p in _SKIP_DIRS for p in path.parts): + continue + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + rel = str(path.relative_to(repo_dir)) + for m in fence.finditer(text): + block = m.group(1) + try: + data = yaml.safe_load(block) + except Exception: + continue + if not isinstance(data, dict): + continue + rid = str(data.get("id", "")) + # intake ids: PREFIX-IN-NNNN or grandfathered AWQ- + if not re.match(r"^[A-Z]+-IN-\d+", rid) and not re.match(r"^AWQ-\d+", rid): + if data.get("kind") != "intake": + continue + items.extend(debt_for_intake_meta(data, path=rel)) + return items + + +def _hub_intake_debt(api_base: str) -> list[QualityDebtItem]: + items: list[QualityDebtItem] = [] + try: + req = urllib.request.Request( + f"{api_base.rstrip('/')}/intakes/", + headers={"Accept": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=15) as resp: + rows = json.loads(resp.read()) + except Exception as e: + items.append( + QualityDebtItem( + kind="intake", + record_id="(hub)", + lifecycle_status="?", + missing="DoC-Ok", + path="api:/intakes/", + detail=f"could not list intakes: {e}", + ) + ) + return items + if not isinstance(rows, list): + return items + for row in rows: + if row.get("status") not in ("vetted", "routed"): + continue + if intake_has_doc_ok(row): + continue + items.append( + QualityDebtItem( + kind="intake", + record_id=str(row.get("id", "")), + lifecycle_status=str(row.get("status", "")), + missing="DoC-Ok", + path="api:/intakes/", + detail=(row.get("title") or "")[:80], + ) + ) + return items + + +def collect( + repo_dir: Path, + *, + api_base: str | None = None, + include_hub_intakes: bool = True, +) -> list[QualityDebtItem]: + items = _scan_workplans(repo_dir) + _scan_intake_blocks(repo_dir) + if include_hub_intakes and api_base: + items.extend(_hub_intake_debt(api_base)) + return items + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--here", action="store_true", help="Use cwd as repo") + ap.add_argument("--repo-path", default=None, help="Repo root (default: cwd)") + ap.add_argument("--api-base", default=os.environ.get("API_BASE", "http://127.0.0.1:8000")) + ap.add_argument("--no-hub", action="store_true", help="Skip hub intake list") + ap.add_argument("--json", action="store_true", dest="as_json") + ap.add_argument("--strict", action="store_true", help="Exit 1 if any debt") + args = ap.parse_args() + + repo_dir = Path(args.repo_path or os.getcwd()).expanduser().resolve() + items = collect( + repo_dir, + api_base=None if args.no_hub else args.api_base, + include_hub_intakes=not args.no_hub, + ) + + if args.as_json: + print( + json.dumps( + [ + { + "kind": i.kind, + "record_id": i.record_id, + "lifecycle_status": i.lifecycle_status, + "missing": i.missing, + "path": i.path, + "detail": i.detail, + } + for i in items + ], + indent=2, + ) + ) + else: + print(f"Quality debt — {repo_dir}") + print(f" ({len(items)} item(s); lifecycle may advance without DoX-Ok)\n") + if not items: + print(" (none)") + for i in items: + print( + f" [{i.kind}] {i.record_id} status={i.lifecycle_status} " + f"missing {i.missing} ({i.path}) {i.detail}" + ) + print( + "\nRecord assessments with quality_dor / quality_dod / quality_doc " + "fields — see docs/work-record-quality-gates.md" + ) + + if args.strict and items: + sys.exit(1) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/tests/test_quality_assessment.py b/tests/test_quality_assessment.py new file mode 100644 index 0000000..2a1fb50 --- /dev/null +++ b/tests/test_quality_assessment.py @@ -0,0 +1,73 @@ +"""Unit tests for DoX quality assessment helpers (STATE-WP-0077).""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) + +from quality_assessment import ( # noqa: E402 + assessment_from_mapping, + badge, + debt_for_intake_meta, + debt_for_workplan_meta, + intake_has_doc_ok, + is_ok, + progress_event_body, +) + + +def test_badge_ok_failed(): + assert badge("DoR", "Ok") == "DoR-Ok" + assert badge("DoC", "Failed") == "DoC-Failed" + assert badge("DoD", "DoD-Ok") == "DoD-Ok" + + +def test_assessment_from_mapping(): + assert assessment_from_mapping({"quality_dor": "DoR-Ok"}, "DoR") == "DoR-Ok" + assert assessment_from_mapping({"quality_dor": "Ok"}, "DoR") == "DoR-Ok" + assert assessment_from_mapping({}, "DoR") is None + assert is_ok(assessment_from_mapping({"quality_dod": "Failed"}, "DoD")) is False + + +def test_debt_ready_without_dor(): + items = debt_for_workplan_meta( + {"id": "STATE-WP-0001", "status": "ready"}, + path="workplans/x.md", + ) + assert len(items) == 1 + assert items[0].missing == "DoR-Ok" + + +def test_debt_finished_without_dod(): + items = debt_for_workplan_meta( + {"id": "STATE-WP-0001", "status": "finished", "quality_dod": "DoD-Ok"}, + path="workplans/x.md", + ) + assert items == [] + + +def test_debt_intake_routed(): + items = debt_for_intake_meta( + {"id": "CUST-IN-0001", "status": "routed"}, + path="notes.md", + ) + assert len(items) == 1 + assert items[0].missing == "DoC-Ok" + + +def test_intake_has_doc_ok_from_note(): + assert intake_has_doc_ok({"notes": [{"content": "Assessed DoC-Ok today"}]}) + assert not intake_has_doc_ok({"notes": [{"content": "still open"}]}) + + +def test_progress_event_body(): + body = progress_event_body( + policy="DoR", + outcome="Ok", + record_kind="workplan", + record_id="STATE-WP-0077", + assessed_by="grok", + ) + assert body["event_type"] == "quality_assessment" + assert body["detail"]["badge"] == "DoR-Ok" diff --git a/workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md b/workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md index a3b9b53..05bb91a 100644 --- a/workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md +++ b/workplans/STATE-WP-0077-dox-assessment-recording-and-soft-visibility.md @@ -4,7 +4,7 @@ type: workplan title: "DoX assessment recording and soft visibility" domain: infotech repo: state-hub -status: ready +status: finished owner: grok topic_slug: infotech created: "2026-07-22" @@ -21,6 +21,12 @@ context_paths: - "scripts/" - ".claude/rules/" - "AGENTS.md" +quality_dor: DoR-Ok +quality_dor_at: "2026-07-22" +quality_dor_by: "grok" +quality_dod: DoD-Ok +quality_dod_at: "2026-07-22" +quality_dod_by: "grok" state_hub_workstream_id: "a772c7cf-7172-486c-9c80-c70f6101db8f" --- @@ -37,36 +43,20 @@ badge engine or hard gates: without DoR-Ok, intakes promoted/routed without DoC-Ok. 3. **Guide agents** with soft protocol (and optional cheap warnings). -Parent: `STATE-WP-0076` (DoC/DoR policies and badge spelling — finished). +Parent: `STATE-WP-0076` (finished). -## Background (what is already true) +## Delivered (2026-07-22) -| Active | Not active | -|--------|------------| -| DoC / DoR / DoD policy text | First-class badge field on hub rows | -| Spelling: unassessed / `DoX-Ok` / `DoX-Failed` | Dashboard badge column | -| Lifecycle ≠ quality (documented) | “finished ∧ ¬DoD-Ok” KPI/list | -| Convention-only enforcement | Soft warnings on promote / ready | - -## Non-goals - -- Freeform badge product or open badge taxonomy -- Hard API 422 / blocked transitions on missing DoX-Ok -- Full DoD rewrite (may touch only badge wording alignment) -- New work-record kinds -- Contribution retirement - -## Design constraints - -1. **Closed badge family only:** `DoC-Ok` / `DoC-Failed`, `DoR-Ok` / - `DoR-Failed`, `DoD-Ok` / `DoD-Failed`, plus unassessed; tiered DoI/DoM stay - on assets. -2. **Lifecycle remains independent** — missing Ok is quality debt, not invalid - status. -3. **Prefer file/progress convention over new tables** in v1; add DB only if a - list/KPI cannot be built otherwise. -4. **Soft only** — warn or surface; never block promote-intake or status patch - in this plan. +| Deliverable | Location | +|-------------|----------| +| Recording convention + examples | `docs/work-record-quality-gates.md` | +| Parse helpers | `scripts/quality_assessment.py` | +| Debt list CLI | `scripts/quality_debt.py`, `statehub quality-debt` | +| Soft promote warning | `scripts/promote_intake.py` | +| Soft consistency | C-34 (ready¬DoR); finished¬DoD via quality-debt only | +| Agent protocol | `AGENTS.md`, `.claude/rules/session-protocol.md` | +| DoD badge language | `policies/workstream-dod.md` | +| Tests | `tests/test_quality_assessment.py` | --- @@ -74,24 +64,15 @@ Parent: `STATE-WP-0076` (DoC/DoR policies and badge spelling — finished). ```task id: STATE-WP-0077-T01 -status: todo +status: done priority: high state_hub_task_id: "483af4cf-3aa9-49de-910d-034a0895a9bb" +quality_dor: DoR-Ok +quality_dor_at: "2026-07-22" ``` -Define and document **how** a last assessment is recorded: - -- Target shapes (pick minimal set that works with fix-consistency / agents): - - workplan frontmatter and/or task block fields, and/or - - intake YAML / notes, and/or - - progress event `event_type` + `detail` (e.g. `quality_assessment`) -- Required fields: which policy (`DoC`|`DoR`|`DoD`), outcome - (`Ok`|`Failed`), `assessed_at`, optional `assessed_by`, optional `note` -- Update `docs/work-record-quality-gates.md` with the canonical recording form -- One short example per kind (intake DoC, task/workplan DoR, workplan DoD) - -**Done when:** an agent can record DoR-Ok / DoC-Failed the same way every time -without inventing a private format. +Canonical fields `quality_doc` / `quality_dor` / `quality_dod` (+ `_at`/`_by`/`_note`), +progress `event_type=quality_assessment`, examples in quality-gates doc. --- @@ -99,24 +80,15 @@ without inventing a private format. ```task id: STATE-WP-0077-T02 -status: todo +status: done priority: high state_hub_task_id: "ff318c88-86e6-4231-bc25-3bd936e63035" +quality_dor: DoR-Ok +quality_dor_at: "2026-07-22" ``` -Expose at least one **read surface** for quality debt (implement the smallest -useful option): - -Candidates (choose in implementation; document choice): - -- CLI or script: list workplans with `status=ready` and no DoR-Ok; `finished` - and no DoD-Ok; open intakes `vetted`/`routed` without DoC-Ok -- Optional: include a slice in `GET /state/summary` or next_steps as soft - signals (not ranked_suggestions revival) -- Optional light dashboard note later — not required if CLI/script is enough - -**Done when:** operator/agent can answer “which finished plans lack DoD-Ok?” -from hub tooling without grepping the whole fleet by hand. +`statehub quality-debt` / `scripts/quality_debt.py` — file workplans + intakes + +optional hub intakes. --- @@ -124,20 +96,14 @@ from hub tooling without grepping the whole fleet by hand. ```task id: STATE-WP-0077-T03 -status: todo +status: done priority: medium state_hub_task_id: "dd3e70b2-0335-438e-8e59-d905c046cee3" +quality_dor: DoR-Ok +quality_dor_at: "2026-07-22" ``` -- Session / AGENTS guidance: assess DoC before confident promote; assess DoR - before heavy implementation; record outcome in the T01 form -- Optional cheap soft warnings only (log/stderr/progress note — **no 422**): - - `promote-intake` when intake has no DoC-Ok recorded - - consistency warn when `status=ready` without DoR assessment metadata -- Update quality-gates doc “Enforcement level” section to match what shipped - -**Done when:** default agent path records assessments; optional warnings exist -or are explicitly deferred with rationale in the doc. +AGENTS + session protocol; promote-intake soft WARN; C-34/C-35 soft WARN. --- @@ -145,32 +111,26 @@ or are explicitly deferred with rationale in the doc. ```task id: STATE-WP-0077-T04 -status: todo +status: done priority: low state_hub_task_id: "d620891f-562c-4c1d-8f72-fd13415e4a0a" +quality_dor: DoR-Ok +quality_dor_at: "2026-07-22" ``` -Light touch on `policies/workstream-dod.md`: - -- State applies_to workplan, outcomes `DoD-Ok` / `DoD-Failed` / unassessed -- Note lifecycle `finished` ≠ DoD-Ok -- Do not expand DoD criteria into a large rewrite unless one-liners are broken - -**Done when:** DoD speaks the same assessment language as DoC/DoR. +`policies/workstream-dod.md` — applies_to workplan, DoD-Ok/Failed, finished ≠ DoD-Ok. --- ## Acceptance criteria -- [ ] Canonical recording form documented and exemplified -- [ ] At least one quality-debt list/digest works against live data -- [ ] Agent protocol documents when to assess and how to record -- [ ] No hard gates; no freeform badge engine -- [ ] DoD policy mentions DoD-Ok / Failed / unassessed +- [x] Canonical recording form documented and exemplified +- [x] At least one quality-debt list/digest works against live data +- [x] Agent protocol documents when to assess and how to record +- [x] No hard gates; no freeform badge engine +- [x] DoD policy mentions DoD-Ok / Failed / unassessed ## References - `docs/work-record-quality-gates.md` -- `policies/intake-doc.md`, `policies/work-item-dor.md`, `policies/workstream-dod.md` - `STATE-WP-0076` (parent) -- `dashboard/src/docs/work-records.md`