Upgrade meta skill governance reports
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_SOURCE = ROOT / "reports" / "review_annotations_input.json"
|
||||
DEFAULT_OUTPUT_JSON = ROOT / "reports" / "review_annotations.json"
|
||||
DEFAULT_OUTPUT_MD = ROOT / "reports" / "review_annotations.md"
|
||||
VALID_GATES = {
|
||||
"intent-canvas",
|
||||
"trigger-lab",
|
||||
"output-lab",
|
||||
"context-budget",
|
||||
"runtime-matrix",
|
||||
"trust-report",
|
||||
"permission-gates",
|
||||
"permission-runtime",
|
||||
"skill-atlas",
|
||||
"operations-loop",
|
||||
"review-waivers",
|
||||
"registry-audit",
|
||||
"release-notes",
|
||||
}
|
||||
VALID_SEVERITIES = {"info", "note", "warning", "blocker"}
|
||||
VALID_STATUSES = {"open", "resolved", "deferred"}
|
||||
|
||||
|
||||
def display_path(root: Path, path: Path) -> str:
|
||||
try:
|
||||
return str(path.resolve().relative_to(root.resolve()))
|
||||
except ValueError:
|
||||
try:
|
||||
return str(path.resolve().relative_to(ROOT.resolve()))
|
||||
except ValueError:
|
||||
return str(path.resolve())
|
||||
|
||||
|
||||
def load_json(path: Path) -> tuple[dict[str, Any], list[str]]:
|
||||
if not path.exists():
|
||||
return {"schema_version": "1.0", "annotations": []}, []
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
return {"schema_version": "1.0", "annotations": []}, [f"Invalid annotation JSON {display_path(ROOT, path)}: {exc}"]
|
||||
if not isinstance(payload, dict):
|
||||
return {"schema_version": "1.0", "annotations": []}, [f"Annotation file root must be an object: {display_path(ROOT, path)}"]
|
||||
if not isinstance(payload.get("annotations", []), list):
|
||||
return {"schema_version": "1.0", "annotations": []}, ["annotations must be a list"]
|
||||
return payload, []
|
||||
|
||||
|
||||
def template_payload() -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"review_contract": {
|
||||
"gate_key": "One Review Studio gate key, such as output-lab or trust-report.",
|
||||
"target_path": "Relative path to the source or report being annotated.",
|
||||
"line": "Optional positive integer line number.",
|
||||
"severity": "info | note | warning | blocker.",
|
||||
"status": "open | resolved | deferred.",
|
||||
"body": "Reviewer-visible annotation text.",
|
||||
},
|
||||
"annotations": [],
|
||||
}
|
||||
|
||||
|
||||
def annotation_id(annotation: dict[str, Any]) -> str:
|
||||
if annotation.get("id"):
|
||||
return str(annotation["id"])
|
||||
raw = "|".join(
|
||||
[
|
||||
str(annotation.get("gate_key", "")),
|
||||
str(annotation.get("target_path", "")),
|
||||
str(annotation.get("line", "")),
|
||||
str(annotation.get("body", "")),
|
||||
]
|
||||
)
|
||||
return "ann-" + hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
|
||||
def source_excerpt(path: Path, line: int | None) -> str:
|
||||
if line is None or not path.exists() or not path.is_file():
|
||||
return ""
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
except OSError:
|
||||
return ""
|
||||
if line < 1 or line > len(lines):
|
||||
return ""
|
||||
return lines[line - 1].strip()[:220]
|
||||
|
||||
|
||||
def normalize_annotation(skill_dir: Path, raw: dict[str, Any], index: int) -> tuple[dict[str, Any], list[str]]:
|
||||
failures: list[str] = []
|
||||
if not isinstance(raw, dict):
|
||||
return {}, [f"annotation #{index} must be an object"]
|
||||
|
||||
gate_key = str(raw.get("gate_key", "")).strip()
|
||||
if gate_key not in VALID_GATES:
|
||||
failures.append(f"{annotation_id(raw)}: invalid gate_key {gate_key!r}")
|
||||
|
||||
severity = str(raw.get("severity", "note")).strip().lower()
|
||||
if severity not in VALID_SEVERITIES:
|
||||
failures.append(f"{annotation_id(raw)}: invalid severity {severity!r}")
|
||||
|
||||
status = str(raw.get("status", "open")).strip().lower()
|
||||
if status not in VALID_STATUSES:
|
||||
failures.append(f"{annotation_id(raw)}: invalid status {status!r}")
|
||||
|
||||
body = str(raw.get("body", "")).strip()
|
||||
if not body:
|
||||
failures.append(f"{annotation_id(raw)}: body is required")
|
||||
|
||||
target_path = str(raw.get("target_path", "")).strip()
|
||||
if not target_path:
|
||||
failures.append(f"{annotation_id(raw)}: target_path is required")
|
||||
rel = Path(target_path)
|
||||
if rel.is_absolute():
|
||||
failures.append(f"{annotation_id(raw)}: target_path must be relative")
|
||||
target = skill_dir / "__invalid_absolute_path__"
|
||||
else:
|
||||
target = (skill_dir / rel).resolve()
|
||||
try:
|
||||
target.relative_to(skill_dir.resolve())
|
||||
except ValueError:
|
||||
failures.append(f"{annotation_id(raw)}: target_path escapes skill directory")
|
||||
target = skill_dir / "__invalid_escaped_path__"
|
||||
|
||||
line_value = raw.get("line", None)
|
||||
line: int | None
|
||||
if line_value is None or line_value == "":
|
||||
line = None
|
||||
else:
|
||||
try:
|
||||
line = int(line_value)
|
||||
except (TypeError, ValueError):
|
||||
line = None
|
||||
failures.append(f"{annotation_id(raw)}: line must be a positive integer")
|
||||
else:
|
||||
if line < 1:
|
||||
failures.append(f"{annotation_id(raw)}: line must be a positive integer")
|
||||
|
||||
normalized = {
|
||||
"id": annotation_id(raw),
|
||||
"gate_key": gate_key,
|
||||
"target_path": target_path,
|
||||
"line": line,
|
||||
"severity": severity,
|
||||
"status": status,
|
||||
"reviewer": str(raw.get("reviewer", "")).strip(),
|
||||
"created_at": str(raw.get("created_at", "")).strip(),
|
||||
"body": body,
|
||||
"suggested_action": str(raw.get("suggested_action", "")).strip(),
|
||||
"evidence": str(raw.get("evidence", "")).strip(),
|
||||
"target_exists": target.exists(),
|
||||
"source_excerpt": source_excerpt(target, line),
|
||||
}
|
||||
return normalized, failures
|
||||
|
||||
|
||||
def append_annotation(source: dict[str, Any], args: argparse.Namespace) -> dict[str, Any]:
|
||||
annotations = list(source.get("annotations", []))
|
||||
raw = {
|
||||
"id": args.annotation_id,
|
||||
"gate_key": args.gate_key,
|
||||
"target_path": args.target_path,
|
||||
"line": args.line,
|
||||
"severity": args.severity,
|
||||
"status": args.status,
|
||||
"reviewer": args.reviewer,
|
||||
"created_at": args.created_at or str(date.today()),
|
||||
"body": args.body,
|
||||
"suggested_action": args.suggested_action,
|
||||
"evidence": args.evidence,
|
||||
}
|
||||
annotations.append({key: value for key, value in raw.items() if value is not None and value != ""})
|
||||
updated = dict(source)
|
||||
updated["annotations"] = annotations
|
||||
updated.setdefault("schema_version", "1.0")
|
||||
return updated
|
||||
|
||||
|
||||
def build_summary(annotations: list[dict[str, Any]], failures: list[str]) -> dict[str, Any]:
|
||||
open_items = [item for item in annotations if item["status"] == "open"]
|
||||
resolved_items = [item for item in annotations if item["status"] == "resolved"]
|
||||
deferred_items = [item for item in annotations if item["status"] == "deferred"]
|
||||
open_blockers = [item for item in open_items if item["severity"] == "blocker"]
|
||||
open_warnings = [item for item in open_items if item["severity"] == "warning"]
|
||||
return {
|
||||
"annotation_count": len(annotations),
|
||||
"open_count": len(open_items),
|
||||
"resolved_count": len(resolved_items),
|
||||
"deferred_count": len(deferred_items),
|
||||
"open_blocker_count": len(open_blockers),
|
||||
"open_warning_count": len(open_warnings),
|
||||
"linked_gate_count": len({item["gate_key"] for item in annotations if item["gate_key"]}),
|
||||
"target_missing_count": sum(1 for item in annotations if not item["target_exists"]),
|
||||
"failure_count": len(failures),
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(payload: dict[str, Any]) -> str:
|
||||
summary = payload["summary"]
|
||||
lines = [
|
||||
"# Review Annotations",
|
||||
"",
|
||||
"This report renders reviewer annotations attached to Review Studio gates and source/report paths.",
|
||||
"",
|
||||
f"- Annotations: `{summary['annotation_count']}`",
|
||||
f"- Open: `{summary['open_count']}`",
|
||||
f"- Resolved: `{summary['resolved_count']}`",
|
||||
f"- Deferred: `{summary['deferred_count']}`",
|
||||
f"- Open blockers: `{summary['open_blocker_count']}`",
|
||||
f"- Open warnings: `{summary['open_warning_count']}`",
|
||||
"",
|
||||
]
|
||||
if not payload["annotations"]:
|
||||
lines.extend(["No reviewer annotations recorded yet.", ""])
|
||||
else:
|
||||
lines.extend(
|
||||
[
|
||||
"## Annotation Ledger",
|
||||
"",
|
||||
"| ID | Gate | Severity | Status | Target | Reviewer | Note |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for item in payload["annotations"]:
|
||||
line = f":{item['line']}" if item.get("line") else ""
|
||||
note = item["body"].replace("|", "\\|")
|
||||
lines.append(
|
||||
f"| {item['id']} | {item['gate_key']} | {item['severity']} | {item['status']} | "
|
||||
f"{item['target_path']}{line} | {item.get('reviewer', '')} | {note} |"
|
||||
)
|
||||
if item.get("source_excerpt"):
|
||||
excerpt = str(item["source_excerpt"]).replace("|", "\\|")
|
||||
lines.append(f"| | | | | | excerpt | `{excerpt}` |")
|
||||
if payload.get("failures"):
|
||||
lines.extend(["", "## Failures", ""])
|
||||
for failure in payload["failures"]:
|
||||
lines.append(f"- {failure}")
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Review Rule",
|
||||
"",
|
||||
"- Use annotations for reviewer comments tied to a gate or source line.",
|
||||
"- Use waivers only for explicit acceptance of warning-level release risk.",
|
||||
"- Open blocker annotations should block a release decision until resolved or deferred with rationale.",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines).strip() + "\n"
|
||||
|
||||
|
||||
def render_review_annotations(
|
||||
skill_dir: Path,
|
||||
source_json: Path,
|
||||
output_json: Path,
|
||||
output_md: Path,
|
||||
args: argparse.Namespace,
|
||||
) -> dict[str, Any]:
|
||||
skill_dir = skill_dir.resolve()
|
||||
source, failures = load_json(source_json)
|
||||
template_written = False
|
||||
should_write_source = False
|
||||
if args.write_template and not source_json.exists():
|
||||
source_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
source_json.write_text(json.dumps(template_payload(), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
source = template_payload()
|
||||
template_written = True
|
||||
if args.add_annotation:
|
||||
source = append_annotation(source, args)
|
||||
should_write_source = True
|
||||
|
||||
annotations: list[dict[str, Any]] = []
|
||||
seen = set()
|
||||
for index, raw in enumerate(source.get("annotations", []), start=1):
|
||||
normalized, item_failures = normalize_annotation(skill_dir, raw, index)
|
||||
failures.extend(item_failures)
|
||||
if normalized:
|
||||
if normalized["id"] in seen:
|
||||
failures.append(f"{normalized['id']}: duplicate annotation id")
|
||||
seen.add(normalized["id"])
|
||||
annotations.append(normalized)
|
||||
|
||||
summary = build_summary(annotations, failures)
|
||||
if should_write_source and not failures:
|
||||
source_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
source_json.write_text(json.dumps(source, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
payload = {
|
||||
"schema_version": "1.0",
|
||||
"ok": not failures,
|
||||
"skill_dir": display_path(ROOT, skill_dir),
|
||||
"source": display_path(skill_dir, source_json),
|
||||
"summary": summary,
|
||||
"annotations": annotations,
|
||||
"failures": failures,
|
||||
"template_written": template_written,
|
||||
"artifacts": {
|
||||
"json": display_path(skill_dir, output_json),
|
||||
"markdown": display_path(skill_dir, output_md),
|
||||
},
|
||||
}
|
||||
output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_md.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
output_md.write_text(render_markdown(payload), encoding="utf-8")
|
||||
return payload
|
||||
|
||||
|
||||
def render_report(
|
||||
skill_dir: Path,
|
||||
annotations_json: Path | None = None,
|
||||
output_json: Path | None = None,
|
||||
output_md: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
skill_dir = skill_dir.resolve()
|
||||
args = argparse.Namespace(
|
||||
write_template=False,
|
||||
add_annotation=False,
|
||||
annotation_id=None,
|
||||
gate_key=None,
|
||||
target_path=None,
|
||||
line=None,
|
||||
severity="note",
|
||||
status="open",
|
||||
reviewer=None,
|
||||
created_at=None,
|
||||
body=None,
|
||||
suggested_action=None,
|
||||
evidence=None,
|
||||
)
|
||||
return render_review_annotations(
|
||||
skill_dir,
|
||||
annotations_json or skill_dir / "reports" / "review_annotations_input.json",
|
||||
output_json or skill_dir / "reports" / "review_annotations.json",
|
||||
output_md or skill_dir / "reports" / "review_annotations.md",
|
||||
args,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Render reviewer annotations for Review Studio gates and source paths.")
|
||||
parser.add_argument("skill_dir", nargs="?", default=".")
|
||||
parser.add_argument("--annotations-json")
|
||||
parser.add_argument("--output-json")
|
||||
parser.add_argument("--output-md")
|
||||
parser.add_argument("--write-template", action="store_true")
|
||||
parser.add_argument("--add-annotation", action="store_true")
|
||||
parser.add_argument("--annotation-id")
|
||||
parser.add_argument("--gate-key", choices=sorted(VALID_GATES))
|
||||
parser.add_argument("--target-path")
|
||||
parser.add_argument("--line", type=int)
|
||||
parser.add_argument("--severity", choices=sorted(VALID_SEVERITIES), default="note")
|
||||
parser.add_argument("--status", choices=sorted(VALID_STATUSES), default="open")
|
||||
parser.add_argument("--reviewer")
|
||||
parser.add_argument("--created-at")
|
||||
parser.add_argument("--body")
|
||||
parser.add_argument("--suggested-action")
|
||||
parser.add_argument("--evidence")
|
||||
args = parser.parse_args()
|
||||
|
||||
skill_dir = Path(args.skill_dir).resolve()
|
||||
annotations_json = Path(args.annotations_json).resolve() if args.annotations_json else skill_dir / "reports" / "review_annotations_input.json"
|
||||
output_json = Path(args.output_json).resolve() if args.output_json else skill_dir / "reports" / "review_annotations.json"
|
||||
output_md = Path(args.output_md).resolve() if args.output_md else skill_dir / "reports" / "review_annotations.md"
|
||||
|
||||
if args.add_annotation:
|
||||
missing = [name for name in ("gate_key", "target_path", "body") if not getattr(args, name)]
|
||||
if missing:
|
||||
payload = {
|
||||
"schema_version": "1.0",
|
||||
"ok": False,
|
||||
"skill_dir": display_path(ROOT, skill_dir),
|
||||
"source": display_path(skill_dir, annotations_json),
|
||||
"summary": build_summary([], missing),
|
||||
"annotations": [],
|
||||
"failures": [f"--add-annotation requires {', '.join(missing)}"],
|
||||
"template_written": False,
|
||||
"artifacts": {
|
||||
"json": display_path(skill_dir, output_json),
|
||||
"markdown": display_path(skill_dir, output_md),
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
raise SystemExit(2)
|
||||
|
||||
payload = render_review_annotations(skill_dir, annotations_json, output_json, output_md, args)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
raise SystemExit(0 if payload["ok"] else 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user