sourcecode 0.15.1__py3-none-any.whl → 0.17.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 +141 -30
- sourcecode/architecture_summary.py +28 -0
- sourcecode/cli.py +30 -3
- sourcecode/code_notes_analyzer.py +25 -9
- sourcecode/dependency_analyzer.py +45 -0
- sourcecode/scanner.py +4 -0
- sourcecode/serializer.py +247 -2
- {sourcecode-0.15.1.dist-info → sourcecode-0.17.0.dist-info}/METADATA +1 -1
- {sourcecode-0.15.1.dist-info → sourcecode-0.17.0.dist-info}/RECORD +12 -12
- {sourcecode-0.15.1.dist-info → sourcecode-0.17.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.15.1.dist-info → sourcecode-0.17.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -33,6 +33,37 @@ _CODE_EXTENSIONS = {
|
|
|
33
33
|
}
|
|
34
34
|
_GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
|
|
35
35
|
|
|
36
|
+
_TEST_DIRS: frozenset[str] = frozenset({"tests", "test", "spec", "specs", "__tests__", "e2e"})
|
|
37
|
+
|
|
38
|
+
# Exact file stems that signal a specific architectural layer
|
|
39
|
+
_LAYER_STEM_EXACT: dict[str, str] = {
|
|
40
|
+
"cli": "orchestration",
|
|
41
|
+
"main": "orchestration",
|
|
42
|
+
"app": "orchestration",
|
|
43
|
+
"server": "orchestration",
|
|
44
|
+
"schema": "data",
|
|
45
|
+
"model": "data",
|
|
46
|
+
"models": "data",
|
|
47
|
+
"config": "data",
|
|
48
|
+
"settings": "data",
|
|
49
|
+
"store": "data",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Suffix patterns (stem == suffix OR stem ends with "_" + suffix) → layer
|
|
53
|
+
_LAYER_STEM_SUFFIXES: list[tuple[str, str]] = [
|
|
54
|
+
("analyzer", "processing"),
|
|
55
|
+
("processor", "processing"),
|
|
56
|
+
("parser", "processing"),
|
|
57
|
+
("detector", "processing"),
|
|
58
|
+
("scanner", "processing"),
|
|
59
|
+
("handler", "orchestration"),
|
|
60
|
+
("controller", "orchestration"),
|
|
61
|
+
("repository", "data"),
|
|
62
|
+
("repo", "data"),
|
|
63
|
+
("serializer", "data"),
|
|
64
|
+
("formatter", "data"),
|
|
65
|
+
]
|
|
66
|
+
|
|
36
67
|
DOMAIN_ROLES: dict[str, str] = {
|
|
37
68
|
"controllers": "HTTP request handlers",
|
|
38
69
|
"handlers": "HTTP request handlers",
|
|
@@ -120,8 +151,9 @@ class ArchitectureAnalyzer:
|
|
|
120
151
|
|
|
121
152
|
# Overall confidence
|
|
122
153
|
confidence: Literal["high", "medium", "low"]
|
|
123
|
-
if
|
|
124
|
-
|
|
154
|
+
if pattern not in (None, "unknown", "flat"):
|
|
155
|
+
# A recognised pattern was detected — at least medium confidence
|
|
156
|
+
confidence = "high" if len(domains) >= 3 else "medium"
|
|
125
157
|
elif len(domains) >= 1:
|
|
126
158
|
confidence = "medium"
|
|
127
159
|
else:
|
|
@@ -165,7 +197,13 @@ class ArchitectureAnalyzer:
|
|
|
165
197
|
if len(parts) == 1:
|
|
166
198
|
return "root"
|
|
167
199
|
first = parts[0]
|
|
168
|
-
if first in _SRC_TRANSPARENT
|
|
200
|
+
if first in _SRC_TRANSPARENT:
|
|
201
|
+
if len(parts) == 2:
|
|
202
|
+
return "root"
|
|
203
|
+
if len(parts) >= 4:
|
|
204
|
+
# src/pkg/subpkg/file.py → subpkg is the meaningful module
|
|
205
|
+
return parts[2]
|
|
206
|
+
# src/pkg/file.py → pkg
|
|
169
207
|
return parts[1]
|
|
170
208
|
return first
|
|
171
209
|
|
|
@@ -173,6 +211,8 @@ class ArchitectureAnalyzer:
|
|
|
173
211
|
groups: dict[str, list[str]] = {}
|
|
174
212
|
for p in paths:
|
|
175
213
|
seg = self._extract_domain_segment(p)
|
|
214
|
+
if seg.lower() in _TEST_DIRS:
|
|
215
|
+
continue # tests are infrastructure, not architecture domains
|
|
176
216
|
groups.setdefault(seg, []).append(p)
|
|
177
217
|
|
|
178
218
|
domains: list[ArchitectureDomain] = []
|
|
@@ -191,12 +231,21 @@ class ArchitectureAnalyzer:
|
|
|
191
231
|
return domains
|
|
192
232
|
|
|
193
233
|
def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
|
|
234
|
+
# Exclude test paths so test directories don't skew layer scoring
|
|
235
|
+
source_paths = [
|
|
236
|
+
p for p in paths
|
|
237
|
+
if not any(part.lower() in _TEST_DIRS for part in p.replace("\\", "/").split("/"))
|
|
238
|
+
]
|
|
239
|
+
if not source_paths:
|
|
240
|
+
return "unknown", []
|
|
241
|
+
|
|
194
242
|
dir_names: set[str] = set()
|
|
195
|
-
for p in
|
|
243
|
+
for p in source_paths:
|
|
196
244
|
parts = p.replace("\\", "/").split("/")
|
|
197
245
|
for part in parts[:-1]:
|
|
198
246
|
dir_names.add(part.lower())
|
|
199
247
|
|
|
248
|
+
# 1. Classical keyword-based pattern matching (MVC, layered, hexagonal, fullstack)
|
|
200
249
|
best_pattern = ""
|
|
201
250
|
best_score = 0
|
|
202
251
|
best_matched: dict[str, list[str]] = {}
|
|
@@ -213,34 +262,96 @@ class ArchitectureAnalyzer:
|
|
|
213
262
|
best_pattern = pattern_name
|
|
214
263
|
best_matched = matched
|
|
215
264
|
|
|
216
|
-
if best_score
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
265
|
+
if best_score >= 2:
|
|
266
|
+
layer_confidence: Literal["high", "medium", "low"] = "high" if best_score >= 3 else "medium"
|
|
267
|
+
layers: list[ArchitectureLayer] = []
|
|
268
|
+
for layer_key, matched_dirs in best_matched.items():
|
|
269
|
+
matched_files = [
|
|
270
|
+
p for p in source_paths
|
|
271
|
+
if any(
|
|
272
|
+
seg.lower() in matched_dirs
|
|
273
|
+
for seg in p.replace("\\", "/").split("/")[:-1]
|
|
274
|
+
)
|
|
275
|
+
]
|
|
276
|
+
layers.append(ArchitectureLayer(
|
|
277
|
+
name=layer_key,
|
|
278
|
+
pattern=best_pattern,
|
|
279
|
+
files=matched_files,
|
|
280
|
+
confidence=layer_confidence,
|
|
281
|
+
))
|
|
282
|
+
return best_pattern, layers
|
|
283
|
+
|
|
284
|
+
# 2. Functional file-naming heuristic: *_analyzer.py, cli.py, schema.py, …
|
|
285
|
+
func_result = self._detect_layered_functional(source_paths)
|
|
286
|
+
if func_result is not None:
|
|
287
|
+
return func_result
|
|
288
|
+
|
|
289
|
+
# 3. Modular sub-package heuristic: ≥2 distinct named sub-packages
|
|
290
|
+
modular_result = self._detect_modular(source_paths)
|
|
291
|
+
if modular_result is not None:
|
|
292
|
+
return modular_result
|
|
293
|
+
|
|
294
|
+
# 4. Fallback: flat (shallow) vs truly unknown (deep but unrecognised)
|
|
295
|
+
max_depth = max(
|
|
296
|
+
(len(p.replace("\\", "/").split("/")) - 1 for p in source_paths),
|
|
297
|
+
default=0,
|
|
298
|
+
)
|
|
299
|
+
return ("flat" if max_depth <= 2 else "unknown"), []
|
|
300
|
+
|
|
301
|
+
def _detect_layered_functional(
|
|
302
|
+
self, paths: list[str]
|
|
303
|
+
) -> Optional[tuple[str, list[ArchitectureLayer]]]:
|
|
304
|
+
"""Detect layered architecture from file-naming conventions.
|
|
305
|
+
|
|
306
|
+
Recognises three logical layers without requiring classical directory names:
|
|
307
|
+
- orchestration: cli.py, main.py, *_handler.py, *_controller.py
|
|
308
|
+
- processing: *_analyzer.py, *_processor.py, *_parser.py, *_detector.py, *_scanner.py
|
|
309
|
+
- data: schema.py, model.py, *_repository.py, *_serializer.py, config.py, …
|
|
310
|
+
"""
|
|
311
|
+
layer_files: dict[str, list[str]] = {"orchestration": [], "processing": [], "data": []}
|
|
312
|
+
for p in paths:
|
|
313
|
+
stem = Path(p.replace("\\", "/").split("/")[-1]).stem.lower()
|
|
314
|
+
if stem in _LAYER_STEM_EXACT:
|
|
315
|
+
layer_files[_LAYER_STEM_EXACT[stem]].append(p)
|
|
316
|
+
continue
|
|
317
|
+
for suffix, layer in _LAYER_STEM_SUFFIXES:
|
|
318
|
+
if stem == suffix or stem.endswith("_" + suffix):
|
|
319
|
+
layer_files[layer].append(p)
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
non_empty = {k: v for k, v in layer_files.items() if v}
|
|
323
|
+
if len(non_empty) >= 2:
|
|
324
|
+
return "layered", [
|
|
325
|
+
ArchitectureLayer(name=k, pattern="layered", files=v, confidence="medium")
|
|
326
|
+
for k, v in non_empty.items()
|
|
235
327
|
]
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def _detect_modular(
|
|
331
|
+
self, paths: list[str]
|
|
332
|
+
) -> Optional[tuple[str, list[ArchitectureLayer]]]:
|
|
333
|
+
"""Detect modular architecture from ≥2 distinct named sub-packages.
|
|
242
334
|
|
|
243
|
-
|
|
335
|
+
Each top-level (or first non-transparent) directory that is not a test or
|
|
336
|
+
generic name is treated as a self-contained module.
|
|
337
|
+
"""
|
|
338
|
+
module_files: dict[str, list[str]] = {}
|
|
339
|
+
for p in paths:
|
|
340
|
+
parts = p.replace("\\", "/").split("/")
|
|
341
|
+
for part in parts[:-1]:
|
|
342
|
+
if (part not in _SRC_TRANSPARENT
|
|
343
|
+
and part.lower() not in _TEST_DIRS
|
|
344
|
+
and part.lower() not in _GENERIC_NAMES):
|
|
345
|
+
module_files.setdefault(part, []).append(p)
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
meaningful = {k: v for k, v in module_files.items() if len(v) >= 2}
|
|
349
|
+
if len(meaningful) >= 2:
|
|
350
|
+
return "modular", [
|
|
351
|
+
ArchitectureLayer(name=k, pattern="modular", files=v, confidence="medium")
|
|
352
|
+
for k, v in meaningful.items()
|
|
353
|
+
]
|
|
354
|
+
return None
|
|
244
355
|
|
|
245
356
|
def _infer_bounded_contexts(
|
|
246
357
|
self,
|
|
@@ -12,6 +12,7 @@ _TOOLING_PREFIXES = (".claude/", ".vscode/", "bin/")
|
|
|
12
12
|
_PYTHON_EXTENSIONS = {".py"}
|
|
13
13
|
_NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
14
14
|
_GO_EXTENSIONS = {".go"}
|
|
15
|
+
_JAVA_EXTENSIONS = {".java", ".kt", ".scala"}
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class ArchitectureSummarizer:
|
|
@@ -58,6 +59,8 @@ class ArchitectureSummarizer:
|
|
|
58
59
|
lines.extend(self._summarize_node_entry(entry_point.path, content))
|
|
59
60
|
elif suffix in _GO_EXTENSIONS:
|
|
60
61
|
lines.extend(self._summarize_go_entry(entry_point.path, content))
|
|
62
|
+
elif suffix in _JAVA_EXTENSIONS:
|
|
63
|
+
lines.extend(self._summarize_java_entry(entry_point.path, content, sm.stacks))
|
|
61
64
|
else:
|
|
62
65
|
lines.append("Orquesta modulos internos no detallados por el analisis estatico disponible.")
|
|
63
66
|
|
|
@@ -122,6 +125,21 @@ class ArchitectureSummarizer:
|
|
|
122
125
|
lines.append("Produce la salida principal del entry point JavaScript/TypeScript detectado.")
|
|
123
126
|
return lines
|
|
124
127
|
|
|
128
|
+
def _summarize_java_entry(self, path: str, content: str, stacks: list[StackDetection]) -> list[str]:
|
|
129
|
+
lines: list[str] = []
|
|
130
|
+
frameworks = [f.name for stack in stacks for f in stack.frameworks]
|
|
131
|
+
if frameworks:
|
|
132
|
+
lines.append(f"Frameworks detectados: {', '.join(frameworks)}.")
|
|
133
|
+
annotations = re.findall(r"@(SpringBootApplication|QuarkusMain|MicronautApplication|Application)\b", content)
|
|
134
|
+
if annotations:
|
|
135
|
+
lines.append(f"Anotacion de arranque: @{annotations[0]}.")
|
|
136
|
+
# Detect Spring Boot profile hints
|
|
137
|
+
if "@SpringBootApplication" in content:
|
|
138
|
+
lines.append("Arranca el contexto de Spring con auto-configuracion y component scan.")
|
|
139
|
+
elif not lines:
|
|
140
|
+
lines.append("Orquesta el arranque de la aplicacion JVM.")
|
|
141
|
+
return lines
|
|
142
|
+
|
|
125
143
|
def _summarize_go_entry(self, path: str, content: str) -> list[str]:
|
|
126
144
|
imports = re.findall(r'"([^"]+)"', content)
|
|
127
145
|
internal = [module for module in imports if not module.startswith(("fmt", "net/", "os", "context"))]
|
|
@@ -196,6 +214,16 @@ class ArchitectureSummarizer:
|
|
|
196
214
|
confidence="medium",
|
|
197
215
|
)
|
|
198
216
|
)
|
|
217
|
+
elif path.endswith("Application.java") or path.endswith("Main.java"):
|
|
218
|
+
candidates.append(
|
|
219
|
+
EntryPoint(
|
|
220
|
+
path=path,
|
|
221
|
+
stack="java",
|
|
222
|
+
kind="application",
|
|
223
|
+
source="convention",
|
|
224
|
+
confidence="medium",
|
|
225
|
+
)
|
|
226
|
+
)
|
|
199
227
|
return candidates
|
|
200
228
|
|
|
201
229
|
def _fallback_priority(self, path: str) -> tuple[int, int, str]:
|
sourcecode/cli.py
CHANGED
|
@@ -202,12 +202,26 @@ def main(
|
|
|
202
202
|
SourceMap,
|
|
203
203
|
StackDetection,
|
|
204
204
|
)
|
|
205
|
-
from sourcecode.serializer import compact_view, write_output
|
|
205
|
+
from sourcecode.serializer import compact_view, normalize_source_map, validate_cross_analyzer_consistency, validate_source_map, write_output
|
|
206
206
|
from sourcecode.workspace import WorkspaceAnalyzer
|
|
207
207
|
|
|
208
208
|
# 1. Escanear el directorio (SCAN-01 a SCAN-05)
|
|
209
209
|
redactor = SecretRedactor(enabled=not no_redact)
|
|
210
|
-
|
|
210
|
+
|
|
211
|
+
# Detectar manifests antes del scan para ajustar depth.
|
|
212
|
+
# find_manifests() solo mira profundidad 0-1, no necesita el arbol.
|
|
213
|
+
_pre_scanner = FileScanner(target, max_depth=1)
|
|
214
|
+
manifests = _pre_scanner.find_manifests()
|
|
215
|
+
|
|
216
|
+
# Maven usa src/main/java/<groupId>/<artifactId>/<module>/ (profundidad 7+).
|
|
217
|
+
# Con depth=4 los ficheros .java son invisibles y todos los analizadores fallan.
|
|
218
|
+
# Necesitamos al menos 8: src(1)+main(2)+java(3)+com(4)+co(5)+app(6)+module(7)+file.
|
|
219
|
+
_java_manifest_names = {"pom.xml", "build.gradle", "build.gradle.kts"}
|
|
220
|
+
_is_java = any(Path(m).name in _java_manifest_names for m in manifests)
|
|
221
|
+
_java_min_depth = 8
|
|
222
|
+
effective_depth = max(depth, _java_min_depth) if _is_java and depth < _java_min_depth else depth
|
|
223
|
+
|
|
224
|
+
scanner = FileScanner(target, max_depth=effective_depth)
|
|
211
225
|
raw_tree = scanner.scan_tree()
|
|
212
226
|
|
|
213
227
|
# 2. Filtrar del arbol las entradas de .env y *.secret (SEC-02, todos los niveles)
|
|
@@ -244,7 +258,6 @@ def main(
|
|
|
244
258
|
return pruned
|
|
245
259
|
|
|
246
260
|
file_tree = filter_sensitive_files(raw_tree)
|
|
247
|
-
manifests = scanner.find_manifests()
|
|
248
261
|
detector = ProjectDetector(build_default_detectors())
|
|
249
262
|
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
250
263
|
dependency_analyzer = DependencyAnalyzer() if dependencies else None
|
|
@@ -557,6 +570,20 @@ def main(
|
|
|
557
570
|
cn_notes, cn_adrs, cn_summary = CodeNotesAnalyzer().analyze(target)
|
|
558
571
|
sm = replace(sm, code_notes=cn_notes, code_adrs=cn_adrs, code_notes_summary=cn_summary)
|
|
559
572
|
|
|
573
|
+
# Normalize optional analyzer outputs → validate schema contracts.
|
|
574
|
+
# normalize_source_map fills None fields with typed empty defaults so that
|
|
575
|
+
# consumers never need to null-check architecture or module_graph.
|
|
576
|
+
# validate_source_map then asserts the contracts hold; it raises here
|
|
577
|
+
# (pre-serialization) rather than silently producing invalid JSON.
|
|
578
|
+
sm = normalize_source_map(sm)
|
|
579
|
+
validate_source_map(sm)
|
|
580
|
+
|
|
581
|
+
# Cross-analyzer semantic consistency (non-blocking: warnings to stderr).
|
|
582
|
+
# strict=False so a mismatched dependency or orphan semantic link never
|
|
583
|
+
# aborts a run — findings are informational until the team decides to harden.
|
|
584
|
+
for _finding in validate_cross_analyzer_consistency(sm, strict=False):
|
|
585
|
+
typer.echo(f"[consistency] {_finding}", err=True)
|
|
586
|
+
|
|
560
587
|
# 4. Serializar (con o sin modo compact)
|
|
561
588
|
if compact:
|
|
562
589
|
data = compact_view(sm)
|
|
@@ -5,6 +5,11 @@ from collections import Counter
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
|
+
# Schema imports are hoisted here (not lazily inside _scan_source_file) so that
|
|
9
|
+
# a stale .pyc / editable-install mismatch is caught at import time rather than
|
|
10
|
+
# silently dropping notes mid-scan on the first failed late-import.
|
|
11
|
+
from sourcecode.schema import AdrRecord, CodeNote, CodeNotesSummary
|
|
12
|
+
|
|
8
13
|
_MAX_NOTES = 500
|
|
9
14
|
_MAX_NOTES_PER_FILE = 30
|
|
10
15
|
_MAX_ADRS = 50
|
|
@@ -103,7 +108,6 @@ def _scan_source_file(
|
|
|
103
108
|
line_num = content.count("\n", 0, m.start()) + 1
|
|
104
109
|
symbol = _find_nearby_symbol(lines, line_num - 1)
|
|
105
110
|
|
|
106
|
-
from sourcecode.schema import CodeNote
|
|
107
111
|
notes.append(CodeNote(
|
|
108
112
|
kind=kind,
|
|
109
113
|
path=rel_path,
|
|
@@ -121,8 +125,6 @@ def _parse_adr(path: Path, rel_path: str) -> Optional[object]:
|
|
|
121
125
|
except OSError:
|
|
122
126
|
return None
|
|
123
127
|
|
|
124
|
-
from sourcecode.schema import AdrRecord
|
|
125
|
-
|
|
126
128
|
title: Optional[str] = None
|
|
127
129
|
status: Optional[str] = None
|
|
128
130
|
summary_lines: list[str] = []
|
|
@@ -167,15 +169,13 @@ def _parse_adr(path: Path, rel_path: str) -> Optional[object]:
|
|
|
167
169
|
if not title:
|
|
168
170
|
title = path.stem
|
|
169
171
|
|
|
170
|
-
return AdrRecord(path=rel_path, title=title, status=status, summary=summary)
|
|
172
|
+
return AdrRecord(path=rel_path, title=title, status=status, summary=summary) # noqa: RET504
|
|
171
173
|
|
|
172
174
|
|
|
173
175
|
class CodeNotesAnalyzer:
|
|
174
176
|
"""Extrae notas de código (TODO/FIXME/HACK/etc.) y ADRs del proyecto."""
|
|
175
177
|
|
|
176
178
|
def analyze(self, root: Path) -> tuple[list, list, object]:
|
|
177
|
-
from sourcecode.schema import CodeNotesSummary
|
|
178
|
-
|
|
179
179
|
notes: list = []
|
|
180
180
|
adrs: list = []
|
|
181
181
|
limitations: list[str] = []
|
|
@@ -186,9 +186,19 @@ class CodeNotesAnalyzer:
|
|
|
186
186
|
if total_count[0] >= _MAX_NOTES:
|
|
187
187
|
limitations.append(f"notes_truncated_at:{_MAX_NOTES}")
|
|
188
188
|
|
|
189
|
+
# Explicit canonical sort guarantees identical output for identical input
|
|
190
|
+
# regardless of filesystem traversal order or OS/platform differences.
|
|
191
|
+
# Without this, output order depended on sorted(Path.iterdir()) which
|
|
192
|
+
# varies across Python versions, APFS vs ext4, and future walk refactors.
|
|
193
|
+
notes.sort(key=lambda n: (n.path, n.line))
|
|
194
|
+
adrs.sort(key=lambda a: a.path)
|
|
195
|
+
|
|
189
196
|
kind_counts: Counter = Counter(n.kind for n in notes)
|
|
190
197
|
file_counts: Counter = Counter(n.path for n in notes)
|
|
191
|
-
|
|
198
|
+
# Secondary sort by path makes top_files stable when counts are tied.
|
|
199
|
+
top_files = [
|
|
200
|
+
f for f, _ in sorted(file_counts.items(), key=lambda x: (-x[1], x[0]))[:5]
|
|
201
|
+
]
|
|
192
202
|
|
|
193
203
|
summary = CodeNotesSummary(
|
|
194
204
|
requested=True,
|
|
@@ -211,7 +221,10 @@ class CodeNotesAnalyzer:
|
|
|
211
221
|
) -> None:
|
|
212
222
|
try:
|
|
213
223
|
entries = sorted(current.iterdir())
|
|
214
|
-
except
|
|
224
|
+
except OSError:
|
|
225
|
+
# Catches PermissionError, EMFILE (too many open files), and other
|
|
226
|
+
# OS-level errors that previously aborted the entire walk mid-scan,
|
|
227
|
+
# returning an empty or partial list depending on when the error hit.
|
|
215
228
|
return
|
|
216
229
|
|
|
217
230
|
for entry in entries:
|
|
@@ -237,5 +250,8 @@ class CodeNotesAnalyzer:
|
|
|
237
250
|
adrs.append(adr)
|
|
238
251
|
continue
|
|
239
252
|
|
|
240
|
-
|
|
253
|
+
# The quota check lives inside _scan_source_file; the pre-check
|
|
254
|
+
# here was redundant and caused files to be silently skipped when
|
|
255
|
+
# traversal order varied (different files filled the quota first).
|
|
256
|
+
if suffix in _CODE_EXTENSIONS:
|
|
241
257
|
_scan_source_file(entry, rel, notes, total_count)
|
|
@@ -33,6 +33,7 @@ class DependencyAnalyzer:
|
|
|
33
33
|
self._analyze_rust,
|
|
34
34
|
self._analyze_go,
|
|
35
35
|
self._analyze_dotnet,
|
|
36
|
+
self._analyze_java,
|
|
36
37
|
):
|
|
37
38
|
handler_records, handler_limitations = handler(root)
|
|
38
39
|
records.extend(replace(record, workspace=workspace) for record in handler_records)
|
|
@@ -920,6 +921,50 @@ class DependencyAnalyzer:
|
|
|
920
921
|
result.append(resolved)
|
|
921
922
|
return self._dedupe(result)
|
|
922
923
|
|
|
924
|
+
def _analyze_java(self, root: Path) -> tuple[list[DependencyRecord], list[str]]:
|
|
925
|
+
pom = root / "pom.xml"
|
|
926
|
+
if not pom.exists():
|
|
927
|
+
return [], []
|
|
928
|
+
try:
|
|
929
|
+
tree = ET.parse(pom)
|
|
930
|
+
except (ET.ParseError, OSError):
|
|
931
|
+
return [], ["java: error al parsear pom.xml"]
|
|
932
|
+
|
|
933
|
+
root_elem = tree.getroot()
|
|
934
|
+
ns_match = re.match(r"\{[^}]+\}", root_elem.tag)
|
|
935
|
+
ns = ns_match.group(0) if ns_match else ""
|
|
936
|
+
|
|
937
|
+
records: list[DependencyRecord] = []
|
|
938
|
+
deps_elem = root_elem.find(f"{ns}dependencies")
|
|
939
|
+
if deps_elem is None:
|
|
940
|
+
return [], ["java: pom.xml sin bloque <dependencies>"]
|
|
941
|
+
|
|
942
|
+
for dep in deps_elem.findall(f"{ns}dependency"):
|
|
943
|
+
group_id = (dep.findtext(f"{ns}groupId") or "").strip()
|
|
944
|
+
artifact_id = (dep.findtext(f"{ns}artifactId") or "").strip()
|
|
945
|
+
if not group_id or not artifact_id:
|
|
946
|
+
continue
|
|
947
|
+
version_raw = (dep.findtext(f"{ns}version") or "").strip() or None
|
|
948
|
+
# Versiones interpoladas con propiedades Maven (${...}) no son resolvibles estáticamente
|
|
949
|
+
declared = version_raw if version_raw and not version_raw.startswith("${") else None
|
|
950
|
+
scope_text = (dep.findtext(f"{ns}scope") or "compile").strip().lower()
|
|
951
|
+
scope = "dev" if scope_text == "test" else "direct"
|
|
952
|
+
records.append(
|
|
953
|
+
DependencyRecord(
|
|
954
|
+
name=f"{group_id}:{artifact_id}",
|
|
955
|
+
ecosystem="java",
|
|
956
|
+
scope=scope,
|
|
957
|
+
declared_version=declared,
|
|
958
|
+
source="manifest",
|
|
959
|
+
manifest_path="pom.xml",
|
|
960
|
+
)
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
limitations: list[str] = []
|
|
964
|
+
if not records:
|
|
965
|
+
limitations.append("java: pom.xml sin dependencias parseables (puede usar BOM o propiedades)")
|
|
966
|
+
return records, limitations
|
|
967
|
+
|
|
923
968
|
def _load_yaml_file(self, path: Path) -> Optional[dict[str, Any]]:
|
|
924
969
|
if not path.exists():
|
|
925
970
|
return None
|
sourcecode/scanner.py
CHANGED
|
@@ -128,6 +128,10 @@ class FileScanner:
|
|
|
128
128
|
|
|
129
129
|
# Agregar ficheros al nodo (null = fichero segun D-02)
|
|
130
130
|
for fname in filenames:
|
|
131
|
+
# Skip flag-shaped names (e.g. "-o", "--format") — shell redirect artifacts.
|
|
132
|
+
# No legitimate source file starts with "-".
|
|
133
|
+
if fname.startswith("-"):
|
|
134
|
+
continue
|
|
131
135
|
fpath = current / fname
|
|
132
136
|
# SCAN-03: no incluir symlinks de fichero
|
|
133
137
|
if fpath.is_symlink():
|
sourcecode/serializer.py
CHANGED
|
@@ -10,12 +10,17 @@ Patrones criticos:
|
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
12
|
import sys
|
|
13
|
-
from dataclasses import asdict, is_dataclass
|
|
13
|
+
from dataclasses import asdict, dataclass, is_dataclass, replace
|
|
14
14
|
from io import StringIO
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any, Optional
|
|
17
17
|
|
|
18
|
-
from sourcecode.schema import
|
|
18
|
+
from sourcecode.schema import (
|
|
19
|
+
ArchitectureAnalysis,
|
|
20
|
+
ModuleGraph,
|
|
21
|
+
ModuleGraphSummary,
|
|
22
|
+
SourceMap,
|
|
23
|
+
)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
def to_json(sm: SourceMap | dict[str, Any], indent: int = 2) -> str:
|
|
@@ -92,6 +97,246 @@ def compact_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
|
|
100
|
+
def normalize_source_map(sm: SourceMap) -> SourceMap:
|
|
101
|
+
"""Fill in typed empty defaults for optional analyzer fields.
|
|
102
|
+
|
|
103
|
+
Fields controlled by flags (--architecture, --graph-modules) are None when
|
|
104
|
+
the flag is absent. Downstream consumers and tests then need null-checks
|
|
105
|
+
everywhere. This layer converts None → a well-typed default so the output
|
|
106
|
+
schema is always structurally complete.
|
|
107
|
+
|
|
108
|
+
The ``requested=False`` sentinel on each default tells consumers the
|
|
109
|
+
analysis was not requested, without forcing them to branch on None.
|
|
110
|
+
"""
|
|
111
|
+
changes: dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
# architecture: always an ArchitectureAnalysis, never None
|
|
114
|
+
if sm.architecture is None:
|
|
115
|
+
changes["architecture"] = ArchitectureAnalysis(requested=False)
|
|
116
|
+
|
|
117
|
+
# module_graph: always a ModuleGraph (possibly empty), never None.
|
|
118
|
+
# module_graph_summary is kept in sync as a convenience field.
|
|
119
|
+
if sm.module_graph is None:
|
|
120
|
+
empty_graph = ModuleGraph(summary=ModuleGraphSummary(requested=False))
|
|
121
|
+
changes["module_graph"] = empty_graph
|
|
122
|
+
if sm.module_graph_summary is None:
|
|
123
|
+
changes["module_graph_summary"] = empty_graph.summary
|
|
124
|
+
elif sm.module_graph_summary is None:
|
|
125
|
+
# graph exists but summary was never set — sync it
|
|
126
|
+
changes["module_graph_summary"] = sm.module_graph.summary
|
|
127
|
+
|
|
128
|
+
# dependencies is already list[DependencyRecord] by default_factory, but
|
|
129
|
+
# guard against any future refactor that could accidentally set it to None
|
|
130
|
+
if sm.dependencies is None: # type: ignore[comparison-overlap]
|
|
131
|
+
changes["dependencies"] = []
|
|
132
|
+
|
|
133
|
+
return replace(sm, **changes) if changes else sm
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_source_map(sm: SourceMap) -> None:
|
|
137
|
+
"""Assert structural schema contracts on a (already normalised) SourceMap.
|
|
138
|
+
|
|
139
|
+
Call this *after* normalize_source_map() so that the checks below catch
|
|
140
|
+
bugs in the normaliser itself or in code that bypasses it.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: listing every violated contract, never just the first.
|
|
144
|
+
"""
|
|
145
|
+
errors: list[str] = []
|
|
146
|
+
|
|
147
|
+
# --- architecture ---
|
|
148
|
+
if sm.architecture is None:
|
|
149
|
+
errors.append("architecture must not be null (call normalize_source_map first)")
|
|
150
|
+
else:
|
|
151
|
+
if not isinstance(sm.architecture.domains, list):
|
|
152
|
+
errors.append(
|
|
153
|
+
f"architecture.domains must be list, got {type(sm.architecture.domains).__name__}"
|
|
154
|
+
)
|
|
155
|
+
if sm.architecture.confidence not in ("high", "medium", "low"):
|
|
156
|
+
errors.append(
|
|
157
|
+
f"architecture.confidence must be high|medium|low, "
|
|
158
|
+
f"got {sm.architecture.confidence!r}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# --- module_graph ---
|
|
162
|
+
if sm.module_graph is None:
|
|
163
|
+
errors.append("module_graph must not be null (call normalize_source_map first)")
|
|
164
|
+
else:
|
|
165
|
+
if not isinstance(sm.module_graph.nodes, list):
|
|
166
|
+
errors.append(
|
|
167
|
+
f"module_graph.nodes must be list, got {type(sm.module_graph.nodes).__name__}"
|
|
168
|
+
)
|
|
169
|
+
if not isinstance(sm.module_graph.edges, list):
|
|
170
|
+
errors.append(
|
|
171
|
+
f"module_graph.edges must be list, got {type(sm.module_graph.edges).__name__}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# --- dependencies ---
|
|
175
|
+
if not isinstance(sm.dependencies, list):
|
|
176
|
+
errors.append(
|
|
177
|
+
f"dependencies must be list, got {type(sm.dependencies).__name__}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if errors:
|
|
181
|
+
bullet = "\n - "
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"SourceMap schema violations ({len(errors)}):{bullet}"
|
|
184
|
+
+ bullet.join(errors)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
_GRAPH_CODE_EXTENSIONS: frozenset[str] = frozenset({
|
|
189
|
+
".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
|
|
190
|
+
".go", ".rs", ".java", ".kt", ".rb",
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _rule_dependency_graph(
|
|
195
|
+
sm: SourceMap,
|
|
196
|
+
known_paths: set[str],
|
|
197
|
+
findings: list[str],
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Rule 1 — dependency/graph consistency.
|
|
200
|
+
|
|
201
|
+
Sub-rule 1a: every GraphNode whose path ends with a code extension must
|
|
202
|
+
exist in file_tree. A mismatch means the graph references phantom files
|
|
203
|
+
the scanner never found.
|
|
204
|
+
|
|
205
|
+
Sub-rule 1b: if the graph has *external*-looking edge targets (package
|
|
206
|
+
names without path separators), every non-transitive dependency should
|
|
207
|
+
appear in those targets. This rule is intentionally skipped when the
|
|
208
|
+
graph only tracks internal module-to-module edges, avoiding false
|
|
209
|
+
positives for projects whose graph_analyzer does not emit external imports.
|
|
210
|
+
"""
|
|
211
|
+
if not sm.module_graph.summary.requested:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# 1a — graph node paths must be in file_tree
|
|
215
|
+
for node in sm.module_graph.nodes:
|
|
216
|
+
p = node.path
|
|
217
|
+
if Path(p).suffix.lower() in _GRAPH_CODE_EXTENSIONS and p not in known_paths:
|
|
218
|
+
findings.append(
|
|
219
|
+
f"[dependency_graph] graph node '{node.id}' path '{p}' "
|
|
220
|
+
f"not found in file_tree"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# 1b — dep names should appear in external-facing edge targets
|
|
224
|
+
if sm.dependency_summary is None or not sm.dependency_summary.requested:
|
|
225
|
+
return
|
|
226
|
+
if not sm.module_graph.edges:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
# Only apply when the graph contains at least one package-like target
|
|
230
|
+
# (no path separator, no code extension) — signals external import tracking.
|
|
231
|
+
pkg_targets = {
|
|
232
|
+
e.target.lower()
|
|
233
|
+
for e in sm.module_graph.edges
|
|
234
|
+
if "/" not in e.target
|
|
235
|
+
and not Path(e.target).suffix.lower() in _GRAPH_CODE_EXTENSIONS
|
|
236
|
+
}
|
|
237
|
+
if not pkg_targets:
|
|
238
|
+
return # graph has only internal file edges; dep↔edge check not applicable
|
|
239
|
+
|
|
240
|
+
for dep in sm.dependencies:
|
|
241
|
+
if dep.scope == "transitive":
|
|
242
|
+
continue
|
|
243
|
+
name = dep.name.lower().replace("-", "_")
|
|
244
|
+
if not any(name in t.replace("-", "_") for t in pkg_targets):
|
|
245
|
+
sample = ", ".join(sorted(pkg_targets)[:3])
|
|
246
|
+
findings.append(
|
|
247
|
+
f"[dependency_graph] dependency '{dep.name}' declared in manifest "
|
|
248
|
+
f"but absent from module_graph external edges (visible: {sample})"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _rule_semantic_file_tree(
|
|
253
|
+
sm: SourceMap,
|
|
254
|
+
known_paths: set[str],
|
|
255
|
+
findings: list[str],
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Rule 2 — semantic_links paths must exist in file_tree.
|
|
258
|
+
|
|
259
|
+
Both the importer and the source (when not external) must be files the
|
|
260
|
+
scanner actually found. An orphan path means the semantic_analyzer
|
|
261
|
+
resolved a symbol to a file that does not belong to the project.
|
|
262
|
+
"""
|
|
263
|
+
for link in sm.semantic_links:
|
|
264
|
+
if link.importer_path not in known_paths:
|
|
265
|
+
findings.append(
|
|
266
|
+
f"[semantic_file_tree] semantic_link importer "
|
|
267
|
+
f"'{link.importer_path}' not in file_tree"
|
|
268
|
+
)
|
|
269
|
+
if link.source_path is not None and not link.is_external:
|
|
270
|
+
if link.source_path not in known_paths:
|
|
271
|
+
findings.append(
|
|
272
|
+
f"[semantic_file_tree] semantic_link source "
|
|
273
|
+
f"'{link.source_path}' not in file_tree (is_external=False)"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _rule_architecture_graph(
|
|
278
|
+
sm: SourceMap,
|
|
279
|
+
known_paths: set[str],
|
|
280
|
+
findings: list[str],
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Rule 3 — architecture domain files must be a subset of file_tree.
|
|
283
|
+
|
|
284
|
+
The architecture_analyzer clusters files into domains. Every file it
|
|
285
|
+
assigns to a domain should be a file the scanner found. A mismatch
|
|
286
|
+
means the architecture_analyzer is referencing phantom paths, likely
|
|
287
|
+
from a stale file_paths list or a mis-configured root.
|
|
288
|
+
"""
|
|
289
|
+
if not sm.architecture.requested:
|
|
290
|
+
return
|
|
291
|
+
for domain in sm.architecture.domains:
|
|
292
|
+
for path in domain.files:
|
|
293
|
+
if path not in known_paths:
|
|
294
|
+
findings.append(
|
|
295
|
+
f"[architecture_graph] domain '{domain.name}' "
|
|
296
|
+
f"references '{path}' not in file_tree"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def validate_cross_analyzer_consistency(
|
|
301
|
+
sm: SourceMap,
|
|
302
|
+
*,
|
|
303
|
+
strict: bool = False,
|
|
304
|
+
) -> list[str]:
|
|
305
|
+
"""Check semantic alignment across analyzer outputs.
|
|
306
|
+
|
|
307
|
+
Applies three rules (see helpers above):
|
|
308
|
+
Rule 1 — dependency/graph: graph node paths and external edge targets
|
|
309
|
+
must be consistent with declared dependencies and file_tree.
|
|
310
|
+
Rule 2 — semantic/file_tree: SymbolLink paths must exist in file_tree.
|
|
311
|
+
Rule 3 — architecture/graph: domain files must exist in file_tree.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
sm: A SourceMap that has already been normalised and structurally
|
|
315
|
+
validated (call normalize_source_map + validate_source_map first).
|
|
316
|
+
strict: If True, raises ValueError listing all findings.
|
|
317
|
+
If False (default), returns the findings list so the caller
|
|
318
|
+
can log warnings without aborting the pipeline.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of human-readable finding strings (empty when all rules pass).
|
|
322
|
+
"""
|
|
323
|
+
findings: list[str] = []
|
|
324
|
+
known = set(sm.file_paths)
|
|
325
|
+
|
|
326
|
+
_rule_dependency_graph(sm, known, findings)
|
|
327
|
+
_rule_semantic_file_tree(sm, known, findings)
|
|
328
|
+
_rule_architecture_graph(sm, known, findings)
|
|
329
|
+
|
|
330
|
+
if strict and findings:
|
|
331
|
+
bullet = "\n - "
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"Cross-analyzer consistency violations ({len(findings)}):{bullet}"
|
|
334
|
+
+ bullet.join(findings)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return findings
|
|
338
|
+
|
|
339
|
+
|
|
95
340
|
def write_output(content: str, output: Optional[Path]) -> None:
|
|
96
341
|
"""Escribe el contenido a stdout o a un fichero.
|
|
97
342
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
2
|
-
sourcecode/architecture_analyzer.py,sha256=
|
|
3
|
-
sourcecode/architecture_summary.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=kGV-7biOu1jEQzo70FiCy-DkYFbItVMEkqxy37z-zJ8,100
|
|
2
|
+
sourcecode/architecture_analyzer.py,sha256=zxgqeqn2FhIUDn06X-whjGKiXbrHFkRlzZcr2zKd0Gw,16874
|
|
3
|
+
sourcecode/architecture_summary.py,sha256=r9IciS9z-7h8xpiME7DKBLhLV36xzXtSzbZ1cRDWklo,11709
|
|
4
4
|
sourcecode/classifier.py,sha256=fcp2wIq198wq3Rsh_bDmZIK_W5UI6FEL5oqm1RrQfpA,6846
|
|
5
|
-
sourcecode/cli.py,sha256=
|
|
6
|
-
sourcecode/code_notes_analyzer.py,sha256=
|
|
5
|
+
sourcecode/cli.py,sha256=_ihMuJjjwTr50M77kMafOVX0kY9hqrdvl8NUQ_h0cbc,25178
|
|
6
|
+
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
7
7
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
8
|
-
sourcecode/dependency_analyzer.py,sha256=
|
|
8
|
+
sourcecode/dependency_analyzer.py,sha256=9BEAyLoBAS2R0twYkprJj_e0c83pzz9QPMRWKfinNn4,40948
|
|
9
9
|
sourcecode/doc_analyzer.py,sha256=Ec3orx6vBKsh5cNM3-F4y2Got2KuKx8w3dErwtdtM-A,19891
|
|
10
10
|
sourcecode/env_analyzer.py,sha256=_UTkAzHNxDgJVpSMviWcn9e2V5o-SAV_oF6xsLHDxfI,12206
|
|
11
11
|
sourcecode/git_analyzer.py,sha256=S9PGt8RDasBQYQUsZh9-H_KWVlPvzwR4jgM7TKN9ZCk,7643
|
|
@@ -13,10 +13,10 @@ sourcecode/graph_analyzer.py,sha256=cxl7Xb88aGxIVOCsatZJAE1CtgXGzIbJfqytV-9kkFs,
|
|
|
13
13
|
sourcecode/metrics_analyzer.py,sha256=4uh11v-Q0gdrN87BOxuFWUym3N3AOkOuy21K5N8peB8,20126
|
|
14
14
|
sourcecode/prepare_context.py,sha256=X43dEPe2cb7uMwlpQwVJMe0rQIZ0juaJ7H3F37sPbNc,5371
|
|
15
15
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
16
|
-
sourcecode/scanner.py,sha256=
|
|
16
|
+
sourcecode/scanner.py,sha256=Ga4jDRAOhkcwlRmlE2ko6cE39qKy0aI4iE1lGBWmAlk,6410
|
|
17
17
|
sourcecode/schema.py,sha256=93IrVRLAbzksVfFzr6_FhFSPpKJVwgVoTBcov-jTVo8,15669
|
|
18
18
|
sourcecode/semantic_analyzer.py,sha256=SRQSGJzEi4ZsGvb7U3qvnRiKHabz4hvIzCqiG92x9K8,79349
|
|
19
|
-
sourcecode/serializer.py,sha256=
|
|
19
|
+
sourcecode/serializer.py,sha256=IMU4lmr0FTDSTocws47abpJL01HH1cBK2UMGSUv00yo,13404
|
|
20
20
|
sourcecode/summarizer.py,sha256=2Yz5xJ1bhtJLlrUpKcd74wijypRRo9GJmGT8u971EW0,11572
|
|
21
21
|
sourcecode/tree_utils.py,sha256=xla45Whq24j6U9kEgTztJXkMpmOEDieAulKxP3nl89s,935
|
|
22
22
|
sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
|
|
@@ -39,7 +39,7 @@ sourcecode/detectors/rust.py,sha256=V23NO6Y8NsrhrUlGC2feUEcNK63hf3gc1y3jI4tes1Q,
|
|
|
39
39
|
sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
|
|
40
40
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
41
41
|
sourcecode/detectors/tooling.py,sha256=hIvop80No22pqyGVJ32NKliSdjkHRePQkIRroqG01bY,1875
|
|
42
|
-
sourcecode-0.
|
|
43
|
-
sourcecode-0.
|
|
44
|
-
sourcecode-0.
|
|
45
|
-
sourcecode-0.
|
|
42
|
+
sourcecode-0.17.0.dist-info/METADATA,sha256=sbwLpqrcDu_SkhDmPOqsq5YIT4wPn8V00GF1DRRohoI,25900
|
|
43
|
+
sourcecode-0.17.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
44
|
+
sourcecode-0.17.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
45
|
+
sourcecode-0.17.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|