Skip to content

Commit 28aaaca

Browse files
authored
feat: add AI workflow evidence demo
Adds a minimal deterministic local demo for the AI workflow evidence chain covering safe_pr_gate, agent_artifact_bundle, bundle validation, PR body rendering, AI workflow snapshots, and MCP context references.
1 parent c209a5b commit 28aaaca

2 files changed

Lines changed: 250 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env python3
2+
"""Run a deterministic local demo of the AI workflow evidence chain."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import sys
9+
from pathlib import Path
10+
from typing import Any
11+
12+
REPO_ROOT = Path(__file__).resolve().parents[1]
13+
if str(REPO_ROOT) not in sys.path:
14+
sys.path.insert(0, str(REPO_ROOT))
15+
16+
from scripts.agent_artifact_bundle import build_agent_artifact_bundle
17+
from scripts.ai_workflow_snapshot import build_ai_workflow_snapshot
18+
from scripts.pr_body_from_agent_bundle import render_pr_body_from_payload
19+
from scripts.safe_pr_gate import GateState, evaluate_gate
20+
from scripts.validate_agent_artifact_bundle import _load_json_object, validate_bundle_file
21+
22+
DEFAULT_DEMO_BUNDLE = REPO_ROOT / "artifacts" / "mcp_context_bundle_ref_example.json"
23+
24+
25+
def _parse_args(argv: list[str]) -> argparse.Namespace:
26+
parser = argparse.ArgumentParser(description="Run a deterministic local AI workflow evidence demo.")
27+
parser.add_argument("--bundle", type=Path, default=DEFAULT_DEMO_BUNDLE, help="Agent artifact bundle JSON path.")
28+
return parser.parse_args(argv)
29+
30+
31+
def _relative(path: Path) -> str:
32+
try:
33+
return path.resolve().relative_to(REPO_ROOT).as_posix()
34+
except ValueError:
35+
return path.as_posix()
36+
37+
38+
def _error_response(exc: RuntimeError) -> dict[str, Any]:
39+
return {
40+
"error": {
41+
"message": str(exc),
42+
"type": exc.__class__.__name__,
43+
},
44+
"ok": False,
45+
"result": "ERROR",
46+
}
47+
48+
49+
def _bundle_from_payload(payload: dict[str, Any]) -> dict[str, Any]:
50+
bundle = payload.get("bundle", payload)
51+
if not isinstance(bundle, dict):
52+
raise RuntimeError("demo bundle must contain a JSON object bundle")
53+
return bundle
54+
55+
56+
def _state_from_bundle(bundle: dict[str, Any]) -> GateState:
57+
safe_pr_gate = bundle.get("safe_pr_gate")
58+
if not isinstance(safe_pr_gate, dict):
59+
raise RuntimeError("demo bundle missing safe_pr_gate object")
60+
61+
branch = safe_pr_gate.get("branch")
62+
status_short = safe_pr_gate.get("status_short")
63+
changed_paths = safe_pr_gate.get("changed_paths")
64+
if not isinstance(branch, str):
65+
raise RuntimeError("demo bundle safe_pr_gate.branch must be a string")
66+
if not isinstance(status_short, list) or not all(isinstance(item, str) for item in status_short):
67+
raise RuntimeError("demo bundle safe_pr_gate.status_short must be a list of strings")
68+
if not isinstance(changed_paths, list) or not all(isinstance(item, str) for item in changed_paths):
69+
raise RuntimeError("demo bundle safe_pr_gate.changed_paths must be a list of strings")
70+
71+
return GateState(
72+
branch=branch,
73+
status_short=tuple(status_short),
74+
changed_paths=tuple(changed_paths),
75+
)
76+
77+
78+
def _validation_pairs(bundle: dict[str, Any]) -> tuple[list[str], list[str]]:
79+
validation_evidence = bundle.get("validation_evidence")
80+
if not isinstance(validation_evidence, list):
81+
raise RuntimeError("demo bundle validation_evidence must be a list")
82+
83+
commands: list[str] = []
84+
results: list[str] = []
85+
for index, entry in enumerate(validation_evidence):
86+
if not isinstance(entry, dict):
87+
raise RuntimeError(f"demo bundle validation_evidence[{index}] must be a JSON object")
88+
command = entry.get("command")
89+
result = entry.get("result")
90+
if not isinstance(command, str) or not isinstance(result, str):
91+
raise RuntimeError(f"demo bundle validation_evidence[{index}] must contain command and result strings")
92+
commands.append(command)
93+
results.append(result)
94+
return commands, results
95+
96+
97+
def _pr_body_summary(pr_body: str) -> dict[str, Any]:
98+
headings = [line.removeprefix("## ") for line in pr_body.splitlines() if line.startswith("## ")]
99+
return {
100+
"line_count": len(pr_body.splitlines()),
101+
"ok": True,
102+
"result": "PASS",
103+
"section_headings": headings,
104+
}
105+
106+
107+
def build_demo_summary(bundle_path: Path = DEFAULT_DEMO_BUNDLE) -> dict[str, Any]:
108+
payload = _load_json_object(bundle_path)
109+
bundle = _bundle_from_payload(payload)
110+
validation = validate_bundle_file(bundle_path)
111+
state = _state_from_bundle(bundle)
112+
commands, results = _validation_pairs(bundle)
113+
mcp_context_output_ref = bundle.get("mcp_context_output_ref")
114+
if mcp_context_output_ref is not None and not isinstance(mcp_context_output_ref, str):
115+
raise RuntimeError("demo bundle mcp_context_output_ref must be a string when present")
116+
117+
safe_pr_gate = evaluate_gate(state).to_dict()
118+
agent_artifact_bundle = build_agent_artifact_bundle(
119+
state,
120+
allow_main=True,
121+
validation_commands=commands,
122+
validation_results=results,
123+
mcp_context_output_ref=mcp_context_output_ref,
124+
)
125+
ai_workflow_snapshot = build_ai_workflow_snapshot(
126+
state,
127+
validation_commands=commands,
128+
validation_results=results,
129+
mcp_context_output_ref=mcp_context_output_ref,
130+
)
131+
pr_body = render_pr_body_from_payload(payload)
132+
133+
chain = {
134+
"agent_artifact_bundle": {
135+
"changed_files": agent_artifact_bundle["changed_files"],
136+
"ok": agent_artifact_bundle["ok"],
137+
"result": agent_artifact_bundle["result"],
138+
},
139+
"ai_workflow_snapshot": {
140+
"ok": ai_workflow_snapshot["ok"],
141+
"result": ai_workflow_snapshot["result"],
142+
},
143+
"pr_body_from_agent_bundle": _pr_body_summary(pr_body),
144+
"safe_pr_gate": {
145+
"ok": safe_pr_gate["ok"],
146+
"problems": safe_pr_gate["problems"],
147+
"result": safe_pr_gate["result"],
148+
},
149+
"validate_agent_artifact_bundle": {
150+
"issues": validation["issues"],
151+
"ok": validation["ok"],
152+
"result": validation["result"],
153+
},
154+
}
155+
ok = all(step["ok"] for step in chain.values())
156+
157+
summary: dict[str, Any] = {
158+
"chain": chain,
159+
"inputs": {
160+
"agent_artifact_bundle": _relative(bundle_path),
161+
},
162+
"ok": ok,
163+
"result": "PASS" if ok else "FAIL",
164+
"validation_evidence": bundle["validation_evidence"],
165+
}
166+
if mcp_context_output_ref is not None:
167+
summary["inputs"]["mcp_context_output_ref"] = mcp_context_output_ref
168+
return summary
169+
170+
171+
def _emit_json(payload: dict[str, Any]) -> None:
172+
sys.stdout.write(json.dumps(payload, separators=(",", ":"), sort_keys=True) + "\n")
173+
174+
175+
def main(argv: list[str] | None = None) -> int:
176+
args = _parse_args(sys.argv[1:] if argv is None else argv)
177+
try:
178+
summary = build_demo_summary(args.bundle)
179+
_emit_json(summary)
180+
return 0 if summary["ok"] else 1
181+
except RuntimeError as exc:
182+
_emit_json(_error_response(exc))
183+
return 1
184+
185+
186+
if __name__ == "__main__":
187+
raise SystemExit(main())
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
import json
4+
5+
import scripts.demo_ai_workflow_evidence as demo_ai_workflow_evidence
6+
7+
8+
def test_build_demo_summary_runs_committed_evidence_chain() -> None:
9+
summary = demo_ai_workflow_evidence.build_demo_summary()
10+
11+
assert summary["ok"] is True
12+
assert summary["result"] == "PASS"
13+
assert summary["inputs"] == {
14+
"agent_artifact_bundle": "artifacts/mcp_context_bundle_ref_example.json",
15+
"mcp_context_output_ref": "artifacts/mcp_context_layer_example.json",
16+
}
17+
assert summary["chain"]["safe_pr_gate"] == {
18+
"ok": True,
19+
"problems": [],
20+
"result": "PASS",
21+
}
22+
assert summary["chain"]["validate_agent_artifact_bundle"] == {
23+
"issues": [],
24+
"ok": True,
25+
"result": "PASS",
26+
}
27+
assert summary["chain"]["agent_artifact_bundle"]["ok"] is True
28+
assert summary["chain"]["ai_workflow_snapshot"] == {
29+
"ok": True,
30+
"result": "PASS",
31+
}
32+
assert summary["chain"]["pr_body_from_agent_bundle"]["section_headings"] == [
33+
"Summary",
34+
"Scope",
35+
"Validation",
36+
"Safety Gate",
37+
"Evidence",
38+
]
39+
40+
41+
def test_build_demo_summary_is_deterministic_and_lightweight() -> None:
42+
first = demo_ai_workflow_evidence.build_demo_summary()
43+
second = demo_ai_workflow_evidence.build_demo_summary()
44+
45+
first_json = json.dumps(first, separators=(",", ":"), sort_keys=True)
46+
second_json = json.dumps(second, separators=(",", ":"), sort_keys=True)
47+
48+
assert first_json == second_json
49+
assert "prompt_context" not in first_json
50+
assert "replay_payload" not in first_json
51+
assert "BEGIN PRIVATE KEY" not in first_json
52+
53+
54+
def test_main_emits_compact_deterministic_json(capsys) -> None:
55+
exit_code = demo_ai_workflow_evidence.main([])
56+
captured = capsys.readouterr()
57+
output = json.loads(captured.out)
58+
59+
assert exit_code == 0
60+
assert captured.err == ""
61+
assert captured.out == json.dumps(output, separators=(",", ":"), sort_keys=True) + "\n"
62+
assert output["ok"] is True
63+
assert output["result"] == "PASS"

0 commit comments

Comments
 (0)