sourcecode 1.1.0__py3-none-any.whl → 1.3.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.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +21 -1
- sourcecode/ast_extractor.py +89 -0
- sourcecode/cli.py +29 -4
- sourcecode/confidence_analyzer.py +21 -0
- sourcecode/contract_pipeline.py +10 -2
- sourcecode/dependency_analyzer.py +16 -0
- sourcecode/detectors/java.py +37 -2
- sourcecode/doc_analyzer.py +76 -1
- sourcecode/entrypoint_classifier.py +3 -0
- sourcecode/env_analyzer.py +6 -0
- sourcecode/graph_analyzer.py +1 -1
- sourcecode/metrics_analyzer.py +40 -1
- sourcecode/prepare_context.py +27 -0
- sourcecode/schema.py +1 -0
- sourcecode/semantic_analyzer.py +152 -0
- sourcecode/serializer.py +11 -3
- {sourcecode-1.1.0.dist-info → sourcecode-1.3.0.dist-info}/METADATA +1 -1
- {sourcecode-1.1.0.dist-info → sourcecode-1.3.0.dist-info}/RECORD +22 -22
- {sourcecode-1.1.0.dist-info → sourcecode-1.3.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.1.0.dist-info → sourcecode-1.3.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.1.0.dist-info → sourcecode-1.3.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -183,7 +183,11 @@ class ArchitectureAnalyzer:
|
|
|
183
183
|
if ddd_result is not None:
|
|
184
184
|
ddd_pattern, ddd_layers, ddd_contexts, ddd_layer_names = ddd_result
|
|
185
185
|
domains_for_ddd = self._cluster_domains(filtered) if len(filtered) >= 2 else []
|
|
186
|
-
|
|
186
|
+
module_files = self._build_ddd_module_files(sm.file_paths, ddd_contexts)
|
|
187
|
+
bc_list = [
|
|
188
|
+
BoundedContext(name=n, modules=module_files.get(n, []), confidence="high")
|
|
189
|
+
for n in ddd_contexts
|
|
190
|
+
]
|
|
187
191
|
return ArchitectureAnalysis(
|
|
188
192
|
requested=True,
|
|
189
193
|
pattern=ddd_pattern,
|
|
@@ -414,6 +418,22 @@ class ArchitectureAnalyzer:
|
|
|
414
418
|
]
|
|
415
419
|
return "ddd", arch_layers, bounded_context_names, ddd_layer_names
|
|
416
420
|
|
|
421
|
+
def _build_ddd_module_files(
|
|
422
|
+
self, paths: list[str], bounded_context_names: list[str]
|
|
423
|
+
) -> "dict[str, list[str]]":
|
|
424
|
+
"""Build a mapping of DDD module name → list of file paths."""
|
|
425
|
+
_DDD_LAYERS = frozenset({"application", "domain", "infrastructure"})
|
|
426
|
+
module_files: dict[str, list[str]] = {}
|
|
427
|
+
for p in paths:
|
|
428
|
+
parts = p.replace("\\", "/").split("/")
|
|
429
|
+
for i, part in enumerate(parts):
|
|
430
|
+
if part in _DDD_LAYERS and i >= 2:
|
|
431
|
+
mod = parts[i - 1]
|
|
432
|
+
if mod in bounded_context_names:
|
|
433
|
+
module_files.setdefault(mod, []).append(p)
|
|
434
|
+
break
|
|
435
|
+
return module_files
|
|
436
|
+
|
|
417
437
|
def _is_tooling(self, path: str) -> bool:
|
|
418
438
|
norm = path.replace("\\", "/")
|
|
419
439
|
return any(norm.startswith(p) for p in _TOOLING_PREFIXES)
|
sourcecode/ast_extractor.py
CHANGED
|
@@ -79,6 +79,7 @@ _LANGUAGE_MAP: dict[str, str] = {
|
|
|
79
79
|
".jsx": "jsx",
|
|
80
80
|
".mjs": "javascript",
|
|
81
81
|
".cjs": "javascript",
|
|
82
|
+
".java": "java",
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
_REACT_HOOKS: frozenset[str] = frozenset({
|
|
@@ -938,6 +939,92 @@ def _extract_python(path: str, source: str) -> FileContract:
|
|
|
938
939
|
)
|
|
939
940
|
|
|
940
941
|
|
|
942
|
+
# ---------------------------------------------------------------------------
|
|
943
|
+
# Minimal Java extraction (regex-based, no AST)
|
|
944
|
+
# ---------------------------------------------------------------------------
|
|
945
|
+
|
|
946
|
+
_JAVA_CLASS_DECL_RE = re.compile(
|
|
947
|
+
r'public\s+(?:(?:abstract|final|static)\s+)*(class|interface|enum)\s+(\w+)'
|
|
948
|
+
r'(?:\s+extends\s+([\w.]+))?(?:\s+implements\s+([\w.,\s]+?))?(?=\s*[\{<])',
|
|
949
|
+
re.MULTILINE,
|
|
950
|
+
)
|
|
951
|
+
_JAVA_METHOD_SIG_RE = re.compile(
|
|
952
|
+
r'^\s{0,12}public\s+[^\{]+\(',
|
|
953
|
+
re.MULTILINE,
|
|
954
|
+
)
|
|
955
|
+
_JAVA_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([^;\s]+)\s*;', re.MULTILINE)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _extract_java(path: str, source: str) -> FileContract:
|
|
959
|
+
exports: list[ExportRecord] = []
|
|
960
|
+
types: list[TypeDefinition] = []
|
|
961
|
+
functions: list[FunctionSignature] = []
|
|
962
|
+
imports: list[ImportRecord] = []
|
|
963
|
+
|
|
964
|
+
# Class / interface / enum declarations
|
|
965
|
+
for m in _JAVA_CLASS_DECL_RE.finditer(source):
|
|
966
|
+
name = m.group(2)
|
|
967
|
+
extends_str = m.group(3)
|
|
968
|
+
implements_str = m.group(4)
|
|
969
|
+
all_extends: list[str] = []
|
|
970
|
+
if extends_str:
|
|
971
|
+
all_extends.append(extends_str.strip())
|
|
972
|
+
if implements_str:
|
|
973
|
+
all_extends.extend(i.strip() for i in implements_str.split(",") if i.strip())
|
|
974
|
+
types.append(TypeDefinition(name=name, kind="class", fields=[], extends=all_extends))
|
|
975
|
+
exports.append(ExportRecord(name=name, kind="class"))
|
|
976
|
+
|
|
977
|
+
class_names = {t.name for t in types}
|
|
978
|
+
|
|
979
|
+
# Public method signatures (one-line heuristic)
|
|
980
|
+
seen_methods: set[str] = set()
|
|
981
|
+
for m in _JAVA_METHOD_SIG_RE.finditer(source):
|
|
982
|
+
sig_text = m.group(0).strip()
|
|
983
|
+
name_match = re.search(r'(\w+)\s*\($', sig_text)
|
|
984
|
+
if not name_match:
|
|
985
|
+
name_match = re.search(r'(\w+)\s*\(', sig_text)
|
|
986
|
+
if not name_match:
|
|
987
|
+
continue
|
|
988
|
+
mname = name_match.group(1)
|
|
989
|
+
if mname in class_names or mname in seen_methods or mname in {"if", "for", "while", "switch"}:
|
|
990
|
+
continue
|
|
991
|
+
seen_methods.add(mname)
|
|
992
|
+
functions.append(FunctionSignature(
|
|
993
|
+
name=mname,
|
|
994
|
+
signature=sig_text,
|
|
995
|
+
async_=False,
|
|
996
|
+
exported=True,
|
|
997
|
+
return_type=None,
|
|
998
|
+
))
|
|
999
|
+
|
|
1000
|
+
# Import statements
|
|
1001
|
+
seen_sources: set[str] = set()
|
|
1002
|
+
for m in _JAVA_IMPORT_RE.finditer(source):
|
|
1003
|
+
full_import = m.group(1).strip()
|
|
1004
|
+
if full_import not in seen_sources:
|
|
1005
|
+
seen_sources.add(full_import)
|
|
1006
|
+
imports.append(ImportRecord(source=full_import, kind="named", symbols=[]))
|
|
1007
|
+
|
|
1008
|
+
# External deps: top-2 package segments, skip java.* / javax.*
|
|
1009
|
+
deps = sorted({
|
|
1010
|
+
".".join(imp.source.split(".")[:2])
|
|
1011
|
+
for imp in imports
|
|
1012
|
+
if not imp.source.startswith("java.") and not imp.source.startswith("javax.")
|
|
1013
|
+
and len(imp.source.split(".")) >= 2
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
return FileContract(
|
|
1017
|
+
path=path,
|
|
1018
|
+
language="java",
|
|
1019
|
+
exports=exports,
|
|
1020
|
+
imports=sorted(imports, key=lambda i: i.source)[:30],
|
|
1021
|
+
functions=sorted(functions, key=lambda f: f.name)[:20],
|
|
1022
|
+
types=sorted(types, key=lambda t: t.name),
|
|
1023
|
+
dependencies=deps[:20],
|
|
1024
|
+
extraction_method="heuristic",
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
|
|
941
1028
|
# ---------------------------------------------------------------------------
|
|
942
1029
|
# Role detection
|
|
943
1030
|
# ---------------------------------------------------------------------------
|
|
@@ -1048,6 +1135,8 @@ class AstExtractor:
|
|
|
1048
1135
|
|
|
1049
1136
|
if language == "python":
|
|
1050
1137
|
contract = _extract_python(rel_path, source)
|
|
1138
|
+
elif language == "java":
|
|
1139
|
+
contract = _extract_java(rel_path, source)
|
|
1051
1140
|
else:
|
|
1052
1141
|
if self._ensure_ts():
|
|
1053
1142
|
lang_obj = _get_ts_lang(language)
|
sourcecode/cli.py
CHANGED
|
@@ -790,7 +790,7 @@ def main(
|
|
|
790
790
|
# Require at least 8: src(1)+main(2)+java(3)+com(4)+co(5)+app(6)+module(7)+file.
|
|
791
791
|
_java_manifest_names = {"pom.xml", "build.gradle", "build.gradle.kts"}
|
|
792
792
|
_is_java = any(Path(m).name in _java_manifest_names for m in manifests)
|
|
793
|
-
_java_min_depth =
|
|
793
|
+
_java_min_depth = 10
|
|
794
794
|
effective_depth = max(depth, _java_min_depth) if _is_java and depth < _java_min_depth else depth
|
|
795
795
|
|
|
796
796
|
# --agent: enable signal analyzers; output via agent_view (not compact)
|
|
@@ -1270,10 +1270,24 @@ def main(
|
|
|
1270
1270
|
and d.scope not in {"dev"}
|
|
1271
1271
|
]
|
|
1272
1272
|
|
|
1273
|
-
|
|
1273
|
+
_JAVA_SEMANTIC_PRIORITY: dict[str, int] = {
|
|
1274
|
+
"spring-boot": 0, "spring-security": 1, "mybatis": 2,
|
|
1275
|
+
"poi": 3, "pdfbox": 4, "jackson": 5, "jjwt": 6,
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
def _java_priority(d: Any) -> int:
|
|
1279
|
+
if d.ecosystem != "java":
|
|
1280
|
+
return 99
|
|
1281
|
+
art = (d.name.split(":")[-1] if ":" in d.name else d.name).lower()
|
|
1282
|
+
for key, pri in _JAVA_SEMANTIC_PRIORITY.items():
|
|
1283
|
+
if key in art:
|
|
1284
|
+
return pri
|
|
1285
|
+
return 50
|
|
1286
|
+
|
|
1287
|
+
def _dep_sort_key(d: Any) -> tuple[int, int, int, str]:
|
|
1274
1288
|
role_order = _ROLE_PRIORITY.get(d.role or "runtime", 5)
|
|
1275
1289
|
eco_order = 0 if d.ecosystem == primary_ecosystem else 1
|
|
1276
|
-
return (role_order, eco_order, d.name.lower())
|
|
1290
|
+
return (role_order, eco_order, _java_priority(d), d.name.lower())
|
|
1277
1291
|
|
|
1278
1292
|
_seen_dep_names: set[str] = set()
|
|
1279
1293
|
_deduped_deps: list[Any] = []
|
|
@@ -1281,7 +1295,7 @@ def main(
|
|
|
1281
1295
|
if d.name not in _seen_dep_names:
|
|
1282
1296
|
_seen_dep_names.add(d.name)
|
|
1283
1297
|
_deduped_deps.append(d)
|
|
1284
|
-
sm.key_dependencies = _deduped_deps
|
|
1298
|
+
sm.key_dependencies = _deduped_deps # no cap — all direct deps included
|
|
1285
1299
|
|
|
1286
1300
|
# LQN-02: deterministic NL summary
|
|
1287
1301
|
sm.project_summary = ProjectSummarizer(target).generate(sm)
|
|
@@ -1376,8 +1390,15 @@ def main(
|
|
|
1376
1390
|
))
|
|
1377
1391
|
sm = _replace(sm, pipeline_trace=_trace.build_trace())
|
|
1378
1392
|
|
|
1393
|
+
# P3-B: Auto-switch to centrality ranking when DDD layout detected
|
|
1394
|
+
if (rank_by == "relevance"
|
|
1395
|
+
and sm.architecture is not None
|
|
1396
|
+
and sm.architecture.pattern == "ddd"):
|
|
1397
|
+
rank_by = "centrality"
|
|
1398
|
+
|
|
1379
1399
|
# Contract pipeline — runs for mode=contract|standard|deep|hybrid (skip for raw)
|
|
1380
1400
|
_is_contract_mode = mode in ("contract", "standard")
|
|
1401
|
+
_pipeline_error = False
|
|
1381
1402
|
if _is_contract_mode:
|
|
1382
1403
|
from sourcecode.contract_pipeline import ContractPipeline
|
|
1383
1404
|
from sourcecode.contract_model import ContractSummary as _ContractSummary
|
|
@@ -1402,6 +1423,7 @@ def main(
|
|
|
1402
1423
|
)
|
|
1403
1424
|
except Exception as _exc:
|
|
1404
1425
|
typer.echo(f"[error] contract pipeline failed: {_exc}", err=True)
|
|
1426
|
+
_pipeline_error = True
|
|
1405
1427
|
_contracts = []
|
|
1406
1428
|
_contract_summary = _ContractSummary(
|
|
1407
1429
|
mode=mode,
|
|
@@ -1512,6 +1534,9 @@ def main(
|
|
|
1512
1534
|
# 6. Write output (CLI-04)
|
|
1513
1535
|
write_output(content, output=output)
|
|
1514
1536
|
|
|
1537
|
+
if _pipeline_error:
|
|
1538
|
+
raise typer.Exit(code=2)
|
|
1539
|
+
|
|
1515
1540
|
# 7. Clipboard copy (--copy / -c)
|
|
1516
1541
|
if copy and output is None:
|
|
1517
1542
|
_trimmed = content.strip()
|
|
@@ -193,6 +193,27 @@ class ConfidenceAnalyzer:
|
|
|
193
193
|
impact="low",
|
|
194
194
|
))
|
|
195
195
|
|
|
196
|
+
# ── Java test coverage gap check (P2-A) ──────────────────────────────
|
|
197
|
+
_java_all = [p for p in sm.file_paths if p.endswith(".java")]
|
|
198
|
+
_java_tests = [
|
|
199
|
+
p for p in _java_all
|
|
200
|
+
if "/test/" in p.replace("\\", "/") or "/tests/" in p.replace("\\", "/")
|
|
201
|
+
or Path(p).stem.endswith(("Test", "Tests", "IT", "Spec"))
|
|
202
|
+
]
|
|
203
|
+
_java_prod = [p for p in _java_all if p not in set(_java_tests)]
|
|
204
|
+
if _java_prod and len(_java_prod) >= 10:
|
|
205
|
+
_ratio = len(_java_tests) / len(_java_prod)
|
|
206
|
+
if _ratio < 0.05:
|
|
207
|
+
gaps.append(AnalysisGap(
|
|
208
|
+
area="testing",
|
|
209
|
+
reason=(
|
|
210
|
+
f"Backend test coverage critical: {len(_java_tests)} test files "
|
|
211
|
+
f"for {len(_java_prod)} Java files "
|
|
212
|
+
f"({_ratio:.1%})"
|
|
213
|
+
),
|
|
214
|
+
impact="high",
|
|
215
|
+
))
|
|
216
|
+
|
|
196
217
|
# ── Compute overall confidence ─────────────────────────────────────────
|
|
197
218
|
# Stack: use best manifest-detected stack, fall back to min
|
|
198
219
|
manifest_stacks = [s for s in sm.stacks if s.detection_method != "heuristic"]
|
sourcecode/contract_pipeline.py
CHANGED
|
@@ -370,7 +370,15 @@ class ContractPipeline:
|
|
|
370
370
|
"""
|
|
371
371
|
candidates = _find_symbol_files(root, symbol, known_paths, engine)
|
|
372
372
|
if not candidates:
|
|
373
|
-
return []
|
|
373
|
+
return [], {
|
|
374
|
+
"symbol": symbol,
|
|
375
|
+
"definers_found": 0,
|
|
376
|
+
"importers_found": 0,
|
|
377
|
+
"importers_returned": 0,
|
|
378
|
+
"references_found": 0,
|
|
379
|
+
"total_returned": 0,
|
|
380
|
+
"truncated": False,
|
|
381
|
+
}
|
|
374
382
|
|
|
375
383
|
extra: list[FileContract] = []
|
|
376
384
|
for rel_path in candidates[:300]: # cap to prevent excessive extraction
|
|
@@ -577,7 +585,7 @@ def _find_symbol_files(
|
|
|
577
585
|
"grep", "-rl",
|
|
578
586
|
"--include=*.ts", "--include=*.tsx",
|
|
579
587
|
"--include=*.js", "--include=*.jsx",
|
|
580
|
-
"--include=*.py",
|
|
588
|
+
"--include=*.py", "--include=*.java",
|
|
581
589
|
symbol, ".",
|
|
582
590
|
],
|
|
583
591
|
cwd=str(root),
|
|
@@ -127,6 +127,22 @@ def _infer_role(name: str, ecosystem: str, scope: str) -> str:
|
|
|
127
127
|
return "infra"
|
|
128
128
|
return "runtime"
|
|
129
129
|
|
|
130
|
+
if ecosystem == "java":
|
|
131
|
+
artifact = n.split(":")[-1] if ":" in n else n
|
|
132
|
+
if any(x in artifact for x in ("spring-boot", "spring-security")):
|
|
133
|
+
return "runtime"
|
|
134
|
+
if any(x in artifact for x in ("spring-web", "spring-mvc", "spring-core", "spring-context")):
|
|
135
|
+
return "runtime"
|
|
136
|
+
if any(x in artifact for x in ("mybatis", "hibernate", "jpa", "druid", "datasource")):
|
|
137
|
+
return "infra"
|
|
138
|
+
if any(x in artifact for x in ("jackson", "gson", "fastjson")):
|
|
139
|
+
return "serialization"
|
|
140
|
+
if any(x in artifact for x in ("poi", "pdfbox", "itext", "openpdf")):
|
|
141
|
+
return "parsing"
|
|
142
|
+
if any(x in artifact for x in ("jjwt", "nimbus-jose")):
|
|
143
|
+
return "runtime"
|
|
144
|
+
return "devtool" if is_dev else "runtime"
|
|
145
|
+
|
|
130
146
|
return "devtool" if is_dev else "runtime"
|
|
131
147
|
|
|
132
148
|
|
sourcecode/detectors/java.py
CHANGED
|
@@ -25,8 +25,18 @@ _CONTROLLER_ADVICE_RE = re.compile(r'@ControllerAdvice\b')
|
|
|
25
25
|
_WEB_FILTER_RE = re.compile(r'@WebFilter\b')
|
|
26
26
|
_FILTER_BEAN_RE = re.compile(r'FilterRegistrationBean\b')
|
|
27
27
|
# Extracts path from @RequestMapping("/v1/foo"), @GetMapping("/bar"), etc.
|
|
28
|
+
# Handles attribute order: value= may come after method= in legacy @RequestMapping style.
|
|
28
29
|
_HTTP_PATH_RE = re.compile(
|
|
29
|
-
r'@(?:Request|Get|Post|Put|Delete|Patch)Mapping\s*\(
|
|
30
|
+
r'@(?:Request|Get|Post|Put|Delete|Patch)Mapping\s*\([^)]*?(?:value\s*=\s*)?["\']([^"\']+)["\']'
|
|
31
|
+
)
|
|
32
|
+
_REQUEST_METHOD_VERB_RE = re.compile(
|
|
33
|
+
r'method\s*=\s*RequestMethod\.([A-Z]+)'
|
|
34
|
+
)
|
|
35
|
+
# @M3FiltroSeguridad custom security annotation
|
|
36
|
+
_M3_FILTRO_RE = re.compile(r'@M3FiltroSeguridad\b')
|
|
37
|
+
_M3_FILTRO_PARAMS_RE = re.compile(
|
|
38
|
+
r'@M3FiltroSeguridad\s*\(\s*(?:nombreRecurso\s*=\s*"([^"]*)")?'
|
|
39
|
+
r'(?:[^)]*nivelRequerido\s*=\s*(\d+))?'
|
|
30
40
|
)
|
|
31
41
|
|
|
32
42
|
|
|
@@ -149,16 +159,29 @@ class JavaDetector(AbstractDetector):
|
|
|
149
159
|
|
|
150
160
|
# Quick pre-filter before running regexes
|
|
151
161
|
if ("Controller" not in content and "Filter" not in content
|
|
152
|
-
and "ControllerAdvice" not in content
|
|
162
|
+
and "ControllerAdvice" not in content
|
|
163
|
+
and "M3FiltroSeguridad" not in content):
|
|
153
164
|
return []
|
|
154
165
|
|
|
155
166
|
if _REST_CONTROLLER_RE.search(content):
|
|
156
167
|
http_path_match = _HTTP_PATH_RE.search(content)
|
|
157
168
|
http_path = http_path_match.group(1) if http_path_match else None
|
|
169
|
+
verb_match = _REQUEST_METHOD_VERB_RE.search(content)
|
|
170
|
+
if verb_match and http_path:
|
|
171
|
+
http_path = f"[{verb_match.group(1)}] {http_path}"
|
|
172
|
+
elif verb_match:
|
|
173
|
+
http_path = f"[{verb_match.group(1)}]"
|
|
174
|
+
security_evidence = None
|
|
175
|
+
m3_match = _M3_FILTRO_PARAMS_RE.search(content)
|
|
176
|
+
if m3_match:
|
|
177
|
+
nombre = m3_match.group(1) or ""
|
|
178
|
+
nivel = m3_match.group(2) or ""
|
|
179
|
+
security_evidence = f"@M3FiltroSeguridad(nombreRecurso={nombre!r}, nivelRequerido={nivel})"
|
|
158
180
|
return [EntryPoint(
|
|
159
181
|
path=rel_path, stack="java", kind="rest_controller",
|
|
160
182
|
source="annotation", confidence="high",
|
|
161
183
|
http_path=http_path,
|
|
184
|
+
evidence=security_evidence,
|
|
162
185
|
)]
|
|
163
186
|
if _CONTROLLER_ADVICE_RE.search(content):
|
|
164
187
|
return [EntryPoint(
|
|
@@ -168,10 +191,22 @@ class JavaDetector(AbstractDetector):
|
|
|
168
191
|
if _MVC_CONTROLLER_RE.search(content) and _REQUEST_MAPPING_RE.search(content):
|
|
169
192
|
http_path_match = _HTTP_PATH_RE.search(content)
|
|
170
193
|
http_path = http_path_match.group(1) if http_path_match else None
|
|
194
|
+
verb_match = _REQUEST_METHOD_VERB_RE.search(content)
|
|
195
|
+
if verb_match and http_path:
|
|
196
|
+
http_path = f"[{verb_match.group(1)}] {http_path}"
|
|
197
|
+
elif verb_match:
|
|
198
|
+
http_path = f"[{verb_match.group(1)}]"
|
|
199
|
+
security_evidence = None
|
|
200
|
+
m3_match = _M3_FILTRO_PARAMS_RE.search(content)
|
|
201
|
+
if m3_match:
|
|
202
|
+
nombre = m3_match.group(1) or ""
|
|
203
|
+
nivel = m3_match.group(2) or ""
|
|
204
|
+
security_evidence = f"@M3FiltroSeguridad(nombreRecurso={nombre!r}, nivelRequerido={nivel})"
|
|
171
205
|
return [EntryPoint(
|
|
172
206
|
path=rel_path, stack="java", kind="mvc_controller",
|
|
173
207
|
source="annotation", confidence="medium",
|
|
174
208
|
http_path=http_path,
|
|
209
|
+
evidence=security_evidence,
|
|
175
210
|
)]
|
|
176
211
|
if _WEB_FILTER_RE.search(content):
|
|
177
212
|
return [EntryPoint(
|
sourcecode/doc_analyzer.py
CHANGED
|
@@ -174,6 +174,14 @@ class DocAnalyzer:
|
|
|
174
174
|
limitations.extend(file_limitations)
|
|
175
175
|
if file_records:
|
|
176
176
|
languages.add(lang)
|
|
177
|
+
elif suffix == ".java":
|
|
178
|
+
file_records, file_limitations = self._analyze_java_file(
|
|
179
|
+
norm_path, content, depth, workspace, entry_points
|
|
180
|
+
)
|
|
181
|
+
records.extend(file_records)
|
|
182
|
+
limitations.extend(file_limitations)
|
|
183
|
+
if file_records:
|
|
184
|
+
languages.add("java")
|
|
177
185
|
else:
|
|
178
186
|
# Unsupported language — D-04: no emitir DocRecord, solo registrar limitation
|
|
179
187
|
limitations.append(f"docs_unavailable:{norm_path}:language={lang}")
|
|
@@ -182,7 +190,7 @@ class DocAnalyzer:
|
|
|
182
190
|
# NO records.append() here
|
|
183
191
|
|
|
184
192
|
# Build language_coverage: explicit per-language support status
|
|
185
|
-
_SUPPORTED_LANGS = {"python", "javascript", "typescript"}
|
|
193
|
+
_SUPPORTED_LANGS = {"python", "javascript", "typescript", "java"}
|
|
186
194
|
lang_coverage: dict[str, str] = {}
|
|
187
195
|
for lang in languages:
|
|
188
196
|
if lang in _SUPPORTED_LANGS:
|
|
@@ -225,6 +233,73 @@ class DocAnalyzer:
|
|
|
225
233
|
)
|
|
226
234
|
return records, summary
|
|
227
235
|
|
|
236
|
+
# Javadoc: /** ... */ block followed by class/method declaration
|
|
237
|
+
_JAVADOC_RE = re.compile(r'/\*\*(.*?)\*/', re.DOTALL)
|
|
238
|
+
_JAVA_CLASS_AFTER_RE = re.compile(
|
|
239
|
+
r'(?:public\s+)?(?:abstract\s+)?(?:final\s+)?(?:class|interface|enum)\s+(\w+)',
|
|
240
|
+
)
|
|
241
|
+
_JAVA_METHOD_AFTER_RE = re.compile(
|
|
242
|
+
r'public\s+[\w<>\[\],\s]+?\s+(\w+)\s*\(',
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def _clean_javadoc(self, raw: str) -> str:
|
|
246
|
+
lines = raw.strip().splitlines()
|
|
247
|
+
cleaned: list[str] = []
|
|
248
|
+
for line in lines:
|
|
249
|
+
line = re.sub(r'^\s*\*\s?', '', line).strip()
|
|
250
|
+
if line.startswith('@'):
|
|
251
|
+
continue
|
|
252
|
+
if line:
|
|
253
|
+
cleaned.append(line)
|
|
254
|
+
result = ' '.join(cleaned)
|
|
255
|
+
if len(result) > self._DOCSTRING_MAX_CHARS:
|
|
256
|
+
result = result[:self._DOCSTRING_MAX_CHARS] + self._TRUNCATION_SUFFIX
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
def _analyze_java_file(
|
|
260
|
+
self,
|
|
261
|
+
path: str,
|
|
262
|
+
content: str,
|
|
263
|
+
depth: "DocsDepth",
|
|
264
|
+
workspace: "str | None",
|
|
265
|
+
entry_points: "list[str] | None",
|
|
266
|
+
) -> "tuple[list[DocRecord], list[str]]":
|
|
267
|
+
records: list[DocRecord] = []
|
|
268
|
+
for jd_match in self._JAVADOC_RE.finditer(content):
|
|
269
|
+
lookahead = content[jd_match.end():jd_match.end() + 400].lstrip()
|
|
270
|
+
# Class-level Javadoc
|
|
271
|
+
class_m = self._JAVA_CLASS_AFTER_RE.match(lookahead)
|
|
272
|
+
if class_m:
|
|
273
|
+
name = class_m.group(1)
|
|
274
|
+
doc_text = self._clean_javadoc(jd_match.group(1))
|
|
275
|
+
records.append(DocRecord(
|
|
276
|
+
path=path,
|
|
277
|
+
workspace=workspace,
|
|
278
|
+
kind="class",
|
|
279
|
+
name=name,
|
|
280
|
+
doc_text=doc_text,
|
|
281
|
+
importance=self._infer_importance(path, "class", entry_points),
|
|
282
|
+
))
|
|
283
|
+
continue
|
|
284
|
+
# Method-level Javadoc (only when depth != "module")
|
|
285
|
+
if depth == "module":
|
|
286
|
+
continue
|
|
287
|
+
method_m = self._JAVA_METHOD_AFTER_RE.match(lookahead)
|
|
288
|
+
if method_m:
|
|
289
|
+
name = method_m.group(1)
|
|
290
|
+
if name in {"if", "for", "while", "switch", "return", "new"}:
|
|
291
|
+
continue
|
|
292
|
+
doc_text = self._clean_javadoc(jd_match.group(1))
|
|
293
|
+
records.append(DocRecord(
|
|
294
|
+
path=path,
|
|
295
|
+
workspace=workspace,
|
|
296
|
+
kind="function",
|
|
297
|
+
name=name,
|
|
298
|
+
doc_text=doc_text,
|
|
299
|
+
importance=self._infer_importance(path, "function", entry_points),
|
|
300
|
+
))
|
|
301
|
+
return records, []
|
|
302
|
+
|
|
228
303
|
def merge_summaries(self, summaries: Iterable[DocSummary]) -> DocSummary:
|
|
229
304
|
"""Agrega multiples DocSummary en uno.
|
|
230
305
|
|
|
@@ -67,6 +67,9 @@ def runtime_relevance(ep: EntryPoint, classification: Classification | None = No
|
|
|
67
67
|
classification = classification or classify_entry_point(ep)
|
|
68
68
|
if classification != "production":
|
|
69
69
|
return "low"
|
|
70
|
+
# Annotation-detected HTTP controllers are the primary runtime surface
|
|
71
|
+
if ep.source == "annotation" and ep.kind in {"rest_controller", "mvc_controller"}:
|
|
72
|
+
return "high"
|
|
70
73
|
reason = (ep.reason or "").lower()
|
|
71
74
|
if ep.source == "package.json#bin" or reason == "bin" or reason in _PRODUCTION_SCRIPT_REASONS:
|
|
72
75
|
return "high"
|
sourcecode/env_analyzer.py
CHANGED
|
@@ -27,6 +27,8 @@ _ENV_EXAMPLE_NAMES = {
|
|
|
27
27
|
|
|
28
28
|
# Spring Boot application.properties / application.yml and their profile variants
|
|
29
29
|
_SPRING_CONF_BASE = {"application.properties", "application.yml", "application.yaml"}
|
|
30
|
+
# Matches options/{profile}/ in multi-tenant SAS layout paths
|
|
31
|
+
_OPTIONS_PROFILE_PATH_RE = re.compile(r'options/([a-z0-9_-]+)/', re.IGNORECASE)
|
|
30
32
|
_SPRING_CONF_PROFILE_RE = re.compile(r'^application-([a-z0-9_-]+)\.(properties|ya?ml)$', re.IGNORECASE)
|
|
31
33
|
# Matches ${ENV_VAR} or ${ENV_VAR:default} where ENV_VAR is UPPER_SNAKE_CASE.
|
|
32
34
|
# Group 1 = key, Group 2 = default (may be empty string, absent = no default).
|
|
@@ -507,6 +509,10 @@ class EnvAnalyzer:
|
|
|
507
509
|
# Spring Boot application.properties / application.yml (incl. profiles)
|
|
508
510
|
if name_lower in _SPRING_CONF_BASE or _SPRING_CONF_PROFILE_RE.match(name_lower):
|
|
509
511
|
profile = _extract_spring_profile(name)
|
|
512
|
+
# Override profile if path contains options/{profile}/ (multi-tenant SAS layout)
|
|
513
|
+
path_profile_match = _OPTIONS_PROFILE_PATH_RE.search(rel)
|
|
514
|
+
if path_profile_match:
|
|
515
|
+
profile = path_profile_match.group(1)
|
|
510
516
|
if profile and profile not in profiles_scanned:
|
|
511
517
|
profiles_scanned.append(profile)
|
|
512
518
|
count = _parse_spring_config(entry, rel, findings, profile)
|
sourcecode/graph_analyzer.py
CHANGED
sourcecode/metrics_analyzer.py
CHANGED
|
@@ -137,7 +137,7 @@ def _mccabe(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
|
|
|
137
137
|
class MetricsAnalyzer:
|
|
138
138
|
"""Analiza metricas de calidad de codigo: LOC, simbolos y complejidad ciclomatica."""
|
|
139
139
|
|
|
140
|
-
_MAX_FILES =
|
|
140
|
+
_MAX_FILES = 2000
|
|
141
141
|
_MAX_FILE_SIZE = 500_000 # bytes
|
|
142
142
|
|
|
143
143
|
# ---------------------------------------------------------------------------
|
|
@@ -159,6 +159,21 @@ class MetricsAnalyzer:
|
|
|
159
159
|
# Keep only paths that are actual files (not directories)
|
|
160
160
|
file_paths = [p for p in all_paths if (root / p).is_file()]
|
|
161
161
|
|
|
162
|
+
# Sort: JVM source first, then other source, then config, then dotfiles
|
|
163
|
+
# Prevents the 500-file cap from cutting all Java files when dotfiles sort first.
|
|
164
|
+
def _sort_key(p: str) -> tuple[int, str]:
|
|
165
|
+
if Path(p).suffix.lower() in {".java", ".kt", ".scala"}:
|
|
166
|
+
return (0, p)
|
|
167
|
+
if Path(p).suffix.lower() in {".py", ".go", ".rs", ".ts", ".js", ".tsx", ".jsx"}:
|
|
168
|
+
return (1, p)
|
|
169
|
+
if Path(p).suffix.lower() in {".xml", ".yaml", ".yml", ".json", ".toml", ".properties"}:
|
|
170
|
+
return (2, p)
|
|
171
|
+
if Path(p).name.startswith("."):
|
|
172
|
+
return (4, p)
|
|
173
|
+
return (3, p)
|
|
174
|
+
|
|
175
|
+
file_paths.sort(key=_sort_key)
|
|
176
|
+
|
|
162
177
|
limitations: list[str] = []
|
|
163
178
|
|
|
164
179
|
# Guard: max files
|
|
@@ -229,6 +244,29 @@ class MetricsAnalyzer:
|
|
|
229
244
|
"null complexity fields are expected, not an error."
|
|
230
245
|
)
|
|
231
246
|
|
|
247
|
+
# P2-C: DDD module metrics — group by module, count files/methods per layer
|
|
248
|
+
_DDD_LAYERS = {"domain", "application", "infrastructure"}
|
|
249
|
+
ddd_files = [r for r in records if "/ddd/" in r.path.replace("\\", "/")]
|
|
250
|
+
if ddd_files:
|
|
251
|
+
module_layer_data: dict[str, dict[str, dict]] = {}
|
|
252
|
+
for fm in ddd_files:
|
|
253
|
+
parts = fm.path.replace("\\", "/").split("/")
|
|
254
|
+
for i, part in enumerate(parts):
|
|
255
|
+
if part in _DDD_LAYERS and i >= 2:
|
|
256
|
+
module = parts[i - 1]
|
|
257
|
+
layer = part
|
|
258
|
+
if module not in module_layer_data:
|
|
259
|
+
module_layer_data[module] = {lyr: {"files": 0, "methods": 0} for lyr in _DDD_LAYERS}
|
|
260
|
+
module_layer_data[module][layer]["files"] += 1
|
|
261
|
+
module_layer_data[module][layer]["methods"] += fm.function_count or 0
|
|
262
|
+
break
|
|
263
|
+
ddd_metrics = [
|
|
264
|
+
{"module": mod, "layers": layers}
|
|
265
|
+
for mod, layers in sorted(module_layer_data.items())
|
|
266
|
+
]
|
|
267
|
+
else:
|
|
268
|
+
ddd_metrics = []
|
|
269
|
+
|
|
232
270
|
summary = MetricsSummary(
|
|
233
271
|
requested=True,
|
|
234
272
|
file_count=len(records),
|
|
@@ -238,6 +276,7 @@ class MetricsAnalyzer:
|
|
|
238
276
|
coverage_records=coverage_records,
|
|
239
277
|
coverage_sources_found=sorted({r.format for r in coverage_records}),
|
|
240
278
|
limitations=limitations,
|
|
279
|
+
ddd_module_metrics=ddd_metrics,
|
|
241
280
|
)
|
|
242
281
|
return records, summary
|
|
243
282
|
|
sourcecode/prepare_context.py
CHANGED
|
@@ -760,6 +760,33 @@ class TaskContextBuilder:
|
|
|
760
760
|
elif self._is_source(path) and not content_reasons:
|
|
761
761
|
content_boost += 0.5
|
|
762
762
|
|
|
763
|
+
# Task-specific boosts for differentiated file weighting
|
|
764
|
+
path_lower = path.lower()
|
|
765
|
+
if task_name == "fix-bug":
|
|
766
|
+
if any(x in path_lower for x in ("exception", "error", "handler", "advice")):
|
|
767
|
+
content_boost += 1.5
|
|
768
|
+
content_reasons.append("exception handler — high risk area")
|
|
769
|
+
elif task_name == "generate-tests":
|
|
770
|
+
stem = Path(path).stem.lower()
|
|
771
|
+
has_test = any(
|
|
772
|
+
stem in Path(tp).stem.lower() or Path(tp).stem.lower() in stem
|
|
773
|
+
for tp in test_set
|
|
774
|
+
)
|
|
775
|
+
if not has_test and self._is_source(path):
|
|
776
|
+
content_boost += 1.0
|
|
777
|
+
content_reasons.append("no test pair found")
|
|
778
|
+
elif task_name == "onboard":
|
|
779
|
+
if path in runtime_entry_set:
|
|
780
|
+
content_boost += 2.0
|
|
781
|
+
content_reasons.append("runtime entry point")
|
|
782
|
+
if any(x in path_lower for x in ("config", "application.yml", "application.properties", "settings", "bootstrap")):
|
|
783
|
+
content_boost += 1.0
|
|
784
|
+
content_reasons.append("configuration class")
|
|
785
|
+
elif task_name == "explain":
|
|
786
|
+
if "controller" in path_lower and path in runtime_entry_set:
|
|
787
|
+
content_boost += 1.5
|
|
788
|
+
content_reasons.append("DDD module controller")
|
|
789
|
+
|
|
763
790
|
total = fs.score + content_boost
|
|
764
791
|
if total <= 0:
|
|
765
792
|
continue
|
sourcecode/schema.py
CHANGED
|
@@ -225,6 +225,7 @@ class MetricsSummary:
|
|
|
225
225
|
coverage_records: list[CoverageRecord] = field(default_factory=list)
|
|
226
226
|
coverage_sources_found: list[str] = field(default_factory=list)
|
|
227
227
|
limitations: list[str] = field(default_factory=list)
|
|
228
|
+
ddd_module_metrics: list[dict] = field(default_factory=list)
|
|
228
229
|
|
|
229
230
|
|
|
230
231
|
@dataclass
|
sourcecode/semantic_analyzer.py
CHANGED
|
@@ -40,6 +40,34 @@ _MAX_SYMBOLS = 10_000
|
|
|
40
40
|
# JS/TS keyword and builtin exclusions (Plan 12-03)
|
|
41
41
|
# ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Java/JVM heuristic regex constants (module-level for performance)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
_AUTOWIRED_FIELD_RE = re.compile(
|
|
48
|
+
r'@Autowired\s+(?:private\s+|protected\s+|public\s+)?([A-Z][A-Za-z0-9_<>]*)\s+(\w+)\s*;'
|
|
49
|
+
)
|
|
50
|
+
_MAPPER_IFACE_RE = re.compile(
|
|
51
|
+
r'@Mapper\b.*?(?:public\s+)?interface\s+([A-Z][A-Za-z0-9_]*)',
|
|
52
|
+
re.DOTALL,
|
|
53
|
+
)
|
|
54
|
+
_EXTENDS_RE = re.compile(
|
|
55
|
+
r'(?:class|interface)\s+([A-Z][A-Za-z0-9_]*)\s+extends\s+([A-Z][A-Za-z0-9_]*)'
|
|
56
|
+
)
|
|
57
|
+
_M3_FILTRO_METHOD_RE = re.compile(
|
|
58
|
+
r'@M3FiltroSeguridad(?:\([^)]*\))?\s+(?:@[^\s]+\s+)*'
|
|
59
|
+
r'(?:public|private|protected)\s+\w[\w<>\[\]]*\s+([a-z][A-Za-z0-9_]*)\s*\('
|
|
60
|
+
)
|
|
61
|
+
_LOMBOK_CLASS_RE = re.compile(
|
|
62
|
+
r'(@(?:Data|Slf4j|Builder|AllArgsConstructor|NoArgsConstructor)(?:\([^)]*\))?\s+)*'
|
|
63
|
+
r'(?:public\s+)?(?:class|interface)\s+([A-Z][A-Za-z0-9_]*)',
|
|
64
|
+
re.MULTILINE,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# JS/TS keyword and builtin exclusions (Plan 12-03)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
43
71
|
_JS_KEYWORD_EXCLUSIONS: frozenset[str] = frozenset({
|
|
44
72
|
# JS reserved words
|
|
45
73
|
"if", "else", "for", "while", "do", "switch", "case", "break", "continue",
|
|
@@ -645,6 +673,29 @@ class SemanticAnalyzer:
|
|
|
645
673
|
),
|
|
646
674
|
}
|
|
647
675
|
|
|
676
|
+
# P1-D: Link @Mapper interfaces to their *Mapper.xml files
|
|
677
|
+
xml_paths = [p for p in all_paths if p.endswith(("Mapper.xml", "MyBatis.xml"))]
|
|
678
|
+
mapper_xml_index: dict[str, str] = {}
|
|
679
|
+
for xp in xml_paths:
|
|
680
|
+
stem = Path(xp).stem # e.g. "PacienteMapper"
|
|
681
|
+
mapper_xml_index[stem] = xp
|
|
682
|
+
|
|
683
|
+
for sym in all_symbols:
|
|
684
|
+
if sym.kind == "mapper_interface" and sym.language == "java":
|
|
685
|
+
xml_key = sym.symbol
|
|
686
|
+
if xml_key in mapper_xml_index:
|
|
687
|
+
xml_path = mapper_xml_index[xml_key]
|
|
688
|
+
links.append(SymbolLink(
|
|
689
|
+
importer_path=Path(sym.path).as_posix(),
|
|
690
|
+
symbol=sym.symbol,
|
|
691
|
+
source_path=xml_path,
|
|
692
|
+
source_line=None,
|
|
693
|
+
is_external=False,
|
|
694
|
+
confidence="high",
|
|
695
|
+
method="heuristic",
|
|
696
|
+
workspace=workspace,
|
|
697
|
+
))
|
|
698
|
+
|
|
648
699
|
# Determine explicit analysis status — never emit silent empty results.
|
|
649
700
|
# An agent must be able to tell "analysis ran and found nothing" from
|
|
650
701
|
# "analysis failed to run" or "significant coverage gap".
|
|
@@ -865,6 +916,8 @@ class SemanticAnalyzer:
|
|
|
865
916
|
"""Heuristic Java/Kotlin: detecta class/method declarations y call sites.
|
|
866
917
|
|
|
867
918
|
method="heuristic", confidence="low" para todos los edges Java.
|
|
919
|
+
Includes: Lombok synthetic symbols, @Autowired field edges,
|
|
920
|
+
@Mapper interface detection, inheritance chains, @M3FiltroSeguridad AOP edges.
|
|
868
921
|
"""
|
|
869
922
|
_JAVA_KEYWORDS: frozenset[str] = frozenset({
|
|
870
923
|
"if", "for", "while", "switch", "catch", "super", "this", "new",
|
|
@@ -923,6 +976,105 @@ class SemanticAnalyzer:
|
|
|
923
976
|
method="heuristic",
|
|
924
977
|
))
|
|
925
978
|
|
|
979
|
+
# P1-D: @Mapper interface detection — emit as mapper_interface symbol
|
|
980
|
+
for m in _MAPPER_IFACE_RE.finditer(content):
|
|
981
|
+
iface_name = m.group(1)
|
|
982
|
+
line = content[: m.start()].count("\n") + 1
|
|
983
|
+
symbols.append(SymbolRecord(
|
|
984
|
+
symbol=iface_name,
|
|
985
|
+
kind="mapper_interface",
|
|
986
|
+
language="java",
|
|
987
|
+
path=rel_path,
|
|
988
|
+
line=line,
|
|
989
|
+
exported=True,
|
|
990
|
+
))
|
|
991
|
+
|
|
992
|
+
# P1-E: Inheritance chain — class X extends Y → edge X→Y
|
|
993
|
+
for m in _EXTENDS_RE.finditer(content):
|
|
994
|
+
subclass = m.group(1)
|
|
995
|
+
superclass = m.group(2)
|
|
996
|
+
line = content[: m.start()].count("\n") + 1
|
|
997
|
+
calls.append(CallRecord(
|
|
998
|
+
caller_path=rel_path,
|
|
999
|
+
caller_symbol=subclass,
|
|
1000
|
+
callee_path=rel_path,
|
|
1001
|
+
callee_symbol=superclass,
|
|
1002
|
+
call_line=line,
|
|
1003
|
+
confidence="low",
|
|
1004
|
+
method="heuristic",
|
|
1005
|
+
))
|
|
1006
|
+
|
|
1007
|
+
# P1-F: Lombok annotations — emit synthetic symbols
|
|
1008
|
+
for m in _LOMBOK_CLASS_RE.finditer(content):
|
|
1009
|
+
annotations_block = m.group(0)
|
|
1010
|
+
class_name = m.group(2)
|
|
1011
|
+
line = content[: m.start()].count("\n") + 1
|
|
1012
|
+
if "@Slf4j" in annotations_block:
|
|
1013
|
+
symbols.append(SymbolRecord(
|
|
1014
|
+
symbol="log",
|
|
1015
|
+
kind="field",
|
|
1016
|
+
language="java",
|
|
1017
|
+
path=rel_path,
|
|
1018
|
+
line=line,
|
|
1019
|
+
exported=False,
|
|
1020
|
+
))
|
|
1021
|
+
if "@Builder" in annotations_block:
|
|
1022
|
+
symbols.append(SymbolRecord(
|
|
1023
|
+
symbol=f"{class_name}.builder",
|
|
1024
|
+
kind="function",
|
|
1025
|
+
language="java",
|
|
1026
|
+
path=rel_path,
|
|
1027
|
+
line=line,
|
|
1028
|
+
exported=True,
|
|
1029
|
+
))
|
|
1030
|
+
if "@AllArgsConstructor" in annotations_block or "@Data" in annotations_block:
|
|
1031
|
+
symbols.append(SymbolRecord(
|
|
1032
|
+
symbol=f"{class_name}(allArgs)",
|
|
1033
|
+
kind="function",
|
|
1034
|
+
language="java",
|
|
1035
|
+
path=rel_path,
|
|
1036
|
+
line=line,
|
|
1037
|
+
exported=True,
|
|
1038
|
+
))
|
|
1039
|
+
if "@NoArgsConstructor" in annotations_block:
|
|
1040
|
+
symbols.append(SymbolRecord(
|
|
1041
|
+
symbol=f"{class_name}()",
|
|
1042
|
+
kind="function",
|
|
1043
|
+
language="java",
|
|
1044
|
+
path=rel_path,
|
|
1045
|
+
line=line,
|
|
1046
|
+
exported=True,
|
|
1047
|
+
))
|
|
1048
|
+
|
|
1049
|
+
# P1-G: @Autowired field injection — emit dependency edges
|
|
1050
|
+
for m in _AUTOWIRED_FIELD_RE.finditer(content):
|
|
1051
|
+
field_type = m.group(1)
|
|
1052
|
+
field_name = m.group(2)
|
|
1053
|
+
line = content[: m.start()].count("\n") + 1
|
|
1054
|
+
calls.append(CallRecord(
|
|
1055
|
+
caller_path=rel_path,
|
|
1056
|
+
caller_symbol=field_name,
|
|
1057
|
+
callee_path=rel_path,
|
|
1058
|
+
callee_symbol=field_type,
|
|
1059
|
+
call_line=line,
|
|
1060
|
+
confidence="medium",
|
|
1061
|
+
method="heuristic",
|
|
1062
|
+
))
|
|
1063
|
+
|
|
1064
|
+
# P3-C: @M3FiltroSeguridad AOP proxy edges
|
|
1065
|
+
for m in _M3_FILTRO_METHOD_RE.finditer(content):
|
|
1066
|
+
method_name = m.group(1)
|
|
1067
|
+
line = content[: m.start()].count("\n") + 1
|
|
1068
|
+
calls.append(CallRecord(
|
|
1069
|
+
caller_path=rel_path,
|
|
1070
|
+
caller_symbol="M3FiltroSeguridadImpl",
|
|
1071
|
+
callee_path=rel_path,
|
|
1072
|
+
callee_symbol=method_name,
|
|
1073
|
+
call_line=line,
|
|
1074
|
+
confidence="medium",
|
|
1075
|
+
method="heuristic",
|
|
1076
|
+
))
|
|
1077
|
+
|
|
926
1078
|
return symbols, calls
|
|
927
1079
|
|
|
928
1080
|
# -----------------------------------------------------------------------
|
sourcecode/serializer.py
CHANGED
|
@@ -35,7 +35,7 @@ _FILE_RELEVANCE_MIN_COMBINED = 0.40 # minimum combined score — must earn incl
|
|
|
35
35
|
_PROD_DEPS_CAP = 10 # max production dependencies shown
|
|
36
36
|
_SECONDARY_DEPS_CAP = 5 # max per dev/test/build dependency group
|
|
37
37
|
_MONOREPO_PKGS_CAP = 8 # max workspace/runtime packages shown
|
|
38
|
-
_KEY_DEPS_CAP =
|
|
38
|
+
_KEY_DEPS_CAP = 50 # max key dependencies shown
|
|
39
39
|
_CODE_NOTES_CAP = 15 # max code notes in default output
|
|
40
40
|
_ENV_MAP_CAP = 15 # max env var entries in default output
|
|
41
41
|
|
|
@@ -223,6 +223,11 @@ def _file_relevance(sm: SourceMap, *, limit: int = _FILE_RELEVANCE_LIMIT) -> lis
|
|
|
223
223
|
semantic_hub_scores[p] = h.get("importance_score", 0.0) / max_importance
|
|
224
224
|
|
|
225
225
|
entry_paths = {ep.path for ep in sm.entry_points}
|
|
226
|
+
# REST/MVC controllers are HTTP surface — surface before @Transactional services
|
|
227
|
+
_rest_ctrl_paths = {
|
|
228
|
+
ep.path for ep in sm.entry_points
|
|
229
|
+
if getattr(ep, "kind", "") in {"rest_controller", "mvc_controller"}
|
|
230
|
+
}
|
|
226
231
|
scored: list[tuple[float, dict[str, Any]]] = []
|
|
227
232
|
|
|
228
233
|
for path in sm.file_paths:
|
|
@@ -241,6 +246,9 @@ def _file_relevance(sm: SourceMap, *, limit: int = _FILE_RELEVANCE_LIMIT) -> lis
|
|
|
241
246
|
# Semantic hub bonus: normalised call-graph centrality adds up to +0.30
|
|
242
247
|
sem_hub = semantic_hub_scores.get(path, 0.0) * 0.30
|
|
243
248
|
combined = fs.score + content_rel + sem_hub
|
|
249
|
+
# REST controller boost: surface above @Transactional service files
|
|
250
|
+
if path in _rest_ctrl_paths:
|
|
251
|
+
combined += 2.0
|
|
244
252
|
|
|
245
253
|
# Visibility threshold: require meaningful combined signal.
|
|
246
254
|
# Exception: high/medium-confidence files with strong content relevance
|
|
@@ -1586,8 +1594,8 @@ def write_output(content: str, output: Optional[Path]) -> None:
|
|
|
1586
1594
|
output: Destination file path. None = stdout.
|
|
1587
1595
|
"""
|
|
1588
1596
|
if output is None:
|
|
1589
|
-
sys.stdout.write(content)
|
|
1597
|
+
sys.stdout.buffer.write(content.encode("utf-8"))
|
|
1590
1598
|
if not content.endswith("\n"):
|
|
1591
|
-
sys.stdout.write("\n")
|
|
1599
|
+
sys.stdout.buffer.write(b"\n")
|
|
1592
1600
|
else:
|
|
1593
1601
|
output.write_text(content, encoding="utf-8")
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=OFSkiae_6W3SLXW8WqBF-PGOUxC4FhKuxuKQDTuCuvc,102
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=6dh34C2qZXyRbw-8xBhbEwDdXanM6CRFRWayVoYITnA,10190
|
|
3
|
-
sourcecode/architecture_analyzer.py,sha256=
|
|
3
|
+
sourcecode/architecture_analyzer.py,sha256=qzDW3_lQv__czQ-qs6AqEEoMvTfhfp7M7kNslPuQy7A,32128
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=J9yoLgh8wXwIRrT6q6JooB6PekivbOEYpJz4BUXdalk,20545
|
|
5
|
-
sourcecode/ast_extractor.py,sha256=
|
|
5
|
+
sourcecode/ast_extractor.py,sha256=zvHeO-w0evdS8EAJhwlK7hhrWMGN-lnHm6XFdZlaST8,44389
|
|
6
6
|
sourcecode/classifier.py,sha256=GKTMN8qKZX7ponSwDJfN08RrasI4CVpq1_gFBgEopps,7093
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
7
|
+
sourcecode/cli.py,sha256=0g7U2s1pyoTqFc7ne62Z305bErRKuHuKGyN0VrlsSOk,72971
|
|
8
8
|
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
9
|
-
sourcecode/confidence_analyzer.py,sha256=
|
|
9
|
+
sourcecode/confidence_analyzer.py,sha256=HcaewB2pZaZ_hfKrZWtr_yPMY2-CxS1zzTUD7c4argc,13188
|
|
10
10
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
11
11
|
sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
|
|
12
12
|
sourcecode/contract_model.py,sha256=gCf9-Kj0G7l0lvRTAcRfFAfMgs1Rpizv4mKovQLYUkw,3434
|
|
13
|
-
sourcecode/contract_pipeline.py,sha256=
|
|
13
|
+
sourcecode/contract_pipeline.py,sha256=lDa0MgalW-WPXwbWKX2NB6HWxt_15P6GZNXgAoJeDAE,25985
|
|
14
14
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
15
|
-
sourcecode/dependency_analyzer.py,sha256=
|
|
16
|
-
sourcecode/doc_analyzer.py,sha256=
|
|
17
|
-
sourcecode/entrypoint_classifier.py,sha256=
|
|
18
|
-
sourcecode/env_analyzer.py,sha256=
|
|
15
|
+
sourcecode/dependency_analyzer.py,sha256=EZyBI1-VkYCmPrcIIIpa7WAp888FF9Ct4nY2ronowUg,53555
|
|
16
|
+
sourcecode/doc_analyzer.py,sha256=a1CIClCNmfYM3ku4bdgwHQpmb6Js4wdJZ1V5EYLo04I,24345
|
|
17
|
+
sourcecode/entrypoint_classifier.py,sha256=gvKgl0f5T8ol1r4JMmkeqGHuZTfZJiOwFOWdc7EYwYw,4061
|
|
18
|
+
sourcecode/env_analyzer.py,sha256=GxCidahAAIptTdDFIlVB6URd4HBnBlIX_SqUov3MBRQ,22076
|
|
19
19
|
sourcecode/file_classifier.py,sha256=48ly5Z6exkzBy8lNy1AkdP4-oJqIA1zT3LZfffuTyDo,11572
|
|
20
20
|
sourcecode/git_analyzer.py,sha256=PD3eNWydznQ6KLNpxGzBqizIHoPIKevfwz9Xyf_pDt4,11600
|
|
21
|
-
sourcecode/graph_analyzer.py,sha256=
|
|
22
|
-
sourcecode/metrics_analyzer.py,sha256=
|
|
23
|
-
sourcecode/prepare_context.py,sha256=
|
|
21
|
+
sourcecode/graph_analyzer.py,sha256=Ko3nJp_wx92jic6AuFbdkOstDjzl-O62g1zazYJjm9w,64157
|
|
22
|
+
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
23
|
+
sourcecode/prepare_context.py,sha256=LsFDp7HnHdvtwVa46YUD60uMBfwXaVs4suMfBvc8tyI,37357
|
|
24
24
|
sourcecode/ranking_engine.py,sha256=virVglafZufioHpZpwktjMvUiL0TZELWQCQnQNV8dFo,9360
|
|
25
25
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
26
26
|
sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
|
|
27
27
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
28
28
|
sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
|
|
29
29
|
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
30
|
-
sourcecode/schema.py,sha256=
|
|
31
|
-
sourcecode/semantic_analyzer.py,sha256=
|
|
32
|
-
sourcecode/serializer.py,sha256
|
|
30
|
+
sourcecode/schema.py,sha256=M0aA3sIIm8QHDIAducZUm5mGpgJ7oXfqtyvoMkLGMac,23270
|
|
31
|
+
sourcecode/semantic_analyzer.py,sha256=12TwXYkYbDcBdu0heX_EmfPM2EkO8a_r5osf0SaeQbs,88956
|
|
32
|
+
sourcecode/serializer.py,sha256=-OFruU8NHsmHR_IKOb11VVl3-WOBXKPhyCtM8NZZtb0,65832
|
|
33
33
|
sourcecode/summarizer.py,sha256=ZuzIdm3t8A-d5MuQL0TSNLrd-L0IQIuguIxeNXMNJf8,16070
|
|
34
34
|
sourcecode/tree_utils.py,sha256=Fj9OIuUksBvgibNd3feog0sMDjVypJzPexp5lvMoYWI,1424
|
|
35
35
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -42,7 +42,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
|
|
|
42
42
|
sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
|
|
43
43
|
sourcecode/detectors/heuristic.py,sha256=bCqqgbHavl4Sse3dqT8mwmo1wAdgeJr7VyXOmfClLKo,3387
|
|
44
44
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
45
|
-
sourcecode/detectors/java.py,sha256=
|
|
45
|
+
sourcecode/detectors/java.py,sha256=Lv-a1YeXezJYZCJduWfeeLMTUgyPz4lyiYu9Edi8-7E,10821
|
|
46
46
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
47
47
|
sourcecode/detectors/nodejs.py,sha256=7fsyAmrGkkguX6U80HUQpIe9MRaYyi_A7zbaRtmFmGc,13097
|
|
48
48
|
sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
|
|
@@ -60,8 +60,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
60
60
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
61
61
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
62
62
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
63
|
-
sourcecode-1.
|
|
64
|
-
sourcecode-1.
|
|
65
|
-
sourcecode-1.
|
|
66
|
-
sourcecode-1.
|
|
67
|
-
sourcecode-1.
|
|
63
|
+
sourcecode-1.3.0.dist-info/METADATA,sha256=5tHuzUw0xpsY6eeCd81OI_pL_vl-3qaMCU78kExd6WI,20411
|
|
64
|
+
sourcecode-1.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
65
|
+
sourcecode-1.3.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
66
|
+
sourcecode-1.3.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
67
|
+
sourcecode-1.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|