sphinx-mkdocs-migrate 0.0.1.dev0__py3-none-any.whl
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.
- sphinx_mkdocs_migrate/__init__.py +8 -0
- sphinx_mkdocs_migrate/analyzer/__init__.py +17 -0
- sphinx_mkdocs_migrate/analyzer/ci.py +223 -0
- sphinx_mkdocs_migrate/analyzer/dependencies.py +134 -0
- sphinx_mkdocs_migrate/analyzer/markdown.py +148 -0
- sphinx_mkdocs_migrate/analyzer/mkdocs.py +263 -0
- sphinx_mkdocs_migrate/analyzer/models.py +348 -0
- sphinx_mkdocs_migrate/analyzer/navigation.py +108 -0
- sphinx_mkdocs_migrate/analyzer/project.py +511 -0
- sphinx_mkdocs_migrate/cli.py +507 -0
- sphinx_mkdocs_migrate/parsing/doc_ir.py +533 -0
- sphinx_mkdocs_migrate/parsing/flow_extractor.py +457 -0
- sphinx_mkdocs_migrate/parsing/html_flow_parser.py +349 -0
- sphinx_mkdocs_migrate/parsing/markdown.py +22 -0
- sphinx_mkdocs_migrate/parsing/markdown_ir.py +49 -0
- sphinx_mkdocs_migrate/parsing/markdown_it_adapter.py +496 -0
- sphinx_mkdocs_migrate/parsing/requirements.py +155 -0
- sphinx_mkdocs_migrate/planner/accountability.py +111 -0
- sphinx_mkdocs_migrate/planner/ci.py +142 -0
- sphinx_mkdocs_migrate/planner/conf_builder.py +183 -0
- sphinx_mkdocs_migrate/planner/models.py +379 -0
- sphinx_mkdocs_migrate/planner/planner.py +1867 -0
- sphinx_mkdocs_migrate/planner/policy.py +474 -0
- sphinx_mkdocs_migrate/planner/theme_constants.py +70 -0
- sphinx_mkdocs_migrate/planner/toctree.py +158 -0
- sphinx_mkdocs_migrate/py.typed +1 -0
- sphinx_mkdocs_migrate/rules/catalog.py +154 -0
- sphinx_mkdocs_migrate/rules/engine.py +94 -0
- sphinx_mkdocs_migrate/rules/models.py +176 -0
- sphinx_mkdocs_migrate/transformer/engine.py +897 -0
- sphinx_mkdocs_migrate/transformer/models.py +59 -0
- sphinx_mkdocs_migrate/transformer/myst_transformer.py +393 -0
- sphinx_mkdocs_migrate/validator/models.py +40 -0
- sphinx_mkdocs_migrate/validator/verifier.py +377 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/METADATA +199 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/RECORD +39 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/WHEEL +4 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1867 @@
|
|
|
1
|
+
"""Migration Planner synthesizing findings, declarative rules, policy engine, and provenance into a deterministic MigrationPlan."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import datetime
|
|
5
|
+
import html
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict, Any, Optional, Set
|
|
9
|
+
from ..analyzer.models import Classification
|
|
10
|
+
from .models import (
|
|
11
|
+
MigrationPlan,
|
|
12
|
+
MigrationPlanMetadata,
|
|
13
|
+
PlanActionSummary,
|
|
14
|
+
RequirementProvenance,
|
|
15
|
+
RequirementItem,
|
|
16
|
+
ThemeMigrationProposal,
|
|
17
|
+
ConfigMigrationProposal,
|
|
18
|
+
ManualReviewItem,
|
|
19
|
+
GeneratedDocumentProposal,
|
|
20
|
+
DocumentationPlan,
|
|
21
|
+
DocumentationArtifact,
|
|
22
|
+
DocumentFlowAction,
|
|
23
|
+
ArtifactProvenance,
|
|
24
|
+
ApiGenerationStrategy,
|
|
25
|
+
ApiDirectiveKind,
|
|
26
|
+
NavigationPlan,
|
|
27
|
+
GeneratedPipelinePlan,
|
|
28
|
+
CrossReferenceAction,
|
|
29
|
+
AssetAction,
|
|
30
|
+
ExternalInventoryConfig,
|
|
31
|
+
VersioningDeploymentPlan,
|
|
32
|
+
SystemCapabilityAccountability,
|
|
33
|
+
CapabilityDisposition,
|
|
34
|
+
ImplementationStrategy,
|
|
35
|
+
VerificationStatus,
|
|
36
|
+
)
|
|
37
|
+
from .conf_builder import build_conf_py
|
|
38
|
+
from .toctree import resolve_navigation_docnames
|
|
39
|
+
from .ci import plan_ci_workflow
|
|
40
|
+
from .policy import PolicyEngine
|
|
41
|
+
from ..analyzer.mkdocs import detect_obsolete_generator_scripts
|
|
42
|
+
from ..analyzer.project import ProjectAnalyzer
|
|
43
|
+
from ..rules.engine import MigrationRuleEngine
|
|
44
|
+
from ..rules.models import MigrationAction
|
|
45
|
+
from ..parsing.markdown import MarkdownParser
|
|
46
|
+
from ..parsing.flow_extractor import DocumentFlowExtractor
|
|
47
|
+
from ..parsing.html_flow_parser import HtmlFlowParser, HtmlFlowRole
|
|
48
|
+
from ..parsing.doc_ir import (
|
|
49
|
+
DocumentElementType,
|
|
50
|
+
HeadingElement,
|
|
51
|
+
ParagraphElement,
|
|
52
|
+
ListElement,
|
|
53
|
+
CodeBlockElement,
|
|
54
|
+
AdmonitionElement,
|
|
55
|
+
SnippetElement,
|
|
56
|
+
ApiDocumentationRequest,
|
|
57
|
+
DocumentationPage,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MigrationPlanner:
|
|
62
|
+
"""Orchestrates deterministic planning from ProjectAnalysisReport, FeaturePolicyCatalog, and MigrationRuleEngine."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, project_root: Path):
|
|
65
|
+
self.project_root = project_root
|
|
66
|
+
self.analyzer = ProjectAnalyzer(project_root)
|
|
67
|
+
self.parser = MarkdownParser()
|
|
68
|
+
self.rule_engine = MigrationRuleEngine()
|
|
69
|
+
self.policy_engine = PolicyEngine()
|
|
70
|
+
self.flow_extractor = DocumentFlowExtractor()
|
|
71
|
+
|
|
72
|
+
def create_plan(
|
|
73
|
+
self, deterministic_timestamp: Optional[str] = None
|
|
74
|
+
) -> MigrationPlan:
|
|
75
|
+
"""Generates a canonical, read-only MigrationPlan without mutating repository files."""
|
|
76
|
+
# 1. Analyze repository subsystems
|
|
77
|
+
report = self.analyzer.analyze()
|
|
78
|
+
|
|
79
|
+
# Pass MkDocs config to flow extractor if available
|
|
80
|
+
mkdocs_dict = {}
|
|
81
|
+
if report.mkdocs_config:
|
|
82
|
+
mkdocs_dict["markdown_extensions"] = [
|
|
83
|
+
ext if isinstance(ext, str) else getattr(ext, "name", str(ext))
|
|
84
|
+
for ext in (report.mkdocs_config.markdown_extensions or [])
|
|
85
|
+
]
|
|
86
|
+
self.flow_extractor.mkdocs_config = mkdocs_dict
|
|
87
|
+
|
|
88
|
+
# 2. Extract DocumentIRs, DocumentationPages, and evaluate declarative rules per file
|
|
89
|
+
all_actions: List[MigrationAction] = []
|
|
90
|
+
action_counter = 1
|
|
91
|
+
|
|
92
|
+
# Track detected extensions from markdown inspection
|
|
93
|
+
detected_markdown_extensions: Set[str] = set()
|
|
94
|
+
for c in report.construct_findings:
|
|
95
|
+
if c.construct_type == "tab_group":
|
|
96
|
+
detected_markdown_extensions.add("pymdownx.tabbed")
|
|
97
|
+
elif c.construct_type == "admonition":
|
|
98
|
+
detected_markdown_extensions.add("admonition")
|
|
99
|
+
elif c.construct_type == "details":
|
|
100
|
+
detected_markdown_extensions.add("pymdownx.details")
|
|
101
|
+
elif c.construct_type == "snippet":
|
|
102
|
+
detected_markdown_extensions.add("pymdownx.snippets")
|
|
103
|
+
elif c.construct_type == "mermaid":
|
|
104
|
+
detected_markdown_extensions.add("pymdownx.superfences")
|
|
105
|
+
|
|
106
|
+
docs_dir_name = (
|
|
107
|
+
report.mkdocs_config.docs_dir if report.mkdocs_config else "docs"
|
|
108
|
+
)
|
|
109
|
+
docs_dir = self.project_root / docs_dir_name
|
|
110
|
+
md_files = sorted(list(docs_dir.rglob("*.md"))) if docs_dir.exists() else []
|
|
111
|
+
|
|
112
|
+
# Check MkDocs mkdocstrings inherited_members configuration
|
|
113
|
+
has_inherited_members = False
|
|
114
|
+
global_inherited_members_list: Optional[List[str]] = None
|
|
115
|
+
if report.mkdocs_config:
|
|
116
|
+
mkdocstrings_cfg = report.mkdocs_config.plugins_config.get(
|
|
117
|
+
"mkdocstrings", {}
|
|
118
|
+
)
|
|
119
|
+
if isinstance(mkdocstrings_cfg, dict):
|
|
120
|
+
py_opts = (
|
|
121
|
+
mkdocstrings_cfg.get("handlers", {})
|
|
122
|
+
.get("python", {})
|
|
123
|
+
.get("options", {})
|
|
124
|
+
)
|
|
125
|
+
if not isinstance(py_opts, dict):
|
|
126
|
+
py_opts = mkdocstrings_cfg.get("options", {})
|
|
127
|
+
if isinstance(py_opts, dict) and "inherited_members" in py_opts:
|
|
128
|
+
val = py_opts["inherited_members"]
|
|
129
|
+
if isinstance(val, list):
|
|
130
|
+
has_inherited_members = True
|
|
131
|
+
global_inherited_members_list = [str(x) for x in val]
|
|
132
|
+
else:
|
|
133
|
+
has_inherited_members = bool(val)
|
|
134
|
+
|
|
135
|
+
# Statically analyze all project class hierarchies using AST to distinguish
|
|
136
|
+
# internal base classes (which should have members documented) from external/stdlib base classes.
|
|
137
|
+
src_paths: List[Path] = []
|
|
138
|
+
if report.mkdocs_config:
|
|
139
|
+
mkdocstrings_cfg = report.mkdocs_config.plugins_config.get(
|
|
140
|
+
"mkdocstrings", {}
|
|
141
|
+
)
|
|
142
|
+
if isinstance(mkdocstrings_cfg, dict):
|
|
143
|
+
py_paths = (
|
|
144
|
+
mkdocstrings_cfg.get("handlers", {})
|
|
145
|
+
.get("python", {})
|
|
146
|
+
.get("paths", [])
|
|
147
|
+
)
|
|
148
|
+
for p in py_paths:
|
|
149
|
+
candidate = self.project_root / p
|
|
150
|
+
if candidate.exists() and candidate.is_dir():
|
|
151
|
+
src_paths.append(candidate)
|
|
152
|
+
if not src_paths:
|
|
153
|
+
default_src = self.project_root / "src"
|
|
154
|
+
if default_src.exists() and default_src.is_dir():
|
|
155
|
+
src_paths.append(default_src)
|
|
156
|
+
else:
|
|
157
|
+
src_paths.append(self.project_root)
|
|
158
|
+
|
|
159
|
+
class_bases: Dict[str, List[str]] = {}
|
|
160
|
+
for src_dir in src_paths:
|
|
161
|
+
for py_f in src_dir.rglob("*.py"):
|
|
162
|
+
if any(
|
|
163
|
+
p.startswith(".") or p in ("venv", ".tox", "site-packages")
|
|
164
|
+
for p in py_f.parts
|
|
165
|
+
):
|
|
166
|
+
continue
|
|
167
|
+
try:
|
|
168
|
+
m_ast = ast.parse(
|
|
169
|
+
py_f.read_text(encoding="utf-8"), filename=str(py_f)
|
|
170
|
+
)
|
|
171
|
+
for n in m_ast.body:
|
|
172
|
+
if isinstance(n, ast.ClassDef) and not n.name.startswith("_"):
|
|
173
|
+
b_list = []
|
|
174
|
+
for b in n.bases:
|
|
175
|
+
if isinstance(b, ast.Name):
|
|
176
|
+
b_list.append(b.id)
|
|
177
|
+
elif isinstance(b, ast.Attribute):
|
|
178
|
+
b_list.append(b.attr)
|
|
179
|
+
class_bases[n.name] = b_list
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
project_classes = set(class_bases.keys())
|
|
184
|
+
|
|
185
|
+
def get_ext_bases(cls_name: str, visited: Set[str]) -> Set[str]:
|
|
186
|
+
if cls_name in visited:
|
|
187
|
+
return set()
|
|
188
|
+
visited.add(cls_name)
|
|
189
|
+
ext = set()
|
|
190
|
+
for base in class_bases.get(cls_name, []):
|
|
191
|
+
leaf = base.split(".")[-1]
|
|
192
|
+
if leaf in project_classes:
|
|
193
|
+
ext.update(get_ext_bases(leaf, visited))
|
|
194
|
+
else:
|
|
195
|
+
ext.add(leaf)
|
|
196
|
+
return ext
|
|
197
|
+
|
|
198
|
+
class_external_bases: Dict[str, Set[str]] = {
|
|
199
|
+
c: get_ext_bases(c, set()) for c in class_bases
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
planned_pages: List[DocumentationArtifact] = []
|
|
203
|
+
planned_api_strategies: List[ApiGenerationStrategy] = []
|
|
204
|
+
planned_cross_refs: List[CrossReferenceAction] = []
|
|
205
|
+
planned_assets: List[AssetAction] = []
|
|
206
|
+
site_pages: Dict[str, DocumentationPage] = {}
|
|
207
|
+
|
|
208
|
+
for md_file in md_files:
|
|
209
|
+
rel_file = md_file.relative_to(self.project_root).as_posix()
|
|
210
|
+
raw_text = md_file.read_text(encoding="utf-8")
|
|
211
|
+
raw_lines = raw_text.splitlines()
|
|
212
|
+
|
|
213
|
+
doc_ir = self.parser.parse_text(raw_text, file_path=rel_file)
|
|
214
|
+
file_actions = self.rule_engine.create_actions(doc_ir, raw_lines=raw_lines)
|
|
215
|
+
|
|
216
|
+
# Ensure stable, ordered action IDs
|
|
217
|
+
for act in file_actions:
|
|
218
|
+
act.action_id = f"act_{action_counter:04d}"
|
|
219
|
+
action_counter += 1
|
|
220
|
+
all_actions.append(act)
|
|
221
|
+
|
|
222
|
+
# Flow extraction for DocumentFlowSpec
|
|
223
|
+
page_ir = self.flow_extractor.extract_from_text(
|
|
224
|
+
raw_text, file_path=rel_file
|
|
225
|
+
)
|
|
226
|
+
site_pages[page_ir.logical_route] = page_ir
|
|
227
|
+
|
|
228
|
+
# Build planned flow actions preserving source order
|
|
229
|
+
flow_actions: List[DocumentFlowAction] = []
|
|
230
|
+
page_construct_ids: List[str] = []
|
|
231
|
+
for elem in page_ir.flow.elements:
|
|
232
|
+
page_construct_ids.append(elem.construct_id)
|
|
233
|
+
summary_text = ""
|
|
234
|
+
strategy_name = "PRESERVE"
|
|
235
|
+
target_dir = None
|
|
236
|
+
|
|
237
|
+
if elem.element_type == DocumentElementType.HEADING and isinstance(
|
|
238
|
+
elem.content, HeadingElement
|
|
239
|
+
):
|
|
240
|
+
summary_text = f"H{elem.content.level}: {elem.content.text}"
|
|
241
|
+
strategy_name = "TRANSFORM"
|
|
242
|
+
elif elem.element_type == DocumentElementType.PARAGRAPH and isinstance(
|
|
243
|
+
elem.content, ParagraphElement
|
|
244
|
+
):
|
|
245
|
+
summary_text = elem.content.text[:50]
|
|
246
|
+
strategy_name = "TRANSFORM"
|
|
247
|
+
elif (
|
|
248
|
+
elem.element_type == DocumentElementType.API_REQUEST
|
|
249
|
+
and isinstance(elem.content, ApiDocumentationRequest)
|
|
250
|
+
):
|
|
251
|
+
api_req = elem.content
|
|
252
|
+
summary_text = f"API Request: {api_req.object_path}"
|
|
253
|
+
strategy_name = "AUTODOC"
|
|
254
|
+
|
|
255
|
+
if api_req.object_kind.value == "module":
|
|
256
|
+
kind = ApiDirectiveKind.AUTOMODULE
|
|
257
|
+
directive_name = "automodule"
|
|
258
|
+
elif api_req.object_kind.value == "function":
|
|
259
|
+
kind = ApiDirectiveKind.AUTOFUNCTION
|
|
260
|
+
directive_name = "autofunction"
|
|
261
|
+
elif api_req.object_kind.value == "exception":
|
|
262
|
+
kind = ApiDirectiveKind.AUTOEXCEPTION
|
|
263
|
+
directive_name = "autoexception"
|
|
264
|
+
else:
|
|
265
|
+
kind = ApiDirectiveKind.AUTOCLASS
|
|
266
|
+
directive_name = "autoclass"
|
|
267
|
+
|
|
268
|
+
dir_lines = [f".. {directive_name}:: {api_req.object_path}"]
|
|
269
|
+
if (
|
|
270
|
+
api_req.member_selection.value == "EXPLICIT"
|
|
271
|
+
and api_req.explicit_members
|
|
272
|
+
):
|
|
273
|
+
dir_lines.append(
|
|
274
|
+
f" :members: {', '.join(api_req.explicit_members)}"
|
|
275
|
+
)
|
|
276
|
+
elif api_req.member_selection.value == "ALL_PUBLIC":
|
|
277
|
+
dir_lines.append(" :members:")
|
|
278
|
+
|
|
279
|
+
inh_prop_str = None
|
|
280
|
+
if api_req.object_kind.value in ("class", "exception"):
|
|
281
|
+
dir_lines.append(" :show-inheritance:")
|
|
282
|
+
raw_inh = api_req.normalized_options.get("inherited_members")
|
|
283
|
+
if raw_inh is None:
|
|
284
|
+
raw_inh = api_req.raw_options.get("inherited_members")
|
|
285
|
+
|
|
286
|
+
if raw_inh is not None:
|
|
287
|
+
should_inh = bool(raw_inh)
|
|
288
|
+
allowed_bases = (
|
|
289
|
+
raw_inh if isinstance(raw_inh, list) else None
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
should_inh = has_inherited_members
|
|
293
|
+
allowed_bases = global_inherited_members_list
|
|
294
|
+
|
|
295
|
+
if should_inh:
|
|
296
|
+
cls_leaf = api_req.object_path.split(".")[-1]
|
|
297
|
+
ext_bases = set(class_external_bases.get(cls_leaf, set()))
|
|
298
|
+
if allowed_bases:
|
|
299
|
+
all_superclasses = (
|
|
300
|
+
set(class_bases.get(cls_leaf, [])) | ext_bases
|
|
301
|
+
)
|
|
302
|
+
excluded = (
|
|
303
|
+
all_superclasses - set(allowed_bases)
|
|
304
|
+
) | ext_bases
|
|
305
|
+
if excluded:
|
|
306
|
+
inh_prop_str = ", ".join(sorted(excluded))
|
|
307
|
+
dir_lines.append(
|
|
308
|
+
f" :inherited-members: {inh_prop_str}"
|
|
309
|
+
)
|
|
310
|
+
else:
|
|
311
|
+
dir_lines.append(" :inherited-members:")
|
|
312
|
+
elif ext_bases:
|
|
313
|
+
inh_prop_str = ", ".join(sorted(ext_bases))
|
|
314
|
+
dir_lines.append(
|
|
315
|
+
f" :inherited-members: {inh_prop_str}"
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
dir_lines.append(" :inherited-members:")
|
|
319
|
+
|
|
320
|
+
target_dir = "\n".join(dir_lines)
|
|
321
|
+
|
|
322
|
+
api_strat = ApiGenerationStrategy(
|
|
323
|
+
object_path=api_req.object_path,
|
|
324
|
+
directive_kind=kind,
|
|
325
|
+
options=api_req.normalized_options,
|
|
326
|
+
members=api_req.explicit_members
|
|
327
|
+
if api_req.explicit_members
|
|
328
|
+
else None,
|
|
329
|
+
inherited_members=inh_prop_str,
|
|
330
|
+
source_construct_id=elem.construct_id,
|
|
331
|
+
rationale=f"Synthesized from source {api_req.handler} API request for {api_req.object_path}.",
|
|
332
|
+
)
|
|
333
|
+
planned_api_strategies.append(api_strat)
|
|
334
|
+
elif elem.element_type == DocumentElementType.LIST and isinstance(
|
|
335
|
+
elem.content, ListElement
|
|
336
|
+
):
|
|
337
|
+
summary_text = f"List ({len(elem.content.items)} items, {'ordered' if elem.content.ordered else 'unordered'})"
|
|
338
|
+
strategy_name = "PRESERVE"
|
|
339
|
+
elif elem.element_type == DocumentElementType.CODE_BLOCK and isinstance(
|
|
340
|
+
elem.content, CodeBlockElement
|
|
341
|
+
):
|
|
342
|
+
summary_text = f"Code block ({elem.content.language or 'text'})"
|
|
343
|
+
strategy_name = "TRANSFORM"
|
|
344
|
+
elif elem.element_type == DocumentElementType.ADMONITION and isinstance(
|
|
345
|
+
elem.content, AdmonitionElement
|
|
346
|
+
):
|
|
347
|
+
summary_text = f"Admonition ({elem.content.kind})"
|
|
348
|
+
strategy_name = "TRANSFORM"
|
|
349
|
+
elif elem.element_type == DocumentElementType.SNIPPET and isinstance(
|
|
350
|
+
elem.content, SnippetElement
|
|
351
|
+
):
|
|
352
|
+
summary_text = f"Snippet include: {elem.content.snippet_path}"
|
|
353
|
+
strategy_name = "TRANSFORM"
|
|
354
|
+
target_dir = f"```{{include}} {elem.content.snippet_path}\n```"
|
|
355
|
+
else:
|
|
356
|
+
summary_text = elem.element_type.value
|
|
357
|
+
strategy_name = "PRESERVE"
|
|
358
|
+
|
|
359
|
+
flow_actions.append(
|
|
360
|
+
DocumentFlowAction(
|
|
361
|
+
action_id=f"flow_{elem.construct_id}",
|
|
362
|
+
order_index=elem.source_order_index,
|
|
363
|
+
source_construct_id=elem.construct_id,
|
|
364
|
+
element_type=elem.element_type.value,
|
|
365
|
+
strategy=strategy_name,
|
|
366
|
+
content_summary=summary_text,
|
|
367
|
+
target_directive=target_dir,
|
|
368
|
+
rationale=f"Realize source element {elem.construct_id} in target document.",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Cross references and asset actions
|
|
373
|
+
for link in page_ir.outgoing_links:
|
|
374
|
+
planned_cross_refs.append(
|
|
375
|
+
CrossReferenceAction(
|
|
376
|
+
source_construct_id=link.source_construct_id,
|
|
377
|
+
source_file=rel_file,
|
|
378
|
+
source_target=link.target,
|
|
379
|
+
transformed_target=link.target.replace(".md", ".html")
|
|
380
|
+
if link.target.endswith(".md")
|
|
381
|
+
else link.target,
|
|
382
|
+
is_doc_ref=link.link_kind == "internal_page",
|
|
383
|
+
rationale="Transformed MkDocs relative doc link to Sphinx target reference.",
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for asset in page_ir.referenced_assets:
|
|
388
|
+
planned_assets.append(
|
|
389
|
+
AssetAction(
|
|
390
|
+
source_construct_id=asset.source_construct_id,
|
|
391
|
+
source_path=asset.source_path,
|
|
392
|
+
target_path=f"_static/{Path(asset.source_path).name}",
|
|
393
|
+
asset_kind=asset.asset_kind,
|
|
394
|
+
rationale="Static asset copy to Sphinx _static directory.",
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
planned_pages.append(
|
|
399
|
+
DocumentationArtifact(
|
|
400
|
+
artifact_id=f"art_{page_ir.logical_route.replace('/', '_') or 'index'}",
|
|
401
|
+
target_path=rel_file,
|
|
402
|
+
source_file=rel_file,
|
|
403
|
+
title=page_ir.title or Path(rel_file).stem,
|
|
404
|
+
artifact_kind="markdown_doc",
|
|
405
|
+
provenance=RequirementProvenance.DOCUMENT_CONSTRUCT,
|
|
406
|
+
rationale="Transformed source markdown document preserving ordered semantic flow.",
|
|
407
|
+
flow_actions=flow_actions,
|
|
408
|
+
artifact_provenance=ArtifactProvenance(
|
|
409
|
+
source_construct_ids=page_construct_ids,
|
|
410
|
+
source_files=[rel_file],
|
|
411
|
+
required_extensions=[],
|
|
412
|
+
required_packages=[],
|
|
413
|
+
),
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# 3. Categorize Summary & Collect Requirements with Source Tracking
|
|
418
|
+
summary = PlanActionSummary(total_actions=len(all_actions))
|
|
419
|
+
manual_items: List[ManualReviewItem] = []
|
|
420
|
+
unsupported_items: List[MigrationAction] = []
|
|
421
|
+
|
|
422
|
+
ext_sources: Dict[str, List[str]] = {}
|
|
423
|
+
pkg_sources: Dict[str, List[str]] = {}
|
|
424
|
+
provenance_map: Dict[str, RequirementProvenance] = {}
|
|
425
|
+
rationale_map: Dict[str, str] = {}
|
|
426
|
+
|
|
427
|
+
for action in all_actions:
|
|
428
|
+
loc_str = f"{action.source_file}:{action.start_line}"
|
|
429
|
+
if action.classification == Classification.TRANSFORM:
|
|
430
|
+
summary.transform_count += 1
|
|
431
|
+
elif action.classification == Classification.PRESERVE:
|
|
432
|
+
summary.preserve_count += 1
|
|
433
|
+
elif action.classification == Classification.MANUAL:
|
|
434
|
+
summary.manual_count += 1
|
|
435
|
+
manual_items.append(
|
|
436
|
+
ManualReviewItem(
|
|
437
|
+
item_id=f"manual_{action.action_id}",
|
|
438
|
+
source_file=action.source_file,
|
|
439
|
+
line_number=action.start_line,
|
|
440
|
+
construct_type=action.source_kind.value,
|
|
441
|
+
instruction=action.manual_instruction
|
|
442
|
+
or "Manual review required.",
|
|
443
|
+
rationale=action.description,
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
elif action.classification == Classification.UNSUPPORTED:
|
|
447
|
+
summary.unsupported_count += 1
|
|
448
|
+
unsupported_items.append(action)
|
|
449
|
+
|
|
450
|
+
# Map extensions & packages from construct actions
|
|
451
|
+
for ext in action.required_extensions:
|
|
452
|
+
ext_sources.setdefault(ext, []).append(loc_str)
|
|
453
|
+
provenance_map[ext] = RequirementProvenance.DOCUMENT_CONSTRUCT
|
|
454
|
+
rationale_map[ext] = (
|
|
455
|
+
"Required by document construct(s) in project Markdown files."
|
|
456
|
+
)
|
|
457
|
+
for pkg in action.required_packages:
|
|
458
|
+
pkg_sources.setdefault(pkg, []).append(loc_str)
|
|
459
|
+
provenance_map[pkg] = RequirementProvenance.DOCUMENT_CONSTRUCT
|
|
460
|
+
rationale_map[pkg] = (
|
|
461
|
+
"Required by document construct(s) in project Markdown files."
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# 4. Automatic Source Theme Identification & Sphinx Equivalent Mapping
|
|
465
|
+
source_theme = (
|
|
466
|
+
report.mkdocs_config.theme_name if report.mkdocs_config else "mkdocs"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Declarative theme translation matrix
|
|
470
|
+
THEME_EQUIVALENTS: Dict[str, Dict[str, Any]] = {
|
|
471
|
+
"material": {
|
|
472
|
+
"target_theme": "sphinx_immaterial",
|
|
473
|
+
"target_package": "sphinx-immaterial>=0.11.0",
|
|
474
|
+
"target_extension": "sphinx_immaterial",
|
|
475
|
+
"rationale": "Source uses Material for MkDocs; maps automatically to sphinx_immaterial for exact header, palette, and navigation chrome fidelity.",
|
|
476
|
+
},
|
|
477
|
+
"readthedocs": {
|
|
478
|
+
"target_theme": "sphinx_rtd_theme",
|
|
479
|
+
"target_package": "sphinx-rtd-theme>=2.0.0",
|
|
480
|
+
"target_extension": None,
|
|
481
|
+
"rationale": "Source uses ReadTheDocs theme; maps automatically to sphinx_rtd_theme.",
|
|
482
|
+
},
|
|
483
|
+
"mkdocs": {
|
|
484
|
+
"target_theme": "sphinx_rtd_theme",
|
|
485
|
+
"target_package": "sphinx-rtd-theme>=2.0.0",
|
|
486
|
+
"target_extension": None,
|
|
487
|
+
"rationale": "Source uses standard mkdocs theme; maps automatically to sphinx_rtd_theme.",
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
matched_spec: Optional[Dict[str, Any]] = None
|
|
492
|
+
for k, spec in THEME_EQUIVALENTS.items():
|
|
493
|
+
if k in source_theme.lower():
|
|
494
|
+
matched_spec = spec
|
|
495
|
+
break
|
|
496
|
+
|
|
497
|
+
if matched_spec:
|
|
498
|
+
theme_proposal = ThemeMigrationProposal(
|
|
499
|
+
source_theme=source_theme,
|
|
500
|
+
target_theme=matched_spec["target_theme"],
|
|
501
|
+
target_package=matched_spec["target_package"],
|
|
502
|
+
classification=Classification.TRANSFORM,
|
|
503
|
+
rationale=matched_spec["rationale"],
|
|
504
|
+
)
|
|
505
|
+
pkg_sources.setdefault(matched_spec["target_package"], []).append(
|
|
506
|
+
f"theme_policy:{source_theme}"
|
|
507
|
+
)
|
|
508
|
+
provenance_map[matched_spec["target_package"]] = (
|
|
509
|
+
RequirementProvenance.THEME_POLICY
|
|
510
|
+
)
|
|
511
|
+
rationale_map[matched_spec["target_package"]] = theme_proposal.rationale
|
|
512
|
+
|
|
513
|
+
if matched_spec.get("target_extension"):
|
|
514
|
+
ext_sources.setdefault(matched_spec["target_extension"], []).append(
|
|
515
|
+
f"theme_policy:{source_theme}"
|
|
516
|
+
)
|
|
517
|
+
provenance_map[matched_spec["target_extension"]] = (
|
|
518
|
+
RequirementProvenance.THEME_POLICY
|
|
519
|
+
)
|
|
520
|
+
rationale_map[matched_spec["target_extension"]] = (
|
|
521
|
+
theme_proposal.rationale
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
# Custom or unrecognized theme requires MANUAL decision
|
|
525
|
+
theme_proposal = ThemeMigrationProposal(
|
|
526
|
+
source_theme=source_theme,
|
|
527
|
+
target_theme=None,
|
|
528
|
+
target_package=None,
|
|
529
|
+
classification=Classification.MANUAL,
|
|
530
|
+
rationale=f"Custom/unrecognized theme '{source_theme}' has no automated Sphinx equivalent. Manual theme selection required.",
|
|
531
|
+
)
|
|
532
|
+
manual_items.append(
|
|
533
|
+
ManualReviewItem(
|
|
534
|
+
item_id="manual_theme_selection",
|
|
535
|
+
source_file="mkdocs.yml",
|
|
536
|
+
line_number=1,
|
|
537
|
+
construct_type="theme",
|
|
538
|
+
instruction=f"Select an appropriate Sphinx theme (e.g. Furo, PyData Sphinx Theme, or sphinx-rtd-theme) for '{source_theme}'.",
|
|
539
|
+
rationale=theme_proposal.rationale,
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# 5. Declarative Policy Evaluation for Theme Features, Extensions, and Plugins
|
|
544
|
+
myst_enabled: Set[str] = {"colon_fence"}
|
|
545
|
+
custom_conf_options: Dict[str, Any] = {}
|
|
546
|
+
|
|
547
|
+
if report.mkdocs_config:
|
|
548
|
+
# 5a. Theme Features
|
|
549
|
+
for feat in report.mkdocs_config.theme_features or []:
|
|
550
|
+
rule = self.policy_engine.evaluate_feature(feat)
|
|
551
|
+
if rule:
|
|
552
|
+
for ext in rule.required_extensions:
|
|
553
|
+
ext_sources.setdefault(ext, []).append(f"theme_feature:{feat}")
|
|
554
|
+
provenance_map[ext] = RequirementProvenance.FEATURE_POLICY
|
|
555
|
+
rationale_map[ext] = rule.rationale
|
|
556
|
+
for pkg in rule.required_packages:
|
|
557
|
+
pkg_sources.setdefault(pkg, []).append(f"theme_feature:{feat}")
|
|
558
|
+
provenance_map[pkg] = RequirementProvenance.FEATURE_POLICY
|
|
559
|
+
rationale_map[pkg] = rule.rationale
|
|
560
|
+
for m_ext in rule.myst_extensions:
|
|
561
|
+
myst_enabled.add(m_ext)
|
|
562
|
+
if rule.conf_settings:
|
|
563
|
+
custom_conf_options.update(rule.conf_settings)
|
|
564
|
+
if rule.classification == Classification.MANUAL:
|
|
565
|
+
manual_items.append(
|
|
566
|
+
ManualReviewItem(
|
|
567
|
+
item_id=f"manual_feat_{feat.replace('.', '_')}",
|
|
568
|
+
source_file="mkdocs.yml",
|
|
569
|
+
line_number=1,
|
|
570
|
+
construct_type="theme_feature",
|
|
571
|
+
instruction=rule.manual_instruction
|
|
572
|
+
or "Manual review required.",
|
|
573
|
+
rationale=rule.rationale,
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# 5b. Markdown Extensions
|
|
578
|
+
for ext_entry in report.mkdocs_config.markdown_extensions or []:
|
|
579
|
+
ext_name = (
|
|
580
|
+
ext_entry
|
|
581
|
+
if isinstance(ext_entry, str)
|
|
582
|
+
else getattr(ext_entry, "name", str(ext_entry))
|
|
583
|
+
)
|
|
584
|
+
rule = self.policy_engine.evaluate_feature(ext_name)
|
|
585
|
+
if rule:
|
|
586
|
+
for ext in rule.required_extensions:
|
|
587
|
+
ext_sources.setdefault(ext, []).append(
|
|
588
|
+
f"markdown_extension:{ext_name}"
|
|
589
|
+
)
|
|
590
|
+
provenance_map.setdefault(
|
|
591
|
+
ext, RequirementProvenance.EXTENSION_POLICY
|
|
592
|
+
)
|
|
593
|
+
rationale_map.setdefault(ext, rule.rationale)
|
|
594
|
+
for pkg in rule.required_packages:
|
|
595
|
+
pkg_sources.setdefault(pkg, []).append(
|
|
596
|
+
f"markdown_extension:{ext_name}"
|
|
597
|
+
)
|
|
598
|
+
provenance_map.setdefault(
|
|
599
|
+
pkg, RequirementProvenance.EXTENSION_POLICY
|
|
600
|
+
)
|
|
601
|
+
rationale_map.setdefault(pkg, rule.rationale)
|
|
602
|
+
for m_ext in rule.myst_extensions:
|
|
603
|
+
myst_enabled.add(m_ext)
|
|
604
|
+
if rule.conf_settings:
|
|
605
|
+
custom_conf_options.update(rule.conf_settings)
|
|
606
|
+
if rule.classification == Classification.MANUAL:
|
|
607
|
+
manual_items.append(
|
|
608
|
+
ManualReviewItem(
|
|
609
|
+
item_id=f"manual_ext_{ext_name.replace('.', '_')}",
|
|
610
|
+
source_file="mkdocs.yml",
|
|
611
|
+
line_number=1,
|
|
612
|
+
construct_type="markdown_extension",
|
|
613
|
+
instruction=rule.manual_instruction
|
|
614
|
+
or "Manual review required.",
|
|
615
|
+
rationale=rule.rationale,
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# 5c. Plugins
|
|
620
|
+
for plg_entry in report.mkdocs_config.plugins or []:
|
|
621
|
+
plg_name = (
|
|
622
|
+
plg_entry
|
|
623
|
+
if isinstance(plg_entry, str)
|
|
624
|
+
else getattr(plg_entry, "name", str(plg_entry))
|
|
625
|
+
)
|
|
626
|
+
rule = self.policy_engine.evaluate_feature(plg_name)
|
|
627
|
+
if rule:
|
|
628
|
+
for ext in rule.required_extensions:
|
|
629
|
+
ext_sources.setdefault(ext, []).append(f"plugin:{plg_name}")
|
|
630
|
+
provenance_map.setdefault(
|
|
631
|
+
ext, RequirementProvenance.EXTENSION_POLICY
|
|
632
|
+
)
|
|
633
|
+
rationale_map.setdefault(ext, rule.rationale)
|
|
634
|
+
for pkg in rule.required_packages:
|
|
635
|
+
pkg_sources.setdefault(pkg, []).append(f"plugin:{plg_name}")
|
|
636
|
+
provenance_map.setdefault(
|
|
637
|
+
pkg, RequirementProvenance.EXTENSION_POLICY
|
|
638
|
+
)
|
|
639
|
+
rationale_map.setdefault(pkg, rule.rationale)
|
|
640
|
+
for m_ext in rule.myst_extensions:
|
|
641
|
+
myst_enabled.add(m_ext)
|
|
642
|
+
if rule.conf_settings:
|
|
643
|
+
custom_conf_options.update(rule.conf_settings)
|
|
644
|
+
if rule.classification == Classification.MANUAL:
|
|
645
|
+
manual_items.append(
|
|
646
|
+
ManualReviewItem(
|
|
647
|
+
item_id=f"manual_plugin_{plg_name.replace('.', '_')}",
|
|
648
|
+
source_file="mkdocs.yml",
|
|
649
|
+
line_number=1,
|
|
650
|
+
construct_type="plugin",
|
|
651
|
+
instruction=rule.manual_instruction
|
|
652
|
+
or "Manual review required.",
|
|
653
|
+
rationale=rule.rationale,
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Check in-document detected extensions
|
|
658
|
+
if "tasklist" in detected_markdown_extensions:
|
|
659
|
+
myst_enabled.add("tasklist")
|
|
660
|
+
if "attrs_block" in detected_markdown_extensions:
|
|
661
|
+
myst_enabled.add("attrs_block")
|
|
662
|
+
|
|
663
|
+
# 6. Generated Documentation Pipeline Planning (e.g. gen-files, mkdocstrings, literate-nav)
|
|
664
|
+
# In MkDocs, dynamic generator scripts (e.g. `scripts/gen_ref_nav.py`) run at build time
|
|
665
|
+
# to construct in-memory virtual markdown documents (`mkdocs_gen_files.open(...)`).
|
|
666
|
+
# Sphinx builds from permanent filesystem files. We statically inspect the generator
|
|
667
|
+
# scripts via AST to deduce the source directories, output prefix, and literate nav files,
|
|
668
|
+
# then synthesize concrete MyST reference stubs directly into the documentation tree.
|
|
669
|
+
# This replaces the need for build-time Python generation and allows the obsolete script
|
|
670
|
+
# to be deleted during post-migration cleanup.
|
|
671
|
+
generated_doc_proposals: List[GeneratedDocumentProposal] = []
|
|
672
|
+
api_reference_mappings: Dict[str, str] = {}
|
|
673
|
+
if report.mkdocs_config and "gen-files" in report.mkdocs_config.plugins:
|
|
674
|
+
gen_cfg = report.mkdocs_config.plugins_config.get("gen-files", {})
|
|
675
|
+
scripts = gen_cfg.get("scripts", [])
|
|
676
|
+
|
|
677
|
+
# Statically analyze declared generator scripts (e.g. scripts/gen_ref_nav.py)
|
|
678
|
+
script_meta: Dict[str, Any] = {
|
|
679
|
+
"source_dir": "src",
|
|
680
|
+
"output_dir": "reference",
|
|
681
|
+
"literate_nav": None,
|
|
682
|
+
"pattern": "::: {ident}",
|
|
683
|
+
}
|
|
684
|
+
for s_path_str in scripts:
|
|
685
|
+
s_file = self.project_root / s_path_str
|
|
686
|
+
if s_file.exists():
|
|
687
|
+
try:
|
|
688
|
+
s_tree = ast.parse(
|
|
689
|
+
s_file.read_text(encoding="utf-8"), filename=str(s_file)
|
|
690
|
+
)
|
|
691
|
+
for node in ast.walk(s_tree):
|
|
692
|
+
if isinstance(node, ast.Call):
|
|
693
|
+
if (
|
|
694
|
+
isinstance(node.func, ast.Name)
|
|
695
|
+
and node.func.id == "Path"
|
|
696
|
+
):
|
|
697
|
+
for arg in node.args:
|
|
698
|
+
if isinstance(arg, ast.Constant) and isinstance(
|
|
699
|
+
arg.value, str
|
|
700
|
+
):
|
|
701
|
+
if (self.project_root / arg.value).is_dir():
|
|
702
|
+
script_meta["source_dir"] = arg.value
|
|
703
|
+
elif arg.value in (
|
|
704
|
+
"reference",
|
|
705
|
+
"api",
|
|
706
|
+
"docs",
|
|
707
|
+
):
|
|
708
|
+
script_meta["output_dir"] = arg.value
|
|
709
|
+
elif (
|
|
710
|
+
isinstance(node.func, ast.Attribute)
|
|
711
|
+
and node.func.attr == "open"
|
|
712
|
+
):
|
|
713
|
+
for arg in node.args:
|
|
714
|
+
if isinstance(arg, ast.Constant) and isinstance(
|
|
715
|
+
arg.value, str
|
|
716
|
+
):
|
|
717
|
+
if "SUMMARY" in arg.value:
|
|
718
|
+
script_meta["literate_nav"] = arg.value
|
|
719
|
+
elif "/" in arg.value:
|
|
720
|
+
script_meta["output_dir"] = (
|
|
721
|
+
arg.value.split("/")[0]
|
|
722
|
+
)
|
|
723
|
+
except Exception:
|
|
724
|
+
pass
|
|
725
|
+
|
|
726
|
+
output_prefix = script_meta.get("output_dir", "reference")
|
|
727
|
+
|
|
728
|
+
# Discover Python packages & modules to materialize deterministic reference documentation
|
|
729
|
+
package_modules: Dict[str, List[str]] = {}
|
|
730
|
+
module_members: Dict[str, Dict[str, List[str]]] = {}
|
|
731
|
+
for src_dir in src_paths:
|
|
732
|
+
py_files = sorted(list(src_dir.rglob("*.py")))
|
|
733
|
+
for py_f in py_files:
|
|
734
|
+
rel_mod = py_f.relative_to(src_dir).with_suffix("")
|
|
735
|
+
parts = list(rel_mod.parts)
|
|
736
|
+
|
|
737
|
+
is_pkg_index = False
|
|
738
|
+
if parts[-1] == "__init__":
|
|
739
|
+
parts = parts[:-1]
|
|
740
|
+
is_pkg_index = True
|
|
741
|
+
elif parts[-1].startswith("_"):
|
|
742
|
+
continue
|
|
743
|
+
|
|
744
|
+
if not parts:
|
|
745
|
+
continue
|
|
746
|
+
|
|
747
|
+
# Inspect source statically; never import the project being
|
|
748
|
+
# migrated merely to create an API index.
|
|
749
|
+
members: Dict[str, List[str]] = {
|
|
750
|
+
"classes": [],
|
|
751
|
+
"functions": [],
|
|
752
|
+
"attributes": [],
|
|
753
|
+
}
|
|
754
|
+
module_docstring = None
|
|
755
|
+
try:
|
|
756
|
+
module_ast = ast.parse(
|
|
757
|
+
py_f.read_text(encoding="utf-8"), filename=str(py_f)
|
|
758
|
+
)
|
|
759
|
+
module_docstring = ast.get_docstring(module_ast)
|
|
760
|
+
for node in module_ast.body:
|
|
761
|
+
if isinstance(
|
|
762
|
+
node, ast.ClassDef
|
|
763
|
+
) and not node.name.startswith("_"):
|
|
764
|
+
members["classes"].append(node.name)
|
|
765
|
+
elif isinstance(
|
|
766
|
+
node, (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
767
|
+
) and not node.name.startswith("_"):
|
|
768
|
+
members["functions"].append(node.name)
|
|
769
|
+
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
770
|
+
targets = (
|
|
771
|
+
node.targets
|
|
772
|
+
if isinstance(node, ast.Assign)
|
|
773
|
+
else [node.target]
|
|
774
|
+
)
|
|
775
|
+
for target in targets:
|
|
776
|
+
if isinstance(
|
|
777
|
+
target, ast.Name
|
|
778
|
+
) and not target.id.startswith("_"):
|
|
779
|
+
members["attributes"].append(target.id)
|
|
780
|
+
except (OSError, SyntaxError, UnicodeDecodeError):
|
|
781
|
+
pass
|
|
782
|
+
|
|
783
|
+
pkg_key = (
|
|
784
|
+
"/".join(parts[:-1]) if not is_pkg_index else "/".join(parts)
|
|
785
|
+
)
|
|
786
|
+
if not is_pkg_index:
|
|
787
|
+
package_modules.setdefault(pkg_key, []).append(parts[-1])
|
|
788
|
+
|
|
789
|
+
module_qualname = ".".join(parts)
|
|
790
|
+
module_members[module_qualname] = members
|
|
791
|
+
if is_pkg_index:
|
|
792
|
+
target_rel = f"{docs_dir_name}/{output_prefix}/{'/'.join(parts)}/index.md"
|
|
793
|
+
else:
|
|
794
|
+
target_rel = (
|
|
795
|
+
f"{docs_dir_name}/{output_prefix}/{'/'.join(parts)}.md"
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
# Generate summary tables from the same names that will be
|
|
799
|
+
# rendered by autodoc. ``autosummary_generate`` remains
|
|
800
|
+
# disabled because these migration proposals already own
|
|
801
|
+
# the destination pages and their local navigation.
|
|
802
|
+
doc_title = parts[-1]
|
|
803
|
+
flow_actions = []
|
|
804
|
+
order_idx = 0
|
|
805
|
+
|
|
806
|
+
# Check if rendered HTML from mkdocs build exists
|
|
807
|
+
site_dir_cand = self.project_root / "site"
|
|
808
|
+
cand_html_paths = [
|
|
809
|
+
site_dir_cand / "reference" / ("/".join(parts)) / "index.html",
|
|
810
|
+
site_dir_cand / "reference" / f"{'/'.join(parts)}.html",
|
|
811
|
+
]
|
|
812
|
+
|
|
813
|
+
found_html = None
|
|
814
|
+
for cand in cand_html_paths:
|
|
815
|
+
if cand.exists():
|
|
816
|
+
found_html = cand
|
|
817
|
+
break
|
|
818
|
+
|
|
819
|
+
if found_html:
|
|
820
|
+
html_parser = HtmlFlowParser()
|
|
821
|
+
html_flow = html_parser.parse_file(
|
|
822
|
+
found_html, rel_route=target_rel
|
|
823
|
+
)
|
|
824
|
+
if html_flow.title:
|
|
825
|
+
doc_title = html_flow.title
|
|
826
|
+
|
|
827
|
+
summary_elems = [
|
|
828
|
+
el
|
|
829
|
+
for el in html_flow.elements
|
|
830
|
+
if el.role == HtmlFlowRole.AUTOSUMMARY
|
|
831
|
+
]
|
|
832
|
+
api_member_elems = [
|
|
833
|
+
el
|
|
834
|
+
for el in html_flow.elements
|
|
835
|
+
if el.role
|
|
836
|
+
in (
|
|
837
|
+
HtmlFlowRole.API_CLASS,
|
|
838
|
+
HtmlFlowRole.API_FUNCTION,
|
|
839
|
+
HtmlFlowRole.API_ATTRIBUTE,
|
|
840
|
+
)
|
|
841
|
+
]
|
|
842
|
+
mod_elems = [
|
|
843
|
+
el
|
|
844
|
+
for el in html_flow.elements
|
|
845
|
+
if el.role == HtmlFlowRole.API_MODULE
|
|
846
|
+
]
|
|
847
|
+
|
|
848
|
+
flow_actions.append(
|
|
849
|
+
DocumentFlowAction(
|
|
850
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
851
|
+
order_index=order_idx,
|
|
852
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
853
|
+
element_type="HEADING",
|
|
854
|
+
strategy="PRESERVE",
|
|
855
|
+
content_summary=doc_title,
|
|
856
|
+
rationale="API reference document heading.",
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
order_idx += 1
|
|
860
|
+
|
|
861
|
+
if is_pkg_index:
|
|
862
|
+
lines = [f"# {doc_title}\n"]
|
|
863
|
+
if summary_elems:
|
|
864
|
+
lines.append(
|
|
865
|
+
f"```{{eval-rst}}\n.. currentmodule:: {module_qualname}\n\n.. autosummary::\n :nosignatures:\n\n"
|
|
866
|
+
)
|
|
867
|
+
for sm in summary_elems:
|
|
868
|
+
flow_actions.append(
|
|
869
|
+
DocumentFlowAction(
|
|
870
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
871
|
+
order_index=order_idx,
|
|
872
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
873
|
+
element_type="AUTOSUMMARY",
|
|
874
|
+
strategy="AUTOSUMMARY",
|
|
875
|
+
content_summary=(
|
|
876
|
+
", ".join(sm.symbols)
|
|
877
|
+
if sm.symbols
|
|
878
|
+
else ""
|
|
879
|
+
)[:50],
|
|
880
|
+
target_directive="autosummary",
|
|
881
|
+
rationale="Package index module summary table.",
|
|
882
|
+
)
|
|
883
|
+
)
|
|
884
|
+
order_idx += 1
|
|
885
|
+
for s in sm.symbols:
|
|
886
|
+
lines.append(f" {s}\n")
|
|
887
|
+
lines.append("```\n")
|
|
888
|
+
doc_content = "\n".join(lines).rstrip() + "\n"
|
|
889
|
+
else:
|
|
890
|
+
blocks: list[str] = [f"# {doc_title}\n"]
|
|
891
|
+
|
|
892
|
+
# Block 1: automodule with :no-members: to render module docstring & register module
|
|
893
|
+
blocks.append(
|
|
894
|
+
f"```{{eval-rst}}\n.. automodule:: {module_qualname}\n :no-members:\n```\n"
|
|
895
|
+
)
|
|
896
|
+
flow_actions.append(
|
|
897
|
+
DocumentFlowAction(
|
|
898
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
899
|
+
order_index=order_idx,
|
|
900
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
901
|
+
element_type="API_MODULE",
|
|
902
|
+
strategy="AUTODOC",
|
|
903
|
+
content_summary=f"automodule::{module_qualname}",
|
|
904
|
+
target_directive=f".. automodule:: {module_qualname}",
|
|
905
|
+
rationale="Module registration and docstring presentation without dumping members.",
|
|
906
|
+
)
|
|
907
|
+
)
|
|
908
|
+
order_idx += 1
|
|
909
|
+
|
|
910
|
+
# Block 2: Autosummary tables grouped under currentmodule with rubrics and ~Symbol
|
|
911
|
+
if summary_elems:
|
|
912
|
+
summary_lines = [
|
|
913
|
+
f"```{{eval-rst}}\n.. currentmodule:: {module_qualname}"
|
|
914
|
+
]
|
|
915
|
+
for sm in summary_elems:
|
|
916
|
+
rubric = None
|
|
917
|
+
if sm.headers:
|
|
918
|
+
h = sm.headers[0].upper()
|
|
919
|
+
if "CLASS" in h:
|
|
920
|
+
rubric = "Classes"
|
|
921
|
+
elif "FUNC" in h:
|
|
922
|
+
rubric = "Functions"
|
|
923
|
+
elif "ATTR" in h:
|
|
924
|
+
rubric = "Attributes"
|
|
925
|
+
elif "EXCEPT" in h:
|
|
926
|
+
rubric = "Exceptions"
|
|
927
|
+
if rubric:
|
|
928
|
+
summary_lines.append(f"\n.. rubric:: {rubric}")
|
|
929
|
+
nosig = (
|
|
930
|
+
"\n :nosignatures:"
|
|
931
|
+
if sm.options.get("nosignatures", True)
|
|
932
|
+
else ""
|
|
933
|
+
)
|
|
934
|
+
summary_lines.append(f"\n.. autosummary::{nosig}\n")
|
|
935
|
+
for s in sm.symbols:
|
|
936
|
+
s_clean = s.split("(")[0].strip()
|
|
937
|
+
s_fmt = (
|
|
938
|
+
s_clean
|
|
939
|
+
if s_clean.startswith("~")
|
|
940
|
+
else f"~{s_clean}"
|
|
941
|
+
)
|
|
942
|
+
summary_lines.append(f" {s_fmt}")
|
|
943
|
+
flow_actions.append(
|
|
944
|
+
DocumentFlowAction(
|
|
945
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
946
|
+
order_index=order_idx,
|
|
947
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
948
|
+
element_type="AUTOSUMMARY",
|
|
949
|
+
strategy="AUTOSUMMARY",
|
|
950
|
+
content_summary=(
|
|
951
|
+
", ".join(sm.symbols)
|
|
952
|
+
if sm.symbols
|
|
953
|
+
else ""
|
|
954
|
+
)[:50],
|
|
955
|
+
target_directive="autosummary",
|
|
956
|
+
rationale="Summary table mapped to Sphinx autosummary.",
|
|
957
|
+
)
|
|
958
|
+
)
|
|
959
|
+
order_idx += 1
|
|
960
|
+
summary_lines.append("```\n")
|
|
961
|
+
blocks.append("\n".join(summary_lines))
|
|
962
|
+
|
|
963
|
+
# Block 3: Individual member directives in exact parsed DOM sequence
|
|
964
|
+
if api_member_elems:
|
|
965
|
+
member_lines = [
|
|
966
|
+
f"```{{eval-rst}}\n.. currentmodule:: {module_qualname}"
|
|
967
|
+
]
|
|
968
|
+
for el in api_member_elems:
|
|
969
|
+
target_name = (
|
|
970
|
+
el.qname.split(".")[-1]
|
|
971
|
+
if el.qname
|
|
972
|
+
else module_qualname
|
|
973
|
+
)
|
|
974
|
+
if el.role == HtmlFlowRole.API_ATTRIBUTE:
|
|
975
|
+
directive = el.directive or "autodata"
|
|
976
|
+
member_lines.append(
|
|
977
|
+
f"\n.. {directive}:: {target_name}"
|
|
978
|
+
)
|
|
979
|
+
elif el.role == HtmlFlowRole.API_CLASS:
|
|
980
|
+
raw_inh = el.options.get("inherited_members")
|
|
981
|
+
if raw_inh is None:
|
|
982
|
+
raw_inh = el.options.get(
|
|
983
|
+
"inherited-members"
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
if raw_inh is not None:
|
|
987
|
+
should_inh = bool(raw_inh)
|
|
988
|
+
allowed_bases = (
|
|
989
|
+
raw_inh
|
|
990
|
+
if isinstance(raw_inh, list)
|
|
991
|
+
else None
|
|
992
|
+
)
|
|
993
|
+
else:
|
|
994
|
+
should_inh = has_inherited_members
|
|
995
|
+
allowed_bases = (
|
|
996
|
+
global_inherited_members_list
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
if should_inh:
|
|
1000
|
+
ext_bases = set(
|
|
1001
|
+
class_external_bases.get(
|
|
1002
|
+
target_name, set()
|
|
1003
|
+
)
|
|
1004
|
+
)
|
|
1005
|
+
if allowed_bases:
|
|
1006
|
+
all_superclasses = (
|
|
1007
|
+
set(
|
|
1008
|
+
class_bases.get(target_name, [])
|
|
1009
|
+
)
|
|
1010
|
+
| ext_bases
|
|
1011
|
+
)
|
|
1012
|
+
excluded = (
|
|
1013
|
+
all_superclasses
|
|
1014
|
+
- set(allowed_bases)
|
|
1015
|
+
) | ext_bases
|
|
1016
|
+
if excluded:
|
|
1017
|
+
inh_str = f"\n :inherited-members: {', '.join(sorted(excluded))}"
|
|
1018
|
+
else:
|
|
1019
|
+
inh_str = "\n :inherited-members:"
|
|
1020
|
+
elif ext_bases:
|
|
1021
|
+
inh_str = f"\n :inherited-members: {', '.join(sorted(ext_bases))}"
|
|
1022
|
+
else:
|
|
1023
|
+
inh_str = "\n :inherited-members:"
|
|
1024
|
+
else:
|
|
1025
|
+
inh_str = ""
|
|
1026
|
+
member_lines.append(
|
|
1027
|
+
f"\n.. autoclass:: {target_name}\n :members:\n :undoc-members:\n :show-inheritance:{inh_str}"
|
|
1028
|
+
)
|
|
1029
|
+
elif el.role == HtmlFlowRole.API_FUNCTION:
|
|
1030
|
+
directive = el.directive or "autofunction"
|
|
1031
|
+
member_lines.append(
|
|
1032
|
+
f"\n.. {directive}:: {target_name}"
|
|
1033
|
+
)
|
|
1034
|
+
flow_actions.append(
|
|
1035
|
+
DocumentFlowAction(
|
|
1036
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
1037
|
+
order_index=order_idx,
|
|
1038
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
1039
|
+
element_type=el.role.value,
|
|
1040
|
+
strategy="AUTODOC",
|
|
1041
|
+
content_summary=(el.qname or "")[:50],
|
|
1042
|
+
target_directive=el.directive,
|
|
1043
|
+
rationale=f"Ordered API element {el.role.value} mapped to Sphinx {el.directive}.",
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
order_idx += 1
|
|
1047
|
+
member_lines.append("```\n")
|
|
1048
|
+
blocks.append("\n".join(member_lines))
|
|
1049
|
+
elif not summary_elems and mod_elems:
|
|
1050
|
+
# Stub or memberless module
|
|
1051
|
+
pass
|
|
1052
|
+
|
|
1053
|
+
doc_content = "\n".join(blocks).rstrip() + "\n"
|
|
1054
|
+
else:
|
|
1055
|
+
# Deterministic fallback to Python AST analysis
|
|
1056
|
+
flow_actions.append(
|
|
1057
|
+
DocumentFlowAction(
|
|
1058
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
1059
|
+
order_index=order_idx,
|
|
1060
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
1061
|
+
element_type="HEADING",
|
|
1062
|
+
strategy="PRESERVE",
|
|
1063
|
+
content_summary=doc_title,
|
|
1064
|
+
rationale="API reference document heading.",
|
|
1065
|
+
)
|
|
1066
|
+
)
|
|
1067
|
+
order_idx += 1
|
|
1068
|
+
|
|
1069
|
+
if is_pkg_index:
|
|
1070
|
+
doc_content = f"# {doc_title}\n\n"
|
|
1071
|
+
if module_docstring:
|
|
1072
|
+
for p_txt in module_docstring.strip().split("\n\n"):
|
|
1073
|
+
p_clean = p_txt.strip()
|
|
1074
|
+
if p_clean:
|
|
1075
|
+
flow_actions.append(
|
|
1076
|
+
DocumentFlowAction(
|
|
1077
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
1078
|
+
order_index=order_idx,
|
|
1079
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
1080
|
+
element_type="PARAGRAPH",
|
|
1081
|
+
strategy="PRESERVE",
|
|
1082
|
+
content_summary=p_clean[:50],
|
|
1083
|
+
rationale="Module docstring overview prose.",
|
|
1084
|
+
)
|
|
1085
|
+
)
|
|
1086
|
+
order_idx += 1
|
|
1087
|
+
doc_content += f"{module_docstring.strip()}\n\n"
|
|
1088
|
+
doc_content += f"```{{eval-rst}}\n.. currentmodule:: {module_qualname}\n\n.. autosummary::\n :nosignatures:\n\n"
|
|
1089
|
+
flow_actions.append(
|
|
1090
|
+
DocumentFlowAction(
|
|
1091
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
1092
|
+
order_index=order_idx,
|
|
1093
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
1094
|
+
element_type="AUTOSUMMARY",
|
|
1095
|
+
strategy="AUTOSUMMARY",
|
|
1096
|
+
content_summary="Package index summary table",
|
|
1097
|
+
target_directive="autosummary",
|
|
1098
|
+
rationale="Summary table for package index.",
|
|
1099
|
+
)
|
|
1100
|
+
)
|
|
1101
|
+
order_idx += 1
|
|
1102
|
+
else:
|
|
1103
|
+
doc_content = f"# {doc_title}\n\n"
|
|
1104
|
+
if module_docstring:
|
|
1105
|
+
for p_txt in module_docstring.strip().split("\n\n"):
|
|
1106
|
+
p_clean = p_txt.strip()
|
|
1107
|
+
if p_clean:
|
|
1108
|
+
flow_actions.append(
|
|
1109
|
+
DocumentFlowAction(
|
|
1110
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
1111
|
+
order_index=order_idx,
|
|
1112
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
1113
|
+
element_type="PARAGRAPH",
|
|
1114
|
+
strategy="PRESERVE",
|
|
1115
|
+
content_summary=p_clean[:50],
|
|
1116
|
+
rationale="Module docstring overview prose.",
|
|
1117
|
+
)
|
|
1118
|
+
)
|
|
1119
|
+
order_idx += 1
|
|
1120
|
+
doc_content += f"{module_docstring.strip()}\n\n"
|
|
1121
|
+
doc_content += f"```{{eval-rst}}\n.. currentmodule:: {module_qualname}\n"
|
|
1122
|
+
for heading, key in (
|
|
1123
|
+
("Classes", "classes"),
|
|
1124
|
+
("Functions", "functions"),
|
|
1125
|
+
("Attributes", "attributes"),
|
|
1126
|
+
):
|
|
1127
|
+
if members[key]:
|
|
1128
|
+
doc_content += f"\n.. rubric:: {heading}\n\n.. autosummary::\n :nosignatures:\n\n"
|
|
1129
|
+
doc_content += "".join(
|
|
1130
|
+
f" {name}\n" for name in members[key]
|
|
1131
|
+
)
|
|
1132
|
+
flow_actions.append(
|
|
1133
|
+
DocumentFlowAction(
|
|
1134
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
1135
|
+
order_index=order_idx,
|
|
1136
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
1137
|
+
element_type="AUTOSUMMARY",
|
|
1138
|
+
strategy="AUTOSUMMARY",
|
|
1139
|
+
content_summary=f"Summary table for {heading}",
|
|
1140
|
+
target_directive="autosummary",
|
|
1141
|
+
rationale=f"Summary table for {heading}.",
|
|
1142
|
+
)
|
|
1143
|
+
)
|
|
1144
|
+
order_idx += 1
|
|
1145
|
+
doc_content += f"\n.. automodule:: {module_qualname}\n :members:\n :undoc-members:\n :show-inheritance:\n"
|
|
1146
|
+
flow_actions.append(
|
|
1147
|
+
DocumentFlowAction(
|
|
1148
|
+
action_id=f"flow_doc_{target_rel.replace('/', '_').replace('.', '_')}_{order_idx}",
|
|
1149
|
+
order_index=order_idx,
|
|
1150
|
+
source_construct_id=f"doc:{target_rel}:elem:{order_idx:04d}",
|
|
1151
|
+
element_type="API_REQUEST",
|
|
1152
|
+
strategy="AUTODOC",
|
|
1153
|
+
content_summary=f"automodule::{module_qualname}",
|
|
1154
|
+
target_directive=f".. automodule:: {module_qualname}",
|
|
1155
|
+
rationale="Full module autodoc expansion.",
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1158
|
+
order_idx += 1
|
|
1159
|
+
doc_content += "```\n"
|
|
1160
|
+
|
|
1161
|
+
generated_doc_proposals.append(
|
|
1162
|
+
GeneratedDocumentProposal(
|
|
1163
|
+
target_path=target_rel,
|
|
1164
|
+
title=doc_title,
|
|
1165
|
+
content=doc_content,
|
|
1166
|
+
generator_plugin="gen-files",
|
|
1167
|
+
generator_script=scripts[0] if scripts else None,
|
|
1168
|
+
provenance=RequirementProvenance.GENERATED_PIPELINE,
|
|
1169
|
+
rationale=f"Synthesized API reference documentation for {module_qualname} matching MkDocs gen-files/mkdocstrings pipeline.",
|
|
1170
|
+
flow_actions=flow_actions,
|
|
1171
|
+
)
|
|
1172
|
+
)
|
|
1173
|
+
api_reference_mappings[module_qualname] = target_rel
|
|
1174
|
+
|
|
1175
|
+
# If package index exists, append toctree of child modules so Sphinx tree is complete
|
|
1176
|
+
for gen_prop in generated_doc_proposals:
|
|
1177
|
+
if gen_prop.target_path.endswith("/index.md"):
|
|
1178
|
+
# Find package key
|
|
1179
|
+
rel_no_prefix = gen_prop.target_path[
|
|
1180
|
+
len(f"{docs_dir_name}/{output_prefix}/") : -len("/index.md")
|
|
1181
|
+
]
|
|
1182
|
+
child_mods = package_modules.get(rel_no_prefix, [])
|
|
1183
|
+
if child_mods:
|
|
1184
|
+
if ".. autosummary::" in gen_prop.content and not any(
|
|
1185
|
+
f" {cm}" in gen_prop.content for cm in child_mods
|
|
1186
|
+
):
|
|
1187
|
+
summary_lines = [
|
|
1188
|
+
f" {child}" for child in sorted(child_mods)
|
|
1189
|
+
]
|
|
1190
|
+
insertion = "\n".join(summary_lines) + "\n```\n"
|
|
1191
|
+
gen_prop.content = gen_prop.content.replace(
|
|
1192
|
+
"```\n", insertion, 1
|
|
1193
|
+
)
|
|
1194
|
+
toc_lines = ["\n```{toctree}", ":hidden:", ":maxdepth: 1", ""]
|
|
1195
|
+
for cm in sorted(child_mods):
|
|
1196
|
+
short_title = cm.split(".")[-1]
|
|
1197
|
+
toc_lines.append(f"{short_title} <{cm}>")
|
|
1198
|
+
toc_lines.append("```\n")
|
|
1199
|
+
gen_prop.content += "\n".join(toc_lines)
|
|
1200
|
+
|
|
1201
|
+
if generated_doc_proposals:
|
|
1202
|
+
# Ensure API rendering and its summary tables are enabled.
|
|
1203
|
+
for ext in [
|
|
1204
|
+
"sphinx.ext.autodoc",
|
|
1205
|
+
"sphinx.ext.autosummary",
|
|
1206
|
+
"sphinx.ext.napoleon",
|
|
1207
|
+
]:
|
|
1208
|
+
ext_sources.setdefault(ext, []).append(
|
|
1209
|
+
"pipeline:gen-files+mkdocstrings"
|
|
1210
|
+
)
|
|
1211
|
+
provenance_map.setdefault(
|
|
1212
|
+
ext, RequirementProvenance.GENERATED_PIPELINE
|
|
1213
|
+
)
|
|
1214
|
+
rationale_map.setdefault(
|
|
1215
|
+
ext, "Required for generated API reference documentation."
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
# 7. Build Traceable RequirementItems
|
|
1219
|
+
requirements: List[RequirementItem] = []
|
|
1220
|
+
|
|
1221
|
+
# Target Baseline
|
|
1222
|
+
requirements.append(
|
|
1223
|
+
RequirementItem(
|
|
1224
|
+
name="Sphinx>=7.0.0",
|
|
1225
|
+
kind="package",
|
|
1226
|
+
provenance=RequirementProvenance.TARGET_BASELINE,
|
|
1227
|
+
rationale="Core target documentation engine.",
|
|
1228
|
+
sources=["target_baseline"],
|
|
1229
|
+
)
|
|
1230
|
+
)
|
|
1231
|
+
requirements.append(
|
|
1232
|
+
RequirementItem(
|
|
1233
|
+
name="myst-parser>=2.0.0",
|
|
1234
|
+
kind="package",
|
|
1235
|
+
provenance=RequirementProvenance.TARGET_BASELINE,
|
|
1236
|
+
rationale="Required for CommonMark and MyST Markdown parsing in Sphinx.",
|
|
1237
|
+
sources=["target_baseline"],
|
|
1238
|
+
)
|
|
1239
|
+
)
|
|
1240
|
+
requirements.append(
|
|
1241
|
+
RequirementItem(
|
|
1242
|
+
name="myst_parser",
|
|
1243
|
+
kind="extension",
|
|
1244
|
+
provenance=RequirementProvenance.TARGET_BASELINE,
|
|
1245
|
+
rationale="Sphinx extension for MyST Parser.",
|
|
1246
|
+
sources=["target_baseline"],
|
|
1247
|
+
)
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
# Theme Package Requirement
|
|
1251
|
+
if theme_proposal.target_package:
|
|
1252
|
+
requirements.append(
|
|
1253
|
+
RequirementItem(
|
|
1254
|
+
name=theme_proposal.target_package,
|
|
1255
|
+
kind="package",
|
|
1256
|
+
provenance=RequirementProvenance.THEME_POLICY,
|
|
1257
|
+
rationale=theme_proposal.rationale,
|
|
1258
|
+
sources=pkg_sources.get(
|
|
1259
|
+
theme_proposal.target_package, [f"theme_policy:{source_theme}"]
|
|
1260
|
+
),
|
|
1261
|
+
)
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
# Extensions & Packages with Traceable Provenance
|
|
1265
|
+
for ext, sources in sorted(ext_sources.items()):
|
|
1266
|
+
if ext != "myst_parser":
|
|
1267
|
+
requirements.append(
|
|
1268
|
+
RequirementItem(
|
|
1269
|
+
name=ext,
|
|
1270
|
+
kind="extension",
|
|
1271
|
+
provenance=provenance_map.get(
|
|
1272
|
+
ext, RequirementProvenance.DOCUMENT_CONSTRUCT
|
|
1273
|
+
),
|
|
1274
|
+
rationale=rationale_map.get(
|
|
1275
|
+
ext, f"Required by {len(sources)} source item(s)."
|
|
1276
|
+
),
|
|
1277
|
+
sources=sources,
|
|
1278
|
+
)
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
for pkg, sources in sorted(pkg_sources.items()):
|
|
1282
|
+
if pkg not in (
|
|
1283
|
+
"Sphinx>=7.0.0",
|
|
1284
|
+
"myst-parser>=2.0.0",
|
|
1285
|
+
theme_proposal.target_package,
|
|
1286
|
+
):
|
|
1287
|
+
requirements.append(
|
|
1288
|
+
RequirementItem(
|
|
1289
|
+
name=pkg,
|
|
1290
|
+
kind="package",
|
|
1291
|
+
provenance=provenance_map.get(
|
|
1292
|
+
pkg, RequirementProvenance.DOCUMENT_CONSTRUCT
|
|
1293
|
+
),
|
|
1294
|
+
rationale=rationale_map.get(
|
|
1295
|
+
pkg, f"Required by {len(sources)} source item(s)."
|
|
1296
|
+
),
|
|
1297
|
+
sources=sources,
|
|
1298
|
+
)
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
site_name = (
|
|
1302
|
+
report.mkdocs_config.site_name if report.mkdocs_config else "Documentation"
|
|
1303
|
+
)
|
|
1304
|
+
all_ext_names = sorted(
|
|
1305
|
+
list({r.name for r in requirements if r.kind == "extension"})
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
conf_opts: Dict[str, Any] = {
|
|
1309
|
+
"project": site_name,
|
|
1310
|
+
"html_title": site_name,
|
|
1311
|
+
"html_theme": theme_proposal.target_theme or "sphinx_rtd_theme",
|
|
1312
|
+
"add_module_names": False,
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
# Map MkDocs metadata & theme properties to Sphinx conf options
|
|
1316
|
+
if report.mkdocs_config:
|
|
1317
|
+
cfg = report.mkdocs_config
|
|
1318
|
+
CONFIG_FIELD_MAP = {
|
|
1319
|
+
"copyright": "copyright",
|
|
1320
|
+
"site_author": "author",
|
|
1321
|
+
"theme_language": "language",
|
|
1322
|
+
"theme_logo": "html_logo",
|
|
1323
|
+
"theme_favicon": "html_favicon",
|
|
1324
|
+
"extra_css": "html_css_files",
|
|
1325
|
+
"extra_javascript": "html_js_files",
|
|
1326
|
+
}
|
|
1327
|
+
for attr, conf_key in CONFIG_FIELD_MAP.items():
|
|
1328
|
+
val = getattr(cfg, attr, None)
|
|
1329
|
+
if val:
|
|
1330
|
+
if (
|
|
1331
|
+
attr == "copyright"
|
|
1332
|
+
and isinstance(val, str)
|
|
1333
|
+
and ("<" in val or "&" in val)
|
|
1334
|
+
):
|
|
1335
|
+
clean_c = html.unescape(val)
|
|
1336
|
+
clean_c = re.sub(r"<[^>]+>", "", clean_c)
|
|
1337
|
+
clean_c = re.sub(
|
|
1338
|
+
r"^(?:copyright|\(c\)|©|\s)+",
|
|
1339
|
+
"",
|
|
1340
|
+
clean_c,
|
|
1341
|
+
flags=re.IGNORECASE,
|
|
1342
|
+
).strip()
|
|
1343
|
+
val = clean_c or val
|
|
1344
|
+
conf_opts[conf_key] = val
|
|
1345
|
+
|
|
1346
|
+
# Theme-specific options (e.g. Furo / Immaterial / RTD)
|
|
1347
|
+
theme_opts: Dict[str, Any] = {}
|
|
1348
|
+
target_t = theme_proposal.target_theme or "sphinx_rtd_theme"
|
|
1349
|
+
|
|
1350
|
+
if target_t == "sphinx_immaterial":
|
|
1351
|
+
# Declarative attribute-to-option mapping
|
|
1352
|
+
DIRECT_FIELD_MAPPING = {
|
|
1353
|
+
"theme_icon": "icon",
|
|
1354
|
+
"site_url": "site_url",
|
|
1355
|
+
"repo_url": "repo_url",
|
|
1356
|
+
"edit_uri": "edit_uri",
|
|
1357
|
+
"theme_features": "features",
|
|
1358
|
+
}
|
|
1359
|
+
for attr, opt_key in DIRECT_FIELD_MAPPING.items():
|
|
1360
|
+
val = getattr(cfg, attr, None)
|
|
1361
|
+
if val:
|
|
1362
|
+
theme_opts[opt_key] = val
|
|
1363
|
+
|
|
1364
|
+
if cfg.repo_url and "github.com/" in cfg.repo_url:
|
|
1365
|
+
theme_opts["repo_name"] = cfg.repo_url.rstrip("/").split(
|
|
1366
|
+
"github.com/"
|
|
1367
|
+
)[1]
|
|
1368
|
+
|
|
1369
|
+
# Material palette mapping
|
|
1370
|
+
immaterial_palettes = [
|
|
1371
|
+
{
|
|
1372
|
+
**(
|
|
1373
|
+
{
|
|
1374
|
+
"scheme": p.scheme.value
|
|
1375
|
+
if hasattr(p.scheme, "value")
|
|
1376
|
+
else p.scheme
|
|
1377
|
+
}
|
|
1378
|
+
if p.scheme
|
|
1379
|
+
else {}
|
|
1380
|
+
),
|
|
1381
|
+
**({"primary": p.primary} if p.primary else {}),
|
|
1382
|
+
**({"accent": p.accent} if p.accent else {}),
|
|
1383
|
+
**(
|
|
1384
|
+
{
|
|
1385
|
+
"toggle": {
|
|
1386
|
+
k: v
|
|
1387
|
+
for k, v in [
|
|
1388
|
+
("icon", p.toggle_icon),
|
|
1389
|
+
("name", p.toggle_name),
|
|
1390
|
+
]
|
|
1391
|
+
if v
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (p.toggle_icon or p.toggle_name)
|
|
1395
|
+
else {}
|
|
1396
|
+
),
|
|
1397
|
+
}
|
|
1398
|
+
for p in cfg.theme_palette
|
|
1399
|
+
]
|
|
1400
|
+
if immaterial_palettes:
|
|
1401
|
+
theme_opts["palette"] = immaterial_palettes
|
|
1402
|
+
# Version selector mapping
|
|
1403
|
+
if cfg.extra and "version" in cfg.extra:
|
|
1404
|
+
theme_opts["version_dropdown"] = True
|
|
1405
|
+
v_val = cfg.extra["version"]
|
|
1406
|
+
if isinstance(v_val, dict) and "default" in v_val:
|
|
1407
|
+
conf_opts["version"] = str(v_val["default"])
|
|
1408
|
+
elif isinstance(v_val, str):
|
|
1409
|
+
conf_opts["version"] = v_val
|
|
1410
|
+
|
|
1411
|
+
theme_opts["globaltoc_collapse"] = False
|
|
1412
|
+
|
|
1413
|
+
elif target_t == "furo":
|
|
1414
|
+
if cfg.repo_url:
|
|
1415
|
+
# Parse repo info for Furo
|
|
1416
|
+
clean_repo = cfg.repo_url.rstrip("/")
|
|
1417
|
+
if "github.com/" in clean_repo:
|
|
1418
|
+
repo_path = clean_repo.split("github.com/")[1]
|
|
1419
|
+
theme_opts["source_repository"] = (
|
|
1420
|
+
f"https://github.com/{repo_path}"
|
|
1421
|
+
)
|
|
1422
|
+
theme_opts["source_branch"] = "main"
|
|
1423
|
+
theme_opts["source_directory"] = cfg.docs_dir
|
|
1424
|
+
|
|
1425
|
+
from .theme_constants import resolve_material_color, ThemeScheme
|
|
1426
|
+
|
|
1427
|
+
light_vars = {}
|
|
1428
|
+
dark_vars = {}
|
|
1429
|
+
|
|
1430
|
+
for p in cfg.theme_palette:
|
|
1431
|
+
primary_hex = (
|
|
1432
|
+
resolve_material_color(p.primary) if p.primary else None
|
|
1433
|
+
)
|
|
1434
|
+
accent_hex = (
|
|
1435
|
+
resolve_material_color(p.accent, is_accent=True)
|
|
1436
|
+
if p.accent
|
|
1437
|
+
else None
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
if p.scheme in (
|
|
1441
|
+
ThemeScheme.SLATE,
|
|
1442
|
+
ThemeScheme.DARK,
|
|
1443
|
+
"slate",
|
|
1444
|
+
"dark",
|
|
1445
|
+
):
|
|
1446
|
+
if primary_hex:
|
|
1447
|
+
dark_vars["color-brand-primary"] = primary_hex
|
|
1448
|
+
if accent_hex:
|
|
1449
|
+
dark_vars["color-brand-content"] = accent_hex
|
|
1450
|
+
else:
|
|
1451
|
+
if primary_hex:
|
|
1452
|
+
light_vars["color-brand-primary"] = primary_hex
|
|
1453
|
+
if accent_hex:
|
|
1454
|
+
light_vars["color-brand-content"] = accent_hex
|
|
1455
|
+
|
|
1456
|
+
if light_vars:
|
|
1457
|
+
theme_opts["light_css_variables"] = light_vars
|
|
1458
|
+
if dark_vars:
|
|
1459
|
+
theme_opts["dark_css_variables"] = dark_vars
|
|
1460
|
+
|
|
1461
|
+
elif target_t == "sphinx_rtd_theme":
|
|
1462
|
+
if cfg.theme_logo:
|
|
1463
|
+
theme_opts["logo_only"] = False
|
|
1464
|
+
if cfg.repo_url:
|
|
1465
|
+
theme_opts["vcs_pageview_mode"] = "blob"
|
|
1466
|
+
|
|
1467
|
+
if theme_opts:
|
|
1468
|
+
conf_opts["html_theme_options"] = theme_opts
|
|
1469
|
+
|
|
1470
|
+
# Check mkdocstrings options to set native Sphinx conf.py properties
|
|
1471
|
+
if report.mkdocs_config:
|
|
1472
|
+
mkdocstrings_cfg = report.mkdocs_config.plugins_config.get(
|
|
1473
|
+
"mkdocstrings", {}
|
|
1474
|
+
)
|
|
1475
|
+
if isinstance(mkdocstrings_cfg, dict):
|
|
1476
|
+
py_opts = (
|
|
1477
|
+
mkdocstrings_cfg.get("handlers", {})
|
|
1478
|
+
.get("python", {})
|
|
1479
|
+
.get("options", {})
|
|
1480
|
+
)
|
|
1481
|
+
if not isinstance(py_opts, dict):
|
|
1482
|
+
py_opts = mkdocstrings_cfg.get("options", {})
|
|
1483
|
+
if isinstance(py_opts, dict):
|
|
1484
|
+
if py_opts.get("merge_init_into_class"):
|
|
1485
|
+
conf_opts["autoclass_content"] = "both"
|
|
1486
|
+
|
|
1487
|
+
# Statically inspect pyproject.toml for optional dependencies to mock in autodoc if needed
|
|
1488
|
+
mock_imports: List[str] = []
|
|
1489
|
+
pyproject_path = self.project_root / "pyproject.toml"
|
|
1490
|
+
if pyproject_path.exists():
|
|
1491
|
+
try:
|
|
1492
|
+
import tomllib
|
|
1493
|
+
|
|
1494
|
+
pyproj_data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
1495
|
+
opt_deps = pyproj_data.get("project", {}).get(
|
|
1496
|
+
"optional-dependencies", {}
|
|
1497
|
+
)
|
|
1498
|
+
for group_name in opt_deps:
|
|
1499
|
+
if group_name != "all":
|
|
1500
|
+
mock_imports.append(group_name)
|
|
1501
|
+
except Exception:
|
|
1502
|
+
pass
|
|
1503
|
+
if mock_imports:
|
|
1504
|
+
conf_opts["autodoc_mock_imports"] = sorted(list(set(mock_imports)))
|
|
1505
|
+
|
|
1506
|
+
conf_opts.update(custom_conf_options)
|
|
1507
|
+
if generated_doc_proposals:
|
|
1508
|
+
conf_opts["autosummary_generate"] = False
|
|
1509
|
+
|
|
1510
|
+
sphinx_config_proposal = ConfigMigrationProposal(
|
|
1511
|
+
project_name=site_name or self.project_root.name,
|
|
1512
|
+
theme=theme_proposal,
|
|
1513
|
+
extensions_to_add=all_ext_names,
|
|
1514
|
+
myst_enable_extensions=sorted(list(myst_enabled)),
|
|
1515
|
+
custom_options=conf_opts,
|
|
1516
|
+
rationale={
|
|
1517
|
+
"extensions": "Derived from declarative policy engine across document actions, plugins, and theme features.",
|
|
1518
|
+
"myst_syntax": f"Derived from MkDocs extensions config and document syntax ({', '.join(sorted(list(myst_enabled)))}).",
|
|
1519
|
+
},
|
|
1520
|
+
)
|
|
1521
|
+
sphinx_config_proposal.rendered_content = build_conf_py(
|
|
1522
|
+
cfg=sphinx_config_proposal,
|
|
1523
|
+
project_root=self.project_root,
|
|
1524
|
+
has_generated_docs=bool(generated_doc_proposals),
|
|
1525
|
+
)
|
|
1526
|
+
|
|
1527
|
+
# 8. Synthesize DocumentationPlan from parsed pages and generated proposals
|
|
1528
|
+
doc_plan_artifacts: List[DocumentationArtifact] = list(planned_pages)
|
|
1529
|
+
for gen_prop in generated_doc_proposals:
|
|
1530
|
+
doc_plan_artifacts.append(
|
|
1531
|
+
DocumentationArtifact(
|
|
1532
|
+
artifact_id=f"art_gen_{gen_prop.target_path.replace('/', '_').replace('.', '_')}",
|
|
1533
|
+
target_path=gen_prop.target_path,
|
|
1534
|
+
source_file=None,
|
|
1535
|
+
title=gen_prop.title,
|
|
1536
|
+
artifact_kind="generated_stub",
|
|
1537
|
+
provenance=gen_prop.provenance,
|
|
1538
|
+
rationale=gen_prop.rationale,
|
|
1539
|
+
flow_actions=gen_prop.flow_actions,
|
|
1540
|
+
artifact_provenance=ArtifactProvenance(
|
|
1541
|
+
source_construct_ids=[
|
|
1542
|
+
act.source_construct_id
|
|
1543
|
+
for act in gen_prop.flow_actions
|
|
1544
|
+
if act.source_construct_id
|
|
1545
|
+
],
|
|
1546
|
+
source_files=[],
|
|
1547
|
+
generated_from_pipeline=gen_prop.generator_plugin,
|
|
1548
|
+
required_extensions=[
|
|
1549
|
+
"sphinx.ext.autodoc",
|
|
1550
|
+
"sphinx.ext.autosummary",
|
|
1551
|
+
],
|
|
1552
|
+
required_packages=[],
|
|
1553
|
+
),
|
|
1554
|
+
)
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
docs_dir = self.project_root / docs_dir_name
|
|
1558
|
+
all_md_files: List[Path] = (
|
|
1559
|
+
sorted(list(docs_dir.rglob("*.md"))) if docs_dir.exists() else []
|
|
1560
|
+
)
|
|
1561
|
+
nav_entries = (
|
|
1562
|
+
report.navigation_analysis.tree
|
|
1563
|
+
if (report.navigation_analysis and report.navigation_analysis.has_nav)
|
|
1564
|
+
else []
|
|
1565
|
+
)
|
|
1566
|
+
|
|
1567
|
+
gen_targets: Set[str] = set()
|
|
1568
|
+
for gen_doc in generated_doc_proposals:
|
|
1569
|
+
try:
|
|
1570
|
+
gen_targets.add(
|
|
1571
|
+
Path(gen_doc.target_path)
|
|
1572
|
+
.relative_to(docs_dir_name)
|
|
1573
|
+
.with_suffix("")
|
|
1574
|
+
.as_posix()
|
|
1575
|
+
)
|
|
1576
|
+
except ValueError:
|
|
1577
|
+
pass
|
|
1578
|
+
|
|
1579
|
+
root_toctrees: List[str] = []
|
|
1580
|
+
if report.navigation_analysis and report.navigation_analysis.tree:
|
|
1581
|
+
|
|
1582
|
+
def extract_nav_paths(items: List[Any]) -> List[str]:
|
|
1583
|
+
paths = []
|
|
1584
|
+
for item in items:
|
|
1585
|
+
if getattr(item, "path", None):
|
|
1586
|
+
paths.append(item.path)
|
|
1587
|
+
if getattr(item, "children", None):
|
|
1588
|
+
paths.extend(extract_nav_paths(item.children))
|
|
1589
|
+
return paths
|
|
1590
|
+
|
|
1591
|
+
root_toctrees = extract_nav_paths(report.navigation_analysis.tree)
|
|
1592
|
+
|
|
1593
|
+
resolved_root_toctrees = resolve_navigation_docnames(
|
|
1594
|
+
nav_entries=nav_entries,
|
|
1595
|
+
all_files=all_md_files,
|
|
1596
|
+
docs_dir=docs_dir,
|
|
1597
|
+
project_root=self.project_root,
|
|
1598
|
+
generated_targets=gen_targets,
|
|
1599
|
+
)
|
|
1600
|
+
|
|
1601
|
+
nav_plan = NavigationPlan(
|
|
1602
|
+
root_toctrees=root_toctrees, sub_toctrees={}, hidden_routes=[]
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
gen_pipelines_plan: List[GeneratedPipelinePlan] = []
|
|
1606
|
+
if generated_doc_proposals:
|
|
1607
|
+
first_script = (
|
|
1608
|
+
scripts[0]
|
|
1609
|
+
if (
|
|
1610
|
+
report.mkdocs_config
|
|
1611
|
+
and "gen-files" in report.mkdocs_config.plugins
|
|
1612
|
+
and scripts
|
|
1613
|
+
)
|
|
1614
|
+
else None
|
|
1615
|
+
)
|
|
1616
|
+
gen_pipelines_plan.append(
|
|
1617
|
+
GeneratedPipelinePlan(
|
|
1618
|
+
pipeline_id="pipe_gen_files_mkdocstrings",
|
|
1619
|
+
source_plugin="gen-files",
|
|
1620
|
+
generator_script=first_script,
|
|
1621
|
+
target_strategy="AUTODOC_AUTOSUMMARY_STUBS",
|
|
1622
|
+
target_artifacts=[p.target_path for p in generated_doc_proposals],
|
|
1623
|
+
rationale=(
|
|
1624
|
+
f"Automated synthesis of API reference stubs replacing mkdocstrings/gen-files build step. "
|
|
1625
|
+
f"Analyzed generator script '{first_script}': scanned '{script_meta.get('source_dir', 'src')}', "
|
|
1626
|
+
f"output to '{output_prefix}', literate nav: '{script_meta.get('literate_nav', 'reference/SUMMARY.txt')}'."
|
|
1627
|
+
)
|
|
1628
|
+
if first_script
|
|
1629
|
+
else "Automated synthesis of API reference stubs replacing mkdocstrings/gen-files build step.",
|
|
1630
|
+
)
|
|
1631
|
+
)
|
|
1632
|
+
|
|
1633
|
+
# External Inventory Configuration (e.g. docs.python.org/3 objects.inv)
|
|
1634
|
+
external_invs: List[ExternalInventoryConfig] = []
|
|
1635
|
+
if report.mkdocs_config and "mkdocstrings" in report.mkdocs_config.plugins:
|
|
1636
|
+
mkd_cfg = report.mkdocs_config.plugins_config.get("mkdocstrings", {})
|
|
1637
|
+
import_invs = (
|
|
1638
|
+
mkd_cfg.get("handlers", {}).get("python", {}).get("import", [])
|
|
1639
|
+
if isinstance(mkd_cfg, dict)
|
|
1640
|
+
else []
|
|
1641
|
+
)
|
|
1642
|
+
for inv_url in import_invs:
|
|
1643
|
+
if "docs.python.org" in inv_url:
|
|
1644
|
+
base_url = inv_url.rsplit("/objects.inv", 1)[0]
|
|
1645
|
+
external_invs.append(
|
|
1646
|
+
ExternalInventoryConfig(
|
|
1647
|
+
inventory_id="python",
|
|
1648
|
+
url=base_url,
|
|
1649
|
+
objects_inv=inv_url,
|
|
1650
|
+
provenance=RequirementProvenance.EXTENSION_POLICY,
|
|
1651
|
+
rationale="Maps external Python documentation inventory to sphinx.ext.intersphinx.",
|
|
1652
|
+
)
|
|
1653
|
+
)
|
|
1654
|
+
# Ensure intersphinx is added
|
|
1655
|
+
if "sphinx.ext.intersphinx" not in all_ext_names:
|
|
1656
|
+
all_ext_names.append("sphinx.ext.intersphinx")
|
|
1657
|
+
conf_opts.setdefault("intersphinx_mapping", {})["python"] = (
|
|
1658
|
+
base_url,
|
|
1659
|
+
None,
|
|
1660
|
+
)
|
|
1661
|
+
|
|
1662
|
+
# Versioning / Deployment Strategy (e.g. mike)
|
|
1663
|
+
versioning_plan: Optional[VersioningDeploymentPlan] = None
|
|
1664
|
+
if report.mkdocs_config and "mike" in report.mkdocs_config.plugins:
|
|
1665
|
+
mike_cfg = (
|
|
1666
|
+
report.mkdocs_config.plugins_config.get("mike", {})
|
|
1667
|
+
if isinstance(report.mkdocs_config.plugins_config, dict)
|
|
1668
|
+
else {}
|
|
1669
|
+
)
|
|
1670
|
+
canon_v = mike_cfg.get("canonical_version", "latest")
|
|
1671
|
+
versioning_plan = VersioningDeploymentPlan(
|
|
1672
|
+
source_tool="mike",
|
|
1673
|
+
canonical_version=canon_v,
|
|
1674
|
+
target_strategy="SPHINX_VERSIONING_DEPLOYMENT_WORKFLOW",
|
|
1675
|
+
status="ACCOUNTED",
|
|
1676
|
+
rationale="Separated documentation generation from deployment/versioning. Handled via CI multi-version Sphinx deployment.",
|
|
1677
|
+
)
|
|
1678
|
+
|
|
1679
|
+
# Complete System Capability Accountability Audit
|
|
1680
|
+
capability_accountability: List[SystemCapabilityAccountability] = []
|
|
1681
|
+
if report.mkdocs_config:
|
|
1682
|
+
# Theme
|
|
1683
|
+
capability_accountability.append(
|
|
1684
|
+
SystemCapabilityAccountability(
|
|
1685
|
+
capability_name=report.mkdocs_config.theme_name,
|
|
1686
|
+
source_category="theme",
|
|
1687
|
+
disposition=CapabilityDisposition.TRANSFORM,
|
|
1688
|
+
implementation_strategy=ImplementationStrategy.SPHINX_EXTENSION,
|
|
1689
|
+
verification_status=VerificationStatus.VERIFIED,
|
|
1690
|
+
target_equivalent=theme_proposal.target_theme,
|
|
1691
|
+
rationale=theme_proposal.rationale,
|
|
1692
|
+
)
|
|
1693
|
+
)
|
|
1694
|
+
# Plugins
|
|
1695
|
+
for p_name in report.mkdocs_config.plugins:
|
|
1696
|
+
if p_name == "autorefs":
|
|
1697
|
+
capability_accountability.append(
|
|
1698
|
+
SystemCapabilityAccountability(
|
|
1699
|
+
capability_name=p_name,
|
|
1700
|
+
source_category="plugin",
|
|
1701
|
+
disposition=CapabilityDisposition.PRESERVE,
|
|
1702
|
+
implementation_strategy=ImplementationStrategy.NATIVE_SPHINX,
|
|
1703
|
+
verification_status=VerificationStatus.NOT_YET_VERIFIED,
|
|
1704
|
+
target_equivalent="sphinx_immaterial / stdlib crossrefs",
|
|
1705
|
+
rationale="Autorefs cross-referencing is natively provided by Sphinx domain references.",
|
|
1706
|
+
)
|
|
1707
|
+
)
|
|
1708
|
+
elif p_name == "awesome-pages":
|
|
1709
|
+
capability_accountability.append(
|
|
1710
|
+
SystemCapabilityAccountability(
|
|
1711
|
+
capability_name=p_name,
|
|
1712
|
+
source_category="plugin",
|
|
1713
|
+
disposition=CapabilityDisposition.ACCOUNTED_NO_DIRECT_EQUIVALENT,
|
|
1714
|
+
implementation_strategy=ImplementationStrategy.TOCTREE,
|
|
1715
|
+
verification_status=VerificationStatus.VERIFIED,
|
|
1716
|
+
target_equivalent="NavigationNode graph / toctree",
|
|
1717
|
+
rationale="Effective navigation structure synthesized into Sphinx toctrees; no runtime Sphinx extension needed.",
|
|
1718
|
+
)
|
|
1719
|
+
)
|
|
1720
|
+
elif p_name == "gen-files":
|
|
1721
|
+
capability_accountability.append(
|
|
1722
|
+
SystemCapabilityAccountability(
|
|
1723
|
+
capability_name=p_name,
|
|
1724
|
+
source_category="plugin",
|
|
1725
|
+
disposition=CapabilityDisposition.TRANSFORM,
|
|
1726
|
+
implementation_strategy=ImplementationStrategy.AUTODOC_AUTOSUMMARY_STUBS,
|
|
1727
|
+
verification_status=VerificationStatus.VERIFIED,
|
|
1728
|
+
target_equivalent="AUTODOC_AUTOSUMMARY_STUBS",
|
|
1729
|
+
rationale="Transformed build-time generated file pipeline into deterministic API reference stubs.",
|
|
1730
|
+
)
|
|
1731
|
+
)
|
|
1732
|
+
elif p_name == "mkdocstrings":
|
|
1733
|
+
capability_accountability.append(
|
|
1734
|
+
SystemCapabilityAccountability(
|
|
1735
|
+
capability_name=p_name,
|
|
1736
|
+
source_category="plugin",
|
|
1737
|
+
disposition=CapabilityDisposition.TRANSFORM,
|
|
1738
|
+
implementation_strategy=ImplementationStrategy.AUTODOC,
|
|
1739
|
+
verification_status=VerificationStatus.VERIFIED,
|
|
1740
|
+
target_equivalent="sphinx.ext.autodoc + sphinx.ext.autosummary",
|
|
1741
|
+
rationale="Migrated mkdocstrings Python handler to Sphinx autodoc/autosummary directives.",
|
|
1742
|
+
)
|
|
1743
|
+
)
|
|
1744
|
+
elif p_name == "literate-nav":
|
|
1745
|
+
capability_accountability.append(
|
|
1746
|
+
SystemCapabilityAccountability(
|
|
1747
|
+
capability_name=p_name,
|
|
1748
|
+
source_category="plugin",
|
|
1749
|
+
disposition=CapabilityDisposition.ACCOUNTED_NO_DIRECT_EQUIVALENT,
|
|
1750
|
+
implementation_strategy=ImplementationStrategy.TOCTREE,
|
|
1751
|
+
verification_status=VerificationStatus.VERIFIED,
|
|
1752
|
+
target_equivalent="Sphinx toctrees",
|
|
1753
|
+
rationale="Nav files compiled into deterministic hierarchical toctrees.",
|
|
1754
|
+
)
|
|
1755
|
+
)
|
|
1756
|
+
elif p_name == "mike":
|
|
1757
|
+
capability_accountability.append(
|
|
1758
|
+
SystemCapabilityAccountability(
|
|
1759
|
+
capability_name=p_name,
|
|
1760
|
+
source_category="plugin",
|
|
1761
|
+
disposition=CapabilityDisposition.ACCOUNTED_NO_DIRECT_EQUIVALENT,
|
|
1762
|
+
implementation_strategy=ImplementationStrategy.DEPLOYMENT_WORKFLOW,
|
|
1763
|
+
verification_status=VerificationStatus.NOT_YET_VERIFIED,
|
|
1764
|
+
target_equivalent="Sphinx versioning deployment",
|
|
1765
|
+
rationale="Multi-version hosting decoupled from documentation compilation.",
|
|
1766
|
+
)
|
|
1767
|
+
)
|
|
1768
|
+
elif p_name == "search":
|
|
1769
|
+
capability_accountability.append(
|
|
1770
|
+
SystemCapabilityAccountability(
|
|
1771
|
+
capability_name=p_name,
|
|
1772
|
+
source_category="plugin",
|
|
1773
|
+
disposition=CapabilityDisposition.PRESERVE,
|
|
1774
|
+
implementation_strategy=ImplementationStrategy.NATIVE_SPHINX,
|
|
1775
|
+
verification_status=VerificationStatus.VERIFIED,
|
|
1776
|
+
target_equivalent="Sphinx built-in search",
|
|
1777
|
+
rationale="Search is natively built into Sphinx HTML builder.",
|
|
1778
|
+
)
|
|
1779
|
+
)
|
|
1780
|
+
else:
|
|
1781
|
+
capability_accountability.append(
|
|
1782
|
+
SystemCapabilityAccountability(
|
|
1783
|
+
capability_name=p_name,
|
|
1784
|
+
source_category="plugin",
|
|
1785
|
+
disposition=CapabilityDisposition.ACCOUNTED_NO_DIRECT_EQUIVALENT,
|
|
1786
|
+
implementation_strategy=ImplementationStrategy.NONE,
|
|
1787
|
+
verification_status=VerificationStatus.NOT_YET_VERIFIED,
|
|
1788
|
+
target_equivalent=None,
|
|
1789
|
+
rationale=f"Evaluated plugin {p_name}.",
|
|
1790
|
+
)
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
documentation_plan = DocumentationPlan(
|
|
1794
|
+
pages=doc_plan_artifacts,
|
|
1795
|
+
api_strategies=planned_api_strategies,
|
|
1796
|
+
navigation=nav_plan,
|
|
1797
|
+
generated_pipelines=gen_pipelines_plan,
|
|
1798
|
+
cross_references=planned_cross_refs,
|
|
1799
|
+
external_inventories=external_invs,
|
|
1800
|
+
versioning_deployment=versioning_plan,
|
|
1801
|
+
capability_accountability=capability_accountability,
|
|
1802
|
+
asset_actions=planned_assets,
|
|
1803
|
+
required_extensions=all_ext_names,
|
|
1804
|
+
required_packages=[r.name for r in requirements if r.kind == "package"],
|
|
1805
|
+
manual_items=manual_items,
|
|
1806
|
+
unsupported_items=unsupported_items,
|
|
1807
|
+
# Backward compatibility properties
|
|
1808
|
+
artifacts=doc_plan_artifacts,
|
|
1809
|
+
api_generation_strategy={
|
|
1810
|
+
s.object_path: s.directive_kind.value for s in planned_api_strategies
|
|
1811
|
+
},
|
|
1812
|
+
toctree_hierarchies={"root": resolved_root_toctrees},
|
|
1813
|
+
cross_reference_mappings={
|
|
1814
|
+
**{c.source_target: c.transformed_target for c in planned_cross_refs},
|
|
1815
|
+
**api_reference_mappings,
|
|
1816
|
+
},
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
ci_plan = plan_ci_workflow(
|
|
1820
|
+
ci_analysis=report.ci_analysis,
|
|
1821
|
+
dep_analysis=report.dependency_analysis,
|
|
1822
|
+
project_root=self.project_root,
|
|
1823
|
+
)
|
|
1824
|
+
|
|
1825
|
+
pkgs_to_remove = (
|
|
1826
|
+
report.dependency_analysis.detected_packages_to_remove
|
|
1827
|
+
if report.dependency_analysis
|
|
1828
|
+
else []
|
|
1829
|
+
)
|
|
1830
|
+
ts = (
|
|
1831
|
+
deterministic_timestamp
|
|
1832
|
+
if deterministic_timestamp is not None
|
|
1833
|
+
else datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
1834
|
+
)
|
|
1835
|
+
|
|
1836
|
+
# Planned obsolete files to clean up post-migration (e.g. generator scripts and MkDocs hooks)
|
|
1837
|
+
pipe_scripts = [
|
|
1838
|
+
pipe.generator_script
|
|
1839
|
+
for pipe in gen_pipelines_plan
|
|
1840
|
+
if pipe.generator_script
|
|
1841
|
+
]
|
|
1842
|
+
planned_obsolete_files = detect_obsolete_generator_scripts(
|
|
1843
|
+
project_root=self.project_root,
|
|
1844
|
+
mkdocs_config=report.mkdocs_config,
|
|
1845
|
+
additional_scripts=pipe_scripts,
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1848
|
+
return MigrationPlan(
|
|
1849
|
+
project_root=str(self.project_root),
|
|
1850
|
+
metadata=MigrationPlanMetadata(generated_at=ts),
|
|
1851
|
+
source_mkdocs_config=report.mkdocs_config,
|
|
1852
|
+
version_environment=report.version_env,
|
|
1853
|
+
navigation_analysis=report.navigation_analysis,
|
|
1854
|
+
dependency_analysis=report.dependency_analysis,
|
|
1855
|
+
ci_analysis=report.ci_analysis,
|
|
1856
|
+
documentation_plan=documentation_plan,
|
|
1857
|
+
document_actions=all_actions,
|
|
1858
|
+
generated_documents=generated_doc_proposals,
|
|
1859
|
+
summary=summary,
|
|
1860
|
+
requirements=requirements,
|
|
1861
|
+
packages_to_remove=pkgs_to_remove,
|
|
1862
|
+
proposed_sphinx_config=sphinx_config_proposal,
|
|
1863
|
+
ci_plan=ci_plan,
|
|
1864
|
+
manual_action_items=manual_items,
|
|
1865
|
+
unsupported_constructs=unsupported_items,
|
|
1866
|
+
obsolete_files=planned_obsolete_files,
|
|
1867
|
+
)
|