deepdoc 1.9.3__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.3 → deepdoc-2.0.0}/PKG-INFO +1 -1
  2. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/__init__.py +1 -1
  3. deepdoc-2.0.0/deepdoc/changelog_writer.py +106 -0
  4. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/cli.py +13 -13
  5. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/generator/generation.py +1 -0
  6. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/generator/post_processors.py +36 -0
  7. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/persistence_v2.py +27 -0
  8. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/pipeline_v2.py +15 -0
  9. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/builder/scaffold_files.py +26 -1
  10. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/smart_update_v2.py +104 -21
  11. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc.egg-info/PKG-INFO +1 -1
  12. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc.egg-info/SOURCES.txt +2 -0
  13. {deepdoc-1.9.3 → deepdoc-2.0.0}/pyproject.toml +1 -1
  14. deepdoc-2.0.0/tests/test_changelog.py +74 -0
  15. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_classify.py +8 -12
  16. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_cli_serve.py +2 -2
  17. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_fumadocs_builder.py +1 -1
  18. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_smart_update.py +104 -21
  19. {deepdoc-1.9.3 → deepdoc-2.0.0}/LICENSE +0 -0
  20. {deepdoc-1.9.3 → deepdoc-2.0.0}/README.md +0 -0
  21. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/__main__.py +0 -0
  22. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/_legacy_types.py +0 -0
  23. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/benchmark_v2.py +0 -0
  24. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/call_graph.py +0 -0
  25. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/__init__.py +0 -0
  26. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/answer_mixin.py +0 -0
  27. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/chunker.py +0 -0
  28. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/deep_research.py +0 -0
  29. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/docs_summary.py +0 -0
  30. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/embeddings.py +0 -0
  31. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/indexer.py +0 -0
  32. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/linking.py +0 -0
  33. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/live_fallback_mixin.py +0 -0
  34. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/persistence.py +0 -0
  35. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/providers.py +0 -0
  36. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/retrieval_mixin.py +0 -0
  37. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/routes.py +0 -0
  38. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/scaffold.py +0 -0
  39. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/service.py +0 -0
  40. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/settings.py +0 -0
  41. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/source_archive.py +0 -0
  42. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/symbol_index.py +0 -0
  43. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/chatbot/types.py +0 -0
  44. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/config.py +0 -0
  45. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/generator/__init__.py +0 -0
  46. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/generator/evidence.py +0 -0
  47. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/generator/validation.py +0 -0
  48. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/llm/__init__.py +0 -0
  49. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/llm/client.py +0 -0
  50. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/llm/json_utils.py +0 -0
  51. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/llm/litellm_compat.py +0 -0
  52. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/manifest.py +0 -0
  53. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/openapi.py +0 -0
  54. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/__init__.py +0 -0
  55. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/api_detector.py +0 -0
  56. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/base.py +0 -0
  57. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/go_parser.py +0 -0
  58. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/js_ts_parser.py +0 -0
  59. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/php_parser.py +0 -0
  60. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/python_parser.py +0 -0
  61. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/registry.py +0 -0
  62. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/__init__.py +0 -0
  63. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/base.py +0 -0
  64. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/common.py +0 -0
  65. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/detector.py +0 -0
  66. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/django.py +0 -0
  67. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/express.py +0 -0
  68. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/falcon.py +0 -0
  69. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/fastify.py +0 -0
  70. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/go.py +0 -0
  71. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/js_shared.py +0 -0
  72. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/laravel.py +0 -0
  73. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/nestjs.py +0 -0
  74. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/python_shared.py +0 -0
  75. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/registry.py +0 -0
  76. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/routes/repo_resolver.py +0 -0
  77. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/parser/vue_parser.py +0 -0
  78. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/__init__.py +0 -0
  79. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/bucket_injection.py +0 -0
  80. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/bucket_refinement.py +0 -0
  81. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/common.py +0 -0
  82. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/endpoint_refs.py +0 -0
  83. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/engine.py +0 -0
  84. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/flow_candidates.py +0 -0
  85. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/heuristics.py +0 -0
  86. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/nav_shaping.py +0 -0
  87. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/specializations.py +0 -0
  88. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/topology.py +0 -0
  89. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/planner/utils.py +0 -0
  90. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/prompts/__init__.py +0 -0
  91. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/prompts/bucket_types.py +0 -0
  92. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/prompts/page_types.py +0 -0
  93. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/prompts/selectors.py +0 -0
  94. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/prompts/system.py +0 -0
  95. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/prompts/update.py +0 -0
  96. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/prompts_v2.py +0 -0
  97. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/py.typed +0 -0
  98. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/__init__.py +0 -0
  99. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/artifacts.py +0 -0
  100. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/clustering.py +0 -0
  101. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/common.py +0 -0
  102. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/database.py +0 -0
  103. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/endpoints.py +0 -0
  104. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/integrations.py +0 -0
  105. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/runtime.py +0 -0
  106. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/scanner/utils.py +0 -0
  107. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/__init__.py +0 -0
  108. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/builder/__init__.py +0 -0
  109. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/builder/chatbot_components.py +0 -0
  110. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/builder/common.py +0 -0
  111. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/builder/engine.py +0 -0
  112. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/builder/mdx_utils.py +0 -0
  113. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/site/builder/templates.py +0 -0
  114. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/source_metadata.py +0 -0
  115. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/updater_v2.py +0 -0
  116. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc/v2_models.py +0 -0
  117. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc.egg-info/dependency_links.txt +0 -0
  118. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc.egg-info/entry_points.txt +0 -0
  119. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc.egg-info/requires.txt +0 -0
  120. {deepdoc-1.9.3 → deepdoc-2.0.0}/deepdoc.egg-info/top_level.txt +0 -0
  121. {deepdoc-1.9.3 → deepdoc-2.0.0}/setup.cfg +0 -0
  122. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_benchmark_scorecard.py +0 -0
  123. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_call_graph.py +0 -0
  124. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_config.py +0 -0
  125. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_embeddings.py +0 -0
  126. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_eval.py +0 -0
  127. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_index.py +0 -0
  128. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_persistence.py +0 -0
  129. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_providers.py +0 -0
  130. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_query.py +0 -0
  131. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_relationship.py +0 -0
  132. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_scaffold.py +0 -0
  133. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_chatbot_source_archive.py +0 -0
  134. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_cli_generate.py +0 -0
  135. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_cli_update.py +0 -0
  136. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_flow_candidates.py +0 -0
  137. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_framework_fixtures.py +0 -0
  138. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_framework_support.py +0 -0
  139. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_generation_evidence.py +0 -0
  140. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_internal_docs_metadata.py +0 -0
  141. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_litellm_compat.py +0 -0
  142. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_llm_json_utils.py +0 -0
  143. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_parallel_pipeline.py +0 -0
  144. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_parser_ranges.py +0 -0
  145. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_planner_consolidation.py +0 -0
  146. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_planner_granularity.py +0 -0
  147. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_route_registry.py +0 -0
  148. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_runtime_scan.py +0 -0
  149. {deepdoc-1.9.3 → deepdoc-2.0.0}/tests/test_stale.py +0 -0
  150. {deepdoc-1.9.3 → 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.3
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.3"
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,
@@ -424,6 +426,19 @@ class PipelineV2:
424
426
  "replanned": True,
425
427
  },
426
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
+ )
427
442
  except Exception:
428
443
  pass # Not a git repo or detached HEAD — skip silently
429
444
 
@@ -1421,7 +1421,9 @@ def _docs_page_tsx() -> str:
1421
1421
  """\
1422
1422
  import { notFound } from 'next/navigation';
1423
1423
  import { DocsBody, DocsPage } from 'fumadocs-ui/page';
1424
+ import { findNeighbour } from 'fumadocs-core/server';
1424
1425
  import { docsSource } from '@/lib/source';
1426
+ import { pageTree } from '@/lib/page-tree.generated';
1425
1427
  import { getMDXComponents } from '@/mdx-components';
1426
1428
  import type { ComponentType } from 'react';
1427
1429
  import type { TOCItemType } from 'fumadocs-core/server';
@@ -1442,6 +1444,7 @@ def _docs_page_tsx() -> str:
1442
1444
  const meta = page.data as {
1443
1445
  deepdoc_generated_at?: string;
1444
1446
  deepdoc_generated_commit?: string;
1447
+ deepdoc_prereqs?: string[];
1445
1448
  };
1446
1449
  const lastIndexed = meta.deepdoc_generated_at
1447
1450
  ? new Intl.DateTimeFormat('en-GB', {
@@ -1459,10 +1462,32 @@ def _docs_page_tsx() -> str:
1459
1462
  : commitId
1460
1463
  ? `Last indexed: (${commitId})`
1461
1464
  : null;
1465
+ const prereqs = meta.deepdoc_prereqs ?? [];
1466
+ const { previous, next } = findNeighbour(pageTree, page.url);
1462
1467
 
1463
1468
  return (
1464
- <DocsPage toc={toc}>
1469
+ <DocsPage toc={toc} prev={previous ?? false} next={next ?? false}>
1465
1470
  <DocsBody>
1471
+ {prereqs.length > 0 ? (
1472
+ <p
1473
+ style={{
1474
+ marginTop: 0,
1475
+ marginBottom: '0.5rem',
1476
+ fontSize: '0.875rem',
1477
+ color: 'var(--color-fd-muted-foreground)',
1478
+ }}
1479
+ >
1480
+ {'Read first: '}
1481
+ {prereqs.map((slug: string, i: number) => (
1482
+ <span key={slug}>
1483
+ <a href={`/${slug}`} style={{ textDecoration: 'underline' }}>
1484
+ {slug.replace(/-/g, ' ').replace(/\\b\\w/g, (c: string) => c.toUpperCase())}
1485
+ </a>
1486
+ {i < prereqs.length - 1 ? ', ' : ''}
1487
+ </span>
1488
+ ))}
1489
+ </p>
1490
+ ) : null}
1466
1491
  {lastIndexedLabel ? (
1467
1492
  <p
1468
1493
  style={{
@@ -35,14 +35,17 @@ from .generator import summarize_generation_results
35
35
  from .llm import LLMClient
36
36
  from .manifest import Manifest
37
37
  from .parser import supported_extensions
38
+ from .changelog_writer import record_and_write as _record_changelog
38
39
  from .persistence_v2 import (
39
40
  ENGINE_FINGERPRINT,
41
+ cleanup_stale_generated_files,
40
42
  find_stale_buckets,
41
43
  ledger_summary,
42
44
  load_generation_ledger,
43
45
  load_plan,
44
46
  load_scan_cache,
45
47
  load_sync_state,
48
+ prune_generation_ledger,
46
49
  save_all,
47
50
  save_sync_receipt,
48
51
  save_sync_state,
@@ -51,10 +54,6 @@ from .v2_models import DocPlan, endpoint_owned_files, tracked_bucket_files
51
54
 
52
55
  console = Console()
53
56
 
54
- # What fraction of total files changed triggers a replan
55
- REPLAN_THRESHOLD = 0.20
56
- # New files added beyond this count also triggers replan
57
- NEW_FILES_REPLAN_THRESHOLD = 5
58
57
 
59
58
 
60
59
  # ─────────────────────────────────────────────────────────────────────────────
@@ -98,19 +97,13 @@ class ChangeSet:
98
97
  and not self.orphaned_bucket_slugs
99
98
  ):
100
99
  return "noop"
101
- if self.deleted_files or self.orphaned_bucket_slugs:
102
- return "full_replan"
103
- if len(self.new_files) >= NEW_FILES_REPLAN_THRESHOLD:
104
- return "full_replan"
105
- # If total changes exceed the percentage threshold, replan
106
100
  if (
107
- self.total_plan_files > 0
108
- and self.total_changes / self.total_plan_files > REPLAN_THRESHOLD
101
+ self.new_files
102
+ or self.new_integration_signals
103
+ or self.deleted_files
104
+ or self.orphaned_bucket_slugs
105
+ or self.endpoint_structure_changed
109
106
  ):
110
- return "full_replan"
111
- if self.endpoint_structure_changed:
112
- return "full_replan"
113
- if self.new_files or self.new_integration_signals:
114
107
  return "targeted_replan"
115
108
  return "incremental"
116
109
 
@@ -257,11 +250,12 @@ class SmartUpdater:
257
250
  return stats
258
251
 
259
252
  elif sync_plan.strategy == "full_replan":
253
+ # Only reachable via engine_mismatch or explicit force_replan override
254
+ # in _build_sync_plan — never auto-triggered by ChangeSet.strategy.
260
255
  console.print(
261
256
  Panel(
262
257
  "[bold yellow]Strategy: Full Replan[/bold yellow]\n"
263
- f"Reason: {len(change_set.new_files)} new files, "
264
- f"{len(change_set.deleted_files)} deleted files",
258
+ "Reason: engine fingerprint changed or replan explicitly forced",
265
259
  border_style="yellow",
266
260
  )
267
261
  )
@@ -276,6 +270,12 @@ class SmartUpdater:
276
270
  f"{len(change_set.new_integration_signals)} new integration signal(s): "
277
271
  f"{', '.join(change_set.new_integration_signals[:5])}"
278
272
  )
273
+ if change_set.deleted_files:
274
+ reasons.append(f"{len(change_set.deleted_files)} deleted file(s)")
275
+ if change_set.orphaned_bucket_slugs:
276
+ reasons.append(f"{len(change_set.orphaned_bucket_slugs)} orphaned bucket(s)")
277
+ if change_set.endpoint_structure_changed:
278
+ reasons.append("endpoint structure changed")
279
279
  console.print(
280
280
  Panel(
281
281
  "[bold cyan]Strategy: Targeted Replan[/bold cyan]\n"
@@ -308,12 +308,13 @@ class SmartUpdater:
308
308
  stats["chatbot_failed"] = run_result.chatbot_failed
309
309
 
310
310
  # ── Step 4: Rebuild site nav ───────────────────────────────────
311
- if executed_strategy not in {"noop", "full_replan"}:
311
+ if executed_strategy != "noop":
312
312
  updated_plan = load_plan(self.repo_root) or plan
313
313
  self._rebuild_nav(updated_plan)
314
314
 
315
315
  # ── Step 5: Persist sync baseline ──────────────────────────────
316
- # full_replan already saves via pipeline_v2.py, so skip it here
316
+ # When full_replan fires (engine_mismatch / force_replan), pipeline_v2
317
+ # already saves state internally — skip double-save only for that case.
317
318
  if executed_strategy != "full_replan":
318
319
  self._save_update_sync_state(
319
320
  target_commit=sync_plan.target_commit,
@@ -323,6 +324,8 @@ class SmartUpdater:
323
324
  plan=plan,
324
325
  )
325
326
  self._save_update_sync_receipt(sync_plan, run_result)
327
+ if executed_strategy != "noop":
328
+ self._append_changelog(sync_plan, run_result)
326
329
 
327
330
  console.print(
328
331
  Panel.fit(
@@ -390,6 +393,54 @@ class SmartUpdater:
390
393
  chatbot_failed=bool(result.get("chatbot_error")),
391
394
  )
392
395
 
396
+ def _handle_deleted_files(
397
+ self, plan: DocPlan, change_set: ChangeSet
398
+ ) -> DocPlan:
399
+ """Remove deleted files from bucket owned_files; clean up fully empty buckets."""
400
+ if not change_set.deleted_files and not change_set.orphaned_bucket_slugs:
401
+ return plan
402
+
403
+ deleted_set = set(change_set.deleted_files)
404
+ orphaned_set = set(change_set.orphaned_bucket_slugs)
405
+ updated_buckets = []
406
+ removed_slugs: list[str] = []
407
+
408
+ for bucket in plan.buckets:
409
+ if bucket.slug in orphaned_set:
410
+ removed_slugs.append(bucket.slug)
411
+ continue
412
+ if any(f in deleted_set for f in bucket.owned_files):
413
+ bucket.owned_files = [
414
+ f for f in bucket.owned_files if f not in deleted_set
415
+ ]
416
+ if bucket.slug not in change_set.stale_bucket_slugs:
417
+ change_set.stale_bucket_slugs.append(bucket.slug)
418
+ updated_buckets.append(bucket)
419
+
420
+ if removed_slugs:
421
+ keep_slugs = {b.slug for b in updated_buckets}
422
+ deleted_paths = cleanup_stale_generated_files(
423
+ self.repo_root, self.output_dir, keep_slugs
424
+ )
425
+ prune_generation_ledger(self.repo_root, keep_slugs)
426
+ if deleted_paths:
427
+ console.print(
428
+ f" [dim]Removed {len(deleted_paths)} doc(s) for deleted bucket(s): "
429
+ f"{', '.join(removed_slugs)}[/dim]"
430
+ )
431
+ plan.nav_structure = {
432
+ section: [s for s in slugs if s not in removed_slugs]
433
+ for section, slugs in plan.nav_structure.items()
434
+ }
435
+ plan.nav_structure = {
436
+ section: slugs
437
+ for section, slugs in plan.nav_structure.items()
438
+ if slugs
439
+ }
440
+
441
+ plan.buckets = updated_buckets
442
+ return plan
443
+
393
444
  def _targeted_replan(self, plan: DocPlan, change_set: ChangeSet) -> UpdateRunResult:
394
445
  """Replan only to discover new buckets for new integrations/files,
395
446
  then merge with existing plan and regenerate stale buckets.
@@ -403,6 +454,9 @@ class SmartUpdater:
403
454
  scan_repo as bucket_scan_repo,
404
455
  )
405
456
 
457
+ # Step 0: clean up deleted files and orphaned buckets before any LLM work
458
+ plan = self._handle_deleted_files(plan, change_set)
459
+
406
460
  # Invalidate call graph cache if source files changed
407
461
  _invalidate_call_graph_cache(
408
462
  self.repo_root,
@@ -434,9 +488,8 @@ class SmartUpdater:
434
488
 
435
489
  if change_set.new_files and not added:
436
490
  console.print(
437
- "[yellow]⚠ New files did not map cleanly to new buckets — escalating to full replan[/yellow]"
491
+ "[yellow]⚠ New files did not map to new buckets — they will be picked up on the next explicit replan[/yellow]"
438
492
  )
439
- return self._full_replan_and_generate()
440
493
 
441
494
  if added:
442
495
  console.print(f" [green]+{len(added)} new bucket(s) discovered:[/green]")
@@ -1128,6 +1181,36 @@ class SmartUpdater:
1128
1181
  },
1129
1182
  )
1130
1183
 
1184
+ def _append_changelog(
1185
+ self,
1186
+ sync_plan: UpdateSyncPlan,
1187
+ run_result: UpdateRunResult,
1188
+ ) -> None:
1189
+ """Append a changelog entry and regenerate whats-changed.mdx."""
1190
+ try:
1191
+ import git as _git
1192
+
1193
+ repo = _git.Repo(self.repo_root)
1194
+ commit_obj = repo.commit(sync_plan.target_commit)
1195
+ commit_message = commit_obj.message.strip().splitlines()[0]
1196
+ commit_date = commit_obj.committed_datetime.strftime("%Y-%m-%d")
1197
+ except Exception:
1198
+ commit_message = "update"
1199
+ commit_date = ""
1200
+
1201
+ _record_changelog(
1202
+ self.repo_root,
1203
+ self.output_dir,
1204
+ commit=sync_plan.target_commit,
1205
+ commit_message=commit_message,
1206
+ commit_date=commit_date,
1207
+ strategy=run_result.strategy,
1208
+ pages_updated=list(run_result.updated_slugs),
1209
+ files_changed=list(
1210
+ sync_plan.change_set.changed_files + sync_plan.change_set.new_files
1211
+ ),
1212
+ )
1213
+
1131
1214
  def _resolve_head_commit(self) -> str:
1132
1215
  """Return the current HEAD commit SHA."""
1133
1216
  import git as _git
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepdoc
3
- Version: 1.9.3
3
+ Version: 2.0.0
4
4
  Summary: Auto-generate beautiful docs from any codebase
5
5
  Author: Pranav Kumar
6
6
  License: MIT
@@ -6,6 +6,7 @@ deepdoc/__main__.py
6
6
  deepdoc/_legacy_types.py
7
7
  deepdoc/benchmark_v2.py
8
8
  deepdoc/call_graph.py
9
+ deepdoc/changelog_writer.py
9
10
  deepdoc/cli.py
10
11
  deepdoc/config.py
11
12
  deepdoc/manifest.py
@@ -113,6 +114,7 @@ deepdoc/site/builder/scaffold_files.py
113
114
  deepdoc/site/builder/templates.py
114
115
  tests/test_benchmark_scorecard.py
115
116
  tests/test_call_graph.py
117
+ tests/test_changelog.py
116
118
  tests/test_chatbot_config.py
117
119
  tests/test_chatbot_embeddings.py
118
120
  tests/test_chatbot_eval.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepdoc"
7
- version = "1.9.3"
7
+ version = "2.0.0"
8
8
  description = "Auto-generate beautiful docs from any codebase"
9
9
  readme = "README.md"
10
10
  authors = [