deepdoc 0.4.0__tar.gz → 0.4.2__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 (173) hide show
  1. {deepdoc-0.4.0 → deepdoc-0.4.2}/PKG-INFO +1 -1
  2. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/generator/post_processors.py +8 -0
  3. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/falcon.py +22 -2
  4. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/topology.py +100 -6
  5. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/__init__.py +0 -1
  6. deepdoc-0.4.2/deepdoc/site/builder/next_template/app/ask/page.tsx +1168 -0
  7. deepdoc-0.4.2/deepdoc/site/builder/next_template/app/components/mermaid-runner.tsx +19 -0
  8. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/app/layout.tsx +2 -9
  9. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/next.config.mjs +3 -1
  10. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/package.json +2 -1
  11. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc.egg-info/PKG-INFO +1 -1
  12. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc.egg-info/SOURCES.txt +1 -3
  13. {deepdoc-0.4.0 → deepdoc-0.4.2}/pyproject.toml +1 -1
  14. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_query.py +3 -3
  15. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_framework_support.py +25 -0
  16. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_planner_granularity.py +0 -47
  17. deepdoc-0.4.0/deepdoc/site/builder/mkdocs_builder.py +0 -2150
  18. deepdoc-0.4.0/deepdoc/site/builder/next_template/app/[[...slug]]/page.tsx +0 -37
  19. deepdoc-0.4.0/deepdoc/site/builder/next_template/app/ask/page.tsx +0 -630
  20. deepdoc-0.4.0/tests/test_mkdocs_builder.py +0 -920
  21. {deepdoc-0.4.0 → deepdoc-0.4.2}/LICENSE +0 -0
  22. {deepdoc-0.4.0 → deepdoc-0.4.2}/README.md +0 -0
  23. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/__init__.py +0 -0
  24. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/__main__.py +0 -0
  25. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/benchmark_v2.py +0 -0
  26. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/call_graph.py +0 -0
  27. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/changelog_writer.py +0 -0
  28. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/__init__.py +0 -0
  29. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/answer_mixin.py +0 -0
  30. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/chunker.py +0 -0
  31. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/constants.py +0 -0
  32. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/deep_research.py +0 -0
  33. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/docs_summary.py +0 -0
  34. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/embeddings.py +0 -0
  35. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/indexer.py +0 -0
  36. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/linking.py +0 -0
  37. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/live_fallback_mixin.py +0 -0
  38. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/persistence.py +0 -0
  39. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/providers.py +0 -0
  40. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/retrieval_mixin.py +0 -0
  41. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/routes.py +0 -0
  42. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/scaffold.py +0 -0
  43. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/service.py +0 -0
  44. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/settings.py +0 -0
  45. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/source_archive.py +0 -0
  46. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/symbol_index.py +0 -0
  47. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/chatbot/types.py +0 -0
  48. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/cli.py +0 -0
  49. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/config.py +0 -0
  50. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/generator/__init__.py +0 -0
  51. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/generator/consistency.py +0 -0
  52. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/generator/evidence.py +0 -0
  53. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/generator/generation.py +0 -0
  54. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/generator/validation.py +0 -0
  55. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/llm/__init__.py +0 -0
  56. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/llm/client.py +0 -0
  57. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/llm/json_utils.py +0 -0
  58. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/llm/litellm_compat.py +0 -0
  59. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/llm/retry.py +0 -0
  60. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/manifest.py +0 -0
  61. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/openapi.py +0 -0
  62. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/__init__.py +0 -0
  63. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/api_detector.py +0 -0
  64. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/base.py +0 -0
  65. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/go_parser.py +0 -0
  66. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/js_ts_parser.py +0 -0
  67. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/php_parser.py +0 -0
  68. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/python_parser.py +0 -0
  69. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/registry.py +0 -0
  70. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/__init__.py +0 -0
  71. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/base.py +0 -0
  72. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/common.py +0 -0
  73. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/detector.py +0 -0
  74. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/django.py +0 -0
  75. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/express.py +0 -0
  76. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/fastify.py +0 -0
  77. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/go.py +0 -0
  78. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/js_shared.py +0 -0
  79. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/laravel.py +0 -0
  80. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/nestjs.py +0 -0
  81. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/python_shared.py +0 -0
  82. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/registry.py +0 -0
  83. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/routes/repo_resolver.py +0 -0
  84. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/parser/vue_parser.py +0 -0
  85. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/persistence_v2.py +0 -0
  86. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/pipeline_v2.py +0 -0
  87. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/__init__.py +0 -0
  88. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/bucket_injection.py +0 -0
  89. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/bucket_refinement.py +0 -0
  90. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/common.py +0 -0
  91. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/endpoint_refs.py +0 -0
  92. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/engine.py +0 -0
  93. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/flow_candidates.py +0 -0
  94. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/heuristics.py +0 -0
  95. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/nav_shaping.py +0 -0
  96. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/specializations.py +0 -0
  97. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/planner/utils.py +0 -0
  98. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/prompts/__init__.py +0 -0
  99. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/prompts/bucket_types.py +0 -0
  100. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/prompts/page_types.py +0 -0
  101. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/prompts/selectors.py +0 -0
  102. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/prompts/system.py +0 -0
  103. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/prompts/update.py +0 -0
  104. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/py.typed +0 -0
  105. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/__init__.py +0 -0
  106. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/artifacts.py +0 -0
  107. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/clustering.py +0 -0
  108. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/common.py +0 -0
  109. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/database.py +0 -0
  110. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/endpoints.py +0 -0
  111. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/integrations.py +0 -0
  112. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/runtime.py +0 -0
  113. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/scanner/utils.py +0 -0
  114. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/__init__.py +0 -0
  115. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/common.py +0 -0
  116. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/mdx_utils.py +0 -0
  117. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_builder.py +0 -0
  118. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/.gitignore +0 -0
  119. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/app/(main)/[[...slug]]/page.tsx +0 -0
  120. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/app/(main)/layout.tsx +0 -0
  121. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/app/ask/layout.tsx +0 -0
  122. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/app/globals.css +0 -0
  123. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/components/chatbot.tsx +0 -0
  124. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/deepdoc.config.json +0 -0
  125. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/lib/config.ts +0 -0
  126. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/lib/docs.ts +0 -0
  127. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/lib/nav.ts +0 -0
  128. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/next_template/tsconfig.json +0 -0
  129. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/vendor/highlight.min.js +0 -0
  130. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/vendor/hljs-theme.css +0 -0
  131. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/site/builder/vendor/markdown-it.min.js +0 -0
  132. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/smart_update_v2.py +0 -0
  133. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/source_metadata.py +0 -0
  134. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/updater_v2.py +0 -0
  135. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc/v2_models.py +0 -0
  136. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc.egg-info/dependency_links.txt +0 -0
  137. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc.egg-info/entry_points.txt +0 -0
  138. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc.egg-info/requires.txt +0 -0
  139. {deepdoc-0.4.0 → deepdoc-0.4.2}/deepdoc.egg-info/top_level.txt +0 -0
  140. {deepdoc-0.4.0 → deepdoc-0.4.2}/setup.cfg +0 -0
  141. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_benchmark_scorecard.py +0 -0
  142. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_call_graph.py +0 -0
  143. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_changelog.py +0 -0
  144. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_config.py +0 -0
  145. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_embeddings.py +0 -0
  146. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_eval.py +0 -0
  147. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_index.py +0 -0
  148. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_persistence.py +0 -0
  149. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_providers.py +0 -0
  150. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_relationship.py +0 -0
  151. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_scaffold.py +0 -0
  152. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_chatbot_source_archive.py +0 -0
  153. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_classify.py +0 -0
  154. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_cli_generate.py +0 -0
  155. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_cli_serve.py +0 -0
  156. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_cli_update.py +0 -0
  157. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_consistency_pass.py +0 -0
  158. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_flow_candidates.py +0 -0
  159. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_framework_fixtures.py +0 -0
  160. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_generation_evidence.py +0 -0
  161. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_internal_docs_metadata.py +0 -0
  162. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_litellm_compat.py +0 -0
  163. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_llm_json_utils.py +0 -0
  164. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_llm_retry.py +0 -0
  165. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_next_builder.py +0 -0
  166. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_parallel_pipeline.py +0 -0
  167. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_parser_ranges.py +0 -0
  168. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_planner_consolidation.py +0 -0
  169. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_route_registry.py +0 -0
  170. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_runtime_scan.py +0 -0
  171. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_smart_update.py +0 -0
  172. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_stale.py +0 -0
  173. {deepdoc-0.4.0 → deepdoc-0.4.2}/tests/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepdoc
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Auto-generate beautiful docs from any codebase
5
5
  Author: Pranav Kumar
6
6
  License: MIT
@@ -139,6 +139,14 @@ def _fix_mermaid_diagram(diagram: str) -> str:
139
139
  line,
140
140
  )
141
141
 
142
+ # Fix: backticks inside double-quoted node labels break Mermaid 11+
143
+ # e.g. ["foo() (`bar.py:10`)"] → ["foo() (bar.py:10)"]
144
+ line = re.sub(
145
+ r'("(?:[^"\\]|\\.)*")',
146
+ lambda m: m.group(0).replace('`', ''),
147
+ line,
148
+ )
149
+
142
150
  # Fix: Node labels with colons not in quotes
143
151
  line = re.sub(
144
152
  r'\[([^\]"]*:[^\]"]*)\]',
@@ -114,8 +114,20 @@ def _fallback_falcon_path(path_expr: str) -> str:
114
114
  return "".join(string_parts)
115
115
 
116
116
 
117
- def find_falcon_responders(content: str, class_name: str) -> list[str]:
118
- """Find all on_* methods in a Falcon resource class."""
117
+ def _base_class_name(class_body: str) -> str:
118
+ """Return the first base class name from a class definition line, or ''."""
119
+ m = re.match(r"class\s+\w+\s*\(\s*([A-Za-z_]\w*)", class_body)
120
+ return m.group(1) if m else ""
121
+
122
+
123
+ def find_falcon_responders(
124
+ content: str,
125
+ class_name: str,
126
+ _visited: frozenset[str] | None = None,
127
+ ) -> list[str]:
128
+ """Find all on_* methods in a Falcon resource class, including inherited ones."""
129
+ if _visited is None:
130
+ _visited = frozenset()
119
131
  methods: list[str] = []
120
132
  class_pattern = re.compile(
121
133
  rf"class\s+{re.escape(class_name)}\b.*?(?=\nclass\s|\Z)",
@@ -128,6 +140,14 @@ def find_falcon_responders(content: str, class_name: str) -> list[str]:
128
140
  method_name = match.group(1)
129
141
  http_method = method_name.replace("on_", "").upper()
130
142
  methods.append(http_method)
143
+ base = _base_class_name(class_body)
144
+ if base and base not in _visited:
145
+ inherited = find_falcon_responders(
146
+ content, base, _visited | {class_name}
147
+ )
148
+ for m in inherited:
149
+ if m not in methods:
150
+ methods.append(m)
131
151
  return methods
132
152
 
133
153
 
@@ -41,6 +41,11 @@ _MAX_CLUSTER_DEPTH = 4
41
41
  # docs/planner_tuning.md.
42
42
  _MERGE_JACCARD = 0.60
43
43
 
44
+ # Two clusters whose cross-call ratio exceeds this threshold merge even when
45
+ # their file-Jaccard is low. Prevents tightly coupled micro-clusters from
46
+ # getting separate doc pages.
47
+ _CROSS_EDGE_DENSITY = 0.35
48
+
44
49
 
45
50
  @dataclass
46
51
  class TopologyCluster:
@@ -124,8 +129,16 @@ def build_topology_map(scan: "RepoScan") -> TopologyMap:
124
129
  }
125
130
 
126
131
  # ── 3. Foundational files ─────────────────────────────────────────────
132
+ # Augment call indegree with import indegree so that heavily-imported files
133
+ # (models.py, constants.py, exceptions.py) are treated as foundational even
134
+ # when they have few direct call-graph edges.
135
+ import_indegree, file_imports = _build_import_maps(scan, repo_files)
136
+ effective_indegree = {
137
+ f: max(file_indegree.get(f, 0), import_indegree.get(f, 0))
138
+ for f in repo_files
139
+ }
127
140
  threshold = max(3, int(len(repo_files) * _FOUNDATIONAL_FRACTION))
128
- foundational_set = {f for f, deg in file_indegree.items() if deg >= threshold}
141
+ foundational_set = {f for f, deg in effective_indegree.items() if deg >= threshold}
129
142
 
130
143
  # ── 4. Entry-point files ──────────────────────────────────────────────
131
144
  entry_point_files: set[str] = set()
@@ -211,7 +224,9 @@ def build_topology_map(scan: "RepoScan") -> TopologyMap:
211
224
  if f not in file_cluster_id and f not in foundational_set
212
225
  ]
213
226
  for f in sorted(unassigned):
214
- best_cid = _best_cluster_for_orphan(f, file_calls, file_called_by, proto)
227
+ best_cid = _best_cluster_for_orphan(
228
+ f, file_calls, file_called_by, proto, file_imports
229
+ )
215
230
  if best_cid:
216
231
  file_cluster_id[f] = best_cid
217
232
  proto[best_cid].add(f)
@@ -221,7 +236,7 @@ def build_topology_map(scan: "RepoScan") -> TopologyMap:
221
236
  proto[fallback].add(f)
222
237
 
223
238
  # ── 7. Merge high-overlap clusters ───────────────────────────────────
224
- merged = _merge_proto_clusters(proto, file_cluster_id)
239
+ merged = _merge_proto_clusters(proto, file_cluster_id, file_calls)
225
240
 
226
241
  # ── 8. Build TopologyCluster objects ─────────────────────────────────
227
242
  clusters: list[TopologyCluster] = []
@@ -313,10 +328,12 @@ def _empty_map() -> TopologyMap:
313
328
  def _merge_proto_clusters(
314
329
  proto: dict[str, set[str]],
315
330
  file_cluster_id: dict[str, str],
331
+ file_calls: dict[str, set[str]] | None = None,
316
332
  ) -> dict[str, set[str]]:
317
- """Merge clusters whose file Jaccard overlap exceeds _MERGE_JACCARD."""
333
+ """Merge clusters whose file Jaccard overlap or cross-call density is high."""
318
334
  cluster_ids = list(proto.keys())
319
335
  parent: dict[str, str] = {}
336
+ _fc = file_calls or {}
320
337
 
321
338
  def find(cid: str) -> str:
322
339
  while cid in parent:
@@ -347,6 +364,12 @@ def _merge_proto_clusters(
347
364
  jaccard = len(si & sj) / len(si | sj)
348
365
  if jaccard >= _MERGE_JACCARD:
349
366
  union(ci, cj)
367
+ continue
368
+ # Also merge when cross-call traffic between the two clusters is dense
369
+ cross = sum(1 for f in si for c in _fc.get(f, set()) if c in sj)
370
+ cross += sum(1 for f in sj for c in _fc.get(f, set()) if c in si)
371
+ if cross and cross / min(len(si), len(sj)) >= _CROSS_EDGE_DENSITY:
372
+ union(ci, cj)
350
373
 
351
374
  # Re-assign file_cluster_id to canonical roots
352
375
  for f in list(file_cluster_id.keys()):
@@ -355,14 +378,79 @@ def _merge_proto_clusters(
355
378
  return {find(cid): files for cid, files in proto.items() if files}
356
379
 
357
380
 
381
+ def _build_import_maps(
382
+ scan: "RepoScan",
383
+ repo_files: set[str],
384
+ ) -> tuple[dict[str, int], dict[str, set[str]]]:
385
+ """Build import indegree and file→imported-files adjacency in one pass.
386
+
387
+ Resolves relative and absolute imports to repo file paths; ignores
388
+ third-party/stdlib. Returns (indegree, file_imports) where:
389
+ - indegree[f] = number of distinct repo files that import f
390
+ - file_imports[f] = set of repo files that f imports
391
+ """
392
+ indegree: dict[str, int] = defaultdict(int)
393
+ file_imports: dict[str, set[str]] = defaultdict(set)
394
+
395
+ _suffix_index: dict[str, list[str]] = defaultdict(list)
396
+ for rp in repo_files:
397
+ no_ext = re.sub(r"\.[^/]+$", "", rp)
398
+ _suffix_index[no_ext].append(rp)
399
+ _suffix_index[no_ext.replace("/", ".")].append(rp)
400
+
401
+ import_re = re.compile(
402
+ r"^(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))", re.MULTILINE
403
+ )
404
+
405
+ for src_path, pf in scan.parsed_files.items():
406
+ if src_path not in repo_files:
407
+ continue
408
+ pkg_dir = src_path.rsplit("/", 1)[0] if "/" in src_path else ""
409
+
410
+ for imp_str in pf.imports:
411
+ m = import_re.match(imp_str.strip())
412
+ if not m:
413
+ continue
414
+ module = (m.group(1) or m.group(2) or "").strip()
415
+ if not module:
416
+ continue
417
+
418
+ candidates: list[str] = []
419
+
420
+ if module.startswith("."):
421
+ dots = len(module) - len(module.lstrip("."))
422
+ rel_mod = module[dots:].replace(".", "/")
423
+ base = pkg_dir
424
+ for _ in range(dots - 1):
425
+ base = base.rsplit("/", 1)[0] if "/" in base else ""
426
+ rel_path = f"{base}/{rel_mod}".lstrip("/") if rel_mod else base
427
+ for ext in (".py", "/__init__.py"):
428
+ candidates.append(rel_path + ext)
429
+ else:
430
+ as_path = module.replace(".", "/")
431
+ for ext in (".py", "/__init__.py"):
432
+ candidates.append(as_path + ext)
433
+ candidates.extend(_suffix_index.get(module, []))
434
+ candidates.extend(_suffix_index.get(as_path, []))
435
+
436
+ for candidate in candidates:
437
+ if candidate in repo_files and candidate != src_path:
438
+ indegree[candidate] += 1
439
+ file_imports[src_path].add(candidate)
440
+ break
441
+
442
+ return dict(indegree), dict(file_imports)
443
+
444
+
358
445
  def _best_cluster_for_orphan(
359
446
  file_path: str,
360
447
  file_calls: dict[str, set[str]],
361
448
  file_called_by: dict[str, set[str]],
362
449
  proto: dict[str, set[str]],
450
+ import_targets: dict[str, set[str]] | None = None,
363
451
  ) -> str | None:
364
- """Find the cluster an unassigned file has the most call relationships with."""
365
- scores: dict[str, int] = defaultdict(int)
452
+ """Find the cluster an unassigned file has the most relationships with."""
453
+ scores: dict[str, float] = defaultdict(float)
366
454
  cluster_file_index: dict[str, str] = {}
367
455
  for cid, files in proto.items():
368
456
  for f in files:
@@ -378,6 +466,12 @@ def _best_cluster_for_orphan(
378
466
  if cid:
379
467
  scores[cid] += 1
380
468
 
469
+ # Import edges provide a weaker signal than call edges
470
+ for imp_target in (import_targets or {}).get(file_path, set()):
471
+ cid = cluster_file_index.get(imp_target)
472
+ if cid:
473
+ scores[cid] += 0.5
474
+
381
475
  if not scores:
382
476
  return None
383
477
  return max(scores, key=lambda c: scores[c])
@@ -1,4 +1,3 @@
1
1
  from .next_builder import build_next_from_plan
2
- from .mkdocs_builder import build_mkdocs_from_plan # kept for compatibility
3
2
  from .mdx_utils import _first_mdx_heading, _split_leading_frontmatter, _frontmatter_has_yaml_fields, _ensure_md_frontmatter
4
3
  from .common import *