sourcecode 2.5.3__py3-none-any.whl → 2.5.5__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.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +136 -23
- sourcecode/classifier.py +4 -2
- sourcecode/detectors/java.py +27 -10
- sourcecode/reconciliation.py +346 -0
- sourcecode/repository_ir.py +255 -39
- sourcecode/spring_impact.py +157 -17
- sourcecode/summarizer.py +56 -15
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/METADATA +1 -1
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/RECORD +13 -12
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""reconciliation.py — Evidence Reconciliation Layer (F-1).
|
|
2
|
+
|
|
3
|
+
Cross-validates independent evidence views BEFORE a result is narrated:
|
|
4
|
+
|
|
5
|
+
evidence views → consistency rules → positively-detected contradictions
|
|
6
|
+
|
|
7
|
+
Each analyzer (call graph, endpoint index, …) is treated as an evidence
|
|
8
|
+
provider whose view may be incomplete. A reconciliation rule fires only on a
|
|
9
|
+
*provable* self-contradiction between two views — never on absence alone —
|
|
10
|
+
and its output downgrades confidence and names the gap instead of letting the
|
|
11
|
+
narrative assert an answer the evidence does not support.
|
|
12
|
+
|
|
13
|
+
Rules are pure functions over frozensets of symbols: no I/O, no parsing, no
|
|
14
|
+
framework-name branching (annotation tables come from the open-standard
|
|
15
|
+
declarative sets in repository_ir — VAI-safe). New rules append to the
|
|
16
|
+
registry of the relevant query surface; consumers surface each finding as a
|
|
17
|
+
blind spot + warning.
|
|
18
|
+
|
|
19
|
+
Rule registry (impact queries):
|
|
20
|
+
endpoint_coverage — a class in the call chain declares HTTP handler
|
|
21
|
+
methods (Spring MVC / JAX-RS verb annotations) but the endpoint index
|
|
22
|
+
modeled NO route for it. The call graph and the endpoint index are
|
|
23
|
+
contradicting each other; `endpoints_affected` under-reports real HTTP
|
|
24
|
+
exposure (e.g. JAX-RS sub-resource locator chains the route extractor
|
|
25
|
+
cannot compose). First shipped instance of F-1 (SIM-2, keycloak
|
|
26
|
+
simulacro 2026-07-16).
|
|
27
|
+
|
|
28
|
+
Rule registry (extraction):
|
|
29
|
+
parse_coverage — a scanned Java file provably declares a type (masked
|
|
30
|
+
source matches a type-declaration keyword) but symbol extraction
|
|
31
|
+
produced NOTHING for it. The file inventory and the symbol index are
|
|
32
|
+
contradicting each other; every symbol in that file is silently
|
|
33
|
+
invisible to the graph, callers and endpoints (P1-E: legal-Java
|
|
34
|
+
declarations the regex extractor cannot bind, e.g. `public class\n
|
|
35
|
+
Name` split across lines, Unicode type identifiers).
|
|
36
|
+
|
|
37
|
+
Rule registry (architecture):
|
|
38
|
+
layer_language_coherence — a directory-name-matched layer holds NO file in
|
|
39
|
+
the runtime that carries the rest of the claimed pattern, while being
|
|
40
|
+
owned by a DIFFERENT runtime. The layer table and the file-language
|
|
41
|
+
census are contradicting each other: the "architecture" spans two
|
|
42
|
+
subsystems (SIM-3: keycloak's "MVC … view layer" was a React SPA, zero
|
|
43
|
+
Java files, sitting beside a 1088-file Java model).
|
|
44
|
+
|
|
45
|
+
Never raises. Empty evidence → no findings.
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import re
|
|
50
|
+
from dataclasses import dataclass
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class ReconciliationFinding:
|
|
56
|
+
"""One positively-detected contradiction between two evidence views."""
|
|
57
|
+
rule: str # stable rule id, e.g. "endpoint_coverage"
|
|
58
|
+
symbols: tuple[str, ...] # symbols the contradiction is anchored to
|
|
59
|
+
detail: str # one-sentence human/agent-readable statement
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict:
|
|
62
|
+
return {
|
|
63
|
+
"rule": self.rule,
|
|
64
|
+
"symbols": list(self.symbols),
|
|
65
|
+
"detail": self.detail,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def http_handler_classes(cir) -> frozenset[str]:
|
|
70
|
+
"""Classes with at least one method carrying an HTTP handler annotation.
|
|
71
|
+
|
|
72
|
+
Independent evidence of "this class serves HTTP" derived straight from the
|
|
73
|
+
per-symbol annotation facts — deliberately NOT derived from the endpoint
|
|
74
|
+
index, so the two views can be reconciled against each other.
|
|
75
|
+
"""
|
|
76
|
+
from sourcecode.repository_ir import _ENDPOINT_ANNOTATIONS
|
|
77
|
+
|
|
78
|
+
raw = getattr(cir, "_raw_ir", None) or {}
|
|
79
|
+
nodes = (raw.get("graph") or {}).get("nodes") or []
|
|
80
|
+
out: set[str] = set()
|
|
81
|
+
for node in nodes:
|
|
82
|
+
if not isinstance(node, dict):
|
|
83
|
+
continue
|
|
84
|
+
fqn = node.get("fqn") or ""
|
|
85
|
+
if "#" not in fqn:
|
|
86
|
+
continue
|
|
87
|
+
anns = node.get("annotations") or []
|
|
88
|
+
if any(a in _ENDPOINT_ANNOTATIONS for a in anns):
|
|
89
|
+
out.add(fqn.split("#", 1)[0])
|
|
90
|
+
return frozenset(out)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _rule_endpoint_coverage(
|
|
94
|
+
chain_classes: frozenset[str],
|
|
95
|
+
handler_classes: frozenset[str],
|
|
96
|
+
modeled_classes: frozenset[str],
|
|
97
|
+
) -> list[ReconciliationFinding]:
|
|
98
|
+
"""Chain class declares HTTP handlers but the endpoint index has no route for it.
|
|
99
|
+
|
|
100
|
+
Fires only on total absence from the index (extractor could not model ANY
|
|
101
|
+
route for a class that provably declares handlers). A class that IS in the
|
|
102
|
+
index but contributes only a subset of its endpoints to a query is the
|
|
103
|
+
intentional method-precision rule, not a contradiction — never flagged.
|
|
104
|
+
"""
|
|
105
|
+
unmapped = sorted((chain_classes & handler_classes) - modeled_classes)
|
|
106
|
+
if not unmapped:
|
|
107
|
+
return []
|
|
108
|
+
return [
|
|
109
|
+
ReconciliationFinding(
|
|
110
|
+
rule="endpoint_coverage",
|
|
111
|
+
symbols=tuple(unmapped),
|
|
112
|
+
detail=(
|
|
113
|
+
f"{len(unmapped)} class(es) in the call chain declare HTTP handler "
|
|
114
|
+
"methods but the endpoint index modeled no route for them — "
|
|
115
|
+
"endpoints_affected under-reports real HTTP exposure."
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def reconcile_impact_evidence(
|
|
122
|
+
*,
|
|
123
|
+
chain_classes: frozenset[str],
|
|
124
|
+
handler_classes: frozenset[str],
|
|
125
|
+
modeled_classes: frozenset[str],
|
|
126
|
+
) -> list[ReconciliationFinding]:
|
|
127
|
+
"""Run all impact-query reconciliation rules. Never raises.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
chain_classes: classes of every seed + caller in the resolved chain.
|
|
131
|
+
handler_classes: classes with HTTP-handler annotation evidence
|
|
132
|
+
(from http_handler_classes(cir)).
|
|
133
|
+
modeled_classes: classes the endpoint index has ≥1 route for
|
|
134
|
+
(endpoint_index.controller_fqns).
|
|
135
|
+
"""
|
|
136
|
+
findings: list[ReconciliationFinding] = []
|
|
137
|
+
try:
|
|
138
|
+
findings.extend(
|
|
139
|
+
_rule_endpoint_coverage(chain_classes, handler_classes, modeled_classes)
|
|
140
|
+
)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
return findings
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Extraction-time reconciliation (file inventory × symbol index)
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
# A type-declaration keyword followed by the start of an identifier. Applied to
|
|
151
|
+
# COMMENT/STRING-MASKED source only, so `/* class Ghost */` and "class" inside
|
|
152
|
+
# a string literal never match. `record` is a contextual keyword; requiring a
|
|
153
|
+
# following identifier keeps `record = ...` assignments out, and the rule is
|
|
154
|
+
# only ever consulted for files that yielded zero symbols — where a stray
|
|
155
|
+
# match would still be pointing at real, unmodeled Java.
|
|
156
|
+
_TYPE_DECL_RE = re.compile(r"\b(?:class|interface|enum|record)\s+\w")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def declares_java_type(masked_source: str) -> bool:
|
|
160
|
+
"""True if comment/string-masked Java source declares a type.
|
|
161
|
+
|
|
162
|
+
The caller masks (repository_ir._mask_java) so this stays a pure text
|
|
163
|
+
predicate with no parsing dependency.
|
|
164
|
+
"""
|
|
165
|
+
return bool(_TYPE_DECL_RE.search(masked_source))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _rule_parse_coverage(
|
|
169
|
+
type_decl_files: frozenset[str],
|
|
170
|
+
extracted_files: frozenset[str],
|
|
171
|
+
) -> list[ReconciliationFinding]:
|
|
172
|
+
"""File declares a type but symbol extraction produced nothing for it.
|
|
173
|
+
|
|
174
|
+
Fires only on total extraction absence for the file — a file that yielded
|
|
175
|
+
at least one symbol is never flagged (partial member loss is a different,
|
|
176
|
+
weaker signal this rule does not claim to detect).
|
|
177
|
+
"""
|
|
178
|
+
lost = sorted(type_decl_files - extracted_files)
|
|
179
|
+
if not lost:
|
|
180
|
+
return []
|
|
181
|
+
return [
|
|
182
|
+
ReconciliationFinding(
|
|
183
|
+
rule="parse_coverage",
|
|
184
|
+
symbols=tuple(lost),
|
|
185
|
+
detail=(
|
|
186
|
+
f"{len(lost)} Java file(s) declare a type but produced no symbols — "
|
|
187
|
+
"their classes and methods are invisible to the graph, callers and "
|
|
188
|
+
"endpoints."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Architecture reconciliation (layer table × file-language census)
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
# Source extension → RUNTIME FAMILY. Grouping by runtime, not by extension, is
|
|
199
|
+
# what makes the rule correct: a Kotlin `view/` beside Java `model/` is ONE
|
|
200
|
+
# system (same runtime, direct interop) and must not be called a contradiction,
|
|
201
|
+
# while a TypeScript `view/` beside them is a separate program. Extensions
|
|
202
|
+
# absent here (.html/.ftl/.jsp/.css/.xml …) are deliberately unmapped: a
|
|
203
|
+
# template or asset carries no runtime of its own, so it can never contradict a
|
|
204
|
+
# claim — that carve-out is what keeps a genuine Spring-MVC `templates/` view
|
|
205
|
+
# layer intact.
|
|
206
|
+
_RUNTIME_FAMILY: dict[str, str] = {
|
|
207
|
+
".java": "jvm", ".kt": "jvm", ".kts": "jvm", ".scala": "jvm", ".groovy": "jvm",
|
|
208
|
+
".js": "js", ".jsx": "js", ".ts": "js", ".tsx": "js", ".mjs": "js", ".cjs": "js",
|
|
209
|
+
".py": "python",
|
|
210
|
+
".go": "go",
|
|
211
|
+
".rs": "rust",
|
|
212
|
+
".rb": "ruby",
|
|
213
|
+
".cs": "dotnet",
|
|
214
|
+
".php": "php",
|
|
215
|
+
".swift": "swift",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Patterns that describe REPO COMPOSITION rather than one program's internal
|
|
219
|
+
# architecture — their layers are SUPPOSED to span runtimes, so the coherence
|
|
220
|
+
# rule must never fire on them. (mvc/layered/onion/hexagonal/clean are claims
|
|
221
|
+
# about a single program and must hold together in one runtime.)
|
|
222
|
+
_CROSS_RUNTIME_PATTERNS: frozenset[str] = frozenset({
|
|
223
|
+
"fullstack", "microservices", "monorepo",
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def runtime_census(layer_files: dict[str, list[str]]) -> dict[str, dict[str, int]]:
|
|
228
|
+
"""{layer -> {runtime_family -> file count}} — the language evidence view.
|
|
229
|
+
|
|
230
|
+
Files whose extension carries no runtime (templates, assets, config) are
|
|
231
|
+
simply absent from the census; a layer made only of them yields {}.
|
|
232
|
+
"""
|
|
233
|
+
out: dict[str, dict[str, int]] = {}
|
|
234
|
+
for layer, files in layer_files.items():
|
|
235
|
+
counts: dict[str, int] = {}
|
|
236
|
+
for f in files:
|
|
237
|
+
fam = _RUNTIME_FAMILY.get(Path(str(f)).suffix.lower())
|
|
238
|
+
if fam:
|
|
239
|
+
counts[fam] = counts.get(fam, 0) + 1
|
|
240
|
+
out[layer] = counts
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _rule_layer_language_coherence(
|
|
245
|
+
pattern: str,
|
|
246
|
+
census: dict[str, dict[str, int]],
|
|
247
|
+
repo_runtimes: dict[str, int],
|
|
248
|
+
) -> list[ReconciliationFinding]:
|
|
249
|
+
"""A claimed layer is owned by a different runtime than the repo it describes.
|
|
250
|
+
|
|
251
|
+
The dominant runtime is taken from the WHOLE repo, not from the matched
|
|
252
|
+
layers: which directories happen to match a keyword table is exactly the
|
|
253
|
+
thing under suspicion, so it cannot also be the arbiter. (Measured: on a
|
|
254
|
+
Java backend beside a React SPA, layer-local dominance can invert — the SPA
|
|
255
|
+
out-matches the backend's one matched layer and the JAVA layer gets flagged.)
|
|
256
|
+
|
|
257
|
+
A layer is INCOHERENT iff it holds ZERO files of the repo's runtime AND at
|
|
258
|
+
least one file of another — it is a different program, not a facet of this
|
|
259
|
+
one. A layer with no runtime files at all (templates, assets) is never
|
|
260
|
+
flagged: it does not contradict, it just does not attest.
|
|
261
|
+
|
|
262
|
+
Silent for cross-runtime patterns, whose layers are meant to span runtimes.
|
|
263
|
+
"""
|
|
264
|
+
if pattern in _CROSS_RUNTIME_PATTERNS or len(census) < 2:
|
|
265
|
+
return []
|
|
266
|
+
if len(repo_runtimes) < 2:
|
|
267
|
+
return [] # single-runtime repo — nothing to contradict
|
|
268
|
+
|
|
269
|
+
dominant = max(sorted(repo_runtimes), key=lambda f: repo_runtimes[f])
|
|
270
|
+
|
|
271
|
+
incoherent = sorted(
|
|
272
|
+
layer for layer, counts in census.items()
|
|
273
|
+
if not counts.get(dominant) and any(counts.values())
|
|
274
|
+
)
|
|
275
|
+
if not incoherent:
|
|
276
|
+
return []
|
|
277
|
+
return [
|
|
278
|
+
ReconciliationFinding(
|
|
279
|
+
rule="layer_language_coherence",
|
|
280
|
+
symbols=tuple(incoherent),
|
|
281
|
+
detail=(
|
|
282
|
+
f"{len(incoherent)} layer(s) of the '{pattern}' claim hold no "
|
|
283
|
+
f"{dominant} file while the rest of the pattern is {dominant} — "
|
|
284
|
+
"the claim spans two separate programs, so it does not describe "
|
|
285
|
+
"one architecture."
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def reconcile_architecture_evidence(
|
|
292
|
+
*,
|
|
293
|
+
pattern: str,
|
|
294
|
+
layer_files: dict[str, list[str]],
|
|
295
|
+
repo_files: "list[str]",
|
|
296
|
+
) -> list[ReconciliationFinding]:
|
|
297
|
+
"""Run architecture reconciliation rules. Never raises.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
pattern: candidate pattern name (e.g. "mvc").
|
|
301
|
+
layer_files: {layer -> files matched for that layer}.
|
|
302
|
+
repo_files: every source file considered — supplies the repo's own
|
|
303
|
+
runtime, the arbiter of which layers belong to the claim.
|
|
304
|
+
"""
|
|
305
|
+
findings: list[ReconciliationFinding] = []
|
|
306
|
+
try:
|
|
307
|
+
repo_runtimes = runtime_census({"_repo": list(repo_files)})["_repo"]
|
|
308
|
+
findings.extend(
|
|
309
|
+
_rule_layer_language_coherence(
|
|
310
|
+
pattern, runtime_census(layer_files), repo_runtimes
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
return findings
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def incoherent_layers(
|
|
319
|
+
pattern: str, layer_files: dict[str, list[str]], repo_files: "list[str]"
|
|
320
|
+
) -> frozenset[str]:
|
|
321
|
+
"""Layer names the coherence rule rejects for `pattern` (convenience view)."""
|
|
322
|
+
out: set[str] = set()
|
|
323
|
+
for f in reconcile_architecture_evidence(
|
|
324
|
+
pattern=pattern, layer_files=layer_files, repo_files=repo_files
|
|
325
|
+
):
|
|
326
|
+
out.update(f.symbols)
|
|
327
|
+
return frozenset(out)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def reconcile_extraction_evidence(
|
|
331
|
+
*,
|
|
332
|
+
type_decl_files: frozenset[str],
|
|
333
|
+
extracted_files: frozenset[str],
|
|
334
|
+
) -> list[ReconciliationFinding]:
|
|
335
|
+
"""Run extraction-time reconciliation rules. Never raises.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
type_decl_files: files whose masked source declares a Java type.
|
|
339
|
+
extracted_files: files symbol extraction produced ≥1 symbol for.
|
|
340
|
+
"""
|
|
341
|
+
findings: list[ReconciliationFinding] = []
|
|
342
|
+
try:
|
|
343
|
+
findings.extend(_rule_parse_coverage(type_decl_files, extracted_files))
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
return findings
|