generated from coulomb/repo-seed
retention and run history
This commit is contained in:
@@ -22,6 +22,7 @@ PYTHONPATH=src python3 -m guide_board plan \
|
||||
PYTHONPATH=src python3 -m guide_board run \
|
||||
--target profiles/targets/sample-repository.json \
|
||||
--assessment profiles/assessments/sample-noop.json
|
||||
PYTHONPATH=src python3 -m guide_board runs list
|
||||
PYTHONPATH=src python3 -m unittest discover -s tests
|
||||
```
|
||||
|
||||
|
||||
@@ -691,6 +691,7 @@ Each run should be reproducible from captured metadata where possible.
|
||||
```text
|
||||
runs/<run-id>/
|
||||
run.json
|
||||
retention-summary.json
|
||||
plan.json
|
||||
sources.lock.json
|
||||
target-profile.snapshot.json
|
||||
|
||||
@@ -17,6 +17,7 @@ from guide_board.planning import (
|
||||
validate_assessment_profile,
|
||||
validate_target_profile,
|
||||
)
|
||||
from guide_board.retention import list_retained_runs
|
||||
from guide_board.schema import assert_valid
|
||||
|
||||
|
||||
@@ -74,6 +75,12 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
run.add_argument("--output-dir", type=Path)
|
||||
run.set_defaults(func=cmd_run)
|
||||
|
||||
runs = subcommands.add_parser("runs", help="run history operations")
|
||||
runs_commands = runs.add_subparsers(required=True)
|
||||
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)
|
||||
|
||||
schema = subcommands.add_parser("schema", help="schema validation")
|
||||
schema.add_argument("schema_name")
|
||||
schema.add_argument("path", type=Path)
|
||||
@@ -128,6 +135,14 @@ def cmd_run(args: argparse.Namespace) -> dict[str, Any]:
|
||||
return run_assessment(args.root, args.target, args.assessment, args.output_dir)
|
||||
|
||||
|
||||
def cmd_runs_list(args: argparse.Namespace) -> dict[str, Any]:
|
||||
runs_dir = args.runs_dir or args.root / "runs"
|
||||
return {
|
||||
"runs_dir": str(runs_dir),
|
||||
"runs": list_retained_runs(runs_dir),
|
||||
}
|
||||
|
||||
|
||||
def cmd_schema_validate(args: argparse.Namespace) -> dict[str, Any]:
|
||||
document = load_json(args.path)
|
||||
assert_valid(document, args.schema_name)
|
||||
|
||||
@@ -12,6 +12,7 @@ from guide_board.io import write_json
|
||||
from guide_board.mapping import build_mapping_records, summarize_mappings
|
||||
from guide_board.planning import build_run_plan
|
||||
from guide_board.policy import apply_policy
|
||||
from guide_board.retention import build_retention_summary
|
||||
from guide_board.runners import run_step
|
||||
from guide_board.schema import assert_valid
|
||||
|
||||
@@ -61,6 +62,8 @@ def run_assessment(
|
||||
"target_profile_ref": plan["target_profile_snapshot"]["id"],
|
||||
"assessment_profile_ref": plan["assessment_profile_snapshot"]["id"],
|
||||
}
|
||||
retention_summary = build_retention_summary(run_metadata, plan, assessment_package)
|
||||
assert_valid(retention_summary, "retention-summary")
|
||||
|
||||
_write_run_directory(
|
||||
run_dir,
|
||||
@@ -70,6 +73,7 @@ def run_assessment(
|
||||
findings,
|
||||
mapping_records,
|
||||
assessment_package,
|
||||
retention_summary,
|
||||
)
|
||||
return {
|
||||
"status": run_metadata["status"],
|
||||
@@ -77,6 +81,7 @@ def run_assessment(
|
||||
"run_dir": str(run_dir),
|
||||
"assessment_package": str(run_dir / "reports" / "assessment-package.json"),
|
||||
"report": str(run_dir / "reports" / "report.md"),
|
||||
"retention_summary": str(run_dir / "retention-summary.json"),
|
||||
}
|
||||
|
||||
|
||||
@@ -285,8 +290,10 @@ def _write_run_directory(
|
||||
findings: list[dict[str, Any]],
|
||||
mapping_records: list[dict[str, Any]],
|
||||
assessment_package: dict[str, Any],
|
||||
retention_summary: dict[str, Any],
|
||||
) -> None:
|
||||
write_json(run_dir / "run.json", run_metadata)
|
||||
write_json(run_dir / "retention-summary.json", retention_summary)
|
||||
write_json(run_dir / "plan.json", plan)
|
||||
write_json(run_dir / "sources.lock.json", plan["source_lock"])
|
||||
write_json(run_dir / "target-profile.snapshot.json", plan["target_profile_snapshot"])
|
||||
|
||||
99
src/guide_board/retention.py
Normal file
99
src/guide_board/retention.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Retention summaries and run history helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from guide_board.io import load_json
|
||||
|
||||
|
||||
def build_retention_summary(
|
||||
run_metadata: dict[str, Any],
|
||||
plan: dict[str, Any],
|
||||
assessment_package: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
artifact_manifest = assessment_package.get("artifact_manifest", [])
|
||||
retention_class_counts = Counter(
|
||||
artifact.get("retention_class", "unknown")
|
||||
for artifact in artifact_manifest
|
||||
if isinstance(artifact, dict)
|
||||
)
|
||||
policy_summary = assessment_package.get("policy_summary", {})
|
||||
findings = assessment_package.get("findings", [])
|
||||
|
||||
return {
|
||||
"id": f"retention-summary:{run_metadata['id']}",
|
||||
"run_id": run_metadata["id"],
|
||||
"target_profile_ref": run_metadata["target_profile_ref"],
|
||||
"assessment_profile_ref": run_metadata["assessment_profile_ref"],
|
||||
"created_at": run_metadata["created_at"],
|
||||
"summary": {
|
||||
"status": run_metadata["status"],
|
||||
"evidence_results": assessment_package.get("summary", {}),
|
||||
"finding_count": len(findings),
|
||||
"unexpected_findings": policy_summary.get("unexpected_findings", 0),
|
||||
"expected_findings": sum(1 for finding in findings if finding.get("expected")),
|
||||
"waived_findings": sum(1 for finding in findings if finding.get("waiver_ref")),
|
||||
"mapping_target_count": len(
|
||||
assessment_package.get("mapping_summary", {}).get("targets", [])
|
||||
),
|
||||
"artifact_count": len(artifact_manifest),
|
||||
},
|
||||
"report_refs": [
|
||||
"reports/assessment-package.json",
|
||||
"reports/report.md",
|
||||
],
|
||||
"artifact_retention": {
|
||||
"policy": plan["assessment_profile_snapshot"].get("retention_policy", {}),
|
||||
"output_artifact_retention": plan["assessment_profile_snapshot"]
|
||||
.get("output_policy", {})
|
||||
.get("artifact_retention"),
|
||||
"retention_class_counts": dict(sorted(retention_class_counts.items())),
|
||||
"raw_artifact_count": retention_class_counts.get("raw", 0),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def list_retained_runs(runs_dir: Path) -> list[dict[str, Any]]:
|
||||
if not runs_dir.exists():
|
||||
return []
|
||||
|
||||
summaries = []
|
||||
for run_dir in sorted(path for path in runs_dir.iterdir() if path.is_dir()):
|
||||
try:
|
||||
summary = _summary_for_run_dir(run_dir)
|
||||
except OSError:
|
||||
continue
|
||||
if summary is not None:
|
||||
summaries.append(summary)
|
||||
|
||||
return sorted(summaries, key=lambda item: item.get("created_at", ""), reverse=True)
|
||||
|
||||
|
||||
def _summary_for_run_dir(run_dir: Path) -> dict[str, Any] | None:
|
||||
summary_path = run_dir / "retention-summary.json"
|
||||
if summary_path.exists():
|
||||
summary = load_json(summary_path)
|
||||
summary["run_dir"] = str(run_dir)
|
||||
return summary
|
||||
|
||||
metadata_path = run_dir / "run.json"
|
||||
if not metadata_path.exists():
|
||||
return None
|
||||
|
||||
metadata = load_json(metadata_path)
|
||||
return {
|
||||
"id": f"retention-summary:{metadata.get('id', run_dir.name)}",
|
||||
"run_id": metadata.get("id", run_dir.name),
|
||||
"run_dir": str(run_dir),
|
||||
"target_profile_ref": metadata.get("target_profile_ref"),
|
||||
"assessment_profile_ref": metadata.get("assessment_profile_ref"),
|
||||
"created_at": metadata.get("created_at"),
|
||||
"summary": {
|
||||
"status": metadata.get("status", "unknown"),
|
||||
},
|
||||
"report_refs": [],
|
||||
"artifact_retention": {},
|
||||
}
|
||||
@@ -14,6 +14,7 @@ from guide_board.planning import (
|
||||
validate_assessment_profile,
|
||||
validate_target_profile,
|
||||
)
|
||||
from guide_board.retention import list_retained_runs
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -78,9 +79,27 @@ class CoreArchitectureTests(unittest.TestCase):
|
||||
run_dir = Path(result["run_dir"])
|
||||
self.assertEqual(result["status"], "completed")
|
||||
self.assertTrue((run_dir / "run.json").exists())
|
||||
self.assertTrue((run_dir / "retention-summary.json").exists())
|
||||
self.assertTrue((run_dir / "normalized" / "evidence.json").exists())
|
||||
self.assertTrue((run_dir / "reports" / "assessment-package.json").exists())
|
||||
self.assertTrue((run_dir / "reports" / "report.md").exists())
|
||||
retention = json.loads(
|
||||
(run_dir / "retention-summary.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(
|
||||
result["retention_summary"],
|
||||
str(run_dir / "retention-summary.json"),
|
||||
)
|
||||
self.assertEqual(retention["summary"]["status"], "completed")
|
||||
self.assertEqual(retention["summary"]["artifact_count"], 0)
|
||||
self.assertEqual(
|
||||
retention["artifact_retention"]["policy"],
|
||||
{"raw_artifact_days": 0, "summary_days": 365},
|
||||
)
|
||||
self.assertEqual(
|
||||
[run["run_id"] for run in list_retained_runs(Path(temporary_directory))],
|
||||
[result["run_id"]],
|
||||
)
|
||||
mappings = json.loads(
|
||||
(run_dir / "normalized" / "mappings.json").read_text(encoding="utf-8")
|
||||
)["mappings"]
|
||||
|
||||
Reference in New Issue
Block a user