xrefkit 0.4.10__tar.gz → 0.4.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xrefkit-0.4.10/xrefkit.egg-info → xrefkit-0.4.11}/PKG-INFO +1 -1
- {xrefkit-0.4.10 → xrefkit-0.4.11}/pyproject.toml +1 -1
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_dashboard.py +30 -0
- xrefkit-0.4.11/tests/test_decision_trace.py +301 -0
- xrefkit-0.4.11/tests/test_wbs.py +43 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/__init__.py +1 -1
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/cli.py +2 -1
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/dashboard.py +82 -0
- xrefkit-0.4.11/xrefkit/decision_trace.py +801 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/catalog.py +12 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp_tools.py +1 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/operations_cli.py +9 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/skillrun.py +96 -0
- xrefkit-0.4.11/xrefkit/wbs.py +159 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11/xrefkit.egg-info}/PKG-INFO +1 -1
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit.egg-info/SOURCES.txt +4 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/LICENSE +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/README.md +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/setup.cfg +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_base_sync_ownership.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_boundary_analysis.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_brownfield_file_editing_protocol.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_calibration_lint.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_check_skill_knowledge_xids.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_cli.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_collect_analyzer_sarif.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_convert_to_xrefkit_skill.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_cs_scope_probe.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_csharp_commonality.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_csharp_naming_profile.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_ctx.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_cutover_readiness.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_error_policy_audit.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_error_policy_locator.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_fm_multiroot.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_gate.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_goal_desired_state.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_host_precheck.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_human_evaluation.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_instruction_workflow.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_knowledge_relations_validator.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_mcp_setup.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_ownership.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_packmeta.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_project_quality_baseline.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_resource_provider.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_runtime_contracts.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_sarif_to_locator.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_skill_runtime_audit.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_skillmeta.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_skills_sync.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_structure_catalog.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_xref.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_xrefkit_instance.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_xrefkit_tools.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_xrefkit_v2_discovery.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_xrefkit_v2_models.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/tests/test_xrefkit_v2_pipeline.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/__main__.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/boundary_analysis.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/catalog_cli.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/contracts.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/ctx.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/discovery.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/gate.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/goalstate.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/hashing.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/host_precheck.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/import_skill.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/instance.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/loaders.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/__init__.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/audit.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/bootstrap.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/cli.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/client_cache.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/client_flow.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/context_registry.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/context_token.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/contracts.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/dist.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/ownership.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/repository.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/schemas.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/server.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/setup.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/mcp/startup_contract_pack.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/__init__.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/common.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/effective_bundle.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/human_evaluation.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/local_manifest.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/package_manifest.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/run_log.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/server_config.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/models/skill_definition.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/ownership.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/packmeta.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/registry.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resolver.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resource_provider.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resources/base/contracts.json +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resources/base/current.json +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/resources/base/model_body.md +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/runlog.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/skillmeta.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/skills_sync.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/structure_catalog.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/tools/__init__.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/tools/__main__.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/v2_cli.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/workspace.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit/xref.py +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit.egg-info/dependency_links.txt +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit.egg-info/entry_points.txt +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit.egg-info/requires.txt +0 -0
- {xrefkit-0.4.10 → xrefkit-0.4.11}/xrefkit.egg-info/top_level.txt +0 -0
|
@@ -628,3 +628,33 @@ class DashboardTests(unittest.TestCase):
|
|
|
628
628
|
self.assertIn("<script>alert(1)</script>", html)
|
|
629
629
|
self.assertIn("<script>alert(2)</script>", html)
|
|
630
630
|
self.assertNotIn("<script>alert(1)", html)
|
|
631
|
+
|
|
632
|
+
def test_dashboard_includes_decision_trace_panel_and_records(self) -> None:
|
|
633
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
634
|
+
root = Path(tmp)
|
|
635
|
+
ledger = root / "work" / "decision-trace" / "events.jsonl"
|
|
636
|
+
ledger.parent.mkdir(parents=True, exist_ok=True)
|
|
637
|
+
ledger.write_text(
|
|
638
|
+
json.dumps({
|
|
639
|
+
"recorded_by": "ai_protocol",
|
|
640
|
+
"event_id": "DEC-001",
|
|
641
|
+
"event_type": "decision-change",
|
|
642
|
+
"status": "provisional",
|
|
643
|
+
"resolution": None,
|
|
644
|
+
"reason": "try Y",
|
|
645
|
+
"depends_on": [],
|
|
646
|
+
"branch": "hypothesis/decision-Y",
|
|
647
|
+
}) + "\n",
|
|
648
|
+
encoding="utf-8",
|
|
649
|
+
)
|
|
650
|
+
payload = build_payload(root, root / "work" / "sessions")
|
|
651
|
+
html = _html_page(payload)
|
|
652
|
+
|
|
653
|
+
self.assertEqual(payload["decision_trace"]["summary"]["events"], 1)
|
|
654
|
+
self.assertEqual(payload["decision_trace"]["summary"]["groups"], {"decision": 1})
|
|
655
|
+
self.assertIn('data-panel="decision-trace"', html)
|
|
656
|
+
self.assertIn('id="decision-trace"', html)
|
|
657
|
+
self.assertIn("DEC-001", html)
|
|
658
|
+
self.assertIn("hypothesis/decision-Y", html)
|
|
659
|
+
self.assertIn("Dependency graph (Mermaid)", html)
|
|
660
|
+
self.assertIn("Impact groups", html)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import argparse
|
|
3
|
+
import subprocess
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from xrefkit.decision_trace import main
|
|
7
|
+
from xrefkit.skillrun import run_workflow_instruction
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_event_impact_and_graph(tmp_path, capsys):
|
|
11
|
+
assert main([
|
|
12
|
+
"event", "--root", str(tmp_path), "--event-id", "DEC-1",
|
|
13
|
+
"--event-type", "decision-change", "--reason", "initial decision",
|
|
14
|
+
]) == 0
|
|
15
|
+
assert main([
|
|
16
|
+
"event", "--root", str(tmp_path), "--event-id", "EVAL-1",
|
|
17
|
+
"--event-type", "evaluation", "--reason", "evaluate provisional result",
|
|
18
|
+
"--depends-on", "DEC-1",
|
|
19
|
+
]) == 0
|
|
20
|
+
capsys.readouterr()
|
|
21
|
+
|
|
22
|
+
assert main(["impact", "--root", str(tmp_path), "--event-id", "DEC-1", "--json"]) == 0
|
|
23
|
+
impact = json.loads(capsys.readouterr().out)
|
|
24
|
+
assert impact["direct_impact"] == ["EVAL-1"]
|
|
25
|
+
assert impact["needs_human_review"] == ["EVAL-1"]
|
|
26
|
+
assert impact["summary"]["groups"] == {"evaluation": 1}
|
|
27
|
+
assert impact["groups"]["evaluation"][0]["review_required"] is True
|
|
28
|
+
|
|
29
|
+
assert main(["graph", "--root", str(tmp_path)]) == 0
|
|
30
|
+
graph = capsys.readouterr().out
|
|
31
|
+
assert "DEC-1<br/>provisional" in graph
|
|
32
|
+
assert "EVAL-1<br/>provisional" in graph
|
|
33
|
+
assert "-->" in graph
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_duplicate_event_is_rejected(tmp_path):
|
|
37
|
+
args = [
|
|
38
|
+
"event", "--root", str(tmp_path), "--event-id", "DEC-1",
|
|
39
|
+
"--event-type", "decision-note", "--reason", "record",
|
|
40
|
+
]
|
|
41
|
+
assert main(args) == 0
|
|
42
|
+
assert main(args) == 2
|
|
43
|
+
assert len((tmp_path / "work" / "decision-trace" / "events.jsonl").read_text(encoding="utf-8").splitlines()) == 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_checkpoint_commits_manifest_and_creates_tag(tmp_path):
|
|
47
|
+
def git(*args):
|
|
48
|
+
return subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True)
|
|
49
|
+
|
|
50
|
+
git("init")
|
|
51
|
+
git("config", "user.email", "test@example.invalid")
|
|
52
|
+
git("config", "user.name", "Decision Trace Test")
|
|
53
|
+
(tmp_path / "README.md").write_text("base\n", encoding="utf-8")
|
|
54
|
+
git("add", "README.md")
|
|
55
|
+
git("commit", "-m", "initial")
|
|
56
|
+
|
|
57
|
+
assert main([
|
|
58
|
+
"checkpoint", "--root", str(tmp_path), "--checkpoint-id", "CP-1",
|
|
59
|
+
"--purpose", "before AI run", "--json",
|
|
60
|
+
]) == 0
|
|
61
|
+
manifest = tmp_path / "work" / "decision-trace" / "checkpoints" / "CP-1.json"
|
|
62
|
+
assert manifest.exists()
|
|
63
|
+
assert json.loads(manifest.read_text(encoding="utf-8"))["git_commit"]
|
|
64
|
+
assert git("rev-parse", "checkpoint/CP-1^{}").stdout.strip() == git("rev-parse", "HEAD").stdout.strip()
|
|
65
|
+
assert "checkpoint: CP-1" in git("log", "-1", "--pretty=%s").stdout
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_checkpoint_stops_on_dirty_worktree(tmp_path):
|
|
69
|
+
subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)
|
|
70
|
+
(tmp_path / "README.md").write_text("uncommitted\n", encoding="utf-8")
|
|
71
|
+
assert main([
|
|
72
|
+
"checkpoint", "--root", str(tmp_path), "--checkpoint-id", "CP-1",
|
|
73
|
+
"--purpose", "before AI run",
|
|
74
|
+
]) == 2
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_branch_is_created_from_fixed_ref(tmp_path):
|
|
78
|
+
def git(*args):
|
|
79
|
+
return subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True)
|
|
80
|
+
|
|
81
|
+
git("init")
|
|
82
|
+
git("config", "user.email", "test@example.invalid")
|
|
83
|
+
git("config", "user.name", "Decision Trace Test")
|
|
84
|
+
(tmp_path / "README.md").write_text("base\n", encoding="utf-8")
|
|
85
|
+
git("add", "README.md")
|
|
86
|
+
git("commit", "-m", "initial")
|
|
87
|
+
base = git("rev-parse", "HEAD").stdout.strip()
|
|
88
|
+
|
|
89
|
+
assert main([
|
|
90
|
+
"branch", "--root", str(tmp_path), "--branch", "hypothesis/decision-Y",
|
|
91
|
+
"--from-ref", "HEAD", "--purpose", "provisional decision Y", "--json",
|
|
92
|
+
]) == 0
|
|
93
|
+
assert git("rev-parse", "hypothesis/decision-Y").stdout.strip() == base
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_branch_delete_preserves_disposal_adr(tmp_path):
|
|
97
|
+
def git(*args):
|
|
98
|
+
return subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True)
|
|
99
|
+
|
|
100
|
+
git("init")
|
|
101
|
+
git("config", "user.email", "test@example.invalid")
|
|
102
|
+
git("config", "user.name", "Decision Trace Test")
|
|
103
|
+
(tmp_path / "README.md").write_text("base\n", encoding="utf-8")
|
|
104
|
+
git("add", "README.md")
|
|
105
|
+
git("commit", "-m", "initial")
|
|
106
|
+
git("branch", "hypothesis/decision-Y")
|
|
107
|
+
|
|
108
|
+
assert main([
|
|
109
|
+
"branch-delete", "--root", str(tmp_path), "--branch", "hypothesis/decision-Y",
|
|
110
|
+
"--event-id", "ADR-002", "--reason", "performance evaluation failed", "--force", "--json",
|
|
111
|
+
]) == 0
|
|
112
|
+
assert git("branch", "--list", "hypothesis/decision-Y").stdout.strip() == ""
|
|
113
|
+
events = [
|
|
114
|
+
json.loads(line)
|
|
115
|
+
for line in (tmp_path / "work" / "decision-trace" / "events.jsonl").read_text(encoding="utf-8").splitlines()
|
|
116
|
+
]
|
|
117
|
+
assert [event["status"] for event in events] == ["disposal-requested", "deleted"]
|
|
118
|
+
assert events[1]["depends_on"] == ["ADR-002"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_resolution_is_a_new_final_event(tmp_path):
|
|
122
|
+
assert main([
|
|
123
|
+
"event", "--root", str(tmp_path), "--event-id", "DEC-001",
|
|
124
|
+
"--event-type", "decision-change", "--reason", "try Y",
|
|
125
|
+
]) == 0
|
|
126
|
+
assert main([
|
|
127
|
+
"resolve", "--root", str(tmp_path), "--event-id", "RES-001",
|
|
128
|
+
"--target-event-id", "DEC-001", "--resolution", "rejected",
|
|
129
|
+
"--reason", "evaluation failed", "--json",
|
|
130
|
+
]) == 0
|
|
131
|
+
events = [
|
|
132
|
+
json.loads(line)
|
|
133
|
+
for line in (tmp_path / "work" / "decision-trace" / "events.jsonl").read_text(encoding="utf-8").splitlines()
|
|
134
|
+
]
|
|
135
|
+
assert events[0]["status"] == "provisional"
|
|
136
|
+
assert events[0]["resolution"] is None
|
|
137
|
+
assert events[1]["event_type"] == "resolution"
|
|
138
|
+
assert events[1]["resolution"] == "rejected"
|
|
139
|
+
assert events[1]["target_event_id"] == "DEC-001"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_return_check_and_execute_checkpoint_before_branching(tmp_path):
|
|
143
|
+
def git(*args):
|
|
144
|
+
return subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True)
|
|
145
|
+
|
|
146
|
+
git("init")
|
|
147
|
+
git("config", "user.email", "test@example.invalid")
|
|
148
|
+
git("config", "user.name", "Decision Trace Test")
|
|
149
|
+
(tmp_path / "README.md").write_text("base\n", encoding="utf-8")
|
|
150
|
+
git("add", "README.md")
|
|
151
|
+
git("commit", "-m", "initial")
|
|
152
|
+
assert main([
|
|
153
|
+
"event", "--root", str(tmp_path), "--event-id", "DEC-001",
|
|
154
|
+
"--event-type", "decision-note", "--reason", "historical decision",
|
|
155
|
+
]) == 0
|
|
156
|
+
|
|
157
|
+
assert main([
|
|
158
|
+
"return-check", "--root", str(tmp_path), "--target-event-id", "DEC-001",
|
|
159
|
+
"--to-ref", "HEAD", "--json",
|
|
160
|
+
]) == 0
|
|
161
|
+
assert main([
|
|
162
|
+
"return-execute", "--root", str(tmp_path), "--event-id", "RET-001",
|
|
163
|
+
"--target-event-id", "DEC-001", "--to-ref", "HEAD",
|
|
164
|
+
"--branch", "hypothesis/resume-DEC-001", "--checkpoint-id", "CP-RETURN-001",
|
|
165
|
+
"--reason", "reconsider historical decision", "--confirmed", "--json",
|
|
166
|
+
]) == 0
|
|
167
|
+
assert git("branch", "--list", "hypothesis/resume-DEC-001").stdout.strip()
|
|
168
|
+
events = [
|
|
169
|
+
json.loads(line)
|
|
170
|
+
for line in (tmp_path / "work" / "decision-trace" / "events.jsonl").read_text(encoding="utf-8").splitlines()
|
|
171
|
+
]
|
|
172
|
+
assert events[-1]["event_type"] == "return"
|
|
173
|
+
assert events[-1]["status"] == "provisional"
|
|
174
|
+
assert events[-1]["human_decision"] == "confirmed"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_current_only_context_contains_only_dependency_lineage(tmp_path, capsys):
|
|
178
|
+
for event_id, event_type, reason, dependencies in [
|
|
179
|
+
("DEC-001", "decision-note", "old decision", []),
|
|
180
|
+
("DEC-002", "decision-change", "new decision", ["DEC-001"]),
|
|
181
|
+
("EVAL-001", "evaluation", "evaluate new decision", ["DEC-002"]),
|
|
182
|
+
("DEC-OLD", "decision-note", "unrelated old path", []),
|
|
183
|
+
]:
|
|
184
|
+
command = [
|
|
185
|
+
"event", "--root", str(tmp_path), "--event-id", event_id,
|
|
186
|
+
"--event-type", event_type, "--reason", reason,
|
|
187
|
+
]
|
|
188
|
+
for dependency in dependencies:
|
|
189
|
+
command.extend(["--depends-on", dependency])
|
|
190
|
+
assert main(command) == 0
|
|
191
|
+
capsys.readouterr()
|
|
192
|
+
|
|
193
|
+
assert main(["context", "--root", str(tmp_path), "--event-id", "EVAL-001", "--json"]) == 0
|
|
194
|
+
context = json.loads(capsys.readouterr().out)
|
|
195
|
+
assert [event["event_id"] for event in context["events"]] == ["DEC-001", "DEC-002", "EVAL-001"]
|
|
196
|
+
assert context["excluded_event_ids"] == ["DEC-OLD"]
|
|
197
|
+
assert context["policy"] == "current-only"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_source_check_verifies_commit_and_path(tmp_path):
|
|
201
|
+
source = tmp_path / "source"
|
|
202
|
+
ledger = tmp_path / "ledger"
|
|
203
|
+
source.mkdir()
|
|
204
|
+
ledger.mkdir()
|
|
205
|
+
|
|
206
|
+
def git(*args):
|
|
207
|
+
return subprocess.run(["git", *args], cwd=source, check=True, capture_output=True, text=True)
|
|
208
|
+
|
|
209
|
+
git("init")
|
|
210
|
+
git("config", "user.email", "test@example.invalid")
|
|
211
|
+
git("config", "user.name", "Decision Trace Test")
|
|
212
|
+
(source / "src.txt").write_text("source\n", encoding="utf-8")
|
|
213
|
+
git("add", "src.txt")
|
|
214
|
+
git("commit", "-m", "source")
|
|
215
|
+
commit = git("rev-parse", "HEAD").stdout.strip()
|
|
216
|
+
assert main([
|
|
217
|
+
"event", "--root", str(ledger), "--event-id", "SRC-001",
|
|
218
|
+
"--event-type", "source-link", "--reason", "link source",
|
|
219
|
+
"--source-repo", str(source), "--source-commit", commit, "--source-path", "src.txt",
|
|
220
|
+
]) == 0
|
|
221
|
+
assert main([
|
|
222
|
+
"source-check", "--root", str(ledger), "--event-id", "SRC-001",
|
|
223
|
+
"--source-root", str(source), "--json",
|
|
224
|
+
]) == 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_validate_checks_ledger_references(tmp_path, capsys):
|
|
228
|
+
assert main([
|
|
229
|
+
"event", "--root", str(tmp_path), "--event-id", "DEC-001",
|
|
230
|
+
"--event-type", "decision-note", "--reason", "record",
|
|
231
|
+
]) == 0
|
|
232
|
+
capsys.readouterr()
|
|
233
|
+
assert main(["validate", "--root", str(tmp_path), "--json"]) == 0
|
|
234
|
+
result = json.loads(capsys.readouterr().out)
|
|
235
|
+
assert result["valid"] is True
|
|
236
|
+
assert result["event_count"] == 1
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_evaluation_point_records_criteria_as_provisional(tmp_path):
|
|
240
|
+
assert main([
|
|
241
|
+
"event", "--root", str(tmp_path), "--event-id", "DEC-001",
|
|
242
|
+
"--event-type", "decision-change", "--reason", "try change",
|
|
243
|
+
]) == 0
|
|
244
|
+
assert main([
|
|
245
|
+
"evaluation-point", "--root", str(tmp_path), "--event-id", "EVP-001",
|
|
246
|
+
"--target-event-id", "DEC-001", "--criteria", "performance under threshold",
|
|
247
|
+
"--criteria", "source diff is traceable", "--evidence", "VER-001",
|
|
248
|
+
"--reason", "whole evaluation after provisional application",
|
|
249
|
+
]) == 0
|
|
250
|
+
events = [
|
|
251
|
+
json.loads(line)
|
|
252
|
+
for line in (tmp_path / "work" / "decision-trace" / "events.jsonl").read_text(encoding="utf-8").splitlines()
|
|
253
|
+
]
|
|
254
|
+
assert events[-1]["event_type"] == "evaluation-point"
|
|
255
|
+
assert events[-1]["status"] == "provisional"
|
|
256
|
+
assert events[-1]["criteria"] == ["performance under threshold", "source diff is traceable"]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_workflow_run_creates_automatic_checkpoint_in_git_worktree(tmp_path):
|
|
260
|
+
def git(*args):
|
|
261
|
+
return subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True)
|
|
262
|
+
|
|
263
|
+
git("init")
|
|
264
|
+
git("config", "user.email", "test@example.invalid")
|
|
265
|
+
git("config", "user.name", "Decision Trace Test")
|
|
266
|
+
(tmp_path / "README.md").write_text("base\n", encoding="utf-8")
|
|
267
|
+
git("add", "README.md")
|
|
268
|
+
git("commit", "-m", "initial")
|
|
269
|
+
run_id = str(uuid.uuid4())
|
|
270
|
+
result = run_workflow_instruction(argparse.Namespace(
|
|
271
|
+
root=str(tmp_path), task="continue protocol work", task_file=None,
|
|
272
|
+
completion_condition=["run log is created"], use_default_completion_conditions=False,
|
|
273
|
+
run_id=run_id, out=str(tmp_path / "run.md"), flow_id=None, root_run_id=None,
|
|
274
|
+
parent_run_id=None, work_item_id=None, node_id=None, purpose=None,
|
|
275
|
+
scope_in=[], scope_out=[], owner=None, authority=None, expected_evidence=[], stop_condition=[],
|
|
276
|
+
))
|
|
277
|
+
assert result.ok is True
|
|
278
|
+
assert result.decision_trace_checkpoint["checkpoint"]["run_id"] == run_id
|
|
279
|
+
assert "AI Decision Trace Checkpoint" in (tmp_path / "run.md").read_text(encoding="utf-8")
|
|
280
|
+
assert git("rev-parse", f"checkpoint/CP-RUN-{run_id}^{{}}").stdout.strip()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_worktree_creates_isolated_hypothesis_checkout(tmp_path):
|
|
284
|
+
def git(*args):
|
|
285
|
+
return subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True)
|
|
286
|
+
|
|
287
|
+
git("init")
|
|
288
|
+
git("config", "user.email", "test@example.invalid")
|
|
289
|
+
git("config", "user.name", "Decision Trace Test")
|
|
290
|
+
(tmp_path / "README.md").write_text("base\n", encoding="utf-8")
|
|
291
|
+
git("add", "README.md")
|
|
292
|
+
git("commit", "-m", "initial")
|
|
293
|
+
worktree_path = tmp_path.parent / f"{tmp_path.name}-hypothesis"
|
|
294
|
+
assert main([
|
|
295
|
+
"worktree", "--root", str(tmp_path), "--path", str(worktree_path),
|
|
296
|
+
"--branch", "hypothesis/parallel-Y", "--from-ref", "HEAD",
|
|
297
|
+
"--purpose", "parallel hypothesis evaluation", "--json",
|
|
298
|
+
]) == 0
|
|
299
|
+
assert (worktree_path / "README.md").exists()
|
|
300
|
+
assert git("-C", str(worktree_path), "branch", "--show-current").stdout.strip() == "hypothesis/parallel-Y"
|
|
301
|
+
git("worktree", "remove", "--force", str(worktree_path))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from xrefkit.wbs import WbsError, parse_wbs, render_rollup, write_csv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
VALID = """| ID | Feature | Phase | Task | Assignee | Estimate(Person-Days) | Actual(Person-Days) | Status |
|
|
9
|
+
| --- | --- | --- | --- | --- | ---: | ---: | --- |
|
|
10
|
+
| T-001 | Intake | Analysis | A \\| B | Aki | 2 | 1 | done |
|
|
11
|
+
| T-002 | Intake | Implementation | Build | Aki | 3 | 0 | doing |
|
|
12
|
+
| T-003 | Reporting | Test | Check | Ren | 1 | 0 | todo |
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_parse_accepts_escaped_pipe_and_rolls_up() -> None:
|
|
17
|
+
headers, rows = parse_wbs(VALID)
|
|
18
|
+
assert headers[1] == "Feature"
|
|
19
|
+
assert rows[0][3] == "A | B"
|
|
20
|
+
output = render_rollup(rows)
|
|
21
|
+
assert "| Intake | 5 | 1 | 2 |" in output
|
|
22
|
+
assert "| Total | 6 | 1 | 3 |" in output
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize(
|
|
26
|
+
"text, message",
|
|
27
|
+
[
|
|
28
|
+
(VALID.replace("| T-003 | Reporting | Test | Check | Ren | 1 | 0 | todo |", "| T-003 | Reporting | Test | Check | Ren | 1 | todo |"), "expected 8 cells"),
|
|
29
|
+
(VALID.replace("| T-003 | Reporting | Test | Check | Ren | 1 | 0 | todo |", "| T-003 | Reporting | Test | Check | Ren | -1 | 0 | todo |"), "must not be negative"),
|
|
30
|
+
(VALID.replace("| T-003 | Reporting | Test | Check | Ren | 1 | 0 | todo |", "| T-003 | Reporting | Test | Check | Ren | 1 | 0 | blocked |"), "Status must be"),
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
def test_parse_rejects_invalid_rows(text: str, message: str) -> None:
|
|
34
|
+
with pytest.raises(WbsError, match=message):
|
|
35
|
+
parse_wbs(text)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_csv_is_excel_readable(tmp_path: Path) -> None:
|
|
39
|
+
headers, rows = parse_wbs(VALID)
|
|
40
|
+
target = tmp_path / "wbs.csv"
|
|
41
|
+
write_csv(headers, rows, target)
|
|
42
|
+
assert target.read_bytes().startswith(b"\xef\xbb\xbf")
|
|
43
|
+
assert "T-001" in target.read_text(encoding="utf-8-sig")
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
from collections.abc import Sequence
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
_OPERATIONS = {"xref", "ctx", "goal", "gate", "pack", "dashboard", "analysis", "skill", "workflow", "host"}
|
|
9
|
+
_OPERATIONS = {"xref", "ctx", "goal", "gate", "pack", "dashboard", "analysis", "skill", "workflow", "host", "trace"}
|
|
10
10
|
_V2 = {"package", "show"}
|
|
11
11
|
|
|
12
12
|
|
|
@@ -28,6 +28,7 @@ def _print_help() -> None:
|
|
|
28
28
|
" goal manage desired-state Goals and continuation state\n"
|
|
29
29
|
" dashboard inspect runtime state\n"
|
|
30
30
|
" analysis generate proposal-only observation reports\n"
|
|
31
|
+
" trace AI protocol adapter for decision changes and Git checkpoints\n"
|
|
31
32
|
" mcp start the MCP server or initialize a client Prompt Flow\n"
|
|
32
33
|
" skills synchronize released Skill and Knowledge bundles\n"
|
|
33
34
|
" package inspect installed Skill packages\n"
|
|
@@ -760,6 +760,52 @@ def _missing_information_ranking(runs: list[DashboardRun]) -> list[dict[str, obj
|
|
|
760
760
|
return rows
|
|
761
761
|
|
|
762
762
|
|
|
763
|
+
def _decision_trace_payload(root: Path) -> dict[str, object]:
|
|
764
|
+
try:
|
|
765
|
+
from xrefkit.decision_trace import _graph, _impact_group, _read_events, _validate
|
|
766
|
+
|
|
767
|
+
events = _read_events(root)
|
|
768
|
+
groups: dict[str, int] = {}
|
|
769
|
+
for event in events:
|
|
770
|
+
group = _impact_group(event)
|
|
771
|
+
groups[group] = groups.get(group, 0) + 1
|
|
772
|
+
return {
|
|
773
|
+
"events": events,
|
|
774
|
+
"graph": _graph(root),
|
|
775
|
+
"validation": _validate(root),
|
|
776
|
+
"summary": {"events": len(events), "groups": dict(sorted(groups.items()))},
|
|
777
|
+
}
|
|
778
|
+
except (OSError, ValueError) as exc:
|
|
779
|
+
return {
|
|
780
|
+
"events": [],
|
|
781
|
+
"graph": "flowchart TD\n empty[\"No decision-trace events\"]",
|
|
782
|
+
"validation": {"valid": False, "event_count": 0, "issues": [str(exc)]},
|
|
783
|
+
"summary": {"events": 0, "groups": {}},
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _decision_trace_rows(trace: object) -> str:
|
|
788
|
+
if not isinstance(trace, dict):
|
|
789
|
+
return "<tr><td colspan='6'>No decision-trace records found.</td></tr>"
|
|
790
|
+
events = trace.get("events", [])
|
|
791
|
+
if not isinstance(events, list) or not events:
|
|
792
|
+
return "<tr><td colspan='6'>No decision-trace records found.</td></tr>"
|
|
793
|
+
rows: list[str] = []
|
|
794
|
+
for event in events:
|
|
795
|
+
if not isinstance(event, dict):
|
|
796
|
+
continue
|
|
797
|
+
rows.append(
|
|
798
|
+
"<tr>"
|
|
799
|
+
f"<td><code>{html.escape(str(event.get('event_id') or '-'))}</code></td>"
|
|
800
|
+
f"<td>{html.escape(str(event.get('event_type') or '-'))}</td>"
|
|
801
|
+
f"<td>{html.escape(str(event.get('status') or '-'))}</td>"
|
|
802
|
+
f"<td>{html.escape(str(event.get('resolution') or '-'))}</td>"
|
|
803
|
+
f"<td>{html.escape(str(event.get('branch') or '-'))}</td>"
|
|
804
|
+
f"<td>{html.escape(str(event.get('reason') or '-'))}</td>"
|
|
805
|
+
"</tr>"
|
|
806
|
+
)
|
|
807
|
+
return "\n".join(rows) or "<tr><td colspan='6'>No decision-trace records found.</td></tr>"
|
|
808
|
+
|
|
763
809
|
def _flow_rows(flows: object) -> str:
|
|
764
810
|
if not isinstance(flows, list) or not flows:
|
|
765
811
|
return "<tr><td colspan='9'>No Prompt Flows found.</td></tr>"
|
|
@@ -989,6 +1035,7 @@ def build_payload(
|
|
|
989
1035
|
runs = collect_runs(root, sessions_dir, mcp_events_by_run, audit_errors)
|
|
990
1036
|
flows = collect_flows(runs)
|
|
991
1037
|
recoveries = collect_recoveries(runs)
|
|
1038
|
+
decision_trace = _decision_trace_payload(root)
|
|
992
1039
|
summary = _summary(runs)
|
|
993
1040
|
summary["flows"] = len(flows)
|
|
994
1041
|
summary["recoveries"] = len(recoveries)
|
|
@@ -1003,6 +1050,7 @@ def build_payload(
|
|
|
1003
1050
|
"runs": [run.to_dict() for run in runs],
|
|
1004
1051
|
"flows": flows,
|
|
1005
1052
|
"recoveries": recoveries,
|
|
1053
|
+
"decision_trace": decision_trace,
|
|
1006
1054
|
}
|
|
1007
1055
|
payload["boundary_analysis"] = analyze_dashboard_payload(
|
|
1008
1056
|
payload,
|
|
@@ -1026,8 +1074,10 @@ def _html_page(payload: dict[str, object]) -> str:
|
|
|
1026
1074
|
summary = payload["summary"]
|
|
1027
1075
|
runs = payload["runs"]
|
|
1028
1076
|
flows = payload.get("flows", [])
|
|
1077
|
+
decision_trace = payload.get("decision_trace", {})
|
|
1029
1078
|
assert isinstance(summary, dict)
|
|
1030
1079
|
assert isinstance(runs, list)
|
|
1080
|
+
assert isinstance(decision_trace, dict)
|
|
1031
1081
|
boundary_analysis = payload.get("boundary_analysis")
|
|
1032
1082
|
if not isinstance(boundary_analysis, dict):
|
|
1033
1083
|
boundary_analysis = {}
|
|
@@ -1070,6 +1120,26 @@ def _html_page(payload: dict[str, object]) -> str:
|
|
|
1070
1120
|
xid_rows = "\n".join(_xid_usage_card(run) for run in runs if _has_xid_records(run))
|
|
1071
1121
|
unused_xid_rows = _unused_xid_ranking_table(payload.get("unused_xid_ranking", []))
|
|
1072
1122
|
missing_information_rows = _missing_information_ranking_table(payload.get("missing_information_ranking", []))
|
|
1123
|
+
decision_trace_summary = decision_trace.get("summary", {})
|
|
1124
|
+
if not isinstance(decision_trace_summary, dict):
|
|
1125
|
+
decision_trace_summary = {}
|
|
1126
|
+
decision_trace_rows = _decision_trace_rows(decision_trace)
|
|
1127
|
+
decision_trace_groups = decision_trace_summary.get("groups", {})
|
|
1128
|
+
if not isinstance(decision_trace_groups, dict):
|
|
1129
|
+
decision_trace_groups = {}
|
|
1130
|
+
decision_trace_group_rows = "".join(
|
|
1131
|
+
f"<tr><td>{html.escape(str(group))}</td><td>{html.escape(str(count))}</td></tr>"
|
|
1132
|
+
for group, count in sorted(decision_trace_groups.items())
|
|
1133
|
+
) or "<tr><td colspan='2'>No impact groups found.</td></tr>"
|
|
1134
|
+
decision_trace_validation = decision_trace.get("validation", {})
|
|
1135
|
+
if not isinstance(decision_trace_validation, dict):
|
|
1136
|
+
decision_trace_validation = {}
|
|
1137
|
+
decision_trace_issues = decision_trace_validation.get("issues", [])
|
|
1138
|
+
if not isinstance(decision_trace_issues, list):
|
|
1139
|
+
decision_trace_issues = []
|
|
1140
|
+
decision_trace_issue_html = "" if not decision_trace_issues else "<ul>" + "".join(
|
|
1141
|
+
f"<li>{html.escape(str(issue))}</li>" for issue in decision_trace_issues[:10]
|
|
1142
|
+
) + "</ul>"
|
|
1073
1143
|
missing_information_cards = "\n".join(
|
|
1074
1144
|
_missing_information_card(run)
|
|
1075
1145
|
for run in runs
|
|
@@ -1345,6 +1415,7 @@ def _html_page(payload: dict[str, object]) -> str:
|
|
|
1345
1415
|
<button class="tab" data-panel="xids">XID Usage</button>
|
|
1346
1416
|
<button class="tab" data-panel="analysis">Analysis</button>
|
|
1347
1417
|
<button class="tab" data-panel="missing-information">Missing Information</button>
|
|
1418
|
+
<button class="tab" data-panel="decision-trace">Decision Trace</button>
|
|
1348
1419
|
</nav>
|
|
1349
1420
|
<section class="controls" aria-label="Skill run filters">
|
|
1350
1421
|
<input id="run-search" class="search" type="search" placeholder="Search skill, path, run ID, session, repository, or status" aria-label="Search Skill runs">
|
|
@@ -1413,6 +1484,17 @@ def _html_page(payload: dict[str, object]) -> str:
|
|
|
1413
1484
|
</div>
|
|
1414
1485
|
{analysis_proposal_rows}
|
|
1415
1486
|
</section>
|
|
1487
|
+
<section id="decision-trace" class="panel">
|
|
1488
|
+
<p class="category-note">AI decision-trace events, provisional resolutions, branches, and the current dependency graph. This panel observes records; it does not execute adoption or return operations.</p>
|
|
1489
|
+
<section class="metrics"><div class="metric"><span>Trace events</span><strong>{html.escape(str(decision_trace_summary.get('events', 0)))}</strong></div><div class="metric"><span>Validation</span><strong>{html.escape(str(decision_trace_validation.get('valid', False)))}</strong></div></section>
|
|
1490
|
+
<table class="table">
|
|
1491
|
+
<thead><tr><th>Event</th><th>Type</th><th>Status</th><th>Resolution</th><th>Branch</th><th>Reason</th></tr></thead>
|
|
1492
|
+
<tbody>{decision_trace_rows}</tbody>
|
|
1493
|
+
</table>
|
|
1494
|
+
<div class="box"><h3>Impact groups</h3><table class="table"><thead><tr><th>Group</th><th>Events</th></tr></thead><tbody>{decision_trace_group_rows}</tbody></table></div>
|
|
1495
|
+
<div class="box"><h3>Dependency graph (Mermaid)</h3><pre>{html.escape(str(decision_trace.get('graph') or ''))}</pre></div>
|
|
1496
|
+
{decision_trace_issue_html}
|
|
1497
|
+
</section>
|
|
1416
1498
|
<section id="missing-information" class="panel">
|
|
1417
1499
|
<p class="category-note">Information required to correlate Skill execution, MCP access, Knowledge application, and downstream feedback.</p>
|
|
1418
1500
|
<div class="box"><h3>Missing Information Ranking</h3>{missing_information_rows}</div>
|