guard-cli 0.1.0__py3-none-any.whl
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.
- guard/__init__.py +2 -0
- guard/agent_integration.py +122 -0
- guard/api.py +397 -0
- guard/backend.py +475 -0
- guard/cli.py +320 -0
- guard/db.py +271 -0
- guard/decision.py +316 -0
- guard/doctor_cmd.py +475 -0
- guard/events.py +77 -0
- guard/feedback.py +86 -0
- guard/hook.py +281 -0
- guard/ingest/__init__.py +1 -0
- guard/ingest/coverage.py +403 -0
- guard/ingest/static.py +290 -0
- guard/init_cmd.py +437 -0
- guard/integrations/__init__.py +1 -0
- guard/integrations/claude.py +195 -0
- guard/state.py +104 -0
- guard/thin_slice.py +278 -0
- guard_cli/__init__.py +37 -0
- guard_cli/api.py +2 -0
- guard_cli/backend.py +2 -0
- guard_cli/cli.py +3 -0
- guard_cli/db.py +2 -0
- guard_cli/decision.py +2 -0
- guard_cli/doctor_cmd.py +2 -0
- guard_cli/events.py +2 -0
- guard_cli/feedback.py +2 -0
- guard_cli/hook.py +2 -0
- guard_cli/ingest/__init__.py +1 -0
- guard_cli/ingest/coverage.py +1 -0
- guard_cli/ingest/static.py +1 -0
- guard_cli/init_cmd.py +2 -0
- guard_cli/integrations/__init__.py +1 -0
- guard_cli/integrations/claude.py +1 -0
- guard_cli/state.py +2 -0
- guard_cli/thin_slice.py +2 -0
- guard_cli-0.1.0.dist-info/METADATA +241 -0
- guard_cli-0.1.0.dist-info/RECORD +43 -0
- guard_cli-0.1.0.dist-info/WHEEL +5 -0
- guard_cli-0.1.0.dist-info/entry_points.txt +3 -0
- guard_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- guard_cli-0.1.0.dist-info/top_level.txt +2 -0
guard/__init__.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Agent integration and test execution runner (Task T7).
|
|
2
|
+
|
|
3
|
+
Provides `guard run`: an agent calls this command before declaring work complete.
|
|
4
|
+
- Calls `guard check` on proposed changes.
|
|
5
|
+
- If trust is low or changes are uncovered (fallback), forces a full test suite run.
|
|
6
|
+
- If trust is high, runs only the selected test subset.
|
|
7
|
+
- Blocks task completion if any test fails.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from guard.thin_slice import guard_check
|
|
18
|
+
|
|
19
|
+
REPO_ROOT = Path("target_repos/flask").resolve()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def apply_patch(diff_file: Path, repo_path: Path | None = None) -> bool:
|
|
23
|
+
"""Applies patch to target repo."""
|
|
24
|
+
target = repo_path or REPO_ROOT
|
|
25
|
+
proc = subprocess.run(["git", "apply", str(diff_file)], cwd=str(target), capture_output=True, text=True)
|
|
26
|
+
if proc.returncode != 0:
|
|
27
|
+
patch_proc = subprocess.run(["patch", "-p1", "-i", str(diff_file)], cwd=str(target), capture_output=True)
|
|
28
|
+
return patch_proc.returncode == 0
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def reset_repo(repo_path: Path | None = None):
|
|
33
|
+
"""Restores target repo to clean state."""
|
|
34
|
+
target = repo_path or REPO_ROOT
|
|
35
|
+
subprocess.run(["git", "checkout", "--", "."], cwd=str(target), capture_output=True)
|
|
36
|
+
subprocess.run(["git", "clean", "-fdx"], cwd=str(target), capture_output=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def count_collected_tests(test_targets: list[str], repo_path: Path | None = None) -> int:
|
|
40
|
+
"""Counts how many test items are collected for the targets."""
|
|
41
|
+
target = repo_path or REPO_ROOT
|
|
42
|
+
cmd = [sys.executable, "-m", "pytest", "--collect-only", "-q"] + test_targets
|
|
43
|
+
proc = subprocess.run(cmd, cwd=str(target), capture_output=True, text=True)
|
|
44
|
+
lines = proc.stdout.strip().splitlines()
|
|
45
|
+
for line in reversed(lines):
|
|
46
|
+
if "test" in line:
|
|
47
|
+
parts = line.split()
|
|
48
|
+
for p in parts:
|
|
49
|
+
if p.isdigit():
|
|
50
|
+
return int(p)
|
|
51
|
+
return len(test_targets)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_agent_guard(
|
|
55
|
+
diff_file: str | Path | None = None,
|
|
56
|
+
repo_path: str | Path | None = None,
|
|
57
|
+
graph_name: str = "guard",
|
|
58
|
+
) -> tuple[dict[str, Any], int]:
|
|
59
|
+
"""Runs Guard check and executes tests according to trust level.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
(report_dict, exit_code)
|
|
63
|
+
"""
|
|
64
|
+
repo = Path(repo_path).resolve() if repo_path else REPO_ROOT
|
|
65
|
+
diff_path = Path(diff_file).resolve() if diff_file else None
|
|
66
|
+
|
|
67
|
+
# 1. Run guard check
|
|
68
|
+
check_res = guard_check(
|
|
69
|
+
diff_file=diff_path,
|
|
70
|
+
repo_path=repo,
|
|
71
|
+
graph_name=graph_name,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
forced_full = check_res.is_fallback or (check_res.status == "run-all")
|
|
75
|
+
tests_to_run = ["tests"] if forced_full else check_res.selected_tests
|
|
76
|
+
|
|
77
|
+
# 2. If a patch file was passed, apply it temporarily for test execution
|
|
78
|
+
applied = False
|
|
79
|
+
if diff_path and diff_path.exists():
|
|
80
|
+
reset_repo()
|
|
81
|
+
applied = apply_patch(diff_path)
|
|
82
|
+
if not applied:
|
|
83
|
+
return {
|
|
84
|
+
"status": "error",
|
|
85
|
+
"forced_full": True,
|
|
86
|
+
"fallback_reason": "Failed to apply patch diff",
|
|
87
|
+
"changed_functions": [],
|
|
88
|
+
"tests_run": [],
|
|
89
|
+
"tests_run_count": 0,
|
|
90
|
+
"duration_s": 0.0,
|
|
91
|
+
"tests_passed": False,
|
|
92
|
+
"exit_code": 1,
|
|
93
|
+
"output_summary": "Error: Failed to apply patch cleanly to target repo.",
|
|
94
|
+
}, 1
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Count collected tests
|
|
98
|
+
test_count = count_collected_tests(tests_to_run)
|
|
99
|
+
|
|
100
|
+
# 3. Execute pytest
|
|
101
|
+
cmd = [sys.executable, "-m", "pytest", "-q", "--tb=short"] + tests_to_run
|
|
102
|
+
start = time.time()
|
|
103
|
+
proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True)
|
|
104
|
+
duration_s = time.time() - start
|
|
105
|
+
|
|
106
|
+
passed = (proc.returncode == 0)
|
|
107
|
+
report = {
|
|
108
|
+
"status": check_res.status,
|
|
109
|
+
"forced_full": forced_full,
|
|
110
|
+
"fallback_reason": check_res.fallback_reason,
|
|
111
|
+
"changed_functions": check_res.changed_functions,
|
|
112
|
+
"tests_run": tests_to_run,
|
|
113
|
+
"tests_run_count": test_count,
|
|
114
|
+
"duration_s": round(duration_s, 3),
|
|
115
|
+
"tests_passed": passed,
|
|
116
|
+
"exit_code": proc.returncode,
|
|
117
|
+
"output_summary": proc.stdout.strip().splitlines()[-1] if proc.stdout.strip() else "",
|
|
118
|
+
}
|
|
119
|
+
return report, proc.returncode
|
|
120
|
+
finally:
|
|
121
|
+
if applied:
|
|
122
|
+
reset_repo()
|
guard/api.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Guard Public API and Stable Contract (Phase 0).
|
|
2
|
+
|
|
3
|
+
Provides the unified contract used across the CLI, Claude Code hook,
|
|
4
|
+
MCP server, and external agent loops:
|
|
5
|
+
- `check()`: Evaluates diff, returns test selection decision + versioned schema.
|
|
6
|
+
- `run()`: Executes the selected test subset according to trust policy.
|
|
7
|
+
- `covering_tests()`: Queries graph for tests covering a function.
|
|
8
|
+
- `callers()`: Queries multi-hop callers of a function.
|
|
9
|
+
- `report_miss()`: Feedback loop repairing graph with a miss_repair edge.
|
|
10
|
+
|
|
11
|
+
Fail-open by default: If database or graph is unavailable and strict=False,
|
|
12
|
+
Guard emits a warning to stderr and fails open instead of blocking the agent.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import subprocess
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from guard import db
|
|
23
|
+
from guard import feedback
|
|
24
|
+
from guard.events import log_event
|
|
25
|
+
from guard.thin_slice import (
|
|
26
|
+
guard_check,
|
|
27
|
+
parse_unified_diff,
|
|
28
|
+
resolve_changed_functions,
|
|
29
|
+
thin_slice_decision,
|
|
30
|
+
CheckResult,
|
|
31
|
+
)
|
|
32
|
+
from guard.agent_integration import (
|
|
33
|
+
REPO_ROOT,
|
|
34
|
+
apply_patch,
|
|
35
|
+
reset_repo,
|
|
36
|
+
count_collected_tests,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
SCHEMA_VERSION = "1.0"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _extract_failures(stdout: str, stderr: str) -> list[str]:
|
|
43
|
+
"""Parses failing pytest test identifiers from output."""
|
|
44
|
+
failures = []
|
|
45
|
+
# pytest output lines typically look like:
|
|
46
|
+
# FAILED tests/test_config.py::test_config_class - AssertionError...
|
|
47
|
+
fail_regex = re.compile(r"^FAILED\s+([^\s]+)", re.MULTILINE)
|
|
48
|
+
for match in fail_regex.finditer(stdout):
|
|
49
|
+
failures.append(match.group(1))
|
|
50
|
+
for match in fail_regex.finditer(stderr):
|
|
51
|
+
failures.append(match.group(1))
|
|
52
|
+
return sorted(list(set(failures)))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _format_reasons(check_res: CheckResult) -> dict[str, str]:
|
|
56
|
+
"""Generates a reasons map for selected tests."""
|
|
57
|
+
reasons: dict[str, str] = {}
|
|
58
|
+
if check_res.is_fallback:
|
|
59
|
+
reason_text = check_res.fallback_reason or "Fallback triggered"
|
|
60
|
+
for t in check_res.selected_tests:
|
|
61
|
+
reasons[t] = f"selected: full suite required ({reason_text})"
|
|
62
|
+
else:
|
|
63
|
+
for t in check_res.selected_tests:
|
|
64
|
+
reasons[t] = "selected: covering edge from changed function"
|
|
65
|
+
return reasons
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalize_test_target_for_pytest(target: str) -> str:
|
|
69
|
+
"""Normalizes graph test identifiers into valid pytest CLI invocation targets."""
|
|
70
|
+
if "::" not in target:
|
|
71
|
+
return target
|
|
72
|
+
file_part, func_part = target.split("::", 1)
|
|
73
|
+
if "." in func_part:
|
|
74
|
+
cls_name, method_name = func_part.split(".", 1)
|
|
75
|
+
if method_name.startswith("test"):
|
|
76
|
+
return f"{file_part}::{cls_name}::{method_name}"
|
|
77
|
+
else:
|
|
78
|
+
return f"{file_part}::{cls_name}"
|
|
79
|
+
else:
|
|
80
|
+
if func_part.startswith("test") or func_part.startswith("Test"):
|
|
81
|
+
return target
|
|
82
|
+
else:
|
|
83
|
+
return file_part
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check(
|
|
87
|
+
diff_text: str | None = None,
|
|
88
|
+
diff_file: str | Path | None = None,
|
|
89
|
+
repo_path: str | Path | None = None,
|
|
90
|
+
graph_name: str = "guard",
|
|
91
|
+
strict: bool = False,
|
|
92
|
+
no_confidence_filter: bool = False,
|
|
93
|
+
no_dynamic_widening: bool = False,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
"""Evaluates proposed changes and returns test selection decision.
|
|
96
|
+
|
|
97
|
+
Returns a versioned JSON dictionary conforming to schema v1.0.
|
|
98
|
+
"""
|
|
99
|
+
repo = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
100
|
+
diff_path = Path(diff_file).resolve() if diff_file else None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
res = guard_check(
|
|
104
|
+
diff_text=diff_text,
|
|
105
|
+
diff_file=diff_path,
|
|
106
|
+
repo_path=repo,
|
|
107
|
+
graph_name=graph_name,
|
|
108
|
+
no_confidence_filter=no_confidence_filter,
|
|
109
|
+
no_dynamic_widening=no_dynamic_widening,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
is_fallback = res.is_fallback
|
|
113
|
+
policy = "subset" if not is_fallback else "run_all"
|
|
114
|
+
trust = 0.9 if not is_fallback else 0.0
|
|
115
|
+
|
|
116
|
+
overrides = []
|
|
117
|
+
if is_fallback:
|
|
118
|
+
if res.fallback_reason:
|
|
119
|
+
if "Test files" in res.fallback_reason:
|
|
120
|
+
overrides.append("test_file_touched")
|
|
121
|
+
elif "no_evidence" in res.fallback_reason or "No covering tests" in res.fallback_reason or "No parsed" in res.fallback_reason:
|
|
122
|
+
overrides.append("no_evidence")
|
|
123
|
+
else:
|
|
124
|
+
overrides.append("fallback_override")
|
|
125
|
+
|
|
126
|
+
reasons = _format_reasons(res)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"version": SCHEMA_VERSION,
|
|
130
|
+
"status": res.status,
|
|
131
|
+
"policy": policy,
|
|
132
|
+
"trust": trust,
|
|
133
|
+
"overrides_triggered": overrides,
|
|
134
|
+
"selected_tests": res.selected_tests,
|
|
135
|
+
"selected_tests_count": len(res.selected_tests),
|
|
136
|
+
"failures": [],
|
|
137
|
+
"reasons": reasons,
|
|
138
|
+
"changed_functions": res.changed_functions,
|
|
139
|
+
"is_fallback": res.is_fallback,
|
|
140
|
+
"fallback_reason": res.fallback_reason,
|
|
141
|
+
"execution_time_ms": res.execution_time_ms,
|
|
142
|
+
}
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
if strict:
|
|
145
|
+
raise
|
|
146
|
+
sys.stderr.write(f"Guard warning (fail-open): check failed with {exc}\n")
|
|
147
|
+
log_event(
|
|
148
|
+
event="fail_open",
|
|
149
|
+
repo_path=repo,
|
|
150
|
+
details={"op": "check", "error": str(exc)},
|
|
151
|
+
)
|
|
152
|
+
return {
|
|
153
|
+
"version": SCHEMA_VERSION,
|
|
154
|
+
"status": "run-all",
|
|
155
|
+
"policy": "fail_open",
|
|
156
|
+
"trust": 0.0,
|
|
157
|
+
"overrides_triggered": ["fail_open_error"],
|
|
158
|
+
"selected_tests": ["tests"],
|
|
159
|
+
"selected_tests_count": 1,
|
|
160
|
+
"failures": [],
|
|
161
|
+
"reasons": {"error": f"Guard check error: {exc}"},
|
|
162
|
+
"changed_functions": [],
|
|
163
|
+
"is_fallback": True,
|
|
164
|
+
"fallback_reason": f"Guard database/internal error: {exc}",
|
|
165
|
+
"execution_time_ms": 0.0,
|
|
166
|
+
"fail_open": True,
|
|
167
|
+
"error": str(exc),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run(
|
|
172
|
+
diff_file: str | Path | None = None,
|
|
173
|
+
diff_text: str | None = None,
|
|
174
|
+
repo_path: str | Path | None = None,
|
|
175
|
+
graph_name: str = "guard",
|
|
176
|
+
strict: bool = False,
|
|
177
|
+
session_id: str | None = None,
|
|
178
|
+
) -> tuple[dict[str, Any], int]:
|
|
179
|
+
"""Runs Guard check and executes the selected tests.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
(report_dict, exit_code)
|
|
183
|
+
"""
|
|
184
|
+
repo = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
185
|
+
diff_path = Path(diff_file).resolve() if diff_file else None
|
|
186
|
+
|
|
187
|
+
# 1. Run check
|
|
188
|
+
try:
|
|
189
|
+
check_dict = check(
|
|
190
|
+
diff_text=diff_text,
|
|
191
|
+
diff_file=diff_path,
|
|
192
|
+
repo_path=repo,
|
|
193
|
+
graph_name=graph_name,
|
|
194
|
+
strict=strict,
|
|
195
|
+
)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
if strict:
|
|
198
|
+
raise
|
|
199
|
+
sys.stderr.write(f"Guard warning (fail-open): check failed with {exc}\n")
|
|
200
|
+
log_event(
|
|
201
|
+
event="fail_open",
|
|
202
|
+
session_id=session_id,
|
|
203
|
+
repo_path=repo,
|
|
204
|
+
details={"op": "run", "error": str(exc)},
|
|
205
|
+
)
|
|
206
|
+
return {
|
|
207
|
+
"version": SCHEMA_VERSION,
|
|
208
|
+
"status": "passed",
|
|
209
|
+
"policy": "fail_open",
|
|
210
|
+
"trust": 0.0,
|
|
211
|
+
"overrides_triggered": ["fail_open_error"],
|
|
212
|
+
"selected_tests": [],
|
|
213
|
+
"failures": [],
|
|
214
|
+
"reasons": {},
|
|
215
|
+
"tests_run": [],
|
|
216
|
+
"tests_run_count": 0,
|
|
217
|
+
"tests_passed": True,
|
|
218
|
+
"forced_full": False,
|
|
219
|
+
"fallback_reason": str(exc),
|
|
220
|
+
"duration_s": 0.0,
|
|
221
|
+
"exit_code": 0,
|
|
222
|
+
"output_summary": f"Guard bypassed fail-open: {exc}",
|
|
223
|
+
"fail_open": True,
|
|
224
|
+
}, 0
|
|
225
|
+
|
|
226
|
+
forced_full = check_dict.get("is_fallback", False) or (check_dict.get("status") == "run-all")
|
|
227
|
+
if forced_full:
|
|
228
|
+
tests_to_run = ["tests"]
|
|
229
|
+
else:
|
|
230
|
+
raw_selected = check_dict.get("selected_tests", ["tests"])
|
|
231
|
+
tests_to_run = list(dict.fromkeys(normalize_test_target_for_pytest(t) for t in raw_selected))
|
|
232
|
+
|
|
233
|
+
# 2. Apply patch if explicit diff_file was provided
|
|
234
|
+
applied = False
|
|
235
|
+
if diff_path and diff_path.exists():
|
|
236
|
+
reset_repo(repo)
|
|
237
|
+
applied = apply_patch(diff_path, repo)
|
|
238
|
+
if not applied:
|
|
239
|
+
err_dict = {
|
|
240
|
+
"version": SCHEMA_VERSION,
|
|
241
|
+
"status": "error",
|
|
242
|
+
"policy": check_dict.get("policy", "run_all"),
|
|
243
|
+
"trust": check_dict.get("trust", 0.0),
|
|
244
|
+
"overrides_triggered": check_dict.get("overrides_triggered", []),
|
|
245
|
+
"selected_tests": tests_to_run,
|
|
246
|
+
"failures": ["patch_apply_failure"],
|
|
247
|
+
"reasons": check_dict.get("reasons", {}),
|
|
248
|
+
"forced_full": True,
|
|
249
|
+
"fallback_reason": "Failed to apply patch diff",
|
|
250
|
+
"changed_functions": check_dict.get("changed_functions", []),
|
|
251
|
+
"tests_run": [],
|
|
252
|
+
"tests_run_count": 0,
|
|
253
|
+
"duration_s": 0.0,
|
|
254
|
+
"tests_passed": False,
|
|
255
|
+
"exit_code": 1,
|
|
256
|
+
"output_summary": "Error: Failed to apply patch cleanly to target repo.",
|
|
257
|
+
}
|
|
258
|
+
log_event(
|
|
259
|
+
event="failed",
|
|
260
|
+
session_id=session_id,
|
|
261
|
+
repo_path=repo,
|
|
262
|
+
details=err_dict,
|
|
263
|
+
)
|
|
264
|
+
return err_dict, 1
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
# Count collected tests
|
|
268
|
+
test_count = count_collected_tests(tests_to_run, repo)
|
|
269
|
+
|
|
270
|
+
# Resolve target repo python interpreter
|
|
271
|
+
py_bin = sys.executable
|
|
272
|
+
if repo:
|
|
273
|
+
try:
|
|
274
|
+
from guard.init_cmd import find_test_environment
|
|
275
|
+
py_bin = str(find_test_environment(repo))
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
# 3. Execute pytest (180s timeout for worst-case run-all test suites)
|
|
280
|
+
cmd = [py_bin, "-m", "pytest", "-q", "--tb=short"] + tests_to_run
|
|
281
|
+
start = time.time()
|
|
282
|
+
try:
|
|
283
|
+
proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True, timeout=180)
|
|
284
|
+
duration_s = round(time.time() - start, 3)
|
|
285
|
+
passed = (proc.returncode == 0)
|
|
286
|
+
failures = _extract_failures(proc.stdout, proc.stderr)
|
|
287
|
+
summary_line = proc.stdout.strip().splitlines()[-1] if proc.stdout.strip() else ""
|
|
288
|
+
proc_exit_code = proc.returncode
|
|
289
|
+
except subprocess.TimeoutExpired:
|
|
290
|
+
duration_s = round(time.time() - start, 3)
|
|
291
|
+
passed = False
|
|
292
|
+
failures = ["test_suite_timeout_expired_180s"]
|
|
293
|
+
summary_line = f"Test execution timed out after {duration_s:.1f}s."
|
|
294
|
+
proc_exit_code = 124
|
|
295
|
+
|
|
296
|
+
report = {
|
|
297
|
+
"version": SCHEMA_VERSION,
|
|
298
|
+
"status": "passed" if passed else "failed",
|
|
299
|
+
"policy": check_dict.get("policy", "subset"),
|
|
300
|
+
"trust": check_dict.get("trust", 0.9 if passed else 0.0),
|
|
301
|
+
"overrides_triggered": check_dict.get("overrides_triggered", []),
|
|
302
|
+
"selected_tests": check_dict.get("selected_tests", []),
|
|
303
|
+
"failures": failures,
|
|
304
|
+
"reasons": check_dict.get("reasons", {}),
|
|
305
|
+
"forced_full": forced_full,
|
|
306
|
+
"fallback_reason": check_dict.get("fallback_reason"),
|
|
307
|
+
"changed_functions": check_dict.get("changed_functions", []),
|
|
308
|
+
"tests_run": tests_to_run,
|
|
309
|
+
"tests_run_count": test_count,
|
|
310
|
+
"duration_s": duration_s,
|
|
311
|
+
"tests_passed": passed,
|
|
312
|
+
"exit_code": proc_exit_code,
|
|
313
|
+
"output_summary": summary_line,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Log event
|
|
317
|
+
log_event(
|
|
318
|
+
event="passed" if passed else "blocked",
|
|
319
|
+
session_id=session_id,
|
|
320
|
+
repo_path=repo,
|
|
321
|
+
details={
|
|
322
|
+
"exit_code": proc_exit_code,
|
|
323
|
+
"tests_passed": passed,
|
|
324
|
+
"tests_run_count": test_count,
|
|
325
|
+
"failures": failures,
|
|
326
|
+
"policy": report["policy"],
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return report, proc_exit_code
|
|
331
|
+
finally:
|
|
332
|
+
if applied:
|
|
333
|
+
reset_repo()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def covering_tests(
|
|
337
|
+
function_id: str,
|
|
338
|
+
min_confidence: float = 0.0,
|
|
339
|
+
graph_name: str = "guard",
|
|
340
|
+
strict: bool = False,
|
|
341
|
+
) -> list[dict[str, Any]]:
|
|
342
|
+
"""Find tests covering a given function, ordered by confidence."""
|
|
343
|
+
try:
|
|
344
|
+
g = db.get_graph(graph_name)
|
|
345
|
+
return db.get_covering_tests(g, function_id, min_confidence=min_confidence)
|
|
346
|
+
except Exception as exc:
|
|
347
|
+
if strict:
|
|
348
|
+
raise
|
|
349
|
+
sys.stderr.write(f"Guard warning (fail-open): covering_tests failed with {exc}\n")
|
|
350
|
+
return []
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def callers(
|
|
354
|
+
function_id: str,
|
|
355
|
+
hops: int = 4,
|
|
356
|
+
min_confidence: float = 0.0,
|
|
357
|
+
graph_name: str = "guard",
|
|
358
|
+
strict: bool = False,
|
|
359
|
+
) -> list[dict[str, Any]]:
|
|
360
|
+
"""Traverse inbound CALLS edges up to N hops filtered by min_confidence."""
|
|
361
|
+
try:
|
|
362
|
+
g = db.get_graph(graph_name)
|
|
363
|
+
return db.get_callers(g, function_id, hops=hops, min_confidence=min_confidence)
|
|
364
|
+
except Exception as exc:
|
|
365
|
+
if strict:
|
|
366
|
+
raise
|
|
367
|
+
sys.stderr.write(f"Guard warning (fail-open): callers failed with {exc}\n")
|
|
368
|
+
return []
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def report_miss(
|
|
372
|
+
function_id: str,
|
|
373
|
+
test_id: str,
|
|
374
|
+
new_confidence: float = 1.0,
|
|
375
|
+
graph_name: str = "guard",
|
|
376
|
+
strict: bool = False,
|
|
377
|
+
) -> dict[str, Any]:
|
|
378
|
+
"""Records a missed failure by repairing the graph with a miss_repair edge."""
|
|
379
|
+
try:
|
|
380
|
+
g = db.get_graph(graph_name)
|
|
381
|
+
return feedback.record_miss(
|
|
382
|
+
g,
|
|
383
|
+
function_id=function_id,
|
|
384
|
+
test_id=test_id,
|
|
385
|
+
new_confidence=new_confidence,
|
|
386
|
+
)
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
if strict:
|
|
389
|
+
raise
|
|
390
|
+
sys.stderr.write(f"Guard warning (fail-open): report_miss failed with {exc}\n")
|
|
391
|
+
return {
|
|
392
|
+
"status": "error",
|
|
393
|
+
"error": str(exc),
|
|
394
|
+
"function": function_id,
|
|
395
|
+
"test": test_id,
|
|
396
|
+
"fail_open": True,
|
|
397
|
+
}
|