|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Render the Claw Code 2.0 canonical board JSON as a human-readable Markdown board.""" |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import argparse |
| 6 | +import json |
| 7 | +import sys |
| 8 | +from collections import Counter, defaultdict |
| 9 | +from pathlib import Path |
| 10 | +from typing import Any |
| 11 | + |
| 12 | +STATUS_DESCRIPTIONS = { |
| 13 | + "context": "Context-only heading or evidence anchor; not an implementation work item.", |
| 14 | + "active": "Current Claw Code 2.0 implementation surface that should remain visible on the board.", |
| 15 | + "open": "Actionable unresolved work that needs implementation or acceptance evidence.", |
| 16 | + "done_verify": "Marked as done upstream but retained for verification against current CC2 behavior.", |
| 17 | + "stale_done": "Historically completed or merged work that may be stale and needs freshness checks before relying on it.", |
| 18 | + "superseded": "Replaced by a newer item; keep as traceability context only.", |
| 19 | + "deferred_with_rationale": "Intentionally deferred; rationale must be present in the board item.", |
| 20 | + "rejected_not_claw": "Excluded because it is not Claw Code product work.", |
| 21 | +} |
| 22 | + |
| 23 | +BUCKET_DESCRIPTIONS = { |
| 24 | + "alpha_blocker": "Must be resolved before alpha-quality autonomous coding lanes are dependable.", |
| 25 | + "beta_adoption": "Important for broader dogfood/adoption once alpha blockers are controlled.", |
| 26 | + "ga_ecosystem": "Required for mature plugin/MCP/provider ecosystem behavior.", |
| 27 | + "2.x_intake": "Post-2.0 intake or follow-up candidate retained for sequencing.", |
| 28 | + "post_2_0_research": "Research-oriented item not required for the CC2 board cut.", |
| 29 | + "context": "Non-actionable roadmap context.", |
| 30 | + "rejected_not_claw": "Explicit non-Claw rejection bucket.", |
| 31 | +} |
| 32 | + |
| 33 | +LANE_TITLES = { |
| 34 | + "stream_0_governance": "Stream 0 — Governance, intake, and cross-cutting roadmap triage", |
| 35 | + "stream_1_worker_boot_session_control": "Stream 1 — Worker boot and session control", |
| 36 | + "stream_2_event_reporting_contracts": "Stream 2 — Event/reporting contracts", |
| 37 | + "stream_3_branch_test_recovery": "Stream 3 — Branch/test recovery", |
| 38 | + "stream_4_claws_first_execution": "Stream 4 — Claws-first task execution", |
| 39 | + "stream_5_plugin_mcp_lifecycle": "Stream 5 — Plugin/MCP lifecycle", |
| 40 | + "adoption_overlay": "Adoption overlay — user-visible parity and release polish", |
| 41 | + "parity_overlay": "Parity overlay — opencode/codex comparison context", |
| 42 | +} |
| 43 | + |
| 44 | +REQUIRED_ITEM_FIELDS = [ |
| 45 | + "id", |
| 46 | + "title", |
| 47 | + "source_anchor", |
| 48 | + "source_type", |
| 49 | + "release_bucket", |
| 50 | + "lifecycle_status", |
| 51 | + "dependencies", |
| 52 | + "verification_required", |
| 53 | + "deferral_rationale", |
| 54 | +] |
| 55 | + |
| 56 | + |
| 57 | +def load_board(path: Path) -> dict[str, Any]: |
| 58 | + with path.open() as f: |
| 59 | + board = json.load(f) |
| 60 | + if not isinstance(board, dict): |
| 61 | + raise ValueError("board JSON root must be an object") |
| 62 | + items = board.get("items") |
| 63 | + if not isinstance(items, list): |
| 64 | + raise ValueError("board JSON must contain an items array") |
| 65 | + return board |
| 66 | + |
| 67 | + |
| 68 | +def validate_board(board: dict[str, Any]) -> list[str]: |
| 69 | + errors: list[str] = [] |
| 70 | + coverage = board.get("coverage", {}) |
| 71 | + if coverage.get("unmapped_roadmap_heading_lines"): |
| 72 | + errors.append(f"unmapped roadmap heading lines: {coverage['unmapped_roadmap_heading_lines']}") |
| 73 | + if coverage.get("roadmap_headings_mapped") != coverage.get("roadmap_headings_total"): |
| 74 | + errors.append("roadmap heading coverage is incomplete") |
| 75 | + if coverage.get("roadmap_actions_mapped") != coverage.get("roadmap_actions_total"): |
| 76 | + errors.append("roadmap ordered-action coverage is incomplete") |
| 77 | + |
| 78 | + allowed_status = set(board.get("generation_policy", {}).get("status_values", [])) |
| 79 | + allowed_buckets = set(board.get("generation_policy", {}).get("release_buckets", [])) |
| 80 | + seen_ids: set[str] = set() |
| 81 | + for index, item in enumerate(board["items"], 1): |
| 82 | + for field in REQUIRED_ITEM_FIELDS: |
| 83 | + if field not in item: |
| 84 | + errors.append(f"item {index} missing required field {field}") |
| 85 | + item_id = item.get("id") |
| 86 | + if item_id in seen_ids: |
| 87 | + errors.append(f"duplicate item id {item_id}") |
| 88 | + seen_ids.add(item_id) |
| 89 | + status = item.get("lifecycle_status") |
| 90 | + bucket = item.get("release_bucket") |
| 91 | + if allowed_status and status not in allowed_status: |
| 92 | + errors.append(f"{item_id} has unknown lifecycle_status {status!r}") |
| 93 | + if allowed_buckets and bucket not in allowed_buckets: |
| 94 | + errors.append(f"{item_id} has unknown release_bucket {bucket!r}") |
| 95 | + if status == "deferred_with_rationale" and not str(item.get("deferral_rationale", "")).strip(): |
| 96 | + errors.append(f"{item_id} is deferred without deferral_rationale") |
| 97 | + return errors |
| 98 | + |
| 99 | + |
| 100 | +def table(headers: list[str], rows: list[list[Any]]) -> list[str]: |
| 101 | + out = ["| " + " | ".join(headers) + " |", "| " + " | ".join("---" for _ in headers) + " |"] |
| 102 | + for row in rows: |
| 103 | + out.append("| " + " | ".join(str(cell) for cell in row) + " |") |
| 104 | + return out |
| 105 | + |
| 106 | + |
| 107 | +def fmt_list(value: Any) -> str: |
| 108 | + if not value: |
| 109 | + return "none" |
| 110 | + if isinstance(value, list): |
| 111 | + return ", ".join(f"`{v}`" for v in value) if value else "none" |
| 112 | + return f"`{value}`" |
| 113 | + |
| 114 | + |
| 115 | +def render(board: dict[str, Any]) -> str: |
| 116 | + items: list[dict[str, Any]] = board["items"] |
| 117 | + summary = board.get("summary", {}) |
| 118 | + coverage = board.get("coverage", {}) |
| 119 | + sources = board.get("sources", {}) |
| 120 | + policy = board.get("generation_policy", {}) |
| 121 | + by_lane = Counter(item.get("owner_lane", "unassigned") for item in items) |
| 122 | + by_status = Counter(item.get("lifecycle_status", "unknown") for item in items) |
| 123 | + by_bucket = Counter(item.get("release_bucket", "unknown") for item in items) |
| 124 | + by_source = Counter(item.get("source_type", "unknown") for item in items) |
| 125 | + |
| 126 | + lines: list[str] = [] |
| 127 | + lines.append("# Claw Code 2.0 Canonical Board") |
| 128 | + lines.append("") |
| 129 | + lines.append(f"Generated from board schema: `{board.get('generated_at', 'unknown')}`") |
| 130 | + lines.append(f"Schema version: `{board.get('schema_version', 'unknown')}`") |
| 131 | + lines.append("Ultragoal mutation policy: `.omx/ultragoal` is leader-owned and was not modified by this rendering task.") |
| 132 | + lines.append("") |
| 133 | + |
| 134 | + lines.append("## Evidence Freeze") |
| 135 | + lines.append("") |
| 136 | + roadmap = sources.get("roadmap", {}) |
| 137 | + research = sources.get("research", {}) |
| 138 | + plan = sources.get("approved_plan", {}) |
| 139 | + lines.extend(table(["Source", "Frozen evidence"], [ |
| 140 | + ["Roadmap", f"`{roadmap.get('path', 'ROADMAP.md')}` sha256 prefix `{roadmap.get('sha256_prefix', 'unknown')}`; {roadmap.get('heading_count', '?')} headings; {roadmap.get('ordered_action_count', '?')} ordered actions"], |
| 141 | + ["Approved plan", f"`{plan.get('path', '.omx/plans/claw-code-2-0-adaptive-plan.md')}` sha256 prefix `{plan.get('sha256_prefix', 'unknown')}`"], |
| 142 | + ["Research bundle", f"root `{research.get('root', '.omx/research')}`; latest open issues {research.get('claw_open_latest_count', '?')}; issue corpus {research.get('claw_issues_count', '?')}; codex/opencode clone metadata included"], |
| 143 | + ])) |
| 144 | + lines.append("") |
| 145 | + |
| 146 | + lines.append("## Roadmap Coverage Summary") |
| 147 | + lines.append("") |
| 148 | + heading_total = coverage.get("roadmap_headings_total", 0) |
| 149 | + heading_mapped = coverage.get("roadmap_headings_mapped", 0) |
| 150 | + action_total = coverage.get("roadmap_actions_total", 0) |
| 151 | + action_mapped = coverage.get("roadmap_actions_mapped", 0) |
| 152 | + lines.extend(table(["Coverage gate", "Mapped", "Total", "Status"], [ |
| 153 | + ["ROADMAP headings", heading_mapped, heading_total, "PASS" if heading_mapped == heading_total and not coverage.get("unmapped_roadmap_heading_lines") else "FAIL"], |
| 154 | + ["ROADMAP ordered actions", action_mapped, action_total, "PASS" if action_mapped == action_total else "FAIL"], |
| 155 | + ["Duplicate heading lines", len(coverage.get("duplicate_roadmap_heading_lines", [])), 0, "PASS" if not coverage.get("duplicate_roadmap_heading_lines") else "WARN"], |
| 156 | + ])) |
| 157 | + lines.append("") |
| 158 | + lines.append(f"Total canonical board items: **{len(items)}**") |
| 159 | + lines.append("") |
| 160 | + |
| 161 | + lines.append("## Lifecycle Enum Reference") |
| 162 | + lines.append("") |
| 163 | + status_rows = [] |
| 164 | + for status in policy.get("status_values", sorted(by_status)): |
| 165 | + status_rows.append([f"`{status}`", by_status.get(status, 0), STATUS_DESCRIPTIONS.get(status, "Board-defined lifecycle status.")]) |
| 166 | + lines.extend(table(["Lifecycle", "Count", "Meaning"], status_rows)) |
| 167 | + lines.append("") |
| 168 | + |
| 169 | + lines.append("## Release Bucket Reference") |
| 170 | + lines.append("") |
| 171 | + bucket_rows = [] |
| 172 | + for bucket in policy.get("release_buckets", sorted(by_bucket)): |
| 173 | + bucket_rows.append([f"`{bucket}`", by_bucket.get(bucket, 0), BUCKET_DESCRIPTIONS.get(bucket, "Board-defined release bucket.")]) |
| 174 | + lines.extend(table(["Bucket", "Count", "Meaning"], bucket_rows)) |
| 175 | + lines.append("") |
| 176 | + |
| 177 | + lines.append("## Stream Summaries") |
| 178 | + lines.append("") |
| 179 | + lane_rows = [] |
| 180 | + for lane, count in sorted(by_lane.items()): |
| 181 | + lane_items = [item for item in items if item.get("owner_lane") == lane] |
| 182 | + lane_status = Counter(item.get("lifecycle_status") for item in lane_items) |
| 183 | + open_like = lane_status.get("active", 0) + lane_status.get("open", 0) + lane_status.get("done_verify", 0) |
| 184 | + lane_rows.append([ |
| 185 | + LANE_TITLES.get(lane, lane), |
| 186 | + count, |
| 187 | + open_like, |
| 188 | + ", ".join(f"`{k}` {v}" for k, v in sorted(lane_status.items())), |
| 189 | + ]) |
| 190 | + lines.extend(table(["Stream / lane", "Items", "Active+open+verify", "Lifecycle mix"], lane_rows)) |
| 191 | + lines.append("") |
| 192 | + |
| 193 | + lines.append("## Source-Type Mix") |
| 194 | + lines.append("") |
| 195 | + lines.extend(table(["Source type", "Items"], [[f"`{k}`", v] for k, v in sorted(by_source.items())])) |
| 196 | + lines.append("") |
| 197 | + |
| 198 | + lines.append("## Board Items by Stream") |
| 199 | + lines.append("") |
| 200 | + for lane in sorted(by_lane): |
| 201 | + lane_items = [item for item in items if item.get("owner_lane") == lane] |
| 202 | + lines.append(f"### {LANE_TITLES.get(lane, lane)}") |
| 203 | + lines.append("") |
| 204 | + lines.extend(table( |
| 205 | + ["ID", "Title", "Source", "Bucket", "Lifecycle", "Verification", "Dependencies", "Deferral"], |
| 206 | + [[ |
| 207 | + f"`{item.get('id')}`", |
| 208 | + str(item.get("title", "")).replace("|", "\\|"), |
| 209 | + f"`{item.get('source_anchor')}` / `{item.get('source_type')}`", |
| 210 | + f"`{item.get('release_bucket')}`", |
| 211 | + f"`{item.get('lifecycle_status')}`", |
| 212 | + f"`{item.get('verification_required')}`", |
| 213 | + fmt_list(item.get("dependencies")), |
| 214 | + str(item.get("deferral_rationale") or "—").replace("|", "\\|"), |
| 215 | + ] for item in lane_items] |
| 216 | + )) |
| 217 | + lines.append("") |
| 218 | + |
| 219 | + return "\n".join(lines).rstrip() + "\n" |
| 220 | + |
| 221 | + |
| 222 | +def main() -> int: |
| 223 | + parser = argparse.ArgumentParser(description=__doc__) |
| 224 | + parser.add_argument("board_json", type=Path) |
| 225 | + parser.add_argument("board_md", type=Path) |
| 226 | + parser.add_argument("--check", action="store_true", help="fail if board_md is not up to date") |
| 227 | + args = parser.parse_args() |
| 228 | + |
| 229 | + board = load_board(args.board_json) |
| 230 | + errors = validate_board(board) |
| 231 | + if errors: |
| 232 | + for error in errors: |
| 233 | + print(f"ERROR: {error}", file=sys.stderr) |
| 234 | + return 1 |
| 235 | + rendered = render(board) |
| 236 | + if args.check: |
| 237 | + existing = args.board_md.read_text() if args.board_md.exists() else "" |
| 238 | + if existing != rendered: |
| 239 | + print(f"ERROR: {args.board_md} is not up to date", file=sys.stderr) |
| 240 | + return 1 |
| 241 | + print(f"PASS: {args.board_md} is up to date and roadmap coverage is complete") |
| 242 | + return 0 |
| 243 | + args.board_md.parent.mkdir(parents=True, exist_ok=True) |
| 244 | + args.board_md.write_text(rendered) |
| 245 | + print(f"wrote {args.board_md}") |
| 246 | + return 0 |
| 247 | + |
| 248 | + |
| 249 | +if __name__ == "__main__": |
| 250 | + raise SystemExit(main()) |
0 commit comments