guard-cli 0.1.0rc1__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 +119 -0
- guard/cli.py +194 -0
- guard/db.py +243 -0
- guard/decision.py +316 -0
- guard/feedback.py +86 -0
- guard/ingest/__init__.py +1 -0
- guard/ingest/coverage.py +390 -0
- guard/ingest/static.py +290 -0
- guard/thin_slice.py +266 -0
- guard_cli-0.1.0rc1.dist-info/METADATA +210 -0
- guard_cli-0.1.0rc1.dist-info/RECORD +15 -0
- guard_cli-0.1.0rc1.dist-info/WHEEL +5 -0
- guard_cli-0.1.0rc1.dist-info/entry_points.txt +2 -0
- guard_cli-0.1.0rc1.dist-info/top_level.txt +1 -0
guard/decision.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Guard decision engine (Task T5): trust score, hard overrides, 3-level policy.
|
|
2
|
+
|
|
3
|
+
This module is the *pure* decision core. It takes a change set plus the graph
|
|
4
|
+
evidence gathered for it and returns which tests to run and why. It performs no
|
|
5
|
+
database or subprocess work; wiring it to the live graph / a git diff is the job
|
|
6
|
+
of the thin slice (T5a).
|
|
7
|
+
|
|
8
|
+
Frozen starting parameters (PLAN.md, T5). These are tuned on the calibration
|
|
9
|
+
split only, then frozen. Changing any value requires a DEVIATIONS.md entry.
|
|
10
|
+
|
|
11
|
+
trust = W_EVIDENCE * evidence_ratio
|
|
12
|
+
+ W_FRESHNESS * freshness
|
|
13
|
+
+ W_CERTAINTY * (1 - uncertain_ratio)
|
|
14
|
+
|
|
15
|
+
where, over the changed functions:
|
|
16
|
+
evidence_ratio = mean of the best covering-edge confidence per function
|
|
17
|
+
(0.0 for a function with no covering edge)
|
|
18
|
+
freshness = fraction of covering edges whose last_verified_sha equals
|
|
19
|
+
the current SHA (unknown SHA on either side counts as fresh)
|
|
20
|
+
uncertain_ratio = fraction of changed functions flagged dynamic dispatch
|
|
21
|
+
|
|
22
|
+
Hard overrides (applied before the trust policy):
|
|
23
|
+
1. a changed test file -> run all + warning
|
|
24
|
+
2. a changed function with no covering evidence -> run all
|
|
25
|
+
3. a changed function flagged dynamic dispatch -> add its module tests
|
|
26
|
+
|
|
27
|
+
Policy:
|
|
28
|
+
trust >= TRUST_SUBSET_MIN subset
|
|
29
|
+
trust >= TRUST_MEDIUM_MIN subset + module tests
|
|
30
|
+
otherwise run all
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
# --- frozen parameters -------------------------------------------------
|
|
38
|
+
W_EVIDENCE = 0.5
|
|
39
|
+
W_FRESHNESS = 0.3
|
|
40
|
+
W_CERTAINTY = 0.2
|
|
41
|
+
TRUST_SUBSET_MIN = 0.8
|
|
42
|
+
TRUST_MEDIUM_MIN = 0.5
|
|
43
|
+
|
|
44
|
+
LEVEL_SUBSET = "subset"
|
|
45
|
+
LEVEL_MEDIUM = "medium"
|
|
46
|
+
LEVEL_RUN_ALL = "run_all"
|
|
47
|
+
|
|
48
|
+
STATUS_SELECTED = "selected"
|
|
49
|
+
STATUS_SKIPPED = "skipped"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_test_file(path: str) -> bool:
|
|
53
|
+
"""True if a repo-relative path is a test file or lives under tests/."""
|
|
54
|
+
parts = path.replace("\\", "/").split("/")
|
|
55
|
+
name = parts[-1]
|
|
56
|
+
return (
|
|
57
|
+
name.startswith("test_")
|
|
58
|
+
or name.endswith("_test.py")
|
|
59
|
+
or "tests" in parts[:-1]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class CoveringTest:
|
|
65
|
+
"""A COVERED_BY edge: Test -> covers -> Function (edge confidence)."""
|
|
66
|
+
|
|
67
|
+
test_id: str
|
|
68
|
+
confidence: float
|
|
69
|
+
source: str = "unknown"
|
|
70
|
+
last_verified_sha: str | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class FunctionEvidence:
|
|
75
|
+
"""Graph evidence for one changed function."""
|
|
76
|
+
|
|
77
|
+
function_id: str
|
|
78
|
+
file: str
|
|
79
|
+
covering_tests: tuple[CoveringTest, ...] = ()
|
|
80
|
+
has_dynamic_dispatch: bool = False
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def has_evidence(self) -> bool:
|
|
84
|
+
return len(self.covering_tests) > 0
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def best_confidence(self) -> float:
|
|
88
|
+
if not self.covering_tests:
|
|
89
|
+
return 0.0
|
|
90
|
+
return max(c.confidence for c in self.covering_tests)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class Change:
|
|
95
|
+
"""Inputs to a decision: changed functions, changed files, and the known tests."""
|
|
96
|
+
|
|
97
|
+
functions: tuple[FunctionEvidence, ...] = ()
|
|
98
|
+
changed_files: tuple[str, ...] = ()
|
|
99
|
+
known_tests: tuple[str, ...] = ()
|
|
100
|
+
module_tests: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
101
|
+
current_sha: str | None = None
|
|
102
|
+
|
|
103
|
+
def module_tests_for(self, file: str) -> tuple[str, ...]:
|
|
104
|
+
return tuple(self.module_tests.get(file, ()))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class EvidenceEdge:
|
|
109
|
+
function: str
|
|
110
|
+
source: str
|
|
111
|
+
confidence: float
|
|
112
|
+
fresh: bool
|
|
113
|
+
|
|
114
|
+
def as_dict(self) -> dict[str, Any]:
|
|
115
|
+
return {
|
|
116
|
+
"function": self.function,
|
|
117
|
+
"source": self.source,
|
|
118
|
+
"confidence": self.confidence,
|
|
119
|
+
"fresh": self.fresh,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class Reason:
|
|
125
|
+
test_id: str
|
|
126
|
+
status: str
|
|
127
|
+
text: str
|
|
128
|
+
evidence: tuple[EvidenceEdge, ...] = ()
|
|
129
|
+
queried_functions: tuple[str, ...] = ()
|
|
130
|
+
|
|
131
|
+
def as_dict(self) -> dict[str, Any]:
|
|
132
|
+
return {
|
|
133
|
+
"test_id": self.test_id,
|
|
134
|
+
"status": self.status,
|
|
135
|
+
"text": self.text,
|
|
136
|
+
"evidence": [e.as_dict() for e in self.evidence],
|
|
137
|
+
"queried_functions": list(self.queried_functions),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class Decision:
|
|
143
|
+
run_all: bool
|
|
144
|
+
policy_level: str
|
|
145
|
+
trust: float
|
|
146
|
+
evidence_ratio: float
|
|
147
|
+
freshness: float
|
|
148
|
+
uncertain_ratio: float
|
|
149
|
+
selected_tests: tuple[str, ...]
|
|
150
|
+
skipped_tests: tuple[str, ...]
|
|
151
|
+
reasons: dict[str, Reason]
|
|
152
|
+
overrides: tuple[str, ...] = ()
|
|
153
|
+
warnings: tuple[str, ...] = ()
|
|
154
|
+
|
|
155
|
+
def explain(self, test_id: str) -> Reason:
|
|
156
|
+
"""Queryable reason for any test the engine considered."""
|
|
157
|
+
if test_id not in self.reasons:
|
|
158
|
+
raise KeyError(f"no decision recorded for test {test_id!r}")
|
|
159
|
+
return self.reasons[test_id]
|
|
160
|
+
|
|
161
|
+
def as_dict(self) -> dict[str, Any]:
|
|
162
|
+
return {
|
|
163
|
+
"run_all": self.run_all,
|
|
164
|
+
"policy_level": self.policy_level,
|
|
165
|
+
"trust": round(self.trust, 6),
|
|
166
|
+
"components": {
|
|
167
|
+
"evidence_ratio": round(self.evidence_ratio, 6),
|
|
168
|
+
"freshness": round(self.freshness, 6),
|
|
169
|
+
"uncertain_ratio": round(self.uncertain_ratio, 6),
|
|
170
|
+
},
|
|
171
|
+
"selected_tests": list(self.selected_tests),
|
|
172
|
+
"skipped_tests": list(self.skipped_tests),
|
|
173
|
+
"overrides": list(self.overrides),
|
|
174
|
+
"warnings": list(self.warnings),
|
|
175
|
+
"reasons": {t: r.as_dict() for t, r in self.reasons.items()},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _fresh(edge_sha: str | None, current_sha: str | None) -> bool:
|
|
180
|
+
if edge_sha is None or current_sha is None:
|
|
181
|
+
return True
|
|
182
|
+
return edge_sha == current_sha
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def compute_trust(change: Change) -> tuple[float, float, float, float]:
|
|
186
|
+
"""Return (trust, evidence_ratio, freshness, uncertain_ratio)."""
|
|
187
|
+
funcs = change.functions
|
|
188
|
+
n = len(funcs)
|
|
189
|
+
if n == 0:
|
|
190
|
+
return 0.0, 0.0, 0.0, 0.0
|
|
191
|
+
|
|
192
|
+
evidence_ratio = sum(f.best_confidence for f in funcs) / n
|
|
193
|
+
uncertain_ratio = sum(1 for f in funcs if f.has_dynamic_dispatch) / n
|
|
194
|
+
|
|
195
|
+
edges = [c for f in funcs for c in f.covering_tests]
|
|
196
|
+
if edges:
|
|
197
|
+
freshness = sum(1 for c in edges if _fresh(c.last_verified_sha, change.current_sha)) / len(edges)
|
|
198
|
+
else:
|
|
199
|
+
freshness = 0.0
|
|
200
|
+
|
|
201
|
+
trust = (
|
|
202
|
+
W_EVIDENCE * evidence_ratio
|
|
203
|
+
+ W_FRESHNESS * freshness
|
|
204
|
+
+ W_CERTAINTY * (1.0 - uncertain_ratio)
|
|
205
|
+
)
|
|
206
|
+
return trust, evidence_ratio, freshness, uncertain_ratio
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def policy_for_trust(trust: float) -> str:
|
|
210
|
+
"""Map a trust score to one of the three policy levels."""
|
|
211
|
+
if trust >= TRUST_SUBSET_MIN:
|
|
212
|
+
return LEVEL_SUBSET
|
|
213
|
+
if trust >= TRUST_MEDIUM_MIN:
|
|
214
|
+
return LEVEL_MEDIUM
|
|
215
|
+
return LEVEL_RUN_ALL
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def decide(change: Change) -> Decision:
|
|
219
|
+
"""Apply the hard overrides, then the trust policy, to select tests."""
|
|
220
|
+
funcs = list(change.functions)
|
|
221
|
+
all_functions = tuple(f.function_id for f in funcs)
|
|
222
|
+
|
|
223
|
+
overrides: list[str] = []
|
|
224
|
+
warnings: list[str] = []
|
|
225
|
+
|
|
226
|
+
# Base selection: covering tests of each changed function, with the edge that
|
|
227
|
+
# justifies the selection so every decision stays queryable.
|
|
228
|
+
base: dict[str, list[EvidenceEdge]] = {}
|
|
229
|
+
for f in funcs:
|
|
230
|
+
for c in f.covering_tests:
|
|
231
|
+
base.setdefault(c.test_id, []).append(
|
|
232
|
+
EvidenceEdge(f.function_id, c.source, c.confidence,
|
|
233
|
+
_fresh(c.last_verified_sha, change.current_sha))
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# --- hard override 1: test files touched
|
|
237
|
+
touched_test_files = sorted(p for p in change.changed_files if is_test_file(p))
|
|
238
|
+
force_run_all = False
|
|
239
|
+
if touched_test_files:
|
|
240
|
+
force_run_all = True
|
|
241
|
+
overrides.append("test_file_touched")
|
|
242
|
+
warnings.append(
|
|
243
|
+
f"test files changed ({', '.join(touched_test_files)}); running the full suite"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# --- hard override 2: any changed function with no covering evidence
|
|
247
|
+
no_evidence = sorted(f.function_id for f in funcs if not f.has_evidence)
|
|
248
|
+
if not funcs or no_evidence:
|
|
249
|
+
force_run_all = True
|
|
250
|
+
overrides.append("no_evidence")
|
|
251
|
+
if no_evidence:
|
|
252
|
+
warnings.append(f"no covering evidence for: {', '.join(no_evidence)}")
|
|
253
|
+
else:
|
|
254
|
+
warnings.append("no changed functions carried evidence; running the full suite")
|
|
255
|
+
|
|
256
|
+
# --- hard override 3: dynamic dispatch widens to module tests
|
|
257
|
+
dynamic_funcs = sorted(f.function_id for f in funcs if f.has_dynamic_dispatch)
|
|
258
|
+
if dynamic_funcs:
|
|
259
|
+
overrides.append("dynamic_dispatch")
|
|
260
|
+
|
|
261
|
+
trust, ev_ratio, fresh, unc = compute_trust(change)
|
|
262
|
+
|
|
263
|
+
if force_run_all:
|
|
264
|
+
level = LEVEL_RUN_ALL
|
|
265
|
+
run_all = True
|
|
266
|
+
selected_set = set(change.known_tests)
|
|
267
|
+
else:
|
|
268
|
+
level = policy_for_trust(trust)
|
|
269
|
+
run_all = level == LEVEL_RUN_ALL
|
|
270
|
+
if run_all:
|
|
271
|
+
selected_set = set(change.known_tests)
|
|
272
|
+
else:
|
|
273
|
+
selected_set = set(base)
|
|
274
|
+
if level == LEVEL_MEDIUM or dynamic_funcs:
|
|
275
|
+
for f in funcs:
|
|
276
|
+
selected_set.update(change.module_tests_for(f.file))
|
|
277
|
+
|
|
278
|
+
# --- a queryable reason for every known test
|
|
279
|
+
reasons: dict[str, Reason] = {}
|
|
280
|
+
for test_id in change.known_tests:
|
|
281
|
+
if test_id in selected_set:
|
|
282
|
+
if run_all:
|
|
283
|
+
text = "selected: full suite required"
|
|
284
|
+
if force_run_all:
|
|
285
|
+
text = "selected: full suite (hard override)"
|
|
286
|
+
elif base.get(test_id):
|
|
287
|
+
text = "selected: covers a changed function"
|
|
288
|
+
else:
|
|
289
|
+
text = "selected: module test for a changed function's module"
|
|
290
|
+
reasons[test_id] = Reason(
|
|
291
|
+
test_id, STATUS_SELECTED, text,
|
|
292
|
+
tuple(base.get(test_id, ())), all_functions,
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
reasons[test_id] = Reason(
|
|
296
|
+
test_id, STATUS_SKIPPED,
|
|
297
|
+
"skipped: no covering edge from any changed function",
|
|
298
|
+
(), all_functions,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
selected_tests = tuple(sorted(selected_set))
|
|
302
|
+
skipped_tests = tuple(t for t in change.known_tests if t not in selected_set)
|
|
303
|
+
|
|
304
|
+
return Decision(
|
|
305
|
+
run_all=run_all,
|
|
306
|
+
policy_level=level,
|
|
307
|
+
trust=trust,
|
|
308
|
+
evidence_ratio=ev_ratio,
|
|
309
|
+
freshness=fresh,
|
|
310
|
+
uncertain_ratio=unc,
|
|
311
|
+
selected_tests=selected_tests,
|
|
312
|
+
skipped_tests=skipped_tests,
|
|
313
|
+
reasons=reasons,
|
|
314
|
+
overrides=tuple(overrides),
|
|
315
|
+
warnings=tuple(warnings),
|
|
316
|
+
)
|
guard/feedback.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Feedback loop and repair edge mechanism for Guard (Task T6).
|
|
2
|
+
|
|
3
|
+
When a test failure is missed by Guard (an escape), this module repairs
|
|
4
|
+
the graph by inserting a high-confidence COVERED_BY edge with source='miss_repair',
|
|
5
|
+
logging the old and new confidence values, and unsetting no_evidence flags so that
|
|
6
|
+
future decisions widen to include the missed test.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
from guard import db
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def record_miss(
|
|
15
|
+
g,
|
|
16
|
+
function_id: str,
|
|
17
|
+
test_id: str,
|
|
18
|
+
new_confidence: float = 1.0,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""Records a missed failure by repairing the graph with a miss_repair edge.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
g: FalkorDB graph instance
|
|
24
|
+
function_id: ID of the function where the fault/change occurred
|
|
25
|
+
test_id: ID of the test that failed and was missed
|
|
26
|
+
new_confidence: Confidence score for the repair edge (default: 1.0)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dict reporting the repair event including old and new confidence.
|
|
30
|
+
"""
|
|
31
|
+
# 1. Query existing edge if present
|
|
32
|
+
q_existing = (
|
|
33
|
+
"MATCH (f:Function {id: $fid})-[e:COVERED_BY]->(t:Test {id: $tid}) "
|
|
34
|
+
"RETURN e.confidence, e.source"
|
|
35
|
+
)
|
|
36
|
+
existing_res = g.query(q_existing, {"fid": function_id, "tid": test_id}).result_set
|
|
37
|
+
if existing_res:
|
|
38
|
+
old_confidence = float(existing_res[0][0])
|
|
39
|
+
prev_source = existing_res[0][1]
|
|
40
|
+
else:
|
|
41
|
+
old_confidence = 0.0
|
|
42
|
+
prev_source = None
|
|
43
|
+
|
|
44
|
+
ts = time.time()
|
|
45
|
+
|
|
46
|
+
# 2. Ensure Test node exists and has basic defined properties if new
|
|
47
|
+
file_part = test_id.split("::")[0] if "::" in test_id else "tests/unknown.py"
|
|
48
|
+
g.query(
|
|
49
|
+
"MERGE (t:Test {id: $tid}) "
|
|
50
|
+
"ON CREATE SET t.file = $file, t.flake_rate = 0.0",
|
|
51
|
+
{"tid": test_id, "file": file_part},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 3. Create or update COVERED_BY edge with source='miss_repair'
|
|
55
|
+
q_repair = (
|
|
56
|
+
"""MATCH (f:Function {id: $fid}), (t:Test {id: $tid})
|
|
57
|
+
MERGE (f)-[e:COVERED_BY]->(t)
|
|
58
|
+
SET e.source = 'miss_repair',
|
|
59
|
+
e.old_confidence = $old_conf,
|
|
60
|
+
e.confidence = $new_conf,
|
|
61
|
+
e.evidence_count = coalesce(e.evidence_count, 0) + 1,
|
|
62
|
+
e.last_repaired_at = $ts
|
|
63
|
+
WITH f
|
|
64
|
+
SET f.no_evidence = false"""
|
|
65
|
+
)
|
|
66
|
+
g.query(
|
|
67
|
+
q_repair,
|
|
68
|
+
{
|
|
69
|
+
"fid": function_id,
|
|
70
|
+
"tid": test_id,
|
|
71
|
+
"old_conf": old_confidence,
|
|
72
|
+
"new_conf": new_confidence,
|
|
73
|
+
"ts": ts,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"status": "repaired",
|
|
79
|
+
"function": function_id,
|
|
80
|
+
"test": test_id,
|
|
81
|
+
"old_confidence": old_confidence,
|
|
82
|
+
"new_confidence": new_confidence,
|
|
83
|
+
"source": "miss_repair",
|
|
84
|
+
"previous_source": prev_source,
|
|
85
|
+
"timestamp": ts,
|
|
86
|
+
}
|
guard/ingest/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ingestion modules for Guard."""
|