deepdoc 1.9.2__tar.gz → 2.0.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 (150) hide show
  1. {deepdoc-1.9.2 → deepdoc-2.0.0}/PKG-INFO +1 -1
  2. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/__init__.py +1 -1
  3. deepdoc-2.0.0/deepdoc/changelog_writer.py +106 -0
  4. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/cli.py +13 -13
  5. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/generator/generation.py +1 -0
  6. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/generator/post_processors.py +36 -0
  7. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/persistence_v2.py +27 -0
  8. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/pipeline_v2.py +59 -1
  9. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/bucket_injection.py +103 -11
  10. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/nav_shaping.py +51 -1
  11. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/specializations.py +10 -10
  12. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/builder/engine.py +12 -32
  13. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/builder/scaffold_files.py +58 -1
  14. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/smart_update_v2.py +104 -21
  15. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc.egg-info/PKG-INFO +1 -1
  16. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc.egg-info/SOURCES.txt +2 -0
  17. {deepdoc-1.9.2 → deepdoc-2.0.0}/pyproject.toml +1 -1
  18. deepdoc-2.0.0/tests/test_changelog.py +74 -0
  19. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_classify.py +8 -12
  20. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_cli_serve.py +2 -2
  21. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_fumadocs_builder.py +5 -1
  22. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_planner_granularity.py +74 -2
  23. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_smart_update.py +104 -21
  24. {deepdoc-1.9.2 → deepdoc-2.0.0}/LICENSE +0 -0
  25. {deepdoc-1.9.2 → deepdoc-2.0.0}/README.md +0 -0
  26. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/__main__.py +0 -0
  27. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/_legacy_types.py +0 -0
  28. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/benchmark_v2.py +0 -0
  29. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/call_graph.py +0 -0
  30. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/__init__.py +0 -0
  31. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/answer_mixin.py +0 -0
  32. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/chunker.py +0 -0
  33. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/deep_research.py +0 -0
  34. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/docs_summary.py +0 -0
  35. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/embeddings.py +0 -0
  36. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/indexer.py +0 -0
  37. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/linking.py +0 -0
  38. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/live_fallback_mixin.py +0 -0
  39. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/persistence.py +0 -0
  40. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/providers.py +0 -0
  41. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/retrieval_mixin.py +0 -0
  42. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/routes.py +0 -0
  43. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/scaffold.py +0 -0
  44. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/service.py +0 -0
  45. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/settings.py +0 -0
  46. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/source_archive.py +0 -0
  47. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/symbol_index.py +0 -0
  48. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/chatbot/types.py +0 -0
  49. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/config.py +0 -0
  50. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/generator/__init__.py +0 -0
  51. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/generator/evidence.py +0 -0
  52. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/generator/validation.py +0 -0
  53. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/llm/__init__.py +0 -0
  54. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/llm/client.py +0 -0
  55. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/llm/json_utils.py +0 -0
  56. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/llm/litellm_compat.py +0 -0
  57. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/manifest.py +0 -0
  58. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/openapi.py +0 -0
  59. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/__init__.py +0 -0
  60. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/api_detector.py +0 -0
  61. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/base.py +0 -0
  62. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/go_parser.py +0 -0
  63. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/js_ts_parser.py +0 -0
  64. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/php_parser.py +0 -0
  65. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/python_parser.py +0 -0
  66. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/registry.py +0 -0
  67. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/__init__.py +0 -0
  68. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/base.py +0 -0
  69. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/common.py +0 -0
  70. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/detector.py +0 -0
  71. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/django.py +0 -0
  72. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/express.py +0 -0
  73. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/falcon.py +0 -0
  74. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/fastify.py +0 -0
  75. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/go.py +0 -0
  76. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/js_shared.py +0 -0
  77. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/laravel.py +0 -0
  78. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/nestjs.py +0 -0
  79. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/python_shared.py +0 -0
  80. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/registry.py +0 -0
  81. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/routes/repo_resolver.py +0 -0
  82. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/parser/vue_parser.py +0 -0
  83. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/__init__.py +0 -0
  84. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/bucket_refinement.py +0 -0
  85. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/common.py +0 -0
  86. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/endpoint_refs.py +0 -0
  87. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/engine.py +0 -0
  88. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/flow_candidates.py +0 -0
  89. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/heuristics.py +0 -0
  90. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/topology.py +0 -0
  91. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/planner/utils.py +0 -0
  92. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/prompts/__init__.py +0 -0
  93. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/prompts/bucket_types.py +0 -0
  94. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/prompts/page_types.py +0 -0
  95. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/prompts/selectors.py +0 -0
  96. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/prompts/system.py +0 -0
  97. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/prompts/update.py +0 -0
  98. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/prompts_v2.py +0 -0
  99. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/py.typed +0 -0
  100. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/__init__.py +0 -0
  101. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/artifacts.py +0 -0
  102. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/clustering.py +0 -0
  103. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/common.py +0 -0
  104. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/database.py +0 -0
  105. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/endpoints.py +0 -0
  106. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/integrations.py +0 -0
  107. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/runtime.py +0 -0
  108. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/scanner/utils.py +0 -0
  109. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/__init__.py +0 -0
  110. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/builder/__init__.py +0 -0
  111. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/builder/chatbot_components.py +0 -0
  112. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/builder/common.py +0 -0
  113. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/builder/mdx_utils.py +0 -0
  114. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/site/builder/templates.py +0 -0
  115. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/source_metadata.py +0 -0
  116. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/updater_v2.py +0 -0
  117. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc/v2_models.py +0 -0
  118. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc.egg-info/dependency_links.txt +0 -0
  119. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc.egg-info/entry_points.txt +0 -0
  120. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc.egg-info/requires.txt +0 -0
  121. {deepdoc-1.9.2 → deepdoc-2.0.0}/deepdoc.egg-info/top_level.txt +0 -0
  122. {deepdoc-1.9.2 → deepdoc-2.0.0}/setup.cfg +0 -0
  123. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_benchmark_scorecard.py +0 -0
  124. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_call_graph.py +0 -0
  125. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_config.py +0 -0
  126. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_embeddings.py +0 -0
  127. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_eval.py +0 -0
  128. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_index.py +0 -0
  129. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_persistence.py +0 -0
  130. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_providers.py +0 -0
  131. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_query.py +0 -0
  132. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_relationship.py +0 -0
  133. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_scaffold.py +0 -0
  134. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_chatbot_source_archive.py +0 -0
  135. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_cli_generate.py +0 -0
  136. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_cli_update.py +0 -0
  137. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_flow_candidates.py +0 -0
  138. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_framework_fixtures.py +0 -0
  139. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_framework_support.py +0 -0
  140. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_generation_evidence.py +0 -0
  141. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_internal_docs_metadata.py +0 -0
  142. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_litellm_compat.py +0 -0
  143. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_llm_json_utils.py +0 -0
  144. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_parallel_pipeline.py +0 -0
  145. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_parser_ranges.py +0 -0
  146. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_planner_consolidation.py +0 -0
  147. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_route_registry.py +0 -0
  148. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_runtime_scan.py +0 -0
  149. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_stale.py +0 -0
  150. {deepdoc-1.9.2 → deepdoc-2.0.0}/tests/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepdoc
3
- Version: 1.9.2
3
+ Version: 2.0.0
4
4
  Summary: Auto-generate beautiful docs from any codebase
5
5
  Author: Pranav Kumar
6
6
  License: MIT
@@ -1,3 +1,3 @@
1
1
  """DeepDoc — Auto-generate beautiful docs from any codebase."""
2
2
 
3
- __version__ = "1.9.2"
3
+ __version__ = "2.0.0"
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from .persistence_v2 import append_changelog_entry, load_changelog, load_plan, save_plan
6
+
7
+
8
+ def record_and_write(
9
+ repo_root: Path,
10
+ output_dir: Path,
11
+ *,
12
+ commit: str,
13
+ commit_message: str,
14
+ commit_date: str,
15
+ strategy: str,
16
+ pages_updated: list[str],
17
+ files_changed: list[str],
18
+ is_initial: bool = False,
19
+ ) -> None:
20
+ """Append one changelog entry and regenerate whats-changed.mdx."""
21
+ entry = {
22
+ "commit": commit[:8],
23
+ "date": commit_date,
24
+ "commit_message": commit_message,
25
+ "strategy": strategy,
26
+ "pages_updated": pages_updated,
27
+ "files_changed": files_changed[:20],
28
+ "is_initial": is_initial,
29
+ }
30
+ append_changelog_entry(repo_root, entry)
31
+ write_whats_changed_page(repo_root, output_dir)
32
+
33
+
34
+ def write_whats_changed_page(repo_root: Path, output_dir: Path) -> None:
35
+ """Write docs/whats-changed.mdx from .deepdoc/changelog.json."""
36
+ entries = load_changelog(repo_root)
37
+ mdx = _build_mdx(entries)
38
+ output_dir.mkdir(parents=True, exist_ok=True)
39
+ (output_dir / "whats-changed.mdx").write_text(mdx, encoding="utf-8")
40
+ _ensure_in_nav(repo_root)
41
+
42
+
43
+ def _build_mdx(entries: list[dict]) -> str:
44
+ lines = [
45
+ "---",
46
+ 'title: "What\'s Changed"',
47
+ 'description: "Documentation changes per commit"',
48
+ "---",
49
+ "",
50
+ "# What's Changed",
51
+ "",
52
+ ]
53
+
54
+ if not entries:
55
+ lines.append("No changes recorded yet — this is the initial documentation.")
56
+ return "\n".join(lines)
57
+
58
+ lines.append("<Accordions>")
59
+ for entry in entries:
60
+ date = entry.get("date", "")
61
+ msg = entry.get("commit_message", "update")[:80]
62
+ sha = entry.get("commit", "")
63
+ pages = entry.get("pages_updated", [])
64
+ files = entry.get("files_changed", [])
65
+ is_initial = entry.get("is_initial", False)
66
+
67
+ title = f"{date} — {msg} ({sha})"
68
+ lines.append(f'<Accordion title="{title}">')
69
+ lines.append("")
70
+
71
+ if is_initial:
72
+ lines.append(f"**{len(pages)} pages generated** — initial documentation run.")
73
+ else:
74
+ if pages:
75
+ page_links = ", ".join(f"[{_slug_to_title(s)}](/{s})" for s in pages)
76
+ lines.append(f"**{len(pages)} page(s) updated:** {page_links}")
77
+ else:
78
+ lines.append("No pages regenerated.")
79
+
80
+ if files and not is_initial:
81
+ file_list = ", ".join(f"`{f}`" for f in files[:10])
82
+ if len(files) > 10:
83
+ file_list += f" +{len(files) - 10} more"
84
+ lines.append("")
85
+ lines.append(f"**Files changed:** {file_list}")
86
+
87
+ lines.append("")
88
+ lines.append("</Accordion>")
89
+
90
+ lines.append("</Accordions>")
91
+ return "\n".join(lines)
92
+
93
+
94
+ def _slug_to_title(slug: str) -> str:
95
+ return slug.replace("-", " ").title()
96
+
97
+
98
+ def _ensure_in_nav(repo_root: Path) -> None:
99
+ """Add whats-changed to Start Here section of the saved plan if not already there."""
100
+ plan = load_plan(repo_root)
101
+ if plan is None or not hasattr(plan, "nav_structure"):
102
+ return
103
+ section = plan.nav_structure.setdefault("Start Here", [])
104
+ if "whats-changed" not in section:
105
+ section.append("whats-changed")
106
+ save_plan(plan, repo_root)
@@ -1200,13 +1200,17 @@ def _warn_if_deprecated_generated_version(cfg: dict, repo_root: Path) -> None:
1200
1200
  if not warning_cfg.get("enabled", True):
1201
1201
  return
1202
1202
 
1203
- minimum_version = str(warning_cfg.get("minimum_version") or "1.0.0")
1204
1203
  generated_version = _detect_generated_deepdoc_version(
1205
1204
  repo_root, repo_root / str(cfg.get("output_dir", "docs") or "docs")
1206
1205
  )
1207
1206
  if generated_version is None:
1208
1207
  return
1209
- if _version_tuple(generated_version) >= _version_tuple(minimum_version):
1208
+
1209
+ # Warn when the docs were generated by a different major version of the CLI.
1210
+ # Minor/patch bumps are backwards-compatible — no need to nag.
1211
+ gen_major = _version_tuple(generated_version)[0]
1212
+ cli_major = _version_tuple(__version__)[0]
1213
+ if gen_major >= cli_major:
1210
1214
  return
1211
1215
 
1212
1216
  resolved_root = repo_root.resolve()
@@ -1214,19 +1218,15 @@ def _warn_if_deprecated_generated_version(cfg: dict, repo_root: Path) -> None:
1214
1218
  return
1215
1219
  _DEPRECATED_VERSION_WARNING_REPOS.add(resolved_root)
1216
1220
 
1217
- upgrade_command = str(
1218
- warning_cfg.get("upgrade_command")
1219
- or "python3 -m pip install --upgrade deepdoc"
1220
- )
1221
1221
  console.print(
1222
1222
  Panel.fit(
1223
- "[bold yellow]DeepDoc upgrade recommended[/bold yellow]\n\n"
1224
- f"This repository has generated docs from DeepDoc [bold]{generated_version}[/bold], "
1225
- f"which is older than the supported baseline [bold]{minimum_version}[/bold].\n"
1226
- "Upgrade the CLI before generating or updating docs:\n\n"
1227
- f"[bold]{upgrade_command}[/bold]\n\n"
1228
- "To change or disable this warning, update "
1229
- "`compatibility.deprecated_version_warning.*` in `.deepdoc.yaml`.",
1223
+ "[bold yellow]Docs need regeneration[/bold yellow]\n\n"
1224
+ f"Your docs were generated with DeepDoc [bold]{generated_version}[/bold] "
1225
+ f"but the current CLI is [bold]{__version__}[/bold] (different major version).\n"
1226
+ "Run the following to bring them up to date:\n\n"
1227
+ "[bold]deepdoc generate[/bold]\n\n"
1228
+ "To disable this warning: set "
1229
+ "`compatibility.deprecated_version_warning.enabled: false` in `.deepdoc.yaml`.",
1230
1230
  border_style="yellow",
1231
1231
  )
1232
1232
  )
@@ -1072,6 +1072,7 @@ Re-run `deepdoc generate` to retry.
1072
1072
  "deepdoc_generated_version": DEEPDOC_VERSION,
1073
1073
  "deepdoc_status": status_value,
1074
1074
  "deepdoc_evidence_files": evidence_files[:50],
1075
+ "deepdoc_prereqs": bucket.depends_on,
1075
1076
  }
1076
1077
  return _merge_frontmatter_fields(content, fields)
1077
1078
 
@@ -957,6 +957,10 @@ def repair_mdx_component_blocks(content: str) -> str:
957
957
 
958
958
  content = pattern.sub(_rewrite, content)
959
959
 
960
+ # Fix Accordion nesting first (inserts missing </Accordion> before </Accordions>)
961
+ # so the subsequent count-based repair sees balanced tags and doesn't double-add.
962
+ content = _repair_accordion_nesting(content)
963
+
960
964
  for component in multiline_components:
961
965
  open_count = len(re.findall(rf"<{component}(?:\s[^>]*)?>", content))
962
966
  close_count = len(re.findall(rf"</{component}>", content))
@@ -968,6 +972,38 @@ def repair_mdx_component_blocks(content: str) -> str:
968
972
  return content
969
973
 
970
974
 
975
+ def _repair_accordion_nesting(content: str) -> str:
976
+ """Fix Accordion nesting issues emitted by the LLM.
977
+
978
+ Two cases handled:
979
+ 1. Missing </Accordion> before </Accordions> — inserts them in the right place.
980
+ 2. Swapped order (</Accordions> then </Accordion>) — strips the orphaned
981
+ </Accordion> that appears after </Accordions> with no matching open tag.
982
+ """
983
+ import re as _re
984
+
985
+ def _fix_block(m: re.Match) -> str:
986
+ block = m.group(0)
987
+ opens = len(_re.findall(r"<Accordion(?:\s[^>]*)?>", block))
988
+ closes = len(_re.findall(r"</Accordion>", block))
989
+ deficit = opens - closes
990
+ if deficit <= 0:
991
+ return block
992
+ return block[:-len("</Accordions>")] + ("</Accordion>\n" * deficit) + "</Accordions>"
993
+
994
+ content = re.sub(
995
+ r"<Accordions(?:\s[^>]*)?>[\s\S]*?</Accordions>",
996
+ _fix_block,
997
+ content,
998
+ )
999
+
1000
+ # Strip orphaned </Accordion> tags that appear after </Accordions>
1001
+ # (the LLM sometimes emits </Accordions>\n</Accordion> in the wrong order)
1002
+ content = re.sub(r"(</Accordions>)(\s*</Accordion>)+", r"\1", content)
1003
+
1004
+ return content
1005
+
1006
+
971
1007
  _PROVENANCE_KEYS = frozenset({
972
1008
  "deepdoc_generated_at",
973
1009
  "deepdoc_generated_commit",
@@ -37,6 +37,8 @@ LEDGER_FILE = "ledger.json"
37
37
  FILE_MAP_FILE = "file_map.json"
38
38
  STATE_FILE = "state.json"
39
39
  SYNC_RECEIPT_FILE = "sync_receipt.json"
40
+ CHANGELOG_FILE = "changelog.json"
41
+ CHANGELOG_MAX_ENTRIES = 50
40
42
  ENGINE_FINGERPRINT = "routes_repo_resolution_v2_trimmed_scope"
41
43
 
42
44
  # Legacy top-level files (kept for backwards-compat)
@@ -108,6 +110,31 @@ def load_sync_state(repo_root: Path) -> dict[str, Any] | None:
108
110
  return None
109
111
 
110
112
 
113
+ def append_changelog_entry(repo_root: Path, entry: dict[str, Any]) -> None:
114
+ """Append one entry to .deepdoc/changelog.json, capped at CHANGELOG_MAX_ENTRIES."""
115
+ path = _state_dir(repo_root) / CHANGELOG_FILE
116
+ entries: list[dict[str, Any]] = []
117
+ if path.exists():
118
+ try:
119
+ entries = json.loads(path.read_text(encoding="utf-8"))
120
+ except Exception:
121
+ entries = []
122
+ entries.insert(0, entry)
123
+ entries = entries[:CHANGELOG_MAX_ENTRIES]
124
+ path.write_text(json.dumps(entries, indent=2), encoding="utf-8")
125
+
126
+
127
+ def load_changelog(repo_root: Path) -> list[dict[str, Any]]:
128
+ """Read .deepdoc/changelog.json. Returns empty list if not present or corrupt."""
129
+ path = _state_dir(repo_root) / CHANGELOG_FILE
130
+ if not path.exists():
131
+ return []
132
+ try:
133
+ return json.loads(path.read_text(encoding="utf-8"))
134
+ except Exception:
135
+ return []
136
+
137
+
111
138
  def save_sync_receipt(repo_root: Path, receipt: dict[str, Any]) -> None:
112
139
  """Write a top-level receipt for the latest generate/update sync run."""
113
140
  path = _state_dir(repo_root) / SYNC_RECEIPT_FILE
@@ -51,8 +51,10 @@ from .openapi import (
51
51
  parse_openapi_spec,
52
52
  spec_to_context_string,
53
53
  )
54
+ from .changelog_writer import record_and_write as _record_changelog
54
55
  from .persistence_v2 import (
55
56
  cleanup_stale_generated_files,
57
+ load_changelog,
56
58
  load_generation_ledger,
57
59
  prune_generation_ledger,
58
60
  save_all,
@@ -112,6 +114,36 @@ def _endpoint_ref_slug(method: str, path: str) -> str:
112
114
  return f"{method.lower()}-{path_slug}"
113
115
 
114
116
 
117
+ def _spec_base_path(spec: dict) -> str:
118
+ """Return the base path from the first server URL, e.g. '/api/v2'."""
119
+ from urllib.parse import urlparse
120
+ if "openapi" in spec:
121
+ servers = spec.get("servers", [])
122
+ if servers:
123
+ url = str(servers[0].get("url", "") or "").strip()
124
+ parsed = urlparse(url)
125
+ base = parsed.path if (parsed.scheme or parsed.netloc) else url
126
+ return base.rstrip("/")
127
+ elif "swagger" in spec:
128
+ return spec.get("basePath", "").rstrip("/")
129
+ return ""
130
+
131
+
132
+ def _write_spec(dest: Path, spec: dict) -> None:
133
+ """Write spec dict to dest as YAML or JSON depending on suffix."""
134
+ if dest.suffix == ".json":
135
+ dest.write_text(json.dumps(spec, indent=2) + "\n", encoding="utf-8")
136
+ else:
137
+ try:
138
+ import yaml
139
+ dest.write_text(
140
+ yaml.dump(spec, allow_unicode=True, sort_keys=False, default_flow_style=False),
141
+ encoding="utf-8",
142
+ )
143
+ except ImportError:
144
+ dest.write_text(json.dumps(spec, indent=2) + "\n", encoding="utf-8")
145
+
146
+
115
147
  def stage_openapi_assets(
116
148
  repo_root: Path, openapi_paths: list[str] | None = None
117
149
  ) -> bool:
@@ -136,7 +168,6 @@ def stage_openapi_assets(
136
168
 
137
169
  spec_name = Path(spec_rel_path).name
138
170
  staged_spec = site_openapi_dir / spec_name
139
- shutil.copy2(spec_src, staged_spec)
140
171
 
141
172
  spec = parse_openapi_spec(spec_src)
142
173
  if not spec:
@@ -145,6 +176,20 @@ def stage_openapi_assets(
145
176
  )
146
177
  return False
147
178
 
179
+ # Bake the server base path into each path key so Fumadocs can do a
180
+ # direct dict lookup (it does not prepend the server URL itself).
181
+ base_path = _spec_base_path(spec)
182
+ if base_path and not any(
183
+ k.startswith(base_path) for k in spec.get("paths", {})
184
+ ):
185
+ spec = {
186
+ **spec,
187
+ "paths": {base_path + k: v for k, v in spec["paths"].items()},
188
+ "servers": [{"url": "/"}],
189
+ }
190
+
191
+ _write_spec(staged_spec, spec)
192
+
148
193
  endpoints = extract_endpoints_from_spec(spec)
149
194
  manifest: list[dict[str, str]] = []
150
195
  for ep in endpoints:
@@ -381,6 +426,19 @@ class PipelineV2:
381
426
  "replanned": True,
382
427
  },
383
428
  )
429
+ changelog_exists = bool(load_changelog(self.repo_root))
430
+ _commit_obj = _repo.head.commit
431
+ _record_changelog(
432
+ self.repo_root,
433
+ self.output_dir,
434
+ commit=head_sha,
435
+ commit_message=_commit_obj.message.strip().splitlines()[0],
436
+ commit_date=_commit_obj.committed_datetime.strftime("%Y-%m-%d"),
437
+ strategy="full_generate",
438
+ pages_updated=[b.slug for b in plan.buckets],
439
+ files_changed=[],
440
+ is_initial=not changelog_exists,
441
+ )
384
442
  except Exception:
385
443
  pass # Not a git repo or detached HEAD — skip silently
386
444
 
@@ -405,6 +405,64 @@ _GENERIC_PLACEHOLDER_SECTIONS = {
405
405
  "pages",
406
406
  }
407
407
 
408
+ _BACKEND_INTEGRATION_TOKENS = {
409
+ "cdn",
410
+ "clickpost",
411
+ "express",
412
+ "gateway",
413
+ "integration",
414
+ "provider",
415
+ "third",
416
+ "vinculum",
417
+ "warehouse",
418
+ "wms",
419
+ }
420
+
421
+ _BACKEND_OPERATION_TOKENS = {
422
+ "config",
423
+ "cron",
424
+ "debug",
425
+ "health",
426
+ "log",
427
+ "logger",
428
+ "logging",
429
+ "metric",
430
+ "monitor",
431
+ "observability",
432
+ "scheduler",
433
+ "utility",
434
+ "utilities",
435
+ }
436
+
437
+ _BACKEND_RUNTIME_TOKENS = {
438
+ "agenda",
439
+ "async",
440
+ "background",
441
+ "celery",
442
+ "command",
443
+ "consumer",
444
+ "cron",
445
+ "django",
446
+ "job",
447
+ "queue",
448
+ "scheduler",
449
+ "signal",
450
+ "task",
451
+ "worker",
452
+ }
453
+
454
+ _PATH_SECTION_PREFIXES = (
455
+ "new-src-",
456
+ "src-",
457
+ "app-",
458
+ "lib-",
459
+ "packages-",
460
+ "services-",
461
+ "controllers-",
462
+ "middlewares-",
463
+ "utils-",
464
+ )
465
+
408
466
 
409
467
  def _canonical_section_for_bucket(bucket: DocBucket, primary_type: str) -> str:
410
468
  # Supporting-tier buckets always get re-sectioned (Testing, CI/CD, etc.) regardless
@@ -412,7 +470,12 @@ def _canonical_section_for_bucket(bucket: DocBucket, primary_type: str) -> str:
412
470
  # domain-specific section name rather than overriding with a generic canonical one.
413
471
  if bucket.publication_tier != "supporting":
414
472
  existing = (bucket.section or "").strip()
415
- if existing and existing.lower() not in _GENERIC_PLACEHOLDER_SECTIONS:
473
+ if (
474
+ existing
475
+ and existing.lower() not in _GENERIC_PLACEHOLDER_SECTIONS
476
+ and not _looks_like_path_slug_section(existing)
477
+ and not _is_backend_placeholder_section(existing, primary_type)
478
+ ):
416
479
  return existing
417
480
 
418
481
  title_tokens = _bucket_semantic_tokens(bucket)
@@ -570,23 +633,25 @@ def _canonical_section_for_bucket(bucket: DocBucket, primary_type: str) -> str:
570
633
  "is_endpoint_family"
571
634
  ) or bucket.generation_hints.get("is_endpoint_ref"):
572
635
  return "API Reference"
573
- if any(
574
- token in title_tokens
575
- for token in {"middleware", "auth", "route", "controller", "handler"}
576
- ):
577
- return "Runtime & Frameworks"
578
636
  if any(
579
637
  token in title_tokens
580
638
  for token in {"model", "schema", "migration", "database"}
581
639
  ):
582
640
  return "Data Layer"
583
- if any(
584
- token in title_tokens for token in {"provider", "gateway", "integration"}
641
+ if bucket.bucket_type in {"runtime", "runtime-group"} or any(
642
+ token in title_tokens for token in _BACKEND_RUNTIME_TOKENS
585
643
  ):
586
- return "Integrations"
587
- if any(token in title_tokens for token in {"queue", "task", "worker", "sync"}):
588
644
  return "Background Jobs"
589
- return "Subsystems"
645
+ if any(token in title_tokens for token in _BACKEND_INTEGRATION_TOKENS):
646
+ return "Integrations"
647
+ if any(token in title_tokens for token in _BACKEND_OPERATION_TOKENS):
648
+ return "Operations"
649
+ if any(
650
+ token in title_tokens
651
+ for token in {"middleware", "auth", "route", "controller", "handler"}
652
+ ):
653
+ return "Supporting Infrastructure"
654
+ return "Core Workflows"
590
655
  if bucket.generation_hints.get("is_introduction_page"):
591
656
  return "Overview"
592
657
  if bucket.generation_hints.get("is_endpoint_family") or bucket.generation_hints.get(
@@ -605,3 +670,30 @@ def _canonical_section_for_bucket(bucket: DocBucket, primary_type: str) -> str:
605
670
  ):
606
671
  return "Operations"
607
672
  return "Architecture"
673
+
674
+
675
+ def _looks_like_path_slug_section(section: str) -> bool:
676
+ value = (section or "").strip()
677
+ if not value:
678
+ return False
679
+ if " > " in value or "/" in value or "\\" in value:
680
+ return False
681
+ lower = value.lower()
682
+ if lower != value or "-" not in lower:
683
+ return False
684
+ if lower.endswith(("-ts", "-js", "-tsx", "-jsx", "-py", "-php", "-go")):
685
+ return True
686
+ return lower.startswith(_PATH_SECTION_PREFIXES) and lower.count("-") >= 2
687
+
688
+
689
+ def _is_backend_placeholder_section(section: str, primary_type: str) -> bool:
690
+ if primary_type not in {"backend_service", "backend_api", "falcon_backend"}:
691
+ return False
692
+ return section.strip().lower() in {
693
+ "architecture",
694
+ "core",
695
+ "features",
696
+ "runtime & frameworks",
697
+ "services",
698
+ "subsystems",
699
+ }
@@ -2,6 +2,18 @@ from .common import *
2
2
  from .bucket_refinement import _bucket_semantic_tokens
3
3
  from .bucket_injection import _canonical_section_for_bucket
4
4
 
5
+ _PATH_SECTION_PREFIXES = (
6
+ "new-src-",
7
+ "src-",
8
+ "app-",
9
+ "lib-",
10
+ "packages-",
11
+ "services-",
12
+ "controllers-",
13
+ "middlewares-",
14
+ "utils-",
15
+ )
16
+
5
17
 
6
18
  def _shape_plan_nav(
7
19
  plan: DocPlan,
@@ -24,7 +36,7 @@ def _shape_plan_nav(
24
36
  continue
25
37
 
26
38
  hints = bucket.generation_hints or {}
27
- if not hints.get("preserve_section"):
39
+ if not hints.get("preserve_section") or _is_path_slug_section(bucket.section):
28
40
  bucket.section = _canonical_section_for_bucket(bucket, primary)
29
41
 
30
42
  bucket.section = _normalize_nav_section(bucket.section, primary)
@@ -150,10 +162,27 @@ def _normalize_nav_section(section: str, primary: str) -> str:
150
162
  }.get(top, top)
151
163
 
152
164
  if sep:
165
+ if rest.strip() == top:
166
+ return top
153
167
  return f"{top} > {rest}"
154
168
  return top
155
169
 
156
170
 
171
+ def _is_path_slug_section(section: str) -> bool:
172
+ """Return True when the section looks like a file path cluster id."""
173
+ value = (section or "").strip()
174
+ if not value:
175
+ return False
176
+ if " > " in value or "/" in value or "\\" in value:
177
+ return False
178
+ lower = value.lower()
179
+ if lower != value or "-" not in lower:
180
+ return False
181
+ if lower.endswith(("-ts", "-js", "-tsx", "-jsx", "-py", "-php", "-go")):
182
+ return True
183
+ return lower.startswith(_PATH_SECTION_PREFIXES) and lower.count("-") >= 2
184
+
185
+
157
186
  def _build_endpoint_reference_nav(buckets: list[DocBucket]) -> dict[str, list[str]]:
158
187
  families = [
159
188
  bucket
@@ -283,6 +312,27 @@ def _section_sort_key(
283
312
  rank = _RT_ORDER.index(top) if top in _RT_ORDER else 10
284
313
  return (rank, section_order.get(section, 999), section)
285
314
 
315
+ if primary in {"backend_service", "backend_api", "falcon_backend"}:
316
+ _BACKEND_ORDER = [
317
+ "Start Here",
318
+ "Overview",
319
+ "Core Workflows",
320
+ "API Reference",
321
+ "Data Model",
322
+ "Background Jobs",
323
+ "Integrations",
324
+ "Operations",
325
+ "Runtime & Frameworks",
326
+ "Supporting Infrastructure",
327
+ "Design & Notes",
328
+ "Testing",
329
+ "CI/CD and Release",
330
+ "Supporting Material",
331
+ ]
332
+ rank = _BACKEND_ORDER.index(top) if top in _BACKEND_ORDER else 8
333
+ child_rank = 0 if section == top else 1
334
+ return (rank, child_rank, section_order.get(section, 999), section)
335
+
286
336
  _FIRST = {"Start Here": 0, "Overview": 1, "Getting Started": 2}
287
337
  _LAST = {
288
338
  "Supporting Infrastructure": 55,