diff --git a/.custodian-brief.md b/.custodian-brief.md index 0558be9..6700355 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -1,17 +1,16 @@ # Custodian Brief — guide-board -**Domain:** markitect -**Last synced:** 2026-05-15 11:34 UTC +**Domain:** markitect +**Last synced:** 2026-05-15 11:38 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams ### Assessment Operations Baseline -Progress: 2/6 done | workstream_id: `fc5b1573-91b2-4a19-b6a9-dd4d17057d9b` +Progress: 3/6 done | workstream_id: `fc5b1573-91b2-4a19-b6a9-dd4d17057d9b` **Open tasks:** -- · D2.3 - Run History And Result UX `ce18a2dc` - · D2.4 - Service Job Durability Contract `10e4003c` - · D2.5 - Container Smoke Acceptance `9e2e7fa7` - · D2.6 - External Extension Acceptance Path `65fbf1df` diff --git a/docs/ASSESSMENT-OPERATIONS.md b/docs/ASSESSMENT-OPERATIONS.md index 20a6622..ca6d61f 100644 --- a/docs/ASSESSMENT-OPERATIONS.md +++ b/docs/ASSESSMENT-OPERATIONS.md @@ -92,6 +92,12 @@ Use the retained run helpers for history: ```sh PYTHONPATH=src python3 -m guide_board runs list --runs-dir runs +PYTHONPATH=src python3 -m guide_board runs latest --runs-dir runs \ + --target sample-repository \ + --assessment sample-noop-assessment +PYTHONPATH=src python3 -m guide_board runs report --runs-dir runs \ + --target sample-repository \ + --assessment sample-noop-assessment PYTHONPATH=src python3 -m guide_board runs trend --runs-dir runs PYTHONPATH=src python3 -m guide_board runs gate --runs-dir runs ``` diff --git a/src/guide_board/cli.py b/src/guide_board/cli.py index e734458..c02d385 100644 --- a/src/guide_board/cli.py +++ b/src/guide_board/cli.py @@ -18,7 +18,12 @@ from guide_board.planning import ( validate_assessment_profile, validate_target_profile, ) -from guide_board.retention import build_trend_summary, list_retained_runs +from guide_board.retention import ( + build_trend_summary, + list_retained_runs, + retained_run_report_paths, + select_retained_run, +) from guide_board.schema import assert_valid from guide_board.service import build_server @@ -93,6 +98,17 @@ def build_parser() -> argparse.ArgumentParser: list_runs = runs_commands.add_parser("list", help="list retained run summaries") list_runs.add_argument("--runs-dir", type=Path) list_runs.set_defaults(func=cmd_runs_list) + latest_run = runs_commands.add_parser("latest", help="show the latest retained run") + latest_run.add_argument("--runs-dir", type=Path) + latest_run.add_argument("--target") + latest_run.add_argument("--assessment") + latest_run.set_defaults(func=cmd_runs_latest) + report_run = runs_commands.add_parser("report", help="show report paths for a retained run") + report_run.add_argument("--runs-dir", type=Path) + report_run.add_argument("--run-id") + report_run.add_argument("--target") + report_run.add_argument("--assessment") + report_run.set_defaults(func=cmd_runs_report) trend_runs = runs_commands.add_parser("trend", help="summarize retained run trends") trend_runs.add_argument("--runs-dir", type=Path) trend_runs.set_defaults(func=cmd_runs_trend) @@ -187,6 +203,39 @@ def cmd_runs_list(args: argparse.Namespace) -> dict[str, Any]: } +def cmd_runs_latest(args: argparse.Namespace) -> dict[str, Any]: + runs_dir = args.runs_dir or args.root / "runs" + run = select_retained_run( + runs_dir, + target_profile_ref=args.target, + assessment_profile_ref=args.assessment, + ) + return { + "runs_dir": str(runs_dir), + "selection": { + "target_profile_ref": args.target, + "assessment_profile_ref": args.assessment, + }, + "run": _run_with_report_paths(run) if run else None, + } + + +def cmd_runs_report(args: argparse.Namespace) -> dict[str, Any]: + runs_dir = args.runs_dir or args.root / "runs" + run = select_retained_run( + runs_dir, + run_id=args.run_id, + target_profile_ref=args.target, + assessment_profile_ref=args.assessment, + ) + if run is None: + raise ValueError("no retained run matched the requested selection") + return { + "runs_dir": str(runs_dir), + "run": _run_with_report_paths(run), + } + + def cmd_runs_trend(args: argparse.Namespace) -> dict[str, Any]: runs_dir = args.runs_dir or args.root / "runs" summary = build_trend_summary(runs_dir) @@ -224,3 +273,10 @@ def _display_path(root: Path, path: Path) -> str: return str(path.resolve().relative_to(root.resolve())) except ValueError: return str(path.resolve()) + + +def _run_with_report_paths(run: dict[str, Any]) -> dict[str, Any]: + return { + **run, + "paths": retained_run_report_paths(run), + } diff --git a/src/guide_board/retention.py b/src/guide_board/retention.py index 6b85493..ef01806 100644 --- a/src/guide_board/retention.py +++ b/src/guide_board/retention.py @@ -73,6 +73,47 @@ def list_retained_runs(runs_dir: Path) -> list[dict[str, Any]]: return sorted(summaries, key=lambda item: item.get("created_at", ""), reverse=True) +def select_retained_run( + runs_dir: Path, + run_id: str | None = None, + target_profile_ref: str | None = None, + assessment_profile_ref: str | None = None, +) -> dict[str, Any] | None: + """Return the exact or latest retained run matching the optional selection.""" + for run in list_retained_runs(runs_dir): + if run_id and run.get("run_id") != run_id: + continue + if target_profile_ref and run.get("target_profile_ref") != target_profile_ref: + continue + if assessment_profile_ref and run.get("assessment_profile_ref") != assessment_profile_ref: + continue + return run + return None + + +def retained_run_report_paths(run: dict[str, Any]) -> dict[str, str]: + """Return stable report paths for a retained run summary.""" + run_dir_value = run.get("run_dir") + if not isinstance(run_dir_value, str) or not run_dir_value: + raise ValueError("retained run summary is missing run_dir") + + run_dir = Path(run_dir_value) + paths: dict[str, str] = {} + report_refs = run.get("report_refs", []) + if isinstance(report_refs, list): + for raw_ref in report_refs: + if not isinstance(raw_ref, str) or not raw_ref: + continue + ref = Path(raw_ref) + key = ref.stem.replace("-", "_") + paths[key] = str(run_dir / ref) + + paths.setdefault("assessment_package", str(run_dir / "reports" / "assessment-package.json")) + paths.setdefault("report", str(run_dir / "reports" / "report.md")) + paths.setdefault("retention_summary", str(run_dir / "retention-summary.json")) + return dict(sorted(paths.items())) + + def build_trend_summary( runs_dir: Path, retained_runs: list[dict[str, Any]] | None = None, diff --git a/tests/test_core.py b/tests/test_core.py index 4df3ebd..0e3cdec 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -16,7 +16,12 @@ from guide_board.planning import ( validate_assessment_profile, validate_target_profile, ) -from guide_board.retention import build_trend_summary, list_retained_runs +from guide_board.retention import ( + build_trend_summary, + list_retained_runs, + retained_run_report_paths, + select_retained_run, +) from guide_board.schema import assert_valid from guide_board.service import ServiceHandle, start_service @@ -288,6 +293,19 @@ class CoreArchitectureTests(unittest.TestCase): self.assertEqual(gate["status"], "passed") self.assertEqual(gate["passed_groups"], 1) + latest = select_retained_run( + runs_dir, + target_profile_ref="sample-repository", + assessment_profile_ref="sample-noop-assessment", + ) + self.assertIsNotNone(latest) + assert latest is not None + self.assertEqual(latest["run_id"], "run-new") + self.assertEqual( + retained_run_report_paths(latest)["report"], + str(runs_dir / "run-new" / "reports" / "report.md"), + ) + missing_gate = evaluate_trend_gates( trend, target_profile_ref="missing-target", diff --git a/workplans/GUIDE-BOARD-WP-0002-assessment-operations-baseline.md b/workplans/GUIDE-BOARD-WP-0002-assessment-operations-baseline.md index d1ba945..19dc0fa 100644 --- a/workplans/GUIDE-BOARD-WP-0002-assessment-operations-baseline.md +++ b/workplans/GUIDE-BOARD-WP-0002-assessment-operations-baseline.md @@ -97,7 +97,7 @@ Progress: ```task id: GUIDE-BOARD-WP-0002-T003 -status: todo +status: done priority: medium state_hub_task_id: "ce18a2dc-7cc9-4fff-9272-5333b6118030" ``` @@ -109,6 +109,13 @@ Acceptance: report paths. - Preserve the existing run directory contract and retention summary model. +Progress: + +- Added `guide_board.retention.select_retained_run`. +- Added `guide_board.retention.retained_run_report_paths`. +- Added `guide-board runs latest` and `guide-board runs report`. +- Documented the new commands in `docs/ASSESSMENT-OPERATIONS.md`. + ## D2.4 - Service Job Durability Contract ```task