|
24 | 24 |
|
25 | 25 | from pathlib import Path |
26 | 26 |
|
27 | | -from verityai.consistency.claims import extract_claims |
| 27 | +from verityai.consistency.claims import extract_claims, looks_like_path |
28 | 28 | from verityai.context.rank import bm25_rank |
29 | 29 | from verityai.core.models import ( |
30 | 30 | CheckStatus, |
|
34 | 34 | ConsistencyReport, |
35 | 35 | DecisionStatus, |
36 | 36 | Evidence, |
| 37 | + GraphNode, |
| 38 | + NodeKind, |
37 | 39 | ) |
38 | 40 | from verityai.graph.query import GraphQuery |
39 | 41 | from verityai.graph.store import EdgeKind |
@@ -73,8 +75,62 @@ def check_symbol_exists(claim: Claim, query: GraphQuery) -> ClaimCheck: |
73 | 75 | ) |
74 | 76 |
|
75 | 77 |
|
| 78 | +def check_symbol_calls_file(claim: Claim, query: GraphQuery) -> ClaimCheck: |
| 79 | + """Does the file defining `claim.subject` actually import `claim.target`? |
| 80 | +
|
| 81 | + A relation claim whose target is a file, not a function, asserts a |
| 82 | + module-level dependency, not a call edge -- there is no CALLS edge to a |
| 83 | + file node. Checked instead against the file-level IMPORTS graph |
| 84 | + (`GraphQuery.file_dependencies` uses the same edges). This closes the |
| 85 | + blind spot found in ADR-0018: "`apply_tax` calls `billing/tax_rates.py`" |
| 86 | + used to decompose into two independent, both-true existence checks and |
| 87 | + vanish -- neither symbol/file existence check has any way to see that |
| 88 | + the claimed relationship between them is false. |
| 89 | + """ |
| 90 | + subject_nodes = query.define(claim.subject) |
| 91 | + if not subject_nodes: |
| 92 | + return ClaimCheck( |
| 93 | + claim=claim, |
| 94 | + status=CheckStatus.CONTRADICTED, |
| 95 | + confidence=1.0, |
| 96 | + explanation=f"no definition of {claim.subject!r} found", |
| 97 | + ) |
| 98 | + |
| 99 | + target_path = claim.target or "" |
| 100 | + target_file = query.store.get_node(GraphNode.make_id(NodeKind.FILE, target_path)) |
| 101 | + if target_file is None: |
| 102 | + return ClaimCheck( |
| 103 | + claim=claim, |
| 104 | + status=CheckStatus.CONTRADICTED, |
| 105 | + confidence=1.0, |
| 106 | + explanation=f"no file at {target_path!r} found in the graph", |
| 107 | + ) |
| 108 | + |
| 109 | + for subject_node in subject_nodes: |
| 110 | + subject_file_id = GraphNode.make_id(NodeKind.FILE, subject_node.path) |
| 111 | + imports = query.store.neighbours(subject_file_id, kinds=[EdgeKind.IMPORTS], direction="out") |
| 112 | + if any(node.id == target_file.id for node in imports): |
| 113 | + return ClaimCheck( |
| 114 | + claim=claim, |
| 115 | + status=CheckStatus.SUPPORTED, |
| 116 | + confidence=1.0, |
| 117 | + explanation=f"{subject_node.path!r} imports {target_path!r}", |
| 118 | + evidence=[Evidence(kind="file", locator=subject_node.path)], |
| 119 | + ) |
| 120 | + |
| 121 | + return ClaimCheck( |
| 122 | + claim=claim, |
| 123 | + status=CheckStatus.CONTRADICTED, |
| 124 | + confidence=0.9, |
| 125 | + explanation=(f"the file defining {claim.subject!r} does not import {target_path!r}"), |
| 126 | + ) |
| 127 | + |
| 128 | + |
76 | 129 | def check_symbol_relation(claim: Claim, query: GraphQuery) -> ClaimCheck: |
77 | 130 | """Does the graph actually contain the claimed relationship?""" |
| 131 | + if looks_like_path(claim.target or ""): |
| 132 | + return check_symbol_calls_file(claim, query) |
| 133 | + |
78 | 134 | edge_kind = _RELATION_EDGE_KINDS.get(claim.relation or "") |
79 | 135 | if edge_kind is None: |
80 | 136 | return ClaimCheck( |
|
0 commit comments