sourcecode 0.23.0__tar.gz → 0.25.0__tar.gz
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-0.23.0 → sourcecode-0.25.0}/PKG-INFO +1 -1
- {sourcecode-0.23.0 → sourcecode-0.25.0}/docs/schema.md +2 -2
- {sourcecode-0.23.0 → sourcecode-0.25.0}/pyproject.toml +1 -1
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/architecture_analyzer.py +77 -5
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/architecture_summary.py +153 -27
- sourcecode-0.25.0/src/sourcecode/context_summarizer.py +191 -0
- sourcecode-0.25.0/src/sourcecode/detectors/csproj_parser.py +203 -0
- sourcecode-0.25.0/src/sourcecode/detectors/dotnet.py +187 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/go.py +21 -2
- sourcecode-0.25.0/src/sourcecode/detectors/hybrid.py +235 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/java.py +20 -12
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/nodejs.py +44 -5
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/python.py +16 -1
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/rust.py +37 -3
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/graph_analyzer.py +405 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/prepare_context.py +3 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/schema.py +23 -1
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/serializer.py +5 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_architecture_summary.py +5 -2
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_dependency_analyzer_node_python.py +1 -1
- sourcecode-0.25.0/tests/test_detector_dotnet.py +482 -0
- sourcecode-0.25.0/tests/test_hybrid_inference.py +367 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration.py +1 -1
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_dependencies.py +1 -1
- sourcecode-0.25.0/tests/test_phase1_improvements.py +412 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_schema.py +2 -2
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_signal_hierarchy.py +1 -1
- sourcecode-0.23.0/src/sourcecode/detectors/dotnet.py +0 -57
- {sourcecode-0.23.0 → sourcecode-0.25.0}/.gitignore +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/.ruff.toml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/README.md +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/__init__.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/conftest.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_cli.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.23.0 → sourcecode-0.25.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -81,7 +81,7 @@ Campos:
|
|
|
81
81
|
{
|
|
82
82
|
"schema_version": "1.0",
|
|
83
83
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
84
|
-
"sourcecode_version": "0.
|
|
84
|
+
"sourcecode_version": "0.25.0",
|
|
85
85
|
"analyzed_path": "/abs/path/to/project"
|
|
86
86
|
}
|
|
87
87
|
```
|
|
@@ -645,7 +645,7 @@ Ejemplo de salida para un monorepo con web Node.js y API Python con `--dependenc
|
|
|
645
645
|
"metadata": {
|
|
646
646
|
"schema_version": "1.0",
|
|
647
647
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
648
|
-
"sourcecode_version": "0.
|
|
648
|
+
"sourcecode_version": "0.25.0",
|
|
649
649
|
"analyzed_path": "/abs/path/to/project"
|
|
650
650
|
},
|
|
651
651
|
"file_tree": {
|
|
@@ -92,6 +92,21 @@ DOMAIN_ROLES: dict[str, str] = {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
95
|
+
"cqrs": {
|
|
96
|
+
"commands": ["commands", "command"],
|
|
97
|
+
"queries": ["queries", "query"],
|
|
98
|
+
},
|
|
99
|
+
"clean": {
|
|
100
|
+
"domain": ["domain", "domains"],
|
|
101
|
+
"application": ["application", "usecases", "usecase"],
|
|
102
|
+
"infrastructure": ["infrastructure", "infra", "adapters", "persistence"],
|
|
103
|
+
},
|
|
104
|
+
"onion": {
|
|
105
|
+
"domain": ["domain", "core"],
|
|
106
|
+
"application": ["application", "usecases"],
|
|
107
|
+
"ports": ["ports", "interfaces"],
|
|
108
|
+
"adapters": ["adapters", "secondary"],
|
|
109
|
+
},
|
|
95
110
|
"mvc": {
|
|
96
111
|
"controller": ["controller", "controllers", "routes", "views", "handlers"],
|
|
97
112
|
"model": ["model", "models", "entity", "entities", "domain"],
|
|
@@ -108,12 +123,28 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
108
123
|
"adapter": ["adapter", "adapters"],
|
|
109
124
|
"domain": ["domain", "core", "model", "models"],
|
|
110
125
|
},
|
|
126
|
+
"monorepo": {
|
|
127
|
+
"apps": ["apps", "applications"],
|
|
128
|
+
"packages": ["packages", "libs", "modules"],
|
|
129
|
+
},
|
|
111
130
|
"fullstack": {
|
|
112
131
|
"frontend": ["frontend", "client", "web", "ui", "pages", "components", "app"],
|
|
113
132
|
"backend": ["backend", "server", "api", "services"],
|
|
114
133
|
},
|
|
115
134
|
}
|
|
116
135
|
|
|
136
|
+
# Higher value = wins when score ties
|
|
137
|
+
_PATTERN_PRIORITY: dict[str, int] = {
|
|
138
|
+
"cqrs": 8,
|
|
139
|
+
"clean": 7,
|
|
140
|
+
"onion": 6,
|
|
141
|
+
"hexagonal": 5,
|
|
142
|
+
"monorepo": 4,
|
|
143
|
+
"mvc": 3,
|
|
144
|
+
"layered": 2,
|
|
145
|
+
"fullstack": 1,
|
|
146
|
+
}
|
|
147
|
+
|
|
117
148
|
|
|
118
149
|
class ArchitectureAnalyzer:
|
|
119
150
|
"""Analiza la arquitectura de un proyecto a partir de su estructura de ficheros y grafo de modulos."""
|
|
@@ -245,9 +276,10 @@ class ArchitectureAnalyzer:
|
|
|
245
276
|
for part in parts[:-1]:
|
|
246
277
|
dir_names.add(part.lower())
|
|
247
278
|
|
|
248
|
-
# 1. Classical keyword-based pattern matching
|
|
279
|
+
# 1. Classical keyword-based pattern matching
|
|
249
280
|
best_pattern = ""
|
|
250
281
|
best_score = 0
|
|
282
|
+
best_priority = -1
|
|
251
283
|
best_matched: dict[str, list[str]] = {}
|
|
252
284
|
|
|
253
285
|
for pattern_name, layer_keys in LAYER_PATTERNS.items():
|
|
@@ -257,8 +289,10 @@ class ArchitectureAnalyzer:
|
|
|
257
289
|
if matched_dirs:
|
|
258
290
|
matched[layer_key] = matched_dirs
|
|
259
291
|
score = len(matched)
|
|
260
|
-
|
|
292
|
+
priority = _PATTERN_PRIORITY.get(pattern_name, 0)
|
|
293
|
+
if (score, priority) > (best_score, best_priority):
|
|
261
294
|
best_score = score
|
|
295
|
+
best_priority = priority
|
|
262
296
|
best_pattern = pattern_name
|
|
263
297
|
best_matched = matched
|
|
264
298
|
|
|
@@ -281,23 +315,61 @@ class ArchitectureAnalyzer:
|
|
|
281
315
|
))
|
|
282
316
|
return best_pattern, layers
|
|
283
317
|
|
|
284
|
-
# 2.
|
|
318
|
+
# 2. Microservices structural detection (before file-naming heuristics)
|
|
319
|
+
microservices_result = self._detect_microservices(source_paths)
|
|
320
|
+
if microservices_result is not None:
|
|
321
|
+
return microservices_result
|
|
322
|
+
|
|
323
|
+
# 3. Functional file-naming heuristic: *_analyzer.py, cli.py, schema.py, …
|
|
285
324
|
func_result = self._detect_layered_functional(source_paths)
|
|
286
325
|
if func_result is not None:
|
|
287
326
|
return func_result
|
|
288
327
|
|
|
289
|
-
#
|
|
328
|
+
# 4. Modular sub-package heuristic: ≥2 distinct named sub-packages
|
|
290
329
|
modular_result = self._detect_modular(source_paths)
|
|
291
330
|
if modular_result is not None:
|
|
292
331
|
return modular_result
|
|
293
332
|
|
|
294
|
-
#
|
|
333
|
+
# 5. Fallback: flat (shallow) vs truly unknown (deep but unrecognised)
|
|
295
334
|
max_depth = max(
|
|
296
335
|
(len(p.replace("\\", "/").split("/")) - 1 for p in source_paths),
|
|
297
336
|
default=0,
|
|
298
337
|
)
|
|
299
338
|
return ("flat" if max_depth <= 2 else "unknown"), []
|
|
300
339
|
|
|
340
|
+
def _detect_microservices(
|
|
341
|
+
self, paths: list[str]
|
|
342
|
+
) -> Optional[tuple[str, list[ArchitectureLayer]]]:
|
|
343
|
+
"""Detect microservices from multiple sibling service directories or services/* pattern."""
|
|
344
|
+
# Pattern 1: explicit services/* subdirectories
|
|
345
|
+
service_subdirs: dict[str, list[str]] = {}
|
|
346
|
+
for p in paths:
|
|
347
|
+
parts = p.replace("\\", "/").split("/")
|
|
348
|
+
if len(parts) >= 3 and parts[0].lower() == "services":
|
|
349
|
+
service_subdirs.setdefault(parts[1], []).append(p)
|
|
350
|
+
if len(service_subdirs) >= 3:
|
|
351
|
+
return "microservices", [
|
|
352
|
+
ArchitectureLayer(name=k, pattern="microservices", files=v, confidence="medium")
|
|
353
|
+
for k, v in list(service_subdirs.items())[:8]
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
# Pattern 2: multiple top-level dirs each containing a canonical entry file
|
|
357
|
+
_ENTRY_FILES = {"main.go", "main.py", "server.js", "server.ts", "main.ts", "app.py"}
|
|
358
|
+
entry_dirs: dict[str, list[str]] = {}
|
|
359
|
+
for p in paths:
|
|
360
|
+
parts = p.replace("\\", "/").split("/")
|
|
361
|
+
if len(parts) >= 2 and parts[-1].lower() in _ENTRY_FILES:
|
|
362
|
+
top = parts[0]
|
|
363
|
+
if top.lower() not in _SRC_TRANSPARENT and top.lower() not in _TEST_DIRS:
|
|
364
|
+
entry_dirs.setdefault(top, []).append(p)
|
|
365
|
+
if len(entry_dirs) >= 4:
|
|
366
|
+
return "microservices", [
|
|
367
|
+
ArchitectureLayer(name=k, pattern="microservices", files=v, confidence="low")
|
|
368
|
+
for k, v in list(entry_dirs.items())[:8]
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
return None
|
|
372
|
+
|
|
301
373
|
def _detect_layered_functional(
|
|
302
374
|
self, paths: list[str]
|
|
303
375
|
) -> Optional[tuple[str, list[ArchitectureLayer]]]:
|
|
@@ -49,6 +49,10 @@ class ArchitectureSummarizer:
|
|
|
49
49
|
if not file_paths:
|
|
50
50
|
return None
|
|
51
51
|
|
|
52
|
+
# Rich path: use all available signals (stacks, arch analysis, graph)
|
|
53
|
+
rich_lines = self._build_rich_lines(sm)
|
|
54
|
+
|
|
55
|
+
# Stack-specific entry point analysis (existing logic)
|
|
52
56
|
entry_points = [
|
|
53
57
|
entry for entry in sm.entry_points
|
|
54
58
|
if not self._is_tooling_path(entry.path)
|
|
@@ -57,33 +61,32 @@ class ArchitectureSummarizer:
|
|
|
57
61
|
fallback = self._infer_fallback_entry_points(file_paths, sm.stacks)
|
|
58
62
|
entry_points = fallback[:1]
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
lines = lang_lines
|
|
83
|
-
else:
|
|
64
|
+
lang_lines: list[str] = []
|
|
65
|
+
if entry_points:
|
|
66
|
+
entry_point = entry_points[0]
|
|
67
|
+
content = self._read_file(entry_point.path)
|
|
68
|
+
if content:
|
|
69
|
+
suffix = Path(entry_point.path).suffix
|
|
70
|
+
if suffix in _PYTHON_EXTENSIONS:
|
|
71
|
+
lang_lines = self._summarize_python_entry(entry_point.path, content)
|
|
72
|
+
elif suffix in _NODE_EXTENSIONS:
|
|
73
|
+
lang_lines = self._summarize_node_entry(entry_point.path, content)
|
|
74
|
+
elif suffix in _GO_EXTENSIONS:
|
|
75
|
+
lang_lines = self._summarize_go_entry(entry_point.path, content)
|
|
76
|
+
elif suffix in _JAVA_EXTENSIONS:
|
|
77
|
+
lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
|
|
78
|
+
elif suffix in {".cs", ".fs", ".vb"}:
|
|
79
|
+
lang_lines = self._summarize_dotnet_entry(sm.stacks)
|
|
80
|
+
|
|
81
|
+
# Merge: rich lines first, stack-specific details appended (deduped)
|
|
82
|
+
lines = rich_lines + [l for l in lang_lines if l not in rich_lines]
|
|
83
|
+
|
|
84
|
+
if not lines and entry_points:
|
|
85
|
+
entry_point = entry_points[0]
|
|
84
86
|
lines = [self._describe_entry_point(entry_point, sm.project_type)]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
if not lines:
|
|
89
|
+
return "Arquitectura no inferida con suficiente evidencia estatica."
|
|
87
90
|
|
|
88
91
|
unique_lines: list[str] = []
|
|
89
92
|
seen: set[str] = set()
|
|
@@ -93,7 +96,82 @@ class ArchitectureSummarizer:
|
|
|
93
96
|
continue
|
|
94
97
|
seen.add(line)
|
|
95
98
|
unique_lines.append(line)
|
|
96
|
-
return "\n".join(unique_lines[:
|
|
99
|
+
return "\n".join(unique_lines[:6]) if unique_lines else None
|
|
100
|
+
|
|
101
|
+
def _build_rich_lines(self, sm: SourceMap) -> list[str]:
|
|
102
|
+
"""Generate architect-quality summary lines from stacks, arch analysis, and graph."""
|
|
103
|
+
if not sm.stacks:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
# Compute arch analysis inline if needed
|
|
107
|
+
arch = sm.architecture
|
|
108
|
+
if arch is None and sm.file_paths:
|
|
109
|
+
try:
|
|
110
|
+
from sourcecode.architecture_analyzer import ArchitectureAnalyzer
|
|
111
|
+
arch = ArchitectureAnalyzer().analyze(self.root, sm)
|
|
112
|
+
except Exception:
|
|
113
|
+
arch = None
|
|
114
|
+
|
|
115
|
+
lines: list[str] = []
|
|
116
|
+
|
|
117
|
+
# Line 1: project description (type + stack + frameworks)
|
|
118
|
+
project_line = self._describe_project_type(sm)
|
|
119
|
+
if project_line:
|
|
120
|
+
lines.append(project_line)
|
|
121
|
+
|
|
122
|
+
# Line 2: architecture pattern + layers
|
|
123
|
+
if arch and arch.pattern not in (None, "unknown", "flat"):
|
|
124
|
+
arch_line = self._describe_arch_pattern(arch)
|
|
125
|
+
if arch_line:
|
|
126
|
+
lines.append(arch_line)
|
|
127
|
+
|
|
128
|
+
# Line 3: coupling notes (if graph analytics present)
|
|
129
|
+
if sm.module_graph_summary:
|
|
130
|
+
mgr = sm.module_graph_summary
|
|
131
|
+
coupling_parts: list[str] = []
|
|
132
|
+
if mgr.cycle_count > 0:
|
|
133
|
+
s = "s" if mgr.cycle_count > 1 else ""
|
|
134
|
+
coupling_parts.append(f"{mgr.cycle_count} import cycle{s}")
|
|
135
|
+
if mgr.hubs:
|
|
136
|
+
hub_names = [h.removeprefix("module:").split("/")[-1] for h in mgr.hubs[:2]]
|
|
137
|
+
coupling_parts.append(f"hub modules: {', '.join(hub_names)}")
|
|
138
|
+
if coupling_parts:
|
|
139
|
+
lines.append(f"Coupling: {'; '.join(coupling_parts)}.")
|
|
140
|
+
|
|
141
|
+
return lines
|
|
142
|
+
|
|
143
|
+
def _describe_project_type(self, sm: SourceMap) -> str:
|
|
144
|
+
from sourcecode.context_summarizer import _STACK_LABELS, _TYPE_LABELS
|
|
145
|
+
runtime = _TYPE_LABELS.get(sm.project_type or "", "")
|
|
146
|
+
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
147
|
+
if primary is None:
|
|
148
|
+
return ""
|
|
149
|
+
stack_label = _STACK_LABELS.get(primary.stack, primary.stack)
|
|
150
|
+
fw_names = [f.name for s in sm.stacks for f in s.frameworks[:2]][:3]
|
|
151
|
+
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
152
|
+
if runtime:
|
|
153
|
+
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
154
|
+
return f"{stack_label} project{fw_str}."
|
|
155
|
+
|
|
156
|
+
def _describe_arch_pattern(self, arch: Any) -> str:
|
|
157
|
+
pattern_labels = {
|
|
158
|
+
"clean": "Clean Architecture",
|
|
159
|
+
"onion": "Onion Architecture",
|
|
160
|
+
"hexagonal": "Hexagonal Architecture",
|
|
161
|
+
"layered": "Layered Architecture",
|
|
162
|
+
"mvc": "MVC pattern",
|
|
163
|
+
"cqrs": "CQRS pattern",
|
|
164
|
+
"microservices": "Microservices",
|
|
165
|
+
"monorepo": "Monorepo workspace",
|
|
166
|
+
"modular": "Modular architecture",
|
|
167
|
+
}
|
|
168
|
+
label = pattern_labels.get(arch.pattern, arch.pattern.replace("_", " ").title() if arch.pattern else "")
|
|
169
|
+
if not label:
|
|
170
|
+
return ""
|
|
171
|
+
if arch.layers:
|
|
172
|
+
layer_names = [l.name for l in arch.layers[:4]]
|
|
173
|
+
return f"{label} with {', '.join(layer_names)} layers."
|
|
174
|
+
return f"{label} pattern detected."
|
|
97
175
|
|
|
98
176
|
def _summarize_python_entry(self, path: str, content: str) -> list[str]:
|
|
99
177
|
try:
|
|
@@ -189,6 +267,44 @@ class ArchitectureSummarizer:
|
|
|
189
267
|
lines.append("Orquesta el arranque de la aplicacion JVM.")
|
|
190
268
|
return lines
|
|
191
269
|
|
|
270
|
+
def _summarize_dotnet_entry(self, stacks: list[StackDetection]) -> list[str]:
|
|
271
|
+
dotnet_stacks = [s for s in stacks if s.stack == "dotnet"]
|
|
272
|
+
if not dotnet_stacks:
|
|
273
|
+
return []
|
|
274
|
+
lines: list[str] = []
|
|
275
|
+
signals = [sig for s in dotnet_stacks for sig in s.signals]
|
|
276
|
+
|
|
277
|
+
for sig in signals:
|
|
278
|
+
if "project" in sig and "detected" in sig:
|
|
279
|
+
lines.append(sig[0].upper() + sig[1:] + ".")
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
for sig in signals:
|
|
283
|
+
if sig.startswith("project types:"):
|
|
284
|
+
types = sig.removeprefix("project types:").strip()
|
|
285
|
+
lines.append(f"Stack: {types}.")
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
for sig in signals:
|
|
289
|
+
if sig.startswith("target frameworks:"):
|
|
290
|
+
fws = sig.removeprefix("target frameworks:").strip()
|
|
291
|
+
lines.append(f"Target: {fws}.")
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
for sig in signals:
|
|
295
|
+
if sig.startswith("architecture:"):
|
|
296
|
+
pattern = sig.removeprefix("architecture:").strip()
|
|
297
|
+
lines.append(f"Patrón detectado: {pattern}.")
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
framework_names = [f.name for s in dotnet_stacks for f in s.frameworks]
|
|
301
|
+
if framework_names:
|
|
302
|
+
lines.append(f"Frameworks: {', '.join(framework_names)}.")
|
|
303
|
+
|
|
304
|
+
if not lines:
|
|
305
|
+
lines.append("Solución .NET detectada.")
|
|
306
|
+
return lines
|
|
307
|
+
|
|
192
308
|
def _summarize_go_entry(self, path: str, content: str) -> list[str]:
|
|
193
309
|
imports = re.findall(r'"([^"]+)"', content)
|
|
194
310
|
internal = [module for module in imports if not module.startswith(("fmt", "net/", "os", "context"))]
|
|
@@ -273,6 +389,16 @@ class ArchitectureSummarizer:
|
|
|
273
389
|
confidence="medium",
|
|
274
390
|
)
|
|
275
391
|
)
|
|
392
|
+
elif path.endswith("Program.cs") or path.endswith("Program.fs"):
|
|
393
|
+
candidates.append(
|
|
394
|
+
EntryPoint(
|
|
395
|
+
path=path,
|
|
396
|
+
stack="dotnet",
|
|
397
|
+
kind="cli",
|
|
398
|
+
source="convention",
|
|
399
|
+
confidence="medium",
|
|
400
|
+
)
|
|
401
|
+
)
|
|
276
402
|
return candidates
|
|
277
403
|
|
|
278
404
|
def _fallback_priority(self, path: str) -> tuple[int, int, str]:
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Generates a compact, high-signal ContextSummary for AI agent consumption.
|
|
4
|
+
|
|
5
|
+
Uses all available analysis: stacks, architecture, graph analytics, entry points.
|
|
6
|
+
Fast path: O(1) from pre-computed data. Falls back to inline ArchitectureAnalyzer if needed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from sourcecode.schema import ContextSummary, SourceMap
|
|
13
|
+
|
|
14
|
+
_STACK_LABELS: dict[str, str] = {
|
|
15
|
+
"python": "Python",
|
|
16
|
+
"nodejs": "Node.js/TypeScript",
|
|
17
|
+
"go": "Go",
|
|
18
|
+
"rust": "Rust",
|
|
19
|
+
"dotnet": "C#/.NET",
|
|
20
|
+
"java": "Java",
|
|
21
|
+
"kotlin": "Kotlin",
|
|
22
|
+
"scala": "Scala",
|
|
23
|
+
"ruby": "Ruby",
|
|
24
|
+
"php": "PHP",
|
|
25
|
+
"dart": "Dart/Flutter",
|
|
26
|
+
"elixir": "Elixir",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_TYPE_LABELS: dict[str, str] = {
|
|
30
|
+
"api": "REST API",
|
|
31
|
+
"webapp": "Web app",
|
|
32
|
+
"fullstack": "Full-stack app",
|
|
33
|
+
"cli": "CLI tool",
|
|
34
|
+
"worker": "Background worker / service",
|
|
35
|
+
"library": "Library / package",
|
|
36
|
+
"unknown": "Application",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_LAYER_HINTS: dict[str, str] = {
|
|
40
|
+
"domain": "Core domain logic and entities",
|
|
41
|
+
"application": "Application / use-case orchestration",
|
|
42
|
+
"infrastructure": "External integrations (DB, messaging, APIs)",
|
|
43
|
+
"controller": "HTTP handlers and route definitions",
|
|
44
|
+
"service": "Business logic services",
|
|
45
|
+
"repository": "Data access / persistence",
|
|
46
|
+
"commands": "CQRS write side (commands)",
|
|
47
|
+
"queries": "CQRS read side (queries)",
|
|
48
|
+
"ports": "Hexagonal ports (interfaces)",
|
|
49
|
+
"adapters": "Hexagonal adapters (implementations)",
|
|
50
|
+
"processing": "Data processing / analysis",
|
|
51
|
+
"orchestration": "Orchestration / coordination",
|
|
52
|
+
"data": "Data models / schemas",
|
|
53
|
+
"apps": "Application packages (monorepo apps)",
|
|
54
|
+
"packages": "Shared packages (monorepo libs)",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ContextSummarizer:
|
|
59
|
+
"""Generates ContextSummary from a fully-populated SourceMap."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, root: Path) -> None:
|
|
62
|
+
self.root = root
|
|
63
|
+
|
|
64
|
+
def generate(self, sm: SourceMap) -> Optional[ContextSummary]:
|
|
65
|
+
try:
|
|
66
|
+
return self._build(sm)
|
|
67
|
+
except Exception:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _build(self, sm: SourceMap) -> ContextSummary:
|
|
71
|
+
arch = sm.architecture
|
|
72
|
+
if arch is None and sm.file_paths:
|
|
73
|
+
from sourcecode.architecture_analyzer import ArchitectureAnalyzer
|
|
74
|
+
arch = ArchitectureAnalyzer().analyze(self.root, sm)
|
|
75
|
+
|
|
76
|
+
return ContextSummary(
|
|
77
|
+
runtime_shape=self._infer_runtime_shape(sm),
|
|
78
|
+
dominant_pattern=self._dominant_pattern(arch),
|
|
79
|
+
critical_modules=self._collect_critical_modules(sm),
|
|
80
|
+
layer_map=self._build_layer_map(arch),
|
|
81
|
+
edit_hints=self._generate_edit_hints(arch, sm),
|
|
82
|
+
coupling_notes=self._coupling_notes(sm),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _infer_runtime_shape(self, sm: SourceMap) -> str:
|
|
86
|
+
runtime = _TYPE_LABELS.get(sm.project_type or "", "Application")
|
|
87
|
+
|
|
88
|
+
# Gather framework names from all stacks (deduplicated, max 3)
|
|
89
|
+
fw_names: list[str] = []
|
|
90
|
+
seen: set[str] = set()
|
|
91
|
+
for stack in sm.stacks:
|
|
92
|
+
for fw in stack.frameworks:
|
|
93
|
+
if fw.name not in seen:
|
|
94
|
+
seen.add(fw.name)
|
|
95
|
+
fw_names.append(fw.name)
|
|
96
|
+
fw_names = fw_names[:3]
|
|
97
|
+
|
|
98
|
+
if fw_names:
|
|
99
|
+
return f"{runtime} — {', '.join(fw_names)}"
|
|
100
|
+
|
|
101
|
+
# Fallback: stack label
|
|
102
|
+
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
103
|
+
if primary:
|
|
104
|
+
label = _STACK_LABELS.get(primary.stack, primary.stack)
|
|
105
|
+
return f"{runtime} — {label}"
|
|
106
|
+
|
|
107
|
+
return runtime
|
|
108
|
+
|
|
109
|
+
def _dominant_pattern(self, arch: Any) -> Optional[str]:
|
|
110
|
+
if arch is None:
|
|
111
|
+
return None
|
|
112
|
+
pattern = arch.pattern
|
|
113
|
+
if pattern in (None, "unknown", "flat"):
|
|
114
|
+
return None
|
|
115
|
+
return pattern
|
|
116
|
+
|
|
117
|
+
def _collect_critical_modules(self, sm: SourceMap) -> list[str]:
|
|
118
|
+
critical: list[str] = []
|
|
119
|
+
seen: set[str] = set()
|
|
120
|
+
|
|
121
|
+
for ep in sm.entry_points[:3]:
|
|
122
|
+
if ep.path and ep.path not in seen:
|
|
123
|
+
seen.add(ep.path)
|
|
124
|
+
critical.append(ep.path)
|
|
125
|
+
|
|
126
|
+
mgr = sm.module_graph_summary
|
|
127
|
+
if mgr:
|
|
128
|
+
for hub in mgr.hubs[:4]:
|
|
129
|
+
path = hub.removeprefix("module:")
|
|
130
|
+
if path and path not in seen:
|
|
131
|
+
seen.add(path)
|
|
132
|
+
critical.append(path)
|
|
133
|
+
|
|
134
|
+
return critical[:6]
|
|
135
|
+
|
|
136
|
+
def _build_layer_map(self, arch: Any) -> dict[str, list[str]]:
|
|
137
|
+
if arch is None or not arch.layers:
|
|
138
|
+
return {}
|
|
139
|
+
result: dict[str, list[str]] = {}
|
|
140
|
+
for layer in arch.layers[:6]:
|
|
141
|
+
dirs: set[str] = set()
|
|
142
|
+
for f in layer.files[:8]:
|
|
143
|
+
parts = f.replace("\\", "/").split("/")
|
|
144
|
+
if len(parts) >= 2:
|
|
145
|
+
dirs.add("/".join(parts[:-1]) + "/")
|
|
146
|
+
else:
|
|
147
|
+
dirs.add(f)
|
|
148
|
+
if dirs:
|
|
149
|
+
result[layer.name] = sorted(dirs)[:3]
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
def _generate_edit_hints(self, arch: Any, sm: SourceMap) -> list[str]:
|
|
153
|
+
hints: list[str] = []
|
|
154
|
+
if arch is None or not arch.layers:
|
|
155
|
+
return hints
|
|
156
|
+
|
|
157
|
+
for layer in arch.layers[:5]:
|
|
158
|
+
hint_label = _LAYER_HINTS.get(layer.name, layer.name)
|
|
159
|
+
dirs: list[str] = []
|
|
160
|
+
seen_dirs: set[str] = set()
|
|
161
|
+
for f in layer.files[:6]:
|
|
162
|
+
parts = f.replace("\\", "/").split("/")
|
|
163
|
+
d = "/".join(parts[:-1]) + "/" if len(parts) >= 2 else f
|
|
164
|
+
if d not in seen_dirs:
|
|
165
|
+
seen_dirs.add(d)
|
|
166
|
+
dirs.append(d)
|
|
167
|
+
if dirs:
|
|
168
|
+
dir_str = ", ".join(dirs[:2])
|
|
169
|
+
hints.append(f"{hint_label} → {dir_str}")
|
|
170
|
+
|
|
171
|
+
return hints[:4]
|
|
172
|
+
|
|
173
|
+
def _coupling_notes(self, sm: SourceMap) -> list[str]:
|
|
174
|
+
notes: list[str] = []
|
|
175
|
+
mgr = sm.module_graph_summary
|
|
176
|
+
if mgr is None:
|
|
177
|
+
return notes
|
|
178
|
+
|
|
179
|
+
if mgr.cycle_count > 0:
|
|
180
|
+
s = "s" if mgr.cycle_count > 1 else ""
|
|
181
|
+
notes.append(f"{mgr.cycle_count} circular import cycle{s} detected")
|
|
182
|
+
|
|
183
|
+
if mgr.hubs:
|
|
184
|
+
names = [h.removeprefix("module:").split("/")[-1] for h in mgr.hubs[:3]]
|
|
185
|
+
notes.append(f"High-coupling hubs: {', '.join(names)}")
|
|
186
|
+
|
|
187
|
+
orphan_count = len(mgr.orphans)
|
|
188
|
+
if orphan_count >= 3:
|
|
189
|
+
notes.append(f"{orphan_count} orphan modules (unreferenced)")
|
|
190
|
+
|
|
191
|
+
return notes
|