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.
Files changed (121) hide show
  1. {sourcecode-0.24.0 → sourcecode-0.26.0}/.gitignore +3 -0
  2. {sourcecode-0.24.0 → sourcecode-0.26.0}/PKG-INFO +1 -1
  3. {sourcecode-0.24.0 → sourcecode-0.26.0}/docs/schema.md +2 -2
  4. {sourcecode-0.24.0 → sourcecode-0.26.0}/pyproject.toml +1 -1
  5. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/architecture_analyzer.py +77 -5
  7. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/architecture_summary.py +143 -29
  8. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/cli.py +8 -0
  9. sourcecode-0.26.0/src/sourcecode/context_summarizer.py +191 -0
  10. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/dotnet.py +7 -2
  11. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/go.py +21 -2
  12. sourcecode-0.26.0/src/sourcecode/detectors/hybrid.py +235 -0
  13. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/java.py +20 -12
  14. sourcecode-0.26.0/src/sourcecode/detectors/nodejs.py +265 -0
  15. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/python.py +16 -1
  16. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/rust.py +37 -3
  17. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/graph_analyzer.py +375 -1
  18. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/prepare_context.py +76 -4
  19. sourcecode-0.26.0/src/sourcecode/relevance_scorer.py +182 -0
  20. sourcecode-0.26.0/src/sourcecode/runtime_classifier.py +331 -0
  21. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/schema.py +56 -1
  22. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/serializer.py +37 -2
  23. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/summarizer.py +75 -1
  24. sourcecode-0.26.0/tests/__init__.py +0 -0
  25. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_architecture_summary.py +5 -2
  26. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_dependency_analyzer_node_python.py +1 -1
  27. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_nodejs.py +3 -2
  28. sourcecode-0.26.0/tests/test_hybrid_inference.py +367 -0
  29. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration.py +1 -1
  30. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_dependencies.py +1 -1
  31. sourcecode-0.26.0/tests/test_phase1_improvements.py +412 -0
  32. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_schema.py +2 -2
  33. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_signal_hierarchy.py +1 -1
  34. sourcecode-0.24.0/src/sourcecode/detectors/nodejs.py +0 -136
  35. {sourcecode-0.24.0 → sourcecode-0.26.0}/.ruff.toml +0 -0
  36. {sourcecode-0.24.0 → sourcecode-0.26.0}/README.md +0 -0
  37. /sourcecode-0.24.0/tests/__init__.py → /sourcecode-0.26.0/raw +0 -0
  38. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/classifier.py +0 -0
  39. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  40. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/confidence_analyzer.py +0 -0
  41. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/coverage_parser.py +0 -0
  42. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/dependency_analyzer.py +0 -0
  43. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/__init__.py +0 -0
  44. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/base.py +0 -0
  45. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  46. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/dart.py +0 -0
  47. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/elixir.py +0 -0
  48. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/heuristic.py +0 -0
  49. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  50. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/parsers.py +0 -0
  51. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/php.py +0 -0
  52. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/project.py +0 -0
  53. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/ruby.py +0 -0
  54. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/systems.py +0 -0
  55. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/terraform.py +0 -0
  56. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/detectors/tooling.py +0 -0
  57. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/doc_analyzer.py +0 -0
  58. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/env_analyzer.py +0 -0
  59. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/git_analyzer.py +0 -0
  60. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/metrics_analyzer.py +0 -0
  61. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/redactor.py +0 -0
  62. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/scanner.py +0 -0
  63. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/semantic_analyzer.py +0 -0
  64. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/tree_utils.py +0 -0
  65. {sourcecode-0.24.0 → sourcecode-0.26.0}/src/sourcecode/workspace.py +0 -0
  66. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/conftest.py +0 -0
  67. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/coverage.xml +0 -0
  68. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  69. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  70. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  71. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/go_service/go.mod +0 -0
  72. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/jacoco.xml +0 -0
  73. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/lcov.info +0 -0
  74. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  75. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/package.json +0 -0
  76. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  77. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  78. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  79. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  80. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  81. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  82. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_architecture_analyzer.py +0 -0
  83. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_classifier.py +0 -0
  84. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_cli.py +0 -0
  85. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_code_notes_analyzer.py +0 -0
  86. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_coverage_parser.py +0 -0
  87. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_cross_consistency.py +0 -0
  88. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  89. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_dependency_schema.py +0 -0
  90. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_dotnet.py +0 -0
  91. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_go_rust_java.py +0 -0
  92. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_php_ruby_dart.py +0 -0
  93. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_python.py +0 -0
  94. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_universal_managed.py +0 -0
  95. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detector_universal_systems.py +0 -0
  96. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_detectors_base.py +0 -0
  97. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  98. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_doc_analyzer_python.py +0 -0
  99. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  100. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_graph_analyzer_python_node.py +0 -0
  101. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_graph_schema.py +0 -0
  102. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_detection.py +0 -0
  103. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_docs.py +0 -0
  104. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_graph_modules.py +0 -0
  105. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_lqn.py +0 -0
  106. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_metrics.py +0 -0
  107. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_multistack.py +0 -0
  108. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_semantics.py +0 -0
  109. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_integration_universal.py +0 -0
  110. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_metrics_analyzer.py +0 -0
  111. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_packaging.py +0 -0
  112. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_real_projects.py +0 -0
  113. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_redactor.py +0 -0
  114. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_scanner.py +0 -0
  115. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_schema_normalization.py +0 -0
  116. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_analyzer_node.py +0 -0
  117. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_analyzer_python.py +0 -0
  118. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_import_resolution.py +0 -0
  119. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_semantic_schema.py +0 -0
  120. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_summarizer.py +0 -0
  121. {sourcecode-0.24.0 → sourcecode-0.26.0}/tests/test_workspace_analyzer.py +0 -0
@@ -24,7 +24,10 @@ src/*.egg-info/
24
24
 
25
25
  # Environment / secrets
26
26
  *.env
27
+ .env.*
27
28
  *.secret.json
29
+ *.local.json
30
+ *.local.toml
28
31
 
29
32
  # OS cruft
30
33
  .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.24.0
3
+ Version: 0.26.0
4
4
  Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -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.24.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.24.0",
648
+ "sourcecode_version": "0.26.0",
649
649
  "analyzed_path": "/abs/path/to/project"
650
650
  },
651
651
  "file_tree": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.24.0"
7
+ version = "0.26.0"
8
8
  description = "Genera un mapa de contexto estructurado de proyectos de software para agentes IA"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
2
 
3
- __version__ = "0.24.0"
3
+ __version__ = "0.26.0"
@@ -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 (MVC, layered, hexagonal, fullstack)
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
- if score > best_score:
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. Functional file-naming heuristic: *_analyzer.py, cli.py, schema.py, …
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
- # 3. Modular sub-package heuristic: ≥2 distinct named sub-packages
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
- # 4. Fallback: flat (shallow) vs truly unknown (deep but unrecognised)
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
- if not entry_points:
61
- return "Arquitectura no inferida con suficiente evidencia estatica."
62
-
63
- entry_point = entry_points[0]
64
- content = self._read_file(entry_point.path)
65
- if content is None:
66
- return f"Entry point principal: {entry_point.path}. Arquitectura no inferida con suficiente evidencia estatica."
67
-
68
- suffix = Path(entry_point.path).suffix
69
- if suffix in _PYTHON_EXTENSIONS:
70
- lang_lines = self._summarize_python_entry(entry_point.path, content)
71
- elif suffix in _NODE_EXTENSIONS:
72
- lang_lines = self._summarize_node_entry(entry_point.path, content)
73
- elif suffix in _GO_EXTENSIONS:
74
- lang_lines = self._summarize_go_entry(entry_point.path, content)
75
- elif suffix in _JAVA_EXTENSIONS:
76
- lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
77
- elif suffix in {".cs", ".fs", ".vb"}:
78
- lang_lines = self._summarize_dotnet_entry(sm.stacks)
79
- else:
80
- lang_lines = []
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
- if not lang_lines:
88
- lines.append("Orquesta modulos internos no detallados por el analisis estatico disponible.")
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[:5]) if unique_lines else None
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
- frameworks = self._collect_frameworks(projects)
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, frameworks)
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(