sourcecode 0.24.0__tar.gz → 0.26.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.24.0 → sourcecode-0.26.0}/.gitignore +3 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/PKG-INFO +1 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/docs/schema.md +2 -2
- {sourcecode-0.24.0 → sourcecode-0.26.0}/pyproject.toml +1 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/architecture_analyzer.py +77 -5
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/architecture_summary.py +143 -29
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/cli.py +8 -0
- sourcecode-0.26.0/src/sourcecode/context_summarizer.py +191 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/dotnet.py +7 -2
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/go.py +21 -2
- sourcecode-0.26.0/src/sourcecode/detectors/hybrid.py +235 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/java.py +20 -12
- sourcecode-0.26.0/src/sourcecode/detectors/nodejs.py +265 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/python.py +16 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/rust.py +37 -3
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/graph_analyzer.py +375 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/prepare_context.py +76 -4
- sourcecode-0.26.0/src/sourcecode/relevance_scorer.py +182 -0
- sourcecode-0.26.0/src/sourcecode/runtime_classifier.py +331 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/schema.py +56 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/serializer.py +37 -2
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/summarizer.py +75 -1
- sourcecode-0.26.0/tests/__init__.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_architecture_summary.py +5 -2
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_dependency_analyzer_node_python.py +1 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_nodejs.py +3 -2
- sourcecode-0.26.0/tests/test_hybrid_inference.py +367 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration.py +1 -1
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_dependencies.py +1 -1
- sourcecode-0.26.0/tests/test_phase1_improvements.py +412 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_schema.py +2 -2
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_signal_hierarchy.py +1 -1
- sourcecode-0.24.0/src/sourcecode/detectors/nodejs.py +0 -136
- {sourcecode-0.24.0 → sourcecode-0.26.0}/.ruff.toml +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/README.md +0 -0
- /sourcecode-0.24.0/tests/__init__.py → /sourcecode-0.26.0/raw +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/conftest.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_cli.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_dotnet.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.24.0 → sourcecode-0.26.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.26.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.26.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]]]:
|
|
@@ -9,6 +9,12 @@ from sourcecode.schema import EntryPoint, SourceMap, StackDetection
|
|
|
9
9
|
from sourcecode.tree_utils import flatten_file_tree
|
|
10
10
|
|
|
11
11
|
_TOOLING_PREFIXES = (".claude/", ".vscode/", "bin/")
|
|
12
|
+
_AUXILIARY_PATH_PARTS: frozenset[str] = frozenset({
|
|
13
|
+
"benchmark", "benchmarks", "bench", "benches",
|
|
14
|
+
"demo", "demos", "example", "examples",
|
|
15
|
+
"docs", "doc", "fixture", "fixtures",
|
|
16
|
+
"playground", "playgrounds", "sandbox",
|
|
17
|
+
})
|
|
12
18
|
_PYTHON_EXTENSIONS = {".py"}
|
|
13
19
|
_NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
14
20
|
_GO_EXTENSIONS = {".go"}
|
|
@@ -49,43 +55,46 @@ class ArchitectureSummarizer:
|
|
|
49
55
|
if not file_paths:
|
|
50
56
|
return None
|
|
51
57
|
|
|
58
|
+
# Rich path: use all available signals (stacks, arch analysis, graph)
|
|
59
|
+
rich_lines = self._build_rich_lines(sm)
|
|
60
|
+
|
|
61
|
+
# Stack-specific entry point analysis: exclude tooling + auxiliary paths
|
|
52
62
|
entry_points = [
|
|
53
63
|
entry for entry in sm.entry_points
|
|
54
64
|
if not self._is_tooling_path(entry.path)
|
|
65
|
+
and not self._is_auxiliary_path(entry.path)
|
|
66
|
+
and entry.entrypoint_type not in ("benchmark", "example")
|
|
55
67
|
]
|
|
56
68
|
if not entry_points:
|
|
57
69
|
fallback = self._infer_fallback_entry_points(file_paths, sm.stacks)
|
|
58
70
|
entry_points = fallback[:1]
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if lang_lines:
|
|
83
|
-
# Product-level description available — no need for internal "Entry point: ..." header
|
|
84
|
-
lines = lang_lines
|
|
85
|
-
else:
|
|
72
|
+
lang_lines: list[str] = []
|
|
73
|
+
if entry_points:
|
|
74
|
+
entry_point = entry_points[0]
|
|
75
|
+
content = self._read_file(entry_point.path)
|
|
76
|
+
if content:
|
|
77
|
+
suffix = Path(entry_point.path).suffix
|
|
78
|
+
if suffix in _PYTHON_EXTENSIONS:
|
|
79
|
+
lang_lines = self._summarize_python_entry(entry_point.path, content)
|
|
80
|
+
elif suffix in _NODE_EXTENSIONS:
|
|
81
|
+
lang_lines = self._summarize_node_entry(entry_point.path, content)
|
|
82
|
+
elif suffix in _GO_EXTENSIONS:
|
|
83
|
+
lang_lines = self._summarize_go_entry(entry_point.path, content)
|
|
84
|
+
elif suffix in _JAVA_EXTENSIONS:
|
|
85
|
+
lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
|
|
86
|
+
elif suffix in {".cs", ".fs", ".vb"}:
|
|
87
|
+
lang_lines = self._summarize_dotnet_entry(sm.stacks)
|
|
88
|
+
|
|
89
|
+
# Merge: rich lines first, stack-specific details appended (deduped)
|
|
90
|
+
lines = rich_lines + [l for l in lang_lines if l not in rich_lines]
|
|
91
|
+
|
|
92
|
+
if not lines and entry_points:
|
|
93
|
+
entry_point = entry_points[0]
|
|
86
94
|
lines = [self._describe_entry_point(entry_point, sm.project_type)]
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
|
|
96
|
+
if not lines:
|
|
97
|
+
return "Arquitectura no inferida con suficiente evidencia estatica."
|
|
89
98
|
|
|
90
99
|
unique_lines: list[str] = []
|
|
91
100
|
seen: set[str] = set()
|
|
@@ -95,7 +104,106 @@ class ArchitectureSummarizer:
|
|
|
95
104
|
continue
|
|
96
105
|
seen.add(line)
|
|
97
106
|
unique_lines.append(line)
|
|
98
|
-
return "\n".join(unique_lines[:
|
|
107
|
+
return "\n".join(unique_lines[:6]) if unique_lines else None
|
|
108
|
+
|
|
109
|
+
def _build_rich_lines(self, sm: SourceMap) -> list[str]:
|
|
110
|
+
"""Generate architect-quality summary lines from stacks, arch analysis, and graph."""
|
|
111
|
+
if not sm.stacks:
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
# Compute arch analysis inline if needed
|
|
115
|
+
arch = sm.architecture
|
|
116
|
+
if arch is None and sm.file_paths:
|
|
117
|
+
try:
|
|
118
|
+
from sourcecode.architecture_analyzer import ArchitectureAnalyzer
|
|
119
|
+
arch = ArchitectureAnalyzer().analyze(self.root, sm)
|
|
120
|
+
except Exception:
|
|
121
|
+
arch = None
|
|
122
|
+
|
|
123
|
+
lines: list[str] = []
|
|
124
|
+
|
|
125
|
+
# Line 1: project description (type + stack + frameworks)
|
|
126
|
+
project_line = self._describe_project_type(sm)
|
|
127
|
+
if project_line:
|
|
128
|
+
lines.append(project_line)
|
|
129
|
+
|
|
130
|
+
# Line 2: architecture pattern + layers
|
|
131
|
+
if arch and arch.pattern not in (None, "unknown", "flat"):
|
|
132
|
+
arch_line = self._describe_arch_pattern(arch)
|
|
133
|
+
if arch_line:
|
|
134
|
+
lines.append(arch_line)
|
|
135
|
+
|
|
136
|
+
# Line 3: coupling notes (if graph analytics present)
|
|
137
|
+
if sm.module_graph_summary:
|
|
138
|
+
mgr = sm.module_graph_summary
|
|
139
|
+
coupling_parts: list[str] = []
|
|
140
|
+
if mgr.cycle_count > 0:
|
|
141
|
+
s = "s" if mgr.cycle_count > 1 else ""
|
|
142
|
+
coupling_parts.append(f"{mgr.cycle_count} import cycle{s}")
|
|
143
|
+
if mgr.hubs:
|
|
144
|
+
hub_names = [h.removeprefix("module:").split("/")[-1] for h in mgr.hubs[:2]]
|
|
145
|
+
coupling_parts.append(f"hub modules: {', '.join(hub_names)}")
|
|
146
|
+
if coupling_parts:
|
|
147
|
+
lines.append(f"Coupling: {'; '.join(coupling_parts)}.")
|
|
148
|
+
|
|
149
|
+
return lines
|
|
150
|
+
|
|
151
|
+
def _describe_project_type(self, sm: SourceMap) -> str:
|
|
152
|
+
from sourcecode.context_summarizer import _STACK_LABELS, _TYPE_LABELS
|
|
153
|
+
runtime = _TYPE_LABELS.get(sm.project_type or "", "")
|
|
154
|
+
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
155
|
+
if primary is None:
|
|
156
|
+
return ""
|
|
157
|
+
stack_label = _STACK_LABELS.get(primary.stack, primary.stack)
|
|
158
|
+
|
|
159
|
+
# For monorepos: only show frameworks from runtime-critical packages,
|
|
160
|
+
# not every framework found in every workspace (avoids "React project" misclassification)
|
|
161
|
+
if sm.project_type == "monorepo" and sm.monorepo_packages:
|
|
162
|
+
runtime_roles = {"runtime_core", "plugin_host", "backend_runtime"}
|
|
163
|
+
runtime_paths = {p.path for p in sm.monorepo_packages if p.architectural_role in runtime_roles}
|
|
164
|
+
fw_names = [
|
|
165
|
+
f.name
|
|
166
|
+
for s in sm.stacks
|
|
167
|
+
if not s.workspace or s.workspace in runtime_paths
|
|
168
|
+
for f in s.frameworks[:2]
|
|
169
|
+
]
|
|
170
|
+
fw_names = list(dict.fromkeys(fw_names))[:3] # dedup, preserve order
|
|
171
|
+
else:
|
|
172
|
+
# Filter out frameworks from auxiliary or tooling stacks (docs/, benchmarks/, etc.)
|
|
173
|
+
fw_names = [
|
|
174
|
+
f.name
|
|
175
|
+
for s in sm.stacks
|
|
176
|
+
if not self._is_tooling_path(s.root or "")
|
|
177
|
+
and not self._is_tooling_path(s.workspace or "")
|
|
178
|
+
and not self._is_auxiliary_path(s.root or "")
|
|
179
|
+
and not self._is_auxiliary_path(s.workspace or "")
|
|
180
|
+
for f in s.frameworks[:2]
|
|
181
|
+
][:3]
|
|
182
|
+
|
|
183
|
+
fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
|
|
184
|
+
if runtime:
|
|
185
|
+
return f"{stack_label} {runtime.lower()}{fw_str}."
|
|
186
|
+
return f"{stack_label} project{fw_str}."
|
|
187
|
+
|
|
188
|
+
def _describe_arch_pattern(self, arch: Any) -> str:
|
|
189
|
+
pattern_labels = {
|
|
190
|
+
"clean": "Clean Architecture",
|
|
191
|
+
"onion": "Onion Architecture",
|
|
192
|
+
"hexagonal": "Hexagonal Architecture",
|
|
193
|
+
"layered": "Layered Architecture",
|
|
194
|
+
"mvc": "MVC pattern",
|
|
195
|
+
"cqrs": "CQRS pattern",
|
|
196
|
+
"microservices": "Microservices",
|
|
197
|
+
"monorepo": "Monorepo workspace",
|
|
198
|
+
"modular": "Modular architecture",
|
|
199
|
+
}
|
|
200
|
+
label = pattern_labels.get(arch.pattern, arch.pattern.replace("_", " ").title() if arch.pattern else "")
|
|
201
|
+
if not label:
|
|
202
|
+
return ""
|
|
203
|
+
if arch.layers:
|
|
204
|
+
layer_names = [l.name for l in arch.layers[:4]]
|
|
205
|
+
return f"{label} with {', '.join(layer_names)} layers."
|
|
206
|
+
return f"{label} pattern detected."
|
|
99
207
|
|
|
100
208
|
def _summarize_python_entry(self, path: str, content: str) -> list[str]:
|
|
101
209
|
try:
|
|
@@ -367,6 +475,12 @@ class ArchitectureSummarizer:
|
|
|
367
475
|
except OSError:
|
|
368
476
|
return None
|
|
369
477
|
|
|
478
|
+
def _is_auxiliary_path(self, path: str | None) -> bool:
|
|
479
|
+
if not path:
|
|
480
|
+
return False
|
|
481
|
+
parts = path.replace("\\", "/").strip("/").lower().split("/")
|
|
482
|
+
return any(p in _AUXILIARY_PATH_PARTS for p in parts)
|
|
483
|
+
|
|
370
484
|
def _is_tooling_path(self, path: str | None) -> bool:
|
|
371
485
|
if not path:
|
|
372
486
|
return False
|
|
@@ -555,6 +555,14 @@ def main(
|
|
|
555
555
|
semantic_summary=sem_sum,
|
|
556
556
|
)
|
|
557
557
|
|
|
558
|
+
# Runtime architecture — classify workspace packages for structural summaries
|
|
559
|
+
if workspace_analysis.workspaces:
|
|
560
|
+
from sourcecode.runtime_classifier import RuntimeClassifier
|
|
561
|
+
sm.monorepo_packages = RuntimeClassifier().classify(
|
|
562
|
+
target,
|
|
563
|
+
[ws.path for ws in workspace_analysis.workspaces],
|
|
564
|
+
)
|
|
565
|
+
|
|
558
566
|
# Phase 9: LLM Output Quality — poblar campos derivados
|
|
559
567
|
from sourcecode.architecture_summary import ArchitectureSummarizer
|
|
560
568
|
from sourcecode.summarizer import ProjectSummarizer
|
|
@@ -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
|
|
@@ -55,9 +55,14 @@ class DotnetDetector(AbstractDetector):
|
|
|
55
55
|
if project is not None:
|
|
56
56
|
projects.append(project)
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
from sourcecode.detectors.hybrid import merge_framework_detections, scan_for_frameworks
|
|
59
|
+
|
|
60
|
+
manifest_frameworks = self._collect_frameworks(projects)
|
|
59
61
|
signals = self._build_signals(projects)
|
|
60
|
-
entry_points = self._detect_entry_points(context, projects,
|
|
62
|
+
entry_points = self._detect_entry_points(context, projects, manifest_frameworks)
|
|
63
|
+
priority = [ep.path for ep in entry_points]
|
|
64
|
+
import_frameworks = scan_for_frameworks(context.root, context.file_tree, "dotnet", priority_paths=priority)
|
|
65
|
+
frameworks = merge_framework_detections(manifest_frameworks, import_frameworks)
|
|
61
66
|
|
|
62
67
|
manifests = project_paths + [p for p in all_paths if p.endswith(".sln")]
|
|
63
68
|
stack = StackDetection(
|