sourcecode 3.2.0__py3-none-any.whl → 3.2.2__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.
Potentially problematic release.
This version of sourcecode might be problematic. Click here for more details.
- sourcecode/__init__.py +1 -1
- sourcecode/cache.py +104 -0
- sourcecode/cache_model.py +295 -0
- sourcecode/chain_rules.py +49 -8
- sourcecode/cli.py +419 -205
- sourcecode/confidence_analyzer.py +10 -6
- sourcecode/context_cache.py +7 -27
- sourcecode/deployment_prefix.py +85 -0
- sourcecode/facts/__init__.py +71 -0
- sourcecode/facts/registry.json +158 -0
- sourcecode/license.py +1 -1
- sourcecode/mcp/registry.py +1 -1
- sourcecode/mcp/server.py +3 -3
- sourcecode/metrics_analyzer.py +8 -5
- sourcecode/posture.py +78 -6
- sourcecode/prepare_context.py +51 -26
- sourcecode/reference_facts.py +307 -0
- sourcecode/ris.py +27 -1
- sourcecode/serializer.py +36 -9
- sourcecode/spring_profiles.py +93 -13
- sourcecode/telemetry/__init__.py +4 -3
- sourcecode/telemetry/config.py +31 -23
- sourcecode/telemetry/consent.py +20 -16
- sourcecode/test_sources.py +178 -0
- {sourcecode-3.2.0.dist-info → sourcecode-3.2.2.dist-info}/METADATA +128 -14
- {sourcecode-3.2.0.dist-info → sourcecode-3.2.2.dist-info}/RECORD +29 -24
- {sourcecode-3.2.0.dist-info → sourcecode-3.2.2.dist-info}/WHEEL +0 -0
- {sourcecode-3.2.0.dist-info → sourcecode-3.2.2.dist-info}/entry_points.txt +0 -0
- {sourcecode-3.2.0.dist-info → sourcecode-3.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -315,12 +315,16 @@ class ConfidenceAnalyzer:
|
|
|
315
315
|
_test_exclude_tokens = frozenset({"test", "tests", "spec", "specs", "it", "testing"})
|
|
316
316
|
_tests_deliberately_excluded = bool(_extra_exc & _test_exclude_tokens)
|
|
317
317
|
|
|
318
|
+
# One authority for which files are tests (ADR-0008 R1): the substring
|
|
319
|
+
# rule this replaces counted production classes living in a package
|
|
320
|
+
# named `test` as tests, so a repository with none reported "2 test
|
|
321
|
+
# files" while `has_tests` in the same document said the same wrong
|
|
322
|
+
# thing from a second derivation.
|
|
323
|
+
from sourcecode.test_sources import analyze_test_sources
|
|
324
|
+
|
|
318
325
|
_java_all = [p for p in sm.file_paths if p.endswith(".java")]
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if "/test/" in p.replace("\\", "/") or "/tests/" in p.replace("\\", "/")
|
|
322
|
-
or Path(p).stem.endswith(("Test", "Tests", "IT", "Spec"))
|
|
323
|
-
]
|
|
326
|
+
_java_test_facts = analyze_test_sources(_java_all, extensions=(".java",))
|
|
327
|
+
_java_tests = list(_java_test_facts.test_files)
|
|
324
328
|
_java_prod = [p for p in _java_all if p not in set(_java_tests)]
|
|
325
329
|
if _java_prod and len(_java_prod) >= 10:
|
|
326
330
|
_ratio = len(_java_tests) / len(_java_prod)
|
|
@@ -341,7 +345,7 @@ class ConfidenceAnalyzer:
|
|
|
341
345
|
reason=(
|
|
342
346
|
f"Backend test coverage critical: {len(_java_tests)} test files "
|
|
343
347
|
f"for {len(_java_prod)} Java files "
|
|
344
|
-
f"({_ratio:.1%})"
|
|
348
|
+
f"({_ratio:.1%}) — {_java_test_facts.basis}"
|
|
345
349
|
),
|
|
346
350
|
impact="high",
|
|
347
351
|
))
|
sourcecode/context_cache.py
CHANGED
|
@@ -190,34 +190,14 @@ def _git_head(repo_root: Path) -> str:
|
|
|
190
190
|
def _worktree_signature(repo_root: Path) -> str:
|
|
191
191
|
"""Deterministic fingerprint of the *exact* tree state.
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
Thin alias of the one authority, :func:`sourcecode.cache.worktree_signature`.
|
|
194
|
+
It used to be a second implementation living here, which is how the two caches
|
|
195
|
+
ended up invalidating on different facts: this one on the tree, the snapshot
|
|
196
|
+
cache on the committed HEAD alone.
|
|
197
197
|
"""
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
status = subprocess.run(
|
|
203
|
-
["git", "-C", str(repo_root), "status", "--porcelain"],
|
|
204
|
-
capture_output=True, text=True, timeout=5,
|
|
205
|
-
)
|
|
206
|
-
porcelain = status.stdout if status.returncode == 0 else ""
|
|
207
|
-
except Exception:
|
|
208
|
-
porcelain = ""
|
|
209
|
-
if not porcelain.strip():
|
|
210
|
-
return head # clean tree — HEAD fully describes it
|
|
211
|
-
try:
|
|
212
|
-
diff = subprocess.run(
|
|
213
|
-
["git", "-C", str(repo_root), "diff", "HEAD"],
|
|
214
|
-
capture_output=True, text=True, timeout=10,
|
|
215
|
-
)
|
|
216
|
-
diff_txt = diff.stdout if diff.returncode == 0 else ""
|
|
217
|
-
except Exception:
|
|
218
|
-
diff_txt = ""
|
|
219
|
-
dirty = hashlib.sha256((porcelain + "\x00" + diff_txt).encode("utf-8", "replace")).hexdigest()[:16]
|
|
220
|
-
return f"{head}+dirty:{dirty}"
|
|
198
|
+
from sourcecode.cache import worktree_signature
|
|
199
|
+
|
|
200
|
+
return worktree_signature(repo_root)
|
|
221
201
|
|
|
222
202
|
|
|
223
203
|
def _options_fingerprint(options: Optional[dict[str, Any]]) -> str:
|
sourcecode/deployment_prefix.py
CHANGED
|
@@ -35,6 +35,11 @@ _SERVLET_PATH_KEYS: tuple[str, ...] = (
|
|
|
35
35
|
"server.servlet-path", # Boot 1.x spelling
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
#: The same keys, exported: every consumer that needs to ask a resolved-property
|
|
39
|
+
#: authority for a prefix reads them from here rather than spelling them again.
|
|
40
|
+
CONTEXT_PATH_KEYS: tuple[str, ...] = _CONTEXT_PATH_KEYS
|
|
41
|
+
SERVLET_PATH_KEYS: tuple[str, ...] = _SERVLET_PATH_KEYS
|
|
42
|
+
|
|
38
43
|
# Published multi-document / profile activation keys.
|
|
39
44
|
_PROFILE_KEYS: tuple[str, ...] = (
|
|
40
45
|
"spring.config.activate.on-profile",
|
|
@@ -227,3 +232,83 @@ def detect_deployment_prefix(root: Path) -> DeploymentPrefix:
|
|
|
227
232
|
|
|
228
233
|
result.sources.sort(key=lambda s: (s.file, s.key, s.profile or "", s.value))
|
|
229
234
|
return result
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def normalize_prefix(value: str) -> str:
|
|
238
|
+
"""`api/` → `/api`; empty or `/` → `""`. A prefix is a path segment chain."""
|
|
239
|
+
cleaned = re.sub(r"/+", "/", (value or "").strip()).strip()
|
|
240
|
+
cleaned = "/" + cleaned.strip("/")
|
|
241
|
+
return "" if cleaned == "/" else cleaned
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True)
|
|
245
|
+
class ServletPrefix:
|
|
246
|
+
"""The DispatcherServlet path, and whether it could be decided at all.
|
|
247
|
+
|
|
248
|
+
Security matchers never see the servlet CONTEXT path — the container strips
|
|
249
|
+
it before the filter chain runs — but a servlet path declared by
|
|
250
|
+
``spring.mvc.servlet.path`` IS part of what an ant matcher matches. The two
|
|
251
|
+
prefixes are therefore different facts, and only this one belongs in a
|
|
252
|
+
request-chain answer.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
value: str = ""
|
|
256
|
+
#: Set when a declaration exists but its value could not be decided
|
|
257
|
+
#: (placeholder, or profile documents that disagree). The prefix is then
|
|
258
|
+
#: unknown, never assumed empty.
|
|
259
|
+
undecidable_reason: Optional[str] = None
|
|
260
|
+
key: Optional[str] = None
|
|
261
|
+
source: Optional[str] = None
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def decided(self) -> bool:
|
|
265
|
+
return self.undecidable_reason is None
|
|
266
|
+
|
|
267
|
+
def to_dict(self) -> dict:
|
|
268
|
+
out: dict = {"servlet_path": self.value, "decided": self.decided}
|
|
269
|
+
if self.key:
|
|
270
|
+
out["key"] = self.key
|
|
271
|
+
if self.source:
|
|
272
|
+
out["source"] = self.source
|
|
273
|
+
if self.undecidable_reason:
|
|
274
|
+
out["undecidable_reason"] = self.undecidable_reason
|
|
275
|
+
return out
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def servlet_prefix_from_properties(properties: Any) -> ServletPrefix:
|
|
279
|
+
"""The servlet path a resolved-property authority states, or why it cannot.
|
|
280
|
+
|
|
281
|
+
`properties` is anything with `lookup(key)` / `blocked(key)` — the resolved
|
|
282
|
+
view for a profile set, so a servlet path declared only under `prod` applies
|
|
283
|
+
exactly when `prod` was asked about. An unresolved placeholder is reported
|
|
284
|
+
undecidable rather than treated as "no prefix": the difference between an
|
|
285
|
+
unknown prefix and no prefix is the difference between "cannot say" and a
|
|
286
|
+
confident wrong answer about which rule matches.
|
|
287
|
+
"""
|
|
288
|
+
for key in SERVLET_PATH_KEYS:
|
|
289
|
+
blocked = None
|
|
290
|
+
try:
|
|
291
|
+
blocked = properties.blocked(key)
|
|
292
|
+
except Exception:
|
|
293
|
+
blocked = None
|
|
294
|
+
if blocked:
|
|
295
|
+
return ServletPrefix(undecidable_reason=str(blocked), key=key)
|
|
296
|
+
try:
|
|
297
|
+
found = properties.lookup(key)
|
|
298
|
+
except Exception:
|
|
299
|
+
found = None
|
|
300
|
+
if found is None:
|
|
301
|
+
continue
|
|
302
|
+
raw = str(getattr(found, "value", "") or "")
|
|
303
|
+
if _PLACEHOLDER_RE.search(raw):
|
|
304
|
+
return ServletPrefix(
|
|
305
|
+
undecidable_reason=f"value carries an unresolved placeholder: {raw}",
|
|
306
|
+
key=key,
|
|
307
|
+
source=str(getattr(found, "source", "") or "") or None,
|
|
308
|
+
)
|
|
309
|
+
return ServletPrefix(
|
|
310
|
+
value=normalize_prefix(raw),
|
|
311
|
+
key=key,
|
|
312
|
+
source=str(getattr(found, "source", "") or "") or None,
|
|
313
|
+
)
|
|
314
|
+
return ServletPrefix()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""facts — the registry of facts that have exactly one authority (ADR-0008 §4).
|
|
2
|
+
|
|
3
|
+
A fact in this registry is a question the product answers with a number, a name
|
|
4
|
+
or a verdict, and which more than one command emits. The registry states who
|
|
5
|
+
derives it, what it means, and which modules are allowed to emit it. R13: a new
|
|
6
|
+
consumer that emits a registered fact without binding to its authority fails
|
|
7
|
+
review, and a new fact is registered in the commit that introduces it.
|
|
8
|
+
|
|
9
|
+
The registry is data, shipped inside the package, so an installed version can be
|
|
10
|
+
asked what its own contract is — the same reason the response-envelope schema is
|
|
11
|
+
shipped rather than documented.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
REGISTRY_PATH = Path(__file__).resolve().parent / "registry.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Fact:
|
|
24
|
+
"""One registered fact and the seam that made it one."""
|
|
25
|
+
|
|
26
|
+
fact: str
|
|
27
|
+
authority: str
|
|
28
|
+
definition: str
|
|
29
|
+
consumers: "tuple[str, ...]" = ()
|
|
30
|
+
parity_test: str = ""
|
|
31
|
+
stats_key: str = ""
|
|
32
|
+
seam: str = ""
|
|
33
|
+
was: str = ""
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def authority_module(self) -> str:
|
|
37
|
+
return self.authority.split(":", 1)[0]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def authority_symbol(self) -> str:
|
|
41
|
+
return self.authority.split(":", 1)[1] if ":" in self.authority else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_registry() -> dict:
|
|
45
|
+
"""The registry as published, unmodified."""
|
|
46
|
+
return json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def registered_facts() -> "tuple[Fact, ...]":
|
|
50
|
+
"""Every registered fact, in registry order."""
|
|
51
|
+
return tuple(
|
|
52
|
+
Fact(
|
|
53
|
+
fact=str(entry["fact"]),
|
|
54
|
+
authority=str(entry["authority"]),
|
|
55
|
+
definition=str(entry["definition"]),
|
|
56
|
+
consumers=tuple(entry.get("consumers") or ()),
|
|
57
|
+
parity_test=str(entry.get("parity_test") or ""),
|
|
58
|
+
stats_key=str(entry.get("stats_key") or ""),
|
|
59
|
+
seam=str(entry.get("seam") or ""),
|
|
60
|
+
was=str(entry.get("was") or ""),
|
|
61
|
+
)
|
|
62
|
+
for entry in load_registry().get("facts") or []
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def fact(name: str) -> "Fact":
|
|
67
|
+
"""One registered fact by name. Raises KeyError when it is not registered."""
|
|
68
|
+
for entry in registered_facts():
|
|
69
|
+
if entry.fact == name:
|
|
70
|
+
return entry
|
|
71
|
+
raise KeyError(name)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
{
|
|
2
|
+
"registry_version": "facts-v1",
|
|
3
|
+
"note": "ADR-0008 R11. One authority per fact. `authority` is module:symbol; `consumers` are the modules allowed to emit the fact, and each MUST import the authority rather than re-derive it (R13). `parity_test` is the assertion that keeps every emitter agreeing (R12).",
|
|
4
|
+
"facts": [
|
|
5
|
+
{
|
|
6
|
+
"fact": "transactional_boundaries_touched",
|
|
7
|
+
"authority": "sourcecode.spring_semantic:boundaries_declared_within",
|
|
8
|
+
"definition": "Transaction boundaries declared within a symbol's own body plus those declared by its callers — the blast radius of changing it.",
|
|
9
|
+
"stats_key": "transactional_boundaries_touched",
|
|
10
|
+
"consumers": [
|
|
11
|
+
"sourcecode.repository_ir"
|
|
12
|
+
],
|
|
13
|
+
"parity_test": "tests/test_tx_boundary_authority.py",
|
|
14
|
+
"seam": "M7 seam 1",
|
|
15
|
+
"was": "A graph node role `transaction_boundary` no producer ever assigns, so the figure was structurally always 0.",
|
|
16
|
+
"unresolved_answer": null,
|
|
17
|
+
"unresolved_note": "The tx index either declares a boundary within the scanned symbols or it does not; there is no third state to report. What is unknown is whether a boundary exists in code this analyzer never saw, and that is stated as analysis scope, not as this fact's value."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"fact": "transactional_boundaries_reaching",
|
|
21
|
+
"authority": "sourcecode.spring_semantic:boundaries_declared_within",
|
|
22
|
+
"definition": "Transaction boundaries declared by the callers that reach a symbol — the figure `retrieve transactions-reaching` reports. A different question from `_touched`, so it carries a different name (ADR-0008 R3).",
|
|
23
|
+
"stats_key": "transactional_boundaries_reaching",
|
|
24
|
+
"consumers": [
|
|
25
|
+
"sourcecode.repository_ir",
|
|
26
|
+
"sourcecode.retrieval.steps_graph"
|
|
27
|
+
],
|
|
28
|
+
"parity_test": "tests/test_tx_boundary_authority.py",
|
|
29
|
+
"seam": "M7 seam 1",
|
|
30
|
+
"unresolved_answer": null,
|
|
31
|
+
"unresolved_note": "Same as `transactional_boundaries_touched`: the caller set is the scope, and a boundary is declared or absent within it."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"fact": "endpoint_population",
|
|
35
|
+
"authority": "sourcecode.security_posture:endpoint_population",
|
|
36
|
+
"definition": "The denominator for every endpoint statement, with the three units named apart: endpoints, handler methods, declarations. Every partition sums to it or declares its exclusion set.",
|
|
37
|
+
"stats_key": "population",
|
|
38
|
+
"consumers": [
|
|
39
|
+
"sourcecode.security_posture"
|
|
40
|
+
],
|
|
41
|
+
"parity_test": "tests/test_endpoint_count_parity.py",
|
|
42
|
+
"seam": "M7 seam 2",
|
|
43
|
+
"was": "Three incompatible denominators in one document; only the rollup reconciled.",
|
|
44
|
+
"unresolved_answer": null,
|
|
45
|
+
"unresolved_note": "A population is a count of what was parsed. Where a route could not be modelled it is excluded and the exclusion set is published, so the denominator never carries an undecided member."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"fact": "endpoint_security_surface",
|
|
49
|
+
"authority": "sourcecode.security_posture:endpoint_security_surface",
|
|
50
|
+
"definition": "What guards an endpoint, as a verdict plus a confidence — `coverage_unknown` where a custom gate is undetermined, never `none_detected`.",
|
|
51
|
+
"consumers": [
|
|
52
|
+
"sourcecode.spring_impact"
|
|
53
|
+
],
|
|
54
|
+
"parity_test": "tests/test_endpoint_security_surface.py",
|
|
55
|
+
"seam": "M7 seam 3",
|
|
56
|
+
"was": "`impact-chain` printed `none_detected` for endpoints the same run classified `protected_custom`.",
|
|
57
|
+
"unresolved_answer": "coverage_unknown",
|
|
58
|
+
"unresolved_note": "Absence of a posture entry is absence of evidence, never `none_detected`."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"fact": "integration_coordinates",
|
|
62
|
+
"authority": "sourcecode.integration_coordinates:declared_integration_coordinates",
|
|
63
|
+
"definition": "What the build declares the application integrates with, reported beside what the source scan observed — `declared_clients` and `kinds_declared_not_observed`.",
|
|
64
|
+
"consumers": [
|
|
65
|
+
"sourcecode.cli",
|
|
66
|
+
"sourcecode.retrieval.steps_intf"
|
|
67
|
+
],
|
|
68
|
+
"parity_test": "tests/test_integration_coordinates.py",
|
|
69
|
+
"seam": "M7 seam 4",
|
|
70
|
+
"was": "`export --integrations` reported 0 LDAP while the stack block listed a declared LDAP client.",
|
|
71
|
+
"unresolved_answer": null,
|
|
72
|
+
"unresolved_note": "The authority answers what the build declares, which a manifest either states or does not. The undecided case is the comparison against what the source scan observed, and it is published by the consumers as `kinds_declared_not_observed` rather than resolved either way."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"fact": "test_sources",
|
|
76
|
+
"authority": "sourcecode.test_sources:analyze_test_sources",
|
|
77
|
+
"definition": "Whether a file is a test source: it sits under a declared test source root, or matches an ecosystem naming convention. A `test` package inside a main source root is production code.",
|
|
78
|
+
"stats_key": "has_tests",
|
|
79
|
+
"consumers": [
|
|
80
|
+
"sourcecode.serializer",
|
|
81
|
+
"sourcecode.confidence_analyzer",
|
|
82
|
+
"sourcecode.metrics_analyzer",
|
|
83
|
+
"sourcecode.prepare_context"
|
|
84
|
+
],
|
|
85
|
+
"parity_test": "tests/test_test_sources_authority.py",
|
|
86
|
+
"seam": "M7 seam 5",
|
|
87
|
+
"was": "A substring rule counted production classes in a package named `test`, contradicting `analysis_gaps` in the same document.",
|
|
88
|
+
"unresolved_answer": null,
|
|
89
|
+
"unresolved_note": "Total by construction: every path gets a decided answer, because a layout either declares a test root or it does not. The unknown lives one level up — a repository with no test sources makes a COVERAGE question unanswerable, and the consumers emit `unknown` there rather than `low`."
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"fact": "type_reference_status",
|
|
93
|
+
"authority": "sourcecode.reference_facts:analyze_type_references",
|
|
94
|
+
"definition": "Whether a type is referenced: `referenced` / `unknown_framework_dispatch` / `no_static_callers`. A partition summing to the classes examined, where `no_static_callers` is absence of evidence and never a dead-code claim.",
|
|
95
|
+
"stats_key": "reference_status",
|
|
96
|
+
"consumers": [
|
|
97
|
+
"sourcecode.cli"
|
|
98
|
+
],
|
|
99
|
+
"parity_test": "tests/test_reference_facts.py",
|
|
100
|
+
"seam": "M7 seam 6",
|
|
101
|
+
"was": "`statically_unreferenced: 0` over 3 374 classes: a population that never matched its own definition, and no way to say `unknown`.",
|
|
102
|
+
"unresolved_answer": "unknown_framework_dispatch",
|
|
103
|
+
"unresolved_note": "No static caller plus a dispatch signal is unknown; a static call-graph cannot decide it."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"fact": "profile_conditional_declarations",
|
|
107
|
+
"authority": "sourcecode.spring_profiles:conditional_beans_from_sources_detailed",
|
|
108
|
+
"definition": "Which declaration each `@Profile` annotates, and the expression as written. A member-level annotation belongs to `Type#member`; an annotation whose target cannot be read is not reported at all.",
|
|
109
|
+
"consumers": [
|
|
110
|
+
"sourcecode.serializer"
|
|
111
|
+
],
|
|
112
|
+
"parity_test": "tests/test_deployment_prefix_and_profiles.py",
|
|
113
|
+
"seam": "M7 seam 7",
|
|
114
|
+
"was": "A class was listed under a profile AND its negation, contradicting `posture` in the same run.",
|
|
115
|
+
"unresolved_answer": null,
|
|
116
|
+
"unresolved_note": "An annotation whose target cannot be read is not reported at all — the fact omits what it cannot attribute rather than reporting it undecided."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"fact": "profile_activation",
|
|
120
|
+
"authority": "sourcecode.posture:resolve_beans",
|
|
121
|
+
"definition": "Whether a profile set activates a bean. Resolution — as opposed to which profile a declaration mentions — is `posture`'s answer, and it is three-valued: active, inactive, unresolved.",
|
|
122
|
+
"consumers": [
|
|
123
|
+
"sourcecode.posture"
|
|
124
|
+
],
|
|
125
|
+
"parity_test": "tests/test_deployment_prefix_and_profiles.py",
|
|
126
|
+
"seam": "M7 seam 7",
|
|
127
|
+
"unresolved_answer": "unresolved",
|
|
128
|
+
"unresolved_note": "A condition that cannot be evaluated is never folded into active or inactive."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"fact": "deployment_prefix",
|
|
132
|
+
"authority": "sourcecode.deployment_prefix:detect_deployment_prefix",
|
|
133
|
+
"definition": "The URL prefix the application is served under (`server.servlet.context-path` + `spring.mvc.servlet.path`), applied to mapping-relative paths as `effective_path`.",
|
|
134
|
+
"stats_key": "effective_path",
|
|
135
|
+
"consumers": [
|
|
136
|
+
"sourcecode.repository_ir",
|
|
137
|
+
"sourcecode.posture"
|
|
138
|
+
],
|
|
139
|
+
"parity_test": "tests/test_deployment_prefix_and_profiles.py",
|
|
140
|
+
"seam": "M7 seam 8",
|
|
141
|
+
"unresolved_answer": "unconditional",
|
|
142
|
+
"unresolved_note": "A prefix declared only under a profile, or carrying an unresolved placeholder, is published with `unconditional: false` and never silently applied to every route."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"fact": "servlet_prefix",
|
|
146
|
+
"authority": "sourcecode.deployment_prefix:servlet_prefix_from_properties",
|
|
147
|
+
"definition": "The part of the prefix a security matcher sees: the servlet path for the profile set asked about. The servlet CONTEXT path is excluded — the container strips it before the filter chain runs — so this is a different fact from `deployment_prefix`, not a view of it.",
|
|
148
|
+
"consumers": [
|
|
149
|
+
"sourcecode.posture"
|
|
150
|
+
],
|
|
151
|
+
"parity_test": "tests/test_posture.py",
|
|
152
|
+
"seam": "M7 seam 8",
|
|
153
|
+
"was": "`posture` matched chain rules against unprefixed paths and reported endpoints as covered by no rule — or, with a catch-all behind the rule, as open.",
|
|
154
|
+
"unresolved_answer": "undecidable_reason",
|
|
155
|
+
"unresolved_note": "A declared servlet path that cannot be resolved makes the matching basis unknown, and every path-dependent verdict becomes `undecided`."
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
sourcecode/license.py
CHANGED
|
@@ -396,7 +396,7 @@ _init()
|
|
|
396
396
|
# ---------------------------------------------------------------------------
|
|
397
397
|
|
|
398
398
|
def _emit_telemetry(event: str, **kw: object) -> None:
|
|
399
|
-
"""Best-effort telemetry emit.
|
|
399
|
+
"""Best-effort telemetry emit. Sends nothing unless the user opted in; never raises or blocks."""
|
|
400
400
|
try:
|
|
401
401
|
from sourcecode import telemetry as _tel
|
|
402
402
|
_tel.record(event, **kw) # type: ignore[arg-type]
|
sourcecode/mcp/registry.py
CHANGED
|
@@ -800,7 +800,7 @@ depth: BFS depth for indirect caller traversal (1–8, default: 4).
|
|
|
800
800
|
Analyzes codebase for modernization opportunities: dead zones, hotspot scores, upgrade candidates.
|
|
801
801
|
|
|
802
802
|
Maps to: ask modernize <repo_path>
|
|
803
|
-
Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (
|
|
803
|
+
Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (no static caller AND no dispatch signal found — absence of evidence, NOT confirmed dead code) + framework_dispatched (no static caller but a framework may invoke them: status unknown). Counts live in summary.reference_status, which partitions every class,
|
|
804
804
|
high_coupling_nodes, subsystem_summary, cross_module_tangles, recommendation.
|
|
805
805
|
|
|
806
806
|
Best for: refactor planning, identifying where to start, finding safe removal candidates.
|
sourcecode/mcp/server.py
CHANGED
|
@@ -33,8 +33,8 @@ def _record_tool_invocation(name: Any, success: bool, started: float) -> None:
|
|
|
33
33
|
|
|
34
34
|
Aggregate only: which of our own tools ran, whether it succeeded, and a
|
|
35
35
|
duration bucket. Never the arguments — those carry repository paths — and
|
|
36
|
-
never any result content. Honours the same opt-
|
|
37
|
-
(`ask telemetry
|
|
36
|
+
never any result content. Honours the same opt-in as every other event
|
|
37
|
+
(off until `ask telemetry enable` or SOURCECODE_TELEMETRY=1); when
|
|
38
38
|
telemetry is off, `record` returns before building anything.
|
|
39
39
|
"""
|
|
40
40
|
try:
|
|
@@ -1377,7 +1377,7 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
|
|
|
1377
1377
|
"""Analyzes codebase for modernization opportunities: dead zones, hotspot scores, upgrade candidates.
|
|
1378
1378
|
|
|
1379
1379
|
Maps to: ask modernize <repo_path>
|
|
1380
|
-
Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (
|
|
1380
|
+
Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (no static caller AND no dispatch signal found — absence of evidence, NOT confirmed dead code) + framework_dispatched (no static caller but a framework may invoke them: status unknown). Counts live in summary.reference_status, which partitions every class,
|
|
1381
1381
|
high_coupling_nodes, subsystem_summary, cross_module_tangles, recommendation.
|
|
1382
1382
|
|
|
1383
1383
|
Best for: refactor planning, identifying where to start, finding safe removal candidates.
|
sourcecode/metrics_analyzer.py
CHANGED
|
@@ -60,13 +60,16 @@ _STEM_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def is_test_file(path: str) -> bool:
|
|
63
|
-
"""Return True if path corresponds to a test file
|
|
63
|
+
"""Return True if path corresponds to a test file.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
Delegates to `test_sources.is_test_path`, the one authority for this fact
|
|
66
|
+
(ADR-0008 R1): a declared test source root (`src/test`, `src/it`, `tests/`)
|
|
67
|
+
or an ecosystem naming convention — and never a `test` package that sits
|
|
68
|
+
inside a main source root.
|
|
67
69
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
from sourcecode.test_sources import is_test_path
|
|
71
|
+
|
|
72
|
+
return is_test_path(path)
|
|
70
73
|
|
|
71
74
|
|
|
72
75
|
def infer_production_target(test_path: str) -> str | None:
|
sourcecode/posture.py
CHANGED
|
@@ -542,8 +542,29 @@ _ACCESS_DECISIONS_ORDER = (
|
|
|
542
542
|
)
|
|
543
543
|
|
|
544
544
|
|
|
545
|
+
def _servlet_prefix_from_root(root: Path) -> "ServletPrefix":
|
|
546
|
+
"""The servlet path from the repository's own configuration.
|
|
547
|
+
|
|
548
|
+
Used when no resolved-property view is supplied. It applies only the
|
|
549
|
+
unconditional declaration — a servlet path declared under a profile is a fact
|
|
550
|
+
about that profile, and this caller did not name one.
|
|
551
|
+
"""
|
|
552
|
+
from sourcecode.deployment_prefix import (
|
|
553
|
+
ServletPrefix, detect_deployment_prefix, normalize_prefix,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
detected = detect_deployment_prefix(Path(root))
|
|
558
|
+
except Exception:
|
|
559
|
+
return ServletPrefix()
|
|
560
|
+
return ServletPrefix(value=normalize_prefix(detected.servlet_path))
|
|
561
|
+
|
|
562
|
+
|
|
545
563
|
def access_projection(
|
|
546
|
-
cir: "CanonicalRepositoryIR",
|
|
564
|
+
cir: "CanonicalRepositoryIR",
|
|
565
|
+
resolution: BeanResolution,
|
|
566
|
+
root: Path,
|
|
567
|
+
properties: "Optional[object]" = None,
|
|
547
568
|
) -> "tuple[dict, dict[str, str]]":
|
|
548
569
|
"""What the active chain configuration decides for each endpoint.
|
|
549
570
|
|
|
@@ -555,9 +576,20 @@ def access_projection(
|
|
|
555
576
|
with different decisions, the answer is `undecided`: their relative order is
|
|
556
577
|
an `@Order`/bean-ordering question this analyzer does not resolve, and picking
|
|
557
578
|
one would state a security fact by coin flip.
|
|
579
|
+
|
|
580
|
+
Rules are matched against the path the filter chain receives, which is the
|
|
581
|
+
mapping-relative path prefixed by any declared `spring.mvc.servlet.path`.
|
|
582
|
+
Matching the unprefixed path instead reports `no_rule_matched` for endpoints
|
|
583
|
+
a declared matcher covers — a posture answer that under-reports protection.
|
|
558
584
|
"""
|
|
559
|
-
from sourcecode.chain_rules import extract_chain_rules,
|
|
585
|
+
from sourcecode.chain_rules import extract_chain_rules, first_match
|
|
586
|
+
from sourcecode.deployment_prefix import servlet_prefix_from_properties
|
|
560
587
|
|
|
588
|
+
servlet = (
|
|
589
|
+
servlet_prefix_from_properties(properties) if properties is not None
|
|
590
|
+
else _servlet_prefix_from_root(root)
|
|
591
|
+
)
|
|
592
|
+
prefix = servlet.value if servlet.decided else ""
|
|
561
593
|
states = _chain_state_by_file(cir, resolution)
|
|
562
594
|
rules_by_file = extract_chain_rules(root, sorted(states)) if states else {}
|
|
563
595
|
active_files = sorted(f for f in rules_by_file if states.get(f) == "active")
|
|
@@ -573,12 +605,17 @@ def access_projection(
|
|
|
573
605
|
continue
|
|
574
606
|
decisions: dict[str, dict] = {}
|
|
575
607
|
for source_file in active_files:
|
|
576
|
-
rule =
|
|
608
|
+
rule, matched = first_match(
|
|
609
|
+
rules_by_file[source_file], endpoint.method, endpoint.path, prefix
|
|
610
|
+
)
|
|
577
611
|
if rule is None:
|
|
578
612
|
continue
|
|
579
|
-
|
|
613
|
+
evidence = rule.to_dict()
|
|
614
|
+
if matched != endpoint.path:
|
|
615
|
+
evidence["matched_path"] = matched
|
|
616
|
+
decisions["undecided" if rule.paths_unknown else rule.decision] = evidence
|
|
580
617
|
if any(
|
|
581
|
-
|
|
618
|
+
first_match(rules_by_file[f], endpoint.method, endpoint.path, prefix)[0] is not None
|
|
582
619
|
for f in undecided_files
|
|
583
620
|
):
|
|
584
621
|
decisions["undecided"] = {
|
|
@@ -599,6 +636,28 @@ def access_projection(
|
|
|
599
636
|
by_endpoint[endpoint.id] = decision
|
|
600
637
|
details[endpoint.id] = evidence
|
|
601
638
|
|
|
639
|
+
# A declared-but-undecidable servlet path means the URL the chain matches is
|
|
640
|
+
# unknown. Where any pattern rule is declared, that unknown decides which rule
|
|
641
|
+
# is reached — including whether a later `anyRequest` is reached at all — so
|
|
642
|
+
# no chain-derived verdict survives it. A handler's own guard does, and so
|
|
643
|
+
# does a chain whose only rule is `anyRequest`: it matches every URL.
|
|
644
|
+
path_dependent = any(
|
|
645
|
+
not rule.is_any_request
|
|
646
|
+
for source_file in active_files + undecided_files
|
|
647
|
+
for rule in rules_by_file[source_file]
|
|
648
|
+
)
|
|
649
|
+
if not servlet.decided and path_dependent:
|
|
650
|
+
for endpoint_id, decision in list(by_endpoint.items()):
|
|
651
|
+
if decision == "guarded_by_own_annotation":
|
|
652
|
+
continue
|
|
653
|
+
by_endpoint[endpoint_id] = "undecided"
|
|
654
|
+
details[endpoint_id] = {
|
|
655
|
+
"reason": "the declared servlet path could not be resolved, so the URL "
|
|
656
|
+
"these matchers compare against is unknown",
|
|
657
|
+
"servlet_path": servlet.to_dict(),
|
|
658
|
+
"decision_if_unprefixed": decision,
|
|
659
|
+
}
|
|
660
|
+
|
|
602
661
|
summary = {
|
|
603
662
|
key: sum(1 for d in by_endpoint.values() if d == key)
|
|
604
663
|
for key in _ACCESS_DECISIONS_ORDER
|
|
@@ -612,6 +671,19 @@ def access_projection(
|
|
|
612
671
|
},
|
|
613
672
|
"summary": {k: v for k, v in summary.items() if v},
|
|
614
673
|
}
|
|
674
|
+
# State the URL basis the rules were matched against. `endpoints` publishes
|
|
675
|
+
# `effective_path` (context path included, which the container strips before
|
|
676
|
+
# the chain runs); this is the same authority read for the part a matcher
|
|
677
|
+
# actually sees.
|
|
678
|
+
if servlet.value or not servlet.decided:
|
|
679
|
+
payload["path_basis"] = {
|
|
680
|
+
**servlet.to_dict(),
|
|
681
|
+
"note": (
|
|
682
|
+
"chain rules are matched against the servlet path + the "
|
|
683
|
+
"mapping-relative path; the servlet context path is not part of "
|
|
684
|
+
"it, because the container strips it before the filter chain runs"
|
|
685
|
+
),
|
|
686
|
+
}
|
|
615
687
|
for key in ("permit_all", "deny_all", "no_rule_matched", "undecided"):
|
|
616
688
|
listed = sorted(e for e, d in by_endpoint.items() if d == key)
|
|
617
689
|
if listed:
|
|
@@ -682,7 +754,7 @@ def _posture(
|
|
|
682
754
|
payload["endpoints"] = endpoint_projection(cir, payload["security"])
|
|
683
755
|
# Which chain is wired is a fact about a bean; what that chain permits is the
|
|
684
756
|
# fact about the request, and it is the one a reviewer asked for.
|
|
685
|
-
access, by_endpoint = access_projection(cir, resolution, root)
|
|
757
|
+
access, by_endpoint = access_projection(cir, resolution, root, resolved_properties)
|
|
686
758
|
payload["endpoints"]["effective_access"] = access
|
|
687
759
|
# A requested profile no repository artefact mentions is almost always a typo,
|
|
688
760
|
# and silently resolving every `@Profile` against it would answer confidently
|