sourcecode 1.53.0__tar.gz → 1.54.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-1.53.0 → sourcecode-1.54.0}/CHANGELOG.md +24 -0
  2. {sourcecode-1.53.0 → sourcecode-1.54.0}/PKG-INFO +3 -3
  3. {sourcecode-1.53.0 → sourcecode-1.54.0}/README.md +2 -2
  4. {sourcecode-1.53.0 → sourcecode-1.54.0}/pyproject.toml +1 -1
  5. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/cli.py +79 -0
  7. {sourcecode-1.53.0 → sourcecode-1.54.0}/.github/workflows/build-windows.yml +0 -0
  8. {sourcecode-1.53.0 → sourcecode-1.54.0}/.gitignore +0 -0
  9. {sourcecode-1.53.0 → sourcecode-1.54.0}/.ruff.toml +0 -0
  10. {sourcecode-1.53.0 → sourcecode-1.54.0}/CONTRIBUTING.md +0 -0
  11. {sourcecode-1.53.0 → sourcecode-1.54.0}/LICENSE +0 -0
  12. {sourcecode-1.53.0 → sourcecode-1.54.0}/SECURITY.md +0 -0
  13. {sourcecode-1.53.0 → sourcecode-1.54.0}/raw +0 -0
  14. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/adaptive_scanner.py +0 -0
  15. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/architecture_analyzer.py +0 -0
  16. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/architecture_summary.py +0 -0
  17. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/ast_extractor.py +0 -0
  18. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/cache.py +0 -0
  19. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/canonical_ir.py +0 -0
  20. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/cir_graphs.py +0 -0
  21. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/classifier.py +0 -0
  22. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  23. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/confidence_analyzer.py +0 -0
  24. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/context_scorer.py +0 -0
  25. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/context_summarizer.py +0 -0
  26. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/contract_model.py +0 -0
  27. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/contract_pipeline.py +0 -0
  28. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/coverage_parser.py +0 -0
  29. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/dependency_analyzer.py +0 -0
  30. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/__init__.py +0 -0
  31. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/base.py +0 -0
  32. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  33. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/dart.py +0 -0
  34. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/dotnet.py +0 -0
  35. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/elixir.py +0 -0
  36. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/go.py +0 -0
  37. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/heuristic.py +0 -0
  38. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/hybrid.py +0 -0
  39. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/java.py +0 -0
  40. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  41. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/nodejs.py +0 -0
  42. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/parsers.py +0 -0
  43. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/php.py +0 -0
  44. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/project.py +0 -0
  45. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/python.py +0 -0
  46. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/ruby.py +0 -0
  47. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/rust.py +0 -0
  48. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/systems.py +0 -0
  49. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/terraform.py +0 -0
  50. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/detectors/tooling.py +0 -0
  51. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/doc_analyzer.py +0 -0
  52. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  53. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/env_analyzer.py +0 -0
  54. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/error_schema.py +0 -0
  55. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/explain.py +0 -0
  56. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/file_chunker.py +0 -0
  57. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/flow_analyzer.py +0 -0
  59. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/format_contract.py +0 -0
  60. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/fqn_utils.py +0 -0
  61. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/git_analyzer.py +0 -0
  62. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/graph_analyzer.py +0 -0
  63. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/integration_detector.py +0 -0
  64. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/license.py +0 -0
  65. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/__init__.py +0 -0
  66. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  67. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  68. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  69. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  70. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  71. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  72. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/registry.py +0 -0
  73. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/runner.py +0 -0
  74. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp/server.py +0 -0
  75. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/mcp_nudge.py +0 -0
  76. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/metrics_analyzer.py +0 -0
  77. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/migrate_check.py +0 -0
  78. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/openapi_surface.py +0 -0
  79. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/output_budget.py +0 -0
  80. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/path_filters.py +0 -0
  81. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  82. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/pr_impact.py +0 -0
  83. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/prepare_context.py +0 -0
  84. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/progress.py +0 -0
  85. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/ranking_engine.py +0 -0
  86. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/redactor.py +0 -0
  87. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/relevance_scorer.py +0 -0
  88. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/rename_refactor.py +0 -0
  89. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/repo_classifier.py +0 -0
  90. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/repository_ir.py +0 -0
  91. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/ris.py +0 -0
  92. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/runtime_classifier.py +0 -0
  93. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/scanner.py +0 -0
  94. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/schema.py +0 -0
  95. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/security_config.py +0 -0
  96. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/semantic_analyzer.py +0 -0
  97. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/serializer.py +0 -0
  98. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/spring_event_topology.py +0 -0
  99. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/spring_findings.py +0 -0
  100. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/spring_impact.py +0 -0
  101. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/spring_model.py +0 -0
  102. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/spring_security_audit.py +0 -0
  103. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/spring_semantic.py +0 -0
  104. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  105. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/summarizer.py +0 -0
  106. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/telemetry/__init__.py +0 -0
  107. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/telemetry/config.py +0 -0
  108. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/telemetry/consent.py +0 -0
  109. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/telemetry/events.py +0 -0
  110. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/telemetry/filters.py +0 -0
  111. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/telemetry/transport.py +0 -0
  112. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/tree_utils.py +0 -0
  113. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/validation_surface.py +0 -0
  114. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/version_check.py +0 -0
  115. {sourcecode-1.53.0 → sourcecode-1.54.0}/src/sourcecode/workspace.py +0 -0
  116. {sourcecode-1.53.0 → sourcecode-1.54.0}/supabase/functions/README.md +0 -0
  117. {sourcecode-1.53.0 → sourcecode-1.54.0}/supabase/functions/get-license/index.ts +0 -0
  118. {sourcecode-1.53.0 → sourcecode-1.54.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  119. {sourcecode-1.53.0 → sourcecode-1.54.0}/supabase/functions/telemetry/index.ts +0 -0
  120. {sourcecode-1.53.0 → sourcecode-1.54.0}/supabase/sql/license_event_ordering.sql +0 -0
  121. {sourcecode-1.53.0 → sourcecode-1.54.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.54.0] — 2026-06-19
4
+
5
+ ### Added
6
+ - **`export --c4` now emits `components.module_roots` — architectural module
7
+ enumeration + DDD/legacy classification.** Field test (saint-server C4 doc pass)
8
+ surfaced that the C4 export keyed modules by *leaf source directory*
9
+ (`dirname(source_file)`), so a DDD module split across
10
+ `domain/` / `application/` / `infrastructure/` subdirs fragmented into several
11
+ unrelated "modules". A downstream consumer had to infer module boundaries from
12
+ directory names, which produced a module **undercount** and **DDD-vs-legacy
13
+ misclassification** in the generated docs.
14
+
15
+ Fix: `_detect_module_roots()` rolls leaf dirs up to their architectural module
16
+ root (the directory above the shallowest recognized layer dir) and classifies
17
+ each `layered` (≥2 of `domain`/`application`/`infrastructure`) vs `flat`
18
+ (legacy/flat package). `c4.components.module_roots` carries the per-module
19
+ `{root, pattern, layers, symbol_count, leaf_dir_count}` plus a summary with a
20
+ verifiable `module_count` / `layered_module_count` / `flat_module_count`, so a
21
+ consumer enumerates real modules instead of guessing.
22
+
23
+ Pure-structural (no extra file reads). Non-breaking: top-level `c4` keys and the
24
+ leaf-level `--module-graph` dependency view are unchanged. 3 regression tests
25
+ (layered rollup, flat classification, summary counts).
26
+
3
27
  ## [1.50.0] — 2026-06-17
4
28
 
5
29
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.53.0
3
+ Version: 1.54.0
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.53.0-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.54.0-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
45
45
 
46
46
  ---
@@ -404,7 +404,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
404
404
  | `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
405
405
  | `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
406
406
  | `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `JmsTemplate`, ActiveMQ) with `file:line` evidence and a literal `target` URL/name when present. |
407
- | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). |
407
+ | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
408
408
 
409
409
  The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
410
410
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.53.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.54.0-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
7
7
 
8
8
  ---
@@ -366,7 +366,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
366
366
  | `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
367
367
  | `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
368
368
  | `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `JmsTemplate`, ActiveMQ) with `file:line` evidence and a literal `target` URL/name when present. |
369
- | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). |
369
+ | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
370
370
 
371
371
  The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
372
372
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.53.0"
7
+ version = "1.54.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.53.0"
3
+ __version__ = "1.54.0"
@@ -4137,6 +4137,82 @@ def _directory_hashes(file_list: "list[str]", root: "Path") -> "dict[str, str]":
4137
4137
  return out
4138
4138
 
4139
4139
 
4140
+ # Architectural layer directory names used to recognize a layered module
4141
+ # (DDD / hexagonal). The module *root* is the directory directly above the
4142
+ # shallowest layer dir, so symbols living in domain/application/infrastructure
4143
+ # subdirs all roll up to one module — a consumer counts modules, not leaf dirs.
4144
+ _LAYER_MARKERS: "frozenset[str]" = frozenset({
4145
+ "domain", "application", "infrastructure",
4146
+ "interfaces", "presentation", "adapters", "ports", "api",
4147
+ })
4148
+ # Core DDD layers — presence of >=2 marks a module as DDD-layered vs flat/legacy.
4149
+ _DDD_CORE_LAYERS: "frozenset[str]" = frozenset({
4150
+ "domain", "application", "infrastructure",
4151
+ })
4152
+
4153
+
4154
+ def _module_root_of(leaf_dir: "str") -> "tuple[str, str | None]":
4155
+ """Map a leaf source directory to its architectural module root.
4156
+
4157
+ For a layered module ``<root>/<layer>/...`` the root is the path above the
4158
+ shallowest recognized layer dir, and the layer name is returned alongside.
4159
+ Flat directories (no layer marker) are their own root with a ``None`` layer.
4160
+ Pure-structural — no file reads.
4161
+ """
4162
+ parts = leaf_dir.split("/")
4163
+ for i, seg in enumerate(parts):
4164
+ if seg.lower() in _LAYER_MARKERS:
4165
+ return "/".join(parts[:i]) or ".", seg.lower()
4166
+ return leaf_dir, None
4167
+
4168
+
4169
+ def _detect_module_roots(by_directory: "dict[str, list]") -> "dict":
4170
+ """Roll leaf source dirs up to architectural module roots and classify them.
4171
+
4172
+ Resolves the leaf-directory-vs-module mismatch in the C4 component view: a
4173
+ DDD module split across ``domain/`` / ``application/`` / ``infrastructure/``
4174
+ subdirs is reported once, with its layers and a structural ``pattern``
4175
+ (``layered`` when it carries >=2 core DDD layers, else ``flat``). Gives a
4176
+ downstream consumer a verifiable module enumeration and a DDD-vs-legacy
4177
+ signal instead of forcing it to infer module boundaries from directory names.
4178
+ """
4179
+ roots: "dict[str, dict]" = {}
4180
+ for leaf, symbols in by_directory.items():
4181
+ root, layer = _module_root_of(leaf)
4182
+ slot = roots.setdefault(
4183
+ root, {"layers": set(), "symbol_count": 0, "leaf_dirs": 0}
4184
+ )
4185
+ if layer:
4186
+ slot["layers"].add(layer)
4187
+ slot["symbol_count"] += len(symbols)
4188
+ slot["leaf_dirs"] += 1
4189
+
4190
+ modules: "list[dict]" = []
4191
+ layered = flat = 0
4192
+ for root in sorted(roots):
4193
+ s = roots[root]
4194
+ pattern = "layered" if len(s["layers"] & _DDD_CORE_LAYERS) >= 2 else "flat"
4195
+ if pattern == "layered":
4196
+ layered += 1
4197
+ else:
4198
+ flat += 1
4199
+ modules.append({
4200
+ "root": root,
4201
+ "pattern": pattern,
4202
+ "layers": sorted(s["layers"]),
4203
+ "symbol_count": s["symbol_count"],
4204
+ "leaf_dir_count": s["leaf_dirs"],
4205
+ })
4206
+ return {
4207
+ "modules": modules,
4208
+ "summary": {
4209
+ "module_count": len(modules),
4210
+ "layered_module_count": layered,
4211
+ "flat_module_count": flat,
4212
+ },
4213
+ }
4214
+
4215
+
4140
4216
  def _build_c4_export(
4141
4217
  root: "Path",
4142
4218
  file_list: "list[str]",
@@ -4155,6 +4231,9 @@ def _build_c4_export(
4155
4231
  """
4156
4232
  by_directory = _group_symbols_by_directory(nodes)
4157
4233
  module_graph = _build_module_graph(nodes, edges)
4234
+ # Architectural module-root rollup + DDD/legacy classification, so a
4235
+ # consumer counts/classifies modules instead of inferring them from leaf dirs.
4236
+ module_graph["module_roots"] = _detect_module_roots(by_directory)
4158
4237
  api_surface = _group_endpoints_by_controller(endpoints)
4159
4238
  containers = _detect_containers(root)
4160
4239
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes