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/cli.py ADDED
@@ -0,0 +1,320 @@
1
+ """Guard CLI.
2
+
3
+ Commands:
4
+ - `check`: Evaluate proposed changes and select tests.
5
+ - `run`: Execute selected tests according to trust policy.
6
+ - `hook`: Manage and run agent lifecycle hooks (e.g. Claude Code stop hook).
7
+ - `covering-tests`: Show tests covering a function.
8
+ - `callers`: Show multi-hop callers of a function.
9
+ - `feedback`: Record an escape / missed fault for graph repair.
10
+ - `ingest-static`: Ingest static AST call graph into FalkorDB.
11
+ - `ingest-coverage`: Ingest pytest-cov execution traces into FalkorDB.
12
+ - `stats`: Display graph node and relationship counts.
13
+ """
14
+ from __future__ import annotations
15
+ import argparse
16
+ import json
17
+ import sys
18
+
19
+ from guard.ingest.static import analyze_repo
20
+
21
+
22
+ def cmd_ingest_static(args) -> int:
23
+ res = analyze_repo(args.repo, include_tests=args.include_tests)
24
+ print(json.dumps(res.summary(), indent=2))
25
+ for err in res.parse_errors[:5]:
26
+ print("parse error:", err, file=sys.stderr)
27
+ if args.dry_run:
28
+ return 0
29
+ from guard import db
30
+
31
+ g = db.get_graph(args.graph)
32
+ db.reset_graph(g)
33
+ g = db.get_graph(args.graph)
34
+ db.ensure_schema(g)
35
+ db.write_static(g, res, commit_sha=args.commit_sha)
36
+ print(f"written to graph '{args.graph}'")
37
+ return 0
38
+
39
+
40
+ def cmd_callers(args) -> int:
41
+ from guard import api
42
+
43
+ hops = int(args.hops)
44
+ callers = api.callers(
45
+ args.function,
46
+ hops=hops,
47
+ min_confidence=float(args.min_confidence),
48
+ graph_name=args.graph,
49
+ strict=getattr(args, "strict", False),
50
+ )
51
+ for c in callers:
52
+ print(f"{c['hops']} {c['caller']}")
53
+ return 0
54
+
55
+
56
+ def cmd_stats(args) -> int:
57
+ from guard import db
58
+
59
+ g = db.get_graph(args.graph)
60
+ counts = db.get_schema_counts(g)
61
+ print("=== Nodes ===")
62
+ for label in ("File", "Function", "Test", "Run"):
63
+ print(f"{label}: {counts.get(label, 0)}")
64
+ print("\n=== Edges ===")
65
+ for rel in ("CALLS", "IMPORTS", "DEFINED_IN", "COVERED_BY", "FAILED_IN"):
66
+ print(f"{rel}: {counts.get(rel, 0)}")
67
+ print(f"\nDynamic functions: {counts.get('dynamic_functions', 0)}")
68
+ return 0
69
+
70
+
71
+ def cmd_covering_tests(args) -> int:
72
+ from guard import api
73
+
74
+ tests = api.covering_tests(
75
+ args.function,
76
+ min_confidence=float(args.min_confidence),
77
+ graph_name=args.graph,
78
+ strict=getattr(args, "strict", False),
79
+ )
80
+ for t in tests:
81
+ print(f"[{t['confidence']:.2f}] {t['test_id']} (source: {t['source']})")
82
+ return 0
83
+
84
+
85
+ def cmd_ingest_coverage(args) -> int:
86
+ from guard.ingest.coverage import ingest_coverage
87
+
88
+ res = ingest_coverage(
89
+ repo_path=args.repo,
90
+ graph_name=args.graph,
91
+ commit_sha=args.commit_sha,
92
+ num_runs=args.runs,
93
+ )
94
+ print(json.dumps(res.summary(), indent=2))
95
+ return 0
96
+
97
+
98
+ def cmd_check(args) -> int:
99
+ from guard import api
100
+
101
+ res = api.check(
102
+ diff_file=args.diff,
103
+ repo_path=args.repo,
104
+ graph_name=args.graph,
105
+ strict=getattr(args, "strict", False),
106
+ )
107
+ if args.json:
108
+ print(json.dumps(res, indent=2))
109
+ else:
110
+ print(f"Status: {res['status']}")
111
+ print(f"Policy: {res['policy']} (Trust: {res['trust']:.2f})")
112
+ print(f"Fallback: {res['is_fallback']}")
113
+ if res.get("fallback_reason"):
114
+ print(f"Reason: {res['fallback_reason']}")
115
+ if res.get("overrides_triggered"):
116
+ print(f"Overrides: {', '.join(res['overrides_triggered'])}")
117
+ print(f"Changed Functions ({len(res.get('changed_functions', []))}): {res.get('changed_functions', [])}")
118
+ print(f"Selected Tests ({len(res.get('selected_tests', []))}): {res.get('selected_tests', [])}")
119
+ return 0
120
+
121
+
122
+ def cmd_feedback(args) -> int:
123
+ from guard import api
124
+
125
+ res = api.report_miss(
126
+ function_id=args.function,
127
+ test_id=args.test,
128
+ new_confidence=args.confidence,
129
+ graph_name=args.graph,
130
+ strict=getattr(args, "strict", False),
131
+ )
132
+ if args.json:
133
+ print(json.dumps(res, indent=2))
134
+ else:
135
+ print(f"Repaired: function='{res['function']}' -> test='{res['test']}'")
136
+ print(f"Confidence: {res.get('old_confidence', 0.0)} -> {res.get('new_confidence', 1.0)} (source: {res.get('source')})")
137
+ return 0
138
+
139
+
140
+ def cmd_run(args) -> int:
141
+ from guard import api
142
+
143
+ report, exit_code = api.run(
144
+ diff_file=args.diff,
145
+ repo_path=args.repo,
146
+ graph_name=args.graph,
147
+ strict=getattr(args, "strict", False),
148
+ )
149
+ if args.json:
150
+ print(json.dumps(report, indent=2))
151
+ else:
152
+ print(f"Status: {report['status']}")
153
+ print(f"Policy: {report['policy']} (Trust: {report['trust']:.2f})")
154
+ print(f"Forced Full Suite: {report.get('forced_full', False)}")
155
+ if report.get("fallback_reason"):
156
+ print(f"Fallback Reason: {report['fallback_reason']}")
157
+ print(f"Tests Run ({report.get('tests_run_count', 0)}): {report.get('tests_run', [])}")
158
+ print(f"Passed: {report.get('tests_passed', False)} in {report.get('duration_s', 0.0)}s")
159
+ if report.get("failures"):
160
+ print(f"Failures ({len(report['failures'])}): {report['failures']}")
161
+ if report.get("output_summary"):
162
+ print(f"Summary: {report['output_summary']}")
163
+ return exit_code
164
+
165
+
166
+ def cmd_hook_stop(args) -> int:
167
+ from guard.hook import run_stop_hook
168
+ return run_stop_hook(
169
+ repo_override=args.repo,
170
+ strict=getattr(args, "strict", False),
171
+ )
172
+
173
+
174
+ def cmd_hook_install(args) -> int:
175
+ from guard.integrations import claude
176
+ ok, msg = claude.install(repo_path=args.repo, shared=args.shared)
177
+ print(msg)
178
+ return 0 if ok else 1
179
+
180
+
181
+ def cmd_hook_uninstall(args) -> int:
182
+ from guard.integrations import claude
183
+ ok, msg = claude.uninstall(repo_path=args.repo, shared=args.shared)
184
+ print(msg)
185
+ return 0 if ok else 1
186
+
187
+
188
+ def cmd_hook_status(args) -> int:
189
+ from guard.integrations import claude
190
+ stat = claude.status(repo_path=args.repo, shared=args.shared)
191
+ print(json.dumps(stat, indent=2))
192
+ return 0
193
+
194
+
195
+ def cmd_init(args) -> int:
196
+ from guard.init_cmd import run_init
197
+
198
+ return run_init(
199
+ repo_path=getattr(args, "repo", None),
200
+ backend_preference=getattr(args, "backend", None),
201
+ skip_coverage=getattr(args, "skip_coverage", False),
202
+ auto_yes=getattr(args, "yes", False),
203
+ agents=getattr(args, "agents", "claude,cursor,codex"),
204
+ runs=getattr(args, "runs", None),
205
+ )
206
+
207
+
208
+ def cmd_doctor(args) -> int:
209
+ from guard.doctor_cmd import run_doctor
210
+
211
+ return run_doctor(
212
+ repo_path=getattr(args, "repo", None),
213
+ json_mode=getattr(args, "json", False),
214
+ fix_mode=getattr(args, "fix", False),
215
+ )
216
+
217
+
218
+ def main(argv=None) -> int:
219
+ import guard
220
+
221
+ p = argparse.ArgumentParser(prog="guard")
222
+ p.add_argument("-v", "--version", action="version", version=f"guard {guard.__version__}")
223
+ p.add_argument("--graph", default="guard")
224
+ sub = p.add_subparsers(dest="cmd", required=True)
225
+
226
+ ini = sub.add_parser("init", help="Initialize Guard for repository")
227
+ ini.add_argument("repo", nargs="?", default=None, help="target repository path (default: current directory)")
228
+ ini.add_argument("--backend", choices=["server", "docker", "embedded"], default=None, help="database backend")
229
+ ini.add_argument("--skip-coverage", action="store_true", help="skip per-test coverage ingestion")
230
+ ini.add_argument("-y", "--yes", action="store_true", help="non-interactive mode accepting all defaults")
231
+ ini.add_argument("--agents", default="claude,cursor,codex", help="comma-separated agents to configure")
232
+ ini.add_argument("--runs", type=int, default=None, help="number of baseline test runs (1 or 3)")
233
+ ini.set_defaults(fn=cmd_init)
234
+
235
+ doc = sub.add_parser("doctor", help="Check Guard system health and diagnostics")
236
+ doc.add_argument("repo", nargs="?", default=None, help="target repository path (default: current directory)")
237
+ doc.add_argument("--json", action="store_true", help="output structured JSON")
238
+ doc.add_argument("--fix", action="store_true", help="attempt automated repairs for warnings/failures")
239
+ doc.set_defaults(fn=cmd_doctor)
240
+
241
+ rn = sub.add_parser("run")
242
+ rn.add_argument("--diff", default=None, help="path to unified diff file")
243
+ rn.add_argument("--repo", default=None, help="path to target repo")
244
+ rn.add_argument("--json", action="store_true", help="output JSON")
245
+ rn.add_argument("--strict", action="store_true", help="fail closed on guard error")
246
+ rn.set_defaults(fn=cmd_run)
247
+
248
+ chk = sub.add_parser("check")
249
+ chk.add_argument("--diff", default=None, help="path to unified diff file")
250
+ chk.add_argument("--repo", default=None, help="path to repo to run git diff HEAD")
251
+ chk.add_argument("--json", action="store_true", help="output JSON")
252
+ chk.add_argument("--strict", action="store_true", help="fail closed on guard error")
253
+ chk.set_defaults(fn=cmd_check)
254
+
255
+ hk = sub.add_parser("hook", help="Agent hook management")
256
+ hk_sub = hk.add_subparsers(dest="hook_cmd", required=True)
257
+
258
+ hk_stop = hk_sub.add_parser("stop", help="Run Claude Code Stop hook adapter")
259
+ hk_stop.add_argument("--repo", default=None, help="path to repo root")
260
+ hk_stop.add_argument("--strict", action="store_true", help="fail closed on error")
261
+ hk_stop.set_defaults(fn=cmd_hook_stop)
262
+
263
+ hk_inst = hk_sub.add_parser("install", help="Install Stop hook into Claude Code settings")
264
+ hk_inst.add_argument("--repo", default=None, help="path to repo root")
265
+ hk_inst.add_argument("--shared", action="store_true", help="install to settings.json instead of settings.local.json")
266
+ hk_inst.set_defaults(fn=cmd_hook_install)
267
+
268
+ hk_uninst = hk_sub.add_parser("uninstall", help="Uninstall Stop hook from Claude Code settings")
269
+ hk_uninst.add_argument("--repo", default=None, help="path to repo root")
270
+ hk_uninst.add_argument("--shared", action="store_true", help="uninstall from settings.json instead of settings.local.json")
271
+ hk_uninst.set_defaults(fn=cmd_hook_uninstall)
272
+
273
+ hk_stat = hk_sub.add_parser("status", help="Check Claude Code hook installation status")
274
+ hk_stat.add_argument("--repo", default=None, help="path to repo root")
275
+ hk_stat.add_argument("--shared", action="store_true", help="check settings.json instead of settings.local.json")
276
+ hk_stat.set_defaults(fn=cmd_hook_status)
277
+
278
+ fb = sub.add_parser("feedback")
279
+ fb.add_argument("--function", required=True, help="Function ID where fault was missed")
280
+ fb.add_argument("--test", required=True, help="Test ID that failed and was missed")
281
+ fb.add_argument("--confidence", default=1.0, type=float, help="Confidence of repair edge (default: 1.0)")
282
+ fb.add_argument("--json", action="store_true", help="output JSON")
283
+ fb.add_argument("--strict", action="store_true", help="fail closed on guard error")
284
+ fb.set_defaults(fn=cmd_feedback)
285
+
286
+ s = sub.add_parser("ingest-static")
287
+ s.add_argument("repo")
288
+ s.add_argument("--dry-run", action="store_true", help="parse only, no database")
289
+ s.add_argument("--include-tests", action="store_true")
290
+ s.add_argument("--commit-sha", default=None, help="commit SHA to associate with edges")
291
+ s.set_defaults(fn=cmd_ingest_static)
292
+
293
+ ic = sub.add_parser("ingest-coverage")
294
+ ic.add_argument("repo")
295
+ ic.add_argument("--runs", default=3, type=int, help="number of test runs for flake measurement")
296
+ ic.add_argument("--commit-sha", default=None, help="commit SHA to associate with edges")
297
+ ic.set_defaults(fn=cmd_ingest_coverage)
298
+
299
+ c = sub.add_parser("callers")
300
+ c.add_argument("function", help="function id, e.g. pkg/mod.py::Class.method")
301
+ c.add_argument("--hops", default=4, type=int)
302
+ c.add_argument("--min-confidence", default=0.0, type=float)
303
+ c.add_argument("--strict", action="store_true", help="fail closed on error")
304
+ c.set_defaults(fn=cmd_callers)
305
+
306
+ st = sub.add_parser("stats")
307
+ st.set_defaults(fn=cmd_stats)
308
+
309
+ cov = sub.add_parser("covering-tests")
310
+ cov.add_argument("function", help="function id, e.g. pkg/mod.py::Class.method")
311
+ cov.add_argument("--min-confidence", default=0.0, type=float)
312
+ cov.add_argument("--strict", action="store_true", help="fail closed on error")
313
+ cov.set_defaults(fn=cmd_covering_tests)
314
+
315
+ args = p.parse_args(argv)
316
+ return args.fn(args)
317
+
318
+
319
+ if __name__ == "__main__":
320
+ raise SystemExit(main())
guard/db.py ADDED
@@ -0,0 +1,271 @@
1
+ """FalkorDB connection, complete schema, and batched writers for Guard (Task T2)."""
2
+ from __future__ import annotations
3
+ import os
4
+ from typing import Any, Iterable, Sequence
5
+ from falkordb import FalkorDB
6
+
7
+ GRAPH_NAME = os.getenv("GUARD_GRAPH", "guard")
8
+
9
+ INDEXES = [
10
+ "CREATE INDEX FOR (f:Function) ON (f.id)",
11
+ "CREATE INDEX FOR (t:Test) ON (t.id)",
12
+ "CREATE INDEX FOR (x:File) ON (x.path)",
13
+ "CREATE INDEX FOR (r:Run) ON (r.id)",
14
+ ]
15
+
16
+
17
+ _active_backend: Any = None
18
+
19
+
20
+ def set_backend(backend: Any) -> None:
21
+ """Explicitly set active GraphBackend instance."""
22
+ global _active_backend
23
+ _active_backend = backend
24
+
25
+
26
+ def get_backend(repo_path: Any = None) -> Any:
27
+ """Return active or resolved GraphBackend."""
28
+ global _active_backend
29
+ if _active_backend is not None:
30
+ return _active_backend
31
+ from guard.backend import resolve_backend, ServerBackend
32
+
33
+ if "FALKORDB_HOST" in os.environ or "FALKORDB_PORT" in os.environ:
34
+ host = os.getenv("FALKORDB_HOST", "localhost")
35
+ port = int(os.getenv("FALKORDB_PORT", "6379"))
36
+ return ServerBackend(host=host, port=port)
37
+ return resolve_backend(repo_path=repo_path)
38
+
39
+
40
+ def get_graph(
41
+ name: str | None = None,
42
+ backend: Any = None,
43
+ repo_path: Any = None,
44
+ ):
45
+ """Retrieve FalkorDB graph using resolved backend and repository name."""
46
+ if backend is None:
47
+ backend = get_backend(repo_path=repo_path)
48
+ if name is None:
49
+ name = os.getenv("GUARD_GRAPH", GRAPH_NAME)
50
+ return backend.get_graph(name)
51
+
52
+
53
+ def reset_graph(g) -> None:
54
+ try:
55
+ g.delete()
56
+ except Exception:
57
+ pass # graph did not exist yet
58
+
59
+
60
+ def ensure_schema(g) -> None:
61
+ for q in INDEXES:
62
+ try:
63
+ g.query(q)
64
+ except Exception:
65
+ pass # index already exists
66
+
67
+
68
+ def _batches(rows: Sequence[Any], size: int = 500) -> Iterable[Sequence[Any]]:
69
+ for i in range(0, len(rows), size):
70
+ yield rows[i : i + size]
71
+
72
+
73
+ def write_static(g, result, commit_sha: str | None = None) -> None:
74
+ """Write StaticResult nodes and edges into FalkorDB with T2 schema requirements."""
75
+ # 1. File nodes
76
+ for b in _batches(result.file_rows()):
77
+ g.query("UNWIND $rows AS r MERGE (x:File {path: r.path})", {"rows": b})
78
+
79
+ # 2. Function nodes & DEFINED_IN edges
80
+ for b in _batches(result.function_rows()):
81
+ g.query(
82
+ """UNWIND $rows AS r
83
+ MERGE (f:Function {id: r.id})
84
+ SET f.file = r.file, f.name = r.name, f.qualname = r.qualname,
85
+ f.start = r.start, f.end = r.end,
86
+ f.has_dynamic_dispatch = r.dyn
87
+ WITH f, r
88
+ MATCH (x:File {path: r.file})
89
+ MERGE (f)-[:DEFINED_IN]->(x)""",
90
+ {"rows": b},
91
+ )
92
+
93
+ # 3. Test nodes & DEFINED_IN edges
94
+ if hasattr(result, "test_rows"):
95
+ test_rows = result.test_rows()
96
+ else:
97
+ test_rows = []
98
+
99
+ for b in _batches(test_rows):
100
+ g.query(
101
+ """UNWIND $rows AS r
102
+ MERGE (t:Test {id: r.id})
103
+ SET t.file = r.file, t.name = r.name, t.qualname = r.qualname,
104
+ t.start = r.start, t.end = r.end
105
+ WITH t, r
106
+ MATCH (x:File {path: r.file})
107
+ MERGE (t)-[:DEFINED_IN]->(x)""",
108
+ {"rows": b},
109
+ )
110
+
111
+ # 4. CALLS edges (Function -> Function)
112
+ for b in _batches(result.call_rows()):
113
+ g.query(
114
+ """UNWIND $rows AS r
115
+ MATCH (a:Function {id: r.src}), (b:Function {id: r.dst})
116
+ MERGE (a)-[e:CALLS]->(b)
117
+ SET e.source = 'static',
118
+ e.confidence = 0.6,
119
+ e.evidence_count = coalesce(e.evidence_count, 0),
120
+ e.last_verified_sha = $sha""",
121
+ {"rows": b, "sha": commit_sha},
122
+ )
123
+
124
+ # 5. IMPORTS edges (File -> File)
125
+ for b in _batches(result.import_rows()):
126
+ g.query(
127
+ """UNWIND $rows AS r
128
+ MATCH (a:File {path: r.src}), (b:File {path: r.dst})
129
+ MERGE (a)-[e:IMPORTS]->(b)
130
+ SET e.source = 'static',
131
+ e.confidence = 0.6,
132
+ e.last_verified_sha = $sha""",
133
+ {"rows": b, "sha": commit_sha},
134
+ )
135
+
136
+
137
+ def write_coverage(
138
+ g,
139
+ test_to_funcs: dict[str, list[str]] | list[dict[str, str]],
140
+ commit_sha: str | None = None,
141
+ confidence: float = 0.9,
142
+ ) -> int:
143
+ """Write COVERED_BY edges from Function to Test nodes.
144
+
145
+ Args:
146
+ test_to_funcs: Mapping test_id -> list of function_ids, or list of {'test': id, 'func': id}
147
+ commit_sha: Commit SHA when coverage was verified
148
+ confidence: Confidence score (default 0.9 for dynamic coverage evidence)
149
+
150
+ Returns:
151
+ Number of coverage pairs written
152
+ """
153
+ if isinstance(test_to_funcs, dict):
154
+ pairs = [
155
+ {"test": t_id, "func": f_id}
156
+ for t_id, funcs in test_to_funcs.items()
157
+ for f_id in funcs
158
+ ]
159
+ else:
160
+ pairs = list(test_to_funcs)
161
+
162
+ for b in _batches(pairs):
163
+ g.query(
164
+ """UNWIND $rows AS r
165
+ MATCH (f:Function {id: r.func}), (t:Test {id: r.test})
166
+ MERGE (f)-[e:COVERED_BY]->(t)
167
+ SET e.source = 'coverage',
168
+ e.confidence = $conf,
169
+ e.evidence_count = coalesce(e.evidence_count, 0) + 1,
170
+ e.last_verified_sha = $sha""",
171
+ {"rows": b, "conf": confidence, "sha": commit_sha},
172
+ )
173
+ return len(pairs)
174
+
175
+
176
+ def write_run(
177
+ g,
178
+ run_data: dict[str, Any],
179
+ failures: list[dict[str, Any]] | None = None,
180
+ ) -> None:
181
+ """Write a Run node and optional FAILED_IN edges.
182
+
183
+ Args:
184
+ run_data: Dict with keys: id, timestamp, commit_sha, duration_ms, total_tests, passed, failed
185
+ failures: List of dicts: {'test_id': str, 'error': str}
186
+ """
187
+ g.query(
188
+ """MERGE (r:Run {id: $r.id})
189
+ SET r.timestamp = $r.timestamp,
190
+ r.commit_sha = $r.commit_sha,
191
+ r.duration_ms = $r.duration_ms,
192
+ r.total_tests = $r.total_tests,
193
+ r.passed = $r.passed,
194
+ r.failed = $r.failed""",
195
+ {"r": run_data},
196
+ )
197
+
198
+ if failures:
199
+ for b in _batches(failures):
200
+ g.query(
201
+ """UNWIND $rows AS f
202
+ MATCH (t:Test {id: f.test_id}), (r:Run {id: $run_id})
203
+ MERGE (t)-[e:FAILED_IN]->(r)
204
+ SET e.source = 'test_run',
205
+ e.error_message = f.error,
206
+ e.commit_sha = $sha""",
207
+ {
208
+ "rows": b,
209
+ "run_id": run_data["id"],
210
+ "sha": run_data.get("commit_sha"),
211
+ },
212
+ )
213
+
214
+
215
+ def get_schema_counts(g) -> dict[str, int]:
216
+ """Return dictionary with counts of all nodes and relationships."""
217
+ counts = {}
218
+ for label in ("File", "Function", "Test", "Run"):
219
+ try:
220
+ res = g.query(f"MATCH (n:{label}) RETURN count(n)").result_set
221
+ counts[label] = res[0][0] if res else 0
222
+ except Exception:
223
+ counts[label] = 0
224
+
225
+ for rel in ("CALLS", "IMPORTS", "DEFINED_IN", "COVERED_BY", "FAILED_IN"):
226
+ try:
227
+ res = g.query(f"MATCH ()-[e:{rel}]->() RETURN count(e)").result_set
228
+ counts[rel] = res[0][0] if res else 0
229
+ except Exception:
230
+ counts[rel] = 0
231
+
232
+ try:
233
+ res = g.query(
234
+ "MATCH (f:Function {has_dynamic_dispatch: true}) RETURN count(f)"
235
+ ).result_set
236
+ counts["dynamic_functions"] = res[0][0] if res else 0
237
+ except Exception:
238
+ counts["dynamic_functions"] = 0
239
+
240
+ return counts
241
+
242
+
243
+ def get_callers(
244
+ g, function_id: str, hops: int = 4, min_confidence: float = 0.0
245
+ ) -> list[dict[str, Any]]:
246
+ """Traverse inbound CALLS edges up to N hops filtered by min_confidence."""
247
+ q = (
248
+ f"MATCH p=(caller:Function)-[:CALLS*1..{hops}]->(target:Function {{id: $id}}) "
249
+ "WHERE all(e IN relationships(p) WHERE coalesce(e.confidence, 0.6) >= $min_conf) "
250
+ "RETURN caller.id AS caller, min(length(p)) AS hops "
251
+ "ORDER BY hops, caller"
252
+ )
253
+ res = g.query(q, {"id": function_id, "min_conf": min_confidence}).result_set
254
+ return [{"caller": row[0], "hops": row[1]} for row in res]
255
+
256
+
257
+ def get_covering_tests(
258
+ g, function_id: str, min_confidence: float = 0.0
259
+ ) -> list[dict[str, Any]]:
260
+ """Find tests covering a given function, ordered by confidence."""
261
+ q = (
262
+ "MATCH (f:Function {id: $id})-[e:COVERED_BY]->(t:Test) "
263
+ "WHERE e.confidence >= $min_conf "
264
+ "RETURN t.id AS test_id, e.confidence AS confidence, e.source AS source "
265
+ "ORDER BY confidence DESC"
266
+ )
267
+ res = g.query(q, {"id": function_id, "min_conf": min_confidence}).result_set
268
+ return [
269
+ {"test_id": row[0], "confidence": row[1], "source": row[2]}
270
+ for row in res
271
+ ]