From 7471d7ed814f5150df127c5d611103233be0379b Mon Sep 17 00:00:00 2001 From: Jeff Huber Date: Wed, 2 Sep 2026 03:09:01 -0700 Subject: [PATCH] docs: add board demo rehearsal artifact --- README.md | 1 + docs/quickstart.md | 3 +- examples/board-demo/README.md | 59 +++++++++++++++++++++++++ examples/board-demo/board-events.jsonl | 2 + examples/board-demo/reviewer-spend.json | 29 ++++++++++++ tests/test_release_hygiene.py | 39 ++++++++++++++++ 6 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 examples/board-demo/README.md create mode 100644 examples/board-demo/board-events.jsonl create mode 100644 examples/board-demo/reviewer-spend.json diff --git a/README.md b/README.md index 18a1aa3a..dfa0749b 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ If you want to understand the product before installing anything, start with the checked-in demo calibration package: - [examples/demo-calibration/README.md](examples/demo-calibration/README.md) +- [examples/board-demo/README.md](examples/board-demo/README.md) - [examples/demo-calibration/reviewer-value-report.md](examples/demo-calibration/reviewer-value-report.md) - [docs/first-user-demo-transcript.md](docs/first-user-demo-transcript.md) diff --git a/docs/quickstart.md b/docs/quickstart.md index 5e803a45..9e3ab6cd 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -8,7 +8,8 @@ on one repository and keep all reviewer lanes manual until the output is useful on your codebase. To see the value loop before you touch a product repository, open the -[Demo Calibration Example](../examples/demo-calibration/README.md) and the +[Demo Calibration Example](../examples/demo-calibration/README.md), the +[Board Demo Rehearsal](../examples/board-demo/README.md), and the [First-User Demo Transcript](first-user-demo-transcript.md). ## 1. Install diff --git a/examples/board-demo/README.md b/examples/board-demo/README.md new file mode 100644 index 00000000..1ec7bd5a --- /dev/null +++ b/examples/board-demo/README.md @@ -0,0 +1,59 @@ +# Board Demo Rehearsal + +This public demo shows the native Code Mower Board shape without connecting to a +private repository or running provider CLIs. It is synthetic, small, and safe to +use in an adoption walkthrough. + +## Files + +- `board-events.jsonl` contains two synthetic `code_mower.boardEvent.v1` + snapshots. +- `reviewer-spend.json` contains matching synthetic + `code_mower.reviewerSpend.v1` rows. + +Both files are metadata-only fixtures for demonstrating Board state. + +## Try The Board Shape Locally + +From the repository root: + +```bash +code-mower board events --store-path examples/board-demo/board-events.jsonl +``` + +To open the local browser Board with the same sample history and spend data: + +```bash +code-mower board serve \ + --repo example/widget-service \ + --store-path examples/board-demo/board-events.jsonl \ + --spend-path examples/board-demo/reviewer-spend.json +``` + +The live GitHub panel may say the demo repository is unavailable. That is fine: +the Recent Local History, Reviewer Verdict Timeline, and Spend And Latency +panels still show the sample local artifacts. + +To inspect the Board diagnostic shape without exposing local paths: + +```bash +code-mower board doctor \ + --repo example/widget-service \ + --store-path examples/board-demo/board-events.jsonl \ + --spend-path examples/board-demo/reviewer-spend.json \ + --json +``` + +## What To Look For + +- one ready PR lane with a passing Claude audit; +- one blocked PR lane with a failing Codex audit; +- a gate alert and owner queue item for the blocked lane; +- reviewer spend and latency totals by lane; and +- redacted local path posture in command output. + +## Privacy Boundary + +This example contains no source code, raw diffs, transcripts, issue body text, +raw stdout/stderr, auth output, browser history, local paths, local secret +values, or secrets. diff --git a/examples/board-demo/board-events.jsonl b/examples/board-demo/board-events.jsonl new file mode 100644 index 00000000..e28cc50e --- /dev/null +++ b/examples/board-demo/board-events.jsonl @@ -0,0 +1,2 @@ +{"schema":"code_mower.boardEvent.v1","type":"status_snapshot","created_at":"2026-09-01T12:00:00Z","repo":"example/widget-service","snapshot_schema":"code_mower.laneStatus.v1","board_schema":"code_mower.board.v1","summary":{"next_action":"waiting for checks","remote_available":true,"open_prs":2,"gate_alerts":0,"local_boards":0,"local_processes":0},"snapshot":{"schema":"code_mower.laneStatus.v1","repo":"example/widget-service","generated_at":"2026-09-01T12:00:00Z","remote":{"available":true,"errors":[],"pull_requests":[{"number":12,"title":"Add export retry","url":"https://github.com/example/widget-service/pull/12","branch":"codex/export-retry","head_sha":"1111111111111111111111111111111111111111","author":"codex-bot","is_draft":false,"merge_state":"CLEAN","updated_at":"2026-09-01T11:58:00Z","labels":{"builder":["builder:codex"],"dispatched":[],"needs":["needs-claude-audit"],"done":[],"blocked":[]},"checks":[{"name":"package","state":"success"},{"name":"code-mower/gate","state":"pending"}],"stale":false,"next_action":"waiting for checks","gate_rerun_command":""},{"number":13,"title":"Tighten import validation","url":"https://github.com/example/widget-service/pull/13","branch":"cursor/import-validation","head_sha":"2222222222222222222222222222222222222222","author":"cursor-bot","is_draft":false,"merge_state":"CLEAN","updated_at":"2026-09-01T11:57:00Z","labels":{"builder":["builder:cursor"],"dispatched":[],"needs":["needs-codex-audit"],"done":["claude-audit-done"],"blocked":[]},"checks":[{"name":"package","state":"success"},{"name":"code-mower/gate","state":"pending"}],"stale":false,"next_action":"waiting for checks","gate_rerun_command":""}],"workflow_runs":[{"id":9001,"workflow":"Code Mower gate","title":"Code Mower gate","status":"completed","conclusion":"success","event":"pull_request_target","branch":"codex/export-retry","created_at":"2026-09-01T11:58:30Z","updated_at":"2026-09-01T11:58:45Z","url":"https://github.com/example/widget-service/actions/runs/9001"}],"gate_health":{"status":"pass","alerts":[]}},"agenttrail":{"available":false,"boards":[],"message":"local listener inventory unavailable"},"local_processes":{"available":false,"processes":[],"message":"local process inventory unavailable"},"board":{"schema":"code_mower.board.v1","mode":"local_read_only","refresh_seconds":15,"local_paths":"redacted","recording":{"enabled":false,"interval_seconds":60}}}} +{"schema":"code_mower.boardEvent.v1","type":"status_snapshot","created_at":"2026-09-01T12:05:00Z","repo":"example/widget-service","snapshot_schema":"code_mower.laneStatus.v1","board_schema":"code_mower.board.v1","summary":{"next_action":"fix BLOCKED audit","remote_available":true,"open_prs":2,"gate_alerts":1,"local_boards":0,"local_processes":0},"snapshot":{"schema":"code_mower.laneStatus.v1","repo":"example/widget-service","generated_at":"2026-09-01T12:05:00Z","remote":{"available":true,"errors":[],"pull_requests":[{"number":12,"title":"Add export retry","url":"https://github.com/example/widget-service/pull/12","branch":"codex/export-retry","head_sha":"1111111111111111111111111111111111111111","author":"codex-bot","is_draft":false,"merge_state":"CLEAN","updated_at":"2026-09-01T12:04:00Z","labels":{"builder":["builder:codex"],"dispatched":[],"needs":[],"done":["claude-audit-done"],"blocked":[]},"checks":[{"name":"package","state":"success"},{"name":"code-mower/gate","state":"success"}],"stale":false,"next_action":"ready for merge or auto-merge","gate_rerun_command":""},{"number":13,"title":"Tighten import validation","url":"https://github.com/example/widget-service/pull/13","branch":"cursor/import-validation","head_sha":"2222222222222222222222222222222222222222","author":"cursor-bot","is_draft":false,"merge_state":"CLEAN","updated_at":"2026-09-01T12:03:00Z","labels":{"builder":["builder:cursor"],"dispatched":[],"needs":[],"done":["claude-audit-done"],"blocked":["codex-audit-blocked"]},"checks":[{"name":"package","state":"success"},{"name":"code-mower/gate","state":"failure"}],"stale":false,"next_action":"fix BLOCKED audit","gate_rerun_command":""}],"workflow_runs":[{"id":9002,"workflow":"Code Mower gate","title":"Code Mower gate","status":"completed","conclusion":"failure","event":"pull_request_target","branch":"cursor/import-validation","created_at":"2026-09-01T12:03:30Z","updated_at":"2026-09-01T12:03:45Z","url":"https://github.com/example/widget-service/actions/runs/9002"}],"gate_health":{"status":"warn","alerts":[{"pr_number":13,"kind":"blocked-audit","message":"PR #13 has a blocked audit label"}]}},"agenttrail":{"available":false,"boards":[],"message":"local listener inventory unavailable"},"local_processes":{"available":false,"processes":[],"message":"local process inventory unavailable"},"board":{"schema":"code_mower.board.v1","mode":"local_read_only","refresh_seconds":15,"local_paths":"redacted","recording":{"enabled":false,"interval_seconds":60}}}} diff --git a/examples/board-demo/reviewer-spend.json b/examples/board-demo/reviewer-spend.json new file mode 100644 index 00000000..427fe22d --- /dev/null +++ b/examples/board-demo/reviewer-spend.json @@ -0,0 +1,29 @@ +{ + "schema": "code_mower.reviewerSpend.v1", + "runs": [ + { + "created_at": "2026-09-01T12:04:10Z", + "lane": "claude-audit", + "repo": "example/widget-service", + "pr_number": 12, + "head_sha": "1111111111111111111111111111111111111111", + "model": "sonnet", + "wall_seconds": 31.2, + "cost_usd": 0.18, + "total_tokens": 4200, + "verdict": "PASS" + }, + { + "created_at": "2026-09-01T12:04:50Z", + "lane": "codex-audit", + "repo": "example/widget-service", + "pr_number": 13, + "head_sha": "2222222222222222222222222222222222222222", + "model": "gpt-5", + "wall_seconds": 44.8, + "cost_usd": 0.24, + "total_tokens": 5100, + "verdict": "BLOCKED" + } + ] +} diff --git a/tests/test_release_hygiene.py b/tests/test_release_hygiene.py index cfb12232..0786e3ab 100644 --- a/tests/test_release_hygiene.py +++ b/tests/test_release_hygiene.py @@ -32,6 +32,8 @@ from code_mower import __version__ from code_mower import audit_labeler_lib from code_mower import audit_progress +from code_mower import board as code_mower_board +from code_mower import board_store as code_mower_board_store from code_mower import bootstrap as code_mower_bootstrap from code_mower import calibration as calibration_pkg from code_mower import cloud as code_mower_cloud @@ -4591,6 +4593,43 @@ def test_public_demo_calibration_artifacts_are_parseable_and_sanitized(self) -> with self.subTest(fragment=fragment): self.assertNotIn(fragment, public_text) + def test_public_board_demo_artifacts_are_parseable_and_sanitized(self) -> None: + demo_dir = ROOT / "examples/board-demo" + readme = (ROOT / "README.md").read_text(encoding="utf-8") + quickstart = (ROOT / "docs/quickstart.md").read_text(encoding="utf-8") + required = {"README.md", "board-events.jsonl", "reviewer-spend.json"} + self.assertTrue(demo_dir.is_dir()) + self.assertTrue(required.issubset({path.name for path in demo_dir.iterdir()})) + self.assertIn("examples/board-demo/README.md", readme) + self.assertIn("../examples/board-demo/README.md", quickstart) + + report = code_mower_board_store.event_report( + path=demo_dir / "board-events.jsonl", + limit=10, + ) + timelines = code_mower_board.timelines_payload( + code_mower_board.BoardConfig( + repo="example/widget-service", + store_path=str(demo_dir / "board-events.jsonl"), + spend_path=str(demo_dir / "reviewer-spend.json"), + ) + ) + spend = json.loads((demo_dir / "reviewer-spend.json").read_text(encoding="utf-8")) + public_text = "\n".join(path.read_text(encoding="utf-8") for path in sorted(demo_dir.iterdir())) + + self.assertEqual(report["schema"], code_mower_board_store.BOARD_EVENT_STORE_SCHEMA) + self.assertEqual(report["event_count"], 2) + self.assertEqual(report["malformed"], 0) + self.assertEqual(spend["schema"], "code_mower.reviewerSpend.v1") + self.assertIn( + ("codex-audit", "BLOCKED"), + {(entry["lane"], entry["verdict"]) for entry in timelines["verdicts"]["entries"]}, + ) + self.assertEqual(timelines["spend"]["groups"][0]["lane"], "claude-audit") + self.assertIn("metadata-only", public_text) + self.assertNotIn("/" + "Users/", public_text) + self.assertNotIn("/private" + "/tmp", public_text) + def test_public_demo_calibration_generates_expected_value_report(self) -> None: with tempfile.TemporaryDirectory() as tmp: output = Path(tmp) / "reviewer-value-report.md"