sourcecode 0.27.0__tar.gz → 0.29.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 (133) hide show
  1. {sourcecode-0.27.0 → sourcecode-0.29.0}/PKG-INFO +1 -1
  2. {sourcecode-0.27.0 → sourcecode-0.29.0}/pyproject.toml +2 -2
  3. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/architecture_analyzer.py +76 -11
  5. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/cli.py +406 -25
  6. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/confidence_analyzer.py +54 -2
  7. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/heuristic.py +19 -1
  8. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/nodejs.py +21 -11
  9. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/project.py +7 -0
  10. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/env_analyzer.py +25 -13
  11. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/git_analyzer.py +57 -7
  12. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/schema.py +26 -0
  13. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/serializer.py +10 -2
  14. sourcecode-0.29.0/tests/test_cli.py +93 -0
  15. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detectors_base.py +10 -9
  16. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_packaging.py +1 -1
  17. sourcecode-0.29.0/tests/test_pipeline_integrity.py +441 -0
  18. sourcecode-0.27.0/tests/test_cli.py +0 -58
  19. {sourcecode-0.27.0 → sourcecode-0.29.0}/.gitignore +0 -0
  20. {sourcecode-0.27.0 → sourcecode-0.29.0}/.ruff.toml +0 -0
  21. {sourcecode-0.27.0 → sourcecode-0.29.0}/CONTRIBUTING.md +0 -0
  22. {sourcecode-0.27.0 → sourcecode-0.29.0}/LICENSE +0 -0
  23. {sourcecode-0.27.0 → sourcecode-0.29.0}/README.md +0 -0
  24. {sourcecode-0.27.0 → sourcecode-0.29.0}/SECURITY.md +0 -0
  25. {sourcecode-0.27.0 → sourcecode-0.29.0}/docs/privacy.md +0 -0
  26. {sourcecode-0.27.0 → sourcecode-0.29.0}/docs/schema.md +0 -0
  27. {sourcecode-0.27.0 → sourcecode-0.29.0}/raw +0 -0
  28. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/architecture_summary.py +0 -0
  29. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/classifier.py +0 -0
  30. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  31. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/context_summarizer.py +0 -0
  32. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/coverage_parser.py +0 -0
  33. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/dependency_analyzer.py +0 -0
  34. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/__init__.py +0 -0
  35. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/base.py +0 -0
  36. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  37. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/dart.py +0 -0
  38. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/dotnet.py +0 -0
  39. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/elixir.py +0 -0
  40. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/go.py +0 -0
  41. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/parsers.py +0 -0
  45. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/php.py +0 -0
  46. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/python.py +0 -0
  47. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/ruby.py +0 -0
  48. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/rust.py +0 -0
  49. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/systems.py +0 -0
  50. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/terraform.py +0 -0
  51. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/detectors/tooling.py +0 -0
  52. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/doc_analyzer.py +0 -0
  53. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/graph_analyzer.py +0 -0
  54. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/metrics_analyzer.py +0 -0
  55. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/prepare_context.py +0 -0
  56. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/redactor.py +0 -0
  57. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/relevance_scorer.py +0 -0
  58. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/runtime_classifier.py +0 -0
  59. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/scanner.py +0 -0
  60. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/semantic_analyzer.py +0 -0
  61. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/summarizer.py +0 -0
  62. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/telemetry/__init__.py +0 -0
  63. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/telemetry/config.py +0 -0
  64. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/telemetry/consent.py +0 -0
  65. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/telemetry/events.py +0 -0
  66. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/telemetry/filters.py +0 -0
  67. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/telemetry/transport.py +0 -0
  68. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/tree_utils.py +0 -0
  69. {sourcecode-0.27.0 → sourcecode-0.29.0}/src/sourcecode/workspace.py +0 -0
  70. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/__init__.py +0 -0
  71. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/conftest.py +0 -0
  72. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/coverage.xml +0 -0
  73. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  74. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  75. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  76. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/go_service/go.mod +0 -0
  77. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/jacoco.xml +0 -0
  78. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/lcov.info +0 -0
  79. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  80. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/nextjs_app/package.json +0 -0
  81. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  82. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  83. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  84. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  85. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  86. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  87. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_architecture_analyzer.py +0 -0
  88. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_architecture_summary.py +0 -0
  89. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_classifier.py +0 -0
  90. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_code_notes_analyzer.py +0 -0
  91. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_coverage_parser.py +0 -0
  92. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_cross_consistency.py +0 -0
  93. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  94. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  95. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_dependency_schema.py +0 -0
  96. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detector_dotnet.py +0 -0
  97. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detector_go_rust_java.py +0 -0
  98. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detector_nodejs.py +0 -0
  99. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detector_php_ruby_dart.py +0 -0
  100. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detector_python.py +0 -0
  101. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detector_universal_managed.py +0 -0
  102. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_detector_universal_systems.py +0 -0
  103. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  104. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_doc_analyzer_python.py +0 -0
  105. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  106. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_graph_analyzer_python_node.py +0 -0
  107. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_graph_schema.py +0 -0
  108. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_hybrid_inference.py +0 -0
  109. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration.py +0 -0
  110. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_dependencies.py +0 -0
  111. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_detection.py +0 -0
  112. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_docs.py +0 -0
  113. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_graph_modules.py +0 -0
  114. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_lqn.py +0 -0
  115. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_metrics.py +0 -0
  116. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_multistack.py +0 -0
  117. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_semantics.py +0 -0
  118. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_integration_universal.py +0 -0
  119. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_metrics_analyzer.py +0 -0
  120. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_phase1_improvements.py +0 -0
  121. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_real_projects.py +0 -0
  122. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_redactor.py +0 -0
  123. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_scanner.py +0 -0
  124. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_schema.py +0 -0
  125. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_schema_normalization.py +0 -0
  126. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_semantic_analyzer_node.py +0 -0
  127. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_semantic_analyzer_python.py +0 -0
  128. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_semantic_import_resolution.py +0 -0
  129. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_semantic_schema.py +0 -0
  130. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_signal_hierarchy.py +0 -0
  131. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_summarizer.py +0 -0
  132. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_telemetry.py +0 -0
  133. {sourcecode-0.27.0 → sourcecode-0.29.0}/tests/test_workspace_analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.27.0
3
+ Version: 0.29.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.27.0"
7
+ version = "0.29.0"
8
8
  description = "Deterministic codebase context for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -32,7 +32,7 @@ dependencies = [
32
32
  ]
33
33
 
34
34
  [project.scripts]
35
- sourcecode = "sourcecode.cli:app"
35
+ sourcecode = "sourcecode.cli:main_entry"
36
36
 
37
37
  [project.optional-dependencies]
38
38
  dev = [
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
2
 
3
- __version__ = "0.27.0"
3
+ __version__ = "0.29.0"
@@ -12,6 +12,10 @@ from sourcecode.schema import (
12
12
  SourceMap,
13
13
  )
14
14
 
15
+ _WORKSPACE_CONFIG_FILES: frozenset[str] = frozenset({
16
+ "turbo.json", "nx.json", "pnpm-workspace.yaml", "lerna.json", "rush.json",
17
+ })
18
+
15
19
  _TOOLING_PREFIXES = (
16
20
  ".claude/",
17
21
  ".vscode/",
@@ -34,6 +38,18 @@ _CODE_EXTENSIONS = {
34
38
  _GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
35
39
 
36
40
  _TEST_DIRS: frozenset[str] = frozenset({"tests", "test", "spec", "specs", "__tests__", "e2e"})
41
+ _BENCHMARK_DIRS: frozenset[str] = frozenset({
42
+ "benchmark", "benchmarks", "bench",
43
+ "example", "examples",
44
+ "demo", "demos",
45
+ "playground", "playgrounds",
46
+ "fixture", "fixtures",
47
+ "sandbox",
48
+ })
49
+ _DOCS_DIRS: frozenset[str] = frozenset({"docs", "doc", "documentation", "wiki"})
50
+ _TOOLING_DIRS: frozenset[str] = frozenset({"scripts", "script", "tools", "tool", "ci"})
51
+ # All dirs that are not part of the runtime source architecture
52
+ _NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS
37
53
 
38
54
  # Exact file stems that signal a specific architectural layer
39
55
  _LAYER_STEM_EXACT: dict[str, str] = {
@@ -177,15 +193,35 @@ class ArchitectureAnalyzer:
177
193
  elif pattern == "unknown":
178
194
  limitations.append("Patron de capas no reconocido: estructura de directorios sin senales claras")
179
195
 
196
+ # Step 3b: monorepo override — workspace config is hard evidence
197
+ if self._has_workspace_config(sm.file_paths) and pattern not in (
198
+ "monorepo", "cqrs", "clean", "onion", "hexagonal"
199
+ ):
200
+ mono_layers = self._detect_monorepo_packages(filtered)
201
+ if mono_layers or pattern in (None, "unknown", "flat", "modular", "layered"):
202
+ pattern = "monorepo"
203
+ layers = mono_layers
204
+ limitations.append(
205
+ "Workspace config detectado — arquitectura refleja topologia de paquetes"
206
+ )
207
+
180
208
  # Step 4: bounded context inference
181
209
  bounded_contexts = self._infer_bounded_contexts(domains, graph)
182
210
 
183
- # Overall confidence
211
+ # Overall confidence — based on domain quality, not raw count
184
212
  confidence: Literal["high", "medium", "low"]
213
+ strong_domains = [d for d in domains if d.confidence in ("high", "medium")]
214
+ all_layers_weak = layers and all(l.confidence == "low" for l in layers)
185
215
  if pattern not in (None, "unknown", "flat"):
186
- # A recognised pattern was detected — at least medium confidence
187
- confidence = "high" if len(domains) >= 3 else "medium"
188
- elif len(domains) >= 1:
216
+ if all_layers_weak:
217
+ # Layers came from file-naming heuristic only, not directory structure
218
+ confidence = "medium"
219
+ limitations.append(
220
+ "Patron inferido de nombres de archivo — sin estructura de directorios confirmatoria"
221
+ )
222
+ else:
223
+ confidence = "high" if len(strong_domains) >= 3 else "medium"
224
+ elif len(strong_domains) >= 1:
189
225
  confidence = "medium"
190
226
  else:
191
227
  confidence = "low"
@@ -217,6 +253,10 @@ class ArchitectureAnalyzer:
217
253
  norm = p.replace("\\", "/")
218
254
  if self._is_tooling(norm):
219
255
  continue
256
+ # Exclude non-source dirs at every path segment (benchmarks, docs, tests, scripts…)
257
+ parts = norm.split("/")
258
+ if any(part.lower() in _NON_SOURCE_DIRS for part in parts[:-1]):
259
+ continue
220
260
  ext = Path(norm).suffix.lower()
221
261
  if ext not in _CODE_EXTENSIONS:
222
262
  continue
@@ -250,6 +290,8 @@ class ArchitectureAnalyzer:
250
290
  for name, files in groups.items():
251
291
  if len(files) < 2:
252
292
  continue
293
+ if name.lower() in _NON_SOURCE_DIRS:
294
+ continue
253
295
  role = DOMAIN_ROLES.get(name, "")
254
296
  domain_confidence: Literal["high", "medium", "low"]
255
297
  if name in DOMAIN_ROLES:
@@ -262,10 +304,10 @@ class ArchitectureAnalyzer:
262
304
  return domains
263
305
 
264
306
  def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
265
- # Exclude test paths so test directories don't skew layer scoring
307
+ # Exclude non-source paths (tests, benchmarks, docs, tooling) from layer scoring
266
308
  source_paths = [
267
309
  p for p in paths
268
- if not any(part.lower() in _TEST_DIRS for part in p.replace("\\", "/").split("/"))
310
+ if not any(part.lower() in _NON_SOURCE_DIRS for part in p.replace("\\", "/").split("/"))
269
311
  ]
270
312
  if not source_paths:
271
313
  return "unknown", []
@@ -360,7 +402,7 @@ class ArchitectureAnalyzer:
360
402
  parts = p.replace("\\", "/").split("/")
361
403
  if len(parts) >= 2 and parts[-1].lower() in _ENTRY_FILES:
362
404
  top = parts[0]
363
- if top.lower() not in _SRC_TRANSPARENT and top.lower() not in _TEST_DIRS:
405
+ if top.lower() not in _SRC_TRANSPARENT and top.lower() not in _NON_SOURCE_DIRS:
364
406
  entry_dirs.setdefault(top, []).append(p)
365
407
  if len(entry_dirs) >= 4:
366
408
  return "microservices", [
@@ -394,7 +436,7 @@ class ArchitectureAnalyzer:
394
436
  non_empty = {k: v for k, v in layer_files.items() if v}
395
437
  if len(non_empty) >= 2:
396
438
  return "layered", [
397
- ArchitectureLayer(name=k, pattern="layered", files=v, confidence="medium")
439
+ ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
398
440
  for k, v in non_empty.items()
399
441
  ]
400
442
  return None
@@ -412,19 +454,42 @@ class ArchitectureAnalyzer:
412
454
  parts = p.replace("\\", "/").split("/")
413
455
  for part in parts[:-1]:
414
456
  if (part not in _SRC_TRANSPARENT
415
- and part.lower() not in _TEST_DIRS
457
+ and part.lower() not in _NON_SOURCE_DIRS
416
458
  and part.lower() not in _GENERIC_NAMES):
417
459
  module_files.setdefault(part, []).append(p)
418
460
  break
419
461
 
420
- meaningful = {k: v for k, v in module_files.items() if len(v) >= 2}
462
+ meaningful = {k: v for k, v in module_files.items() if len(v) >= 3}
421
463
  if len(meaningful) >= 2:
422
464
  return "modular", [
423
- ArchitectureLayer(name=k, pattern="modular", files=v, confidence="medium")
465
+ ArchitectureLayer(name=k, pattern="modular", files=v, confidence="low")
424
466
  for k, v in meaningful.items()
425
467
  ]
426
468
  return None
427
469
 
470
+ def _has_workspace_config(self, file_paths: list[str]) -> bool:
471
+ for path in file_paths:
472
+ parts = path.replace("\\", "/").split("/")
473
+ if len(parts) == 1 and parts[0] in _WORKSPACE_CONFIG_FILES:
474
+ return True
475
+ return False
476
+
477
+ def _detect_monorepo_packages(self, paths: list[str]) -> list[ArchitectureLayer]:
478
+ """Find workspace packages (packages/*, apps/*, libs/*) in a monorepo."""
479
+ _WORKSPACE_ROOTS = {"packages", "apps", "libs", "applications"}
480
+ groups: dict[str, list[str]] = {}
481
+ for p in paths:
482
+ parts = p.replace("\\", "/").split("/")
483
+ if len(parts) >= 2 and parts[0].lower() in _WORKSPACE_ROOTS:
484
+ key = f"{parts[0]}/{parts[1]}"
485
+ groups.setdefault(key, []).append(p)
486
+ result = [
487
+ ArchitectureLayer(name=k, pattern="monorepo", files=v, confidence="medium")
488
+ for k, v in groups.items()
489
+ if len(v) >= 2
490
+ ]
491
+ return result[:16]
492
+
428
493
  def _infer_bounded_contexts(
429
494
  self,
430
495
  domains: list[ArchitectureDomain],