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,507 @@
|
|
|
1
|
+
"""Command-line interface for deterministic MkDocs-to-Sphinx migration."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
|
|
12
|
+
from .analyzer.project import ProjectAnalyzer
|
|
13
|
+
from .planner.planner import MigrationPlanner
|
|
14
|
+
from .transformer.engine import TransformationEngine
|
|
15
|
+
from .validator.verifier import TransformationValidator
|
|
16
|
+
from . import __version__
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
@click.version_option(version=__version__, prog_name="sphinx-migrate")
|
|
23
|
+
def main():
|
|
24
|
+
"""Deterministic, version-aware CLI toolkit for migrating MkDocs to Sphinx + MyST."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@main.command()
|
|
29
|
+
@click.argument(
|
|
30
|
+
"project_path",
|
|
31
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
32
|
+
default=".",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--json-output",
|
|
36
|
+
"json_out",
|
|
37
|
+
is_flag=True,
|
|
38
|
+
default=False,
|
|
39
|
+
help="Output factual analysis report as JSON.",
|
|
40
|
+
)
|
|
41
|
+
def analyze(project_path: Path, json_out: bool):
|
|
42
|
+
"""Factual inspection of an MkDocs documentation project."""
|
|
43
|
+
analyzer = ProjectAnalyzer(project_path)
|
|
44
|
+
report = analyzer.analyze()
|
|
45
|
+
|
|
46
|
+
if json_out:
|
|
47
|
+
sys.stdout.write(json.dumps(report.model_dump(), indent=2, default=str) + "\n")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
console.print(
|
|
51
|
+
f"[bold blue]Inspecting MkDocs project:[/bold blue] {project_path.resolve()}"
|
|
52
|
+
)
|
|
53
|
+
console.print()
|
|
54
|
+
|
|
55
|
+
table = Table(
|
|
56
|
+
title="Migration Subsystems", show_header=True, header_style="bold magenta"
|
|
57
|
+
)
|
|
58
|
+
table.add_column("Subsystem", style="cyan", width=24)
|
|
59
|
+
table.add_column("Status", width=16)
|
|
60
|
+
table.add_column("Details")
|
|
61
|
+
|
|
62
|
+
for sub in report.subsystem_summaries:
|
|
63
|
+
status_style = (
|
|
64
|
+
"green"
|
|
65
|
+
if sub.status in ("PRESERVED", "AUTOMATIC")
|
|
66
|
+
else ("yellow" if sub.status == "REVIEW" else "red")
|
|
67
|
+
)
|
|
68
|
+
table.add_row(
|
|
69
|
+
sub.name, f"[{status_style}]{sub.status}[/{status_style}]", sub.details
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
console.print(table)
|
|
73
|
+
console.print()
|
|
74
|
+
|
|
75
|
+
if report.migration_requirements:
|
|
76
|
+
req_table = Table(
|
|
77
|
+
title="Derived Migration Requirements",
|
|
78
|
+
show_header=True,
|
|
79
|
+
header_style="bold cyan",
|
|
80
|
+
)
|
|
81
|
+
req_table.add_column("Category", width=18)
|
|
82
|
+
req_table.add_column("Disposition", width=22)
|
|
83
|
+
req_table.add_column("Source Construct", width=26)
|
|
84
|
+
req_table.add_column("Required Outcome")
|
|
85
|
+
|
|
86
|
+
for req in report.migration_requirements:
|
|
87
|
+
disp_style = (
|
|
88
|
+
"green"
|
|
89
|
+
if req.disposition.value in ("PRESERVE", "GENERATE")
|
|
90
|
+
else ("yellow" if req.disposition.value == "TRANSFORM" else "blue")
|
|
91
|
+
)
|
|
92
|
+
req_table.add_row(
|
|
93
|
+
req.category.value,
|
|
94
|
+
f"[{disp_style}]{req.disposition.value}[/{disp_style}]",
|
|
95
|
+
req.source_construct,
|
|
96
|
+
req.required_outcome,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
console.print(req_table)
|
|
100
|
+
console.print()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@main.command()
|
|
104
|
+
@click.argument(
|
|
105
|
+
"project_path",
|
|
106
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
107
|
+
default=".",
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--output-json",
|
|
111
|
+
"output_json_path",
|
|
112
|
+
type=click.Path(dir_okay=False, writable=True, path_type=Path),
|
|
113
|
+
default=None,
|
|
114
|
+
help="Write canonical machine-readable migration plan JSON to file.",
|
|
115
|
+
)
|
|
116
|
+
@click.option(
|
|
117
|
+
"--json-output",
|
|
118
|
+
"json_stdout",
|
|
119
|
+
is_flag=True,
|
|
120
|
+
default=False,
|
|
121
|
+
help="Print canonical machine-readable migration plan JSON to stdout.",
|
|
122
|
+
)
|
|
123
|
+
def plan(project_path: Path, output_json_path: Optional[Path], json_stdout: bool):
|
|
124
|
+
"""Generates a deterministic, read-only MigrationPlan without mutating source files."""
|
|
125
|
+
planner = MigrationPlanner(project_path)
|
|
126
|
+
plan = planner.create_plan()
|
|
127
|
+
|
|
128
|
+
if output_json_path:
|
|
129
|
+
output_json_path.write_text(
|
|
130
|
+
json.dumps(plan.canonical_dict(), indent=2, default=str), encoding="utf-8"
|
|
131
|
+
)
|
|
132
|
+
console.print(
|
|
133
|
+
f"[bold green]✔ Plan exported to:[/bold green] {output_json_path.resolve()}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if json_stdout:
|
|
137
|
+
sys.stdout.write(
|
|
138
|
+
json.dumps(plan.canonical_dict(), indent=2, default=str) + "\n"
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
console.print(
|
|
143
|
+
f"[bold blue]Generating Migration Plan for:[/bold blue] {project_path.resolve()}"
|
|
144
|
+
)
|
|
145
|
+
console.print()
|
|
146
|
+
|
|
147
|
+
inv_table = Table(
|
|
148
|
+
title="Construct Action Inventory", show_header=True, header_style="bold cyan"
|
|
149
|
+
)
|
|
150
|
+
inv_table.add_column("Classification", width=16)
|
|
151
|
+
inv_table.add_column("Count", justify="right", width=10)
|
|
152
|
+
inv_table.add_column("Description")
|
|
153
|
+
|
|
154
|
+
inv_table.add_row(
|
|
155
|
+
"[green]TRANSFORM[/green]",
|
|
156
|
+
str(plan.summary.transform_count),
|
|
157
|
+
"Deterministic MyST / Sphinx syntax conversions",
|
|
158
|
+
)
|
|
159
|
+
inv_table.add_row(
|
|
160
|
+
"[blue]PRESERVE[/blue]",
|
|
161
|
+
str(plan.summary.preserve_count),
|
|
162
|
+
"Standard Markdown / links preserved as-is",
|
|
163
|
+
)
|
|
164
|
+
inv_table.add_row(
|
|
165
|
+
"[yellow]MANUAL[/yellow]",
|
|
166
|
+
str(plan.summary.manual_count),
|
|
167
|
+
"Requires developer review (e.g. mkdocstrings / complex macros)",
|
|
168
|
+
)
|
|
169
|
+
inv_table.add_row(
|
|
170
|
+
"[red]UNSUPPORTED[/red]",
|
|
171
|
+
str(plan.summary.unsupported_count),
|
|
172
|
+
"No direct Sphinx equivalent",
|
|
173
|
+
)
|
|
174
|
+
console.print(inv_table)
|
|
175
|
+
console.print()
|
|
176
|
+
|
|
177
|
+
ext_items = [r for r in plan.requirements if r.kind == "extension"]
|
|
178
|
+
ext_table = Table(
|
|
179
|
+
title="Required Sphinx Extensions", show_header=True, header_style="bold green"
|
|
180
|
+
)
|
|
181
|
+
ext_table.add_column("Extension Name", style="bold green", width=28)
|
|
182
|
+
ext_table.add_column("Provenance", width=22)
|
|
183
|
+
ext_table.add_column("Trigger Sources", justify="right", width=16)
|
|
184
|
+
ext_table.add_column("Rationale")
|
|
185
|
+
|
|
186
|
+
for r in ext_items:
|
|
187
|
+
ext_table.add_row(r.name, r.provenance.value, str(len(r.sources)), r.rationale)
|
|
188
|
+
|
|
189
|
+
console.print(ext_table)
|
|
190
|
+
console.print()
|
|
191
|
+
|
|
192
|
+
pkg_items = [r for r in plan.requirements if r.kind == "package"]
|
|
193
|
+
pkg_table = Table(
|
|
194
|
+
title="Required Python Packages", show_header=True, header_style="bold yellow"
|
|
195
|
+
)
|
|
196
|
+
pkg_table.add_column("Package Spec", style="bold yellow", width=28)
|
|
197
|
+
pkg_table.add_column("Provenance", width=22)
|
|
198
|
+
pkg_table.add_column("Trigger Sources", justify="right", width=16)
|
|
199
|
+
pkg_table.add_column("Rationale")
|
|
200
|
+
|
|
201
|
+
for r in pkg_items:
|
|
202
|
+
pkg_table.add_row(r.name, r.provenance.value, str(len(r.sources)), r.rationale)
|
|
203
|
+
|
|
204
|
+
console.print(pkg_table)
|
|
205
|
+
console.print()
|
|
206
|
+
|
|
207
|
+
if plan.proposed_sphinx_config:
|
|
208
|
+
cfg = plan.proposed_sphinx_config
|
|
209
|
+
target_str = cfg.theme.target_theme or "MANUAL REVIEW"
|
|
210
|
+
console.print("[bold cyan]Sphinx Configuration Proposal:[/bold cyan]")
|
|
211
|
+
console.print(
|
|
212
|
+
f" • Theme: [magenta]{cfg.theme.source_theme}[/magenta] -> [bold green]{target_str}[/bold green] ({cfg.theme.rationale})"
|
|
213
|
+
)
|
|
214
|
+
console.print(
|
|
215
|
+
f" • Enabled MyST Extensions: {', '.join(cfg.myst_enable_extensions)}"
|
|
216
|
+
)
|
|
217
|
+
console.print(
|
|
218
|
+
f" • Plan Canonical Hash: [dim]{plan.canonical_hash()[:16]}[/dim]"
|
|
219
|
+
)
|
|
220
|
+
console.print()
|
|
221
|
+
|
|
222
|
+
if plan.manual_action_items:
|
|
223
|
+
console.print("[bold yellow]Actionable Manual Review Items:[/bold yellow]")
|
|
224
|
+
manual_table = Table(show_header=True, header_style="bold yellow")
|
|
225
|
+
manual_table.add_column("Location", style="cyan", width=26)
|
|
226
|
+
manual_table.add_column("Construct", style="magenta", width=18)
|
|
227
|
+
manual_table.add_column("Instruction & Rationale")
|
|
228
|
+
|
|
229
|
+
for item in plan.manual_action_items:
|
|
230
|
+
desc = f"[bold]{item.instruction}[/bold] - [dim]{item.rationale}[/dim]"
|
|
231
|
+
manual_table.add_row(
|
|
232
|
+
f"{item.source_file}:{item.line_number}", item.construct_type, desc
|
|
233
|
+
)
|
|
234
|
+
console.print(manual_table)
|
|
235
|
+
console.print()
|
|
236
|
+
|
|
237
|
+
if plan.obsolete_files:
|
|
238
|
+
console.print("[bold yellow]Obsolete MkDocs Files to Remove:[/bold yellow]")
|
|
239
|
+
for f in plan.obsolete_files:
|
|
240
|
+
console.print(f" • [yellow]{f}[/yellow]")
|
|
241
|
+
console.print()
|
|
242
|
+
|
|
243
|
+
console.print(
|
|
244
|
+
"[dim]Plan generated deterministically. No disk changes were applied.[/dim]"
|
|
245
|
+
)
|
|
246
|
+
console.print()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@main.command()
|
|
250
|
+
@click.argument(
|
|
251
|
+
"project_path",
|
|
252
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
253
|
+
default=".",
|
|
254
|
+
)
|
|
255
|
+
@click.option(
|
|
256
|
+
"--apply",
|
|
257
|
+
"write_to_disk",
|
|
258
|
+
is_flag=True,
|
|
259
|
+
default=False,
|
|
260
|
+
help="Write transformed files and conf.py to disk.",
|
|
261
|
+
)
|
|
262
|
+
@click.option(
|
|
263
|
+
"--force-conf",
|
|
264
|
+
"overwrite_conf",
|
|
265
|
+
is_flag=True,
|
|
266
|
+
default=False,
|
|
267
|
+
help="Overwrite existing conflicting conf.py if present.",
|
|
268
|
+
)
|
|
269
|
+
@click.option(
|
|
270
|
+
"--diff",
|
|
271
|
+
"show_diff",
|
|
272
|
+
is_flag=True,
|
|
273
|
+
default=False,
|
|
274
|
+
help="Display unified diff of document transformations.",
|
|
275
|
+
)
|
|
276
|
+
@click.option(
|
|
277
|
+
"--validate/--no-validate",
|
|
278
|
+
"run_validation",
|
|
279
|
+
default=True,
|
|
280
|
+
help="Validate transformed Markdown structure and Sphinx config.",
|
|
281
|
+
)
|
|
282
|
+
@click.option(
|
|
283
|
+
"--build/--no-build",
|
|
284
|
+
"run_sphinx_build",
|
|
285
|
+
default=True,
|
|
286
|
+
help="Run an actual isolated Sphinx HTML build during validation.",
|
|
287
|
+
)
|
|
288
|
+
@click.option(
|
|
289
|
+
"--strict",
|
|
290
|
+
"strict_warnings",
|
|
291
|
+
is_flag=True,
|
|
292
|
+
default=False,
|
|
293
|
+
help="Fail validation if Sphinx produces any warnings (-W).",
|
|
294
|
+
)
|
|
295
|
+
def migrate(
|
|
296
|
+
project_path: Path,
|
|
297
|
+
write_to_disk: bool,
|
|
298
|
+
overwrite_conf: bool,
|
|
299
|
+
show_diff: bool,
|
|
300
|
+
run_validation: bool,
|
|
301
|
+
run_sphinx_build: bool,
|
|
302
|
+
strict_warnings: bool,
|
|
303
|
+
):
|
|
304
|
+
"""Executes deterministic MyST transformation, Sphinx scaffolding, and validation."""
|
|
305
|
+
mode_str = (
|
|
306
|
+
"[bold green]APPLYING[/bold green]"
|
|
307
|
+
if write_to_disk
|
|
308
|
+
else "[bold cyan]DRY-RUN TRANSFORMATION[/bold cyan]"
|
|
309
|
+
)
|
|
310
|
+
console.print(f"{mode_str} for: {project_path.resolve()}")
|
|
311
|
+
console.print()
|
|
312
|
+
|
|
313
|
+
# 1. Generate plan
|
|
314
|
+
planner = MigrationPlanner(project_path)
|
|
315
|
+
plan = planner.create_plan()
|
|
316
|
+
|
|
317
|
+
# 2. Execute Transformation
|
|
318
|
+
engine = TransformationEngine(plan)
|
|
319
|
+
report = engine.execute(write_to_disk=write_to_disk, overwrite_conf=overwrite_conf)
|
|
320
|
+
|
|
321
|
+
res_table = Table(
|
|
322
|
+
title="Document Transformations", show_header=True, header_style="bold magenta"
|
|
323
|
+
)
|
|
324
|
+
res_table.add_column("Document", style="cyan", width=30)
|
|
325
|
+
res_table.add_column("Transforms Applied", justify="right", width=20)
|
|
326
|
+
res_table.add_column("Status", width=18)
|
|
327
|
+
|
|
328
|
+
for doc in report.transformed_documents:
|
|
329
|
+
status_color = (
|
|
330
|
+
"green"
|
|
331
|
+
if doc.status.value == "APPLIED"
|
|
332
|
+
else ("yellow" if doc.status.value == "MANUAL_REQUIRED" else "blue")
|
|
333
|
+
)
|
|
334
|
+
res_table.add_row(
|
|
335
|
+
doc.source_file,
|
|
336
|
+
str(doc.transforms_applied),
|
|
337
|
+
f"[{status_color}]{doc.status.value}[/{status_color}]",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
console.print(res_table)
|
|
341
|
+
console.print()
|
|
342
|
+
|
|
343
|
+
# Report conf.py status
|
|
344
|
+
if report.conf_py_status.value == "CONFLICT":
|
|
345
|
+
console.print(
|
|
346
|
+
"[bold yellow]conf.py Conflict:[/bold yellow] Existing conf.py on disk differs from planned configuration."
|
|
347
|
+
)
|
|
348
|
+
if not overwrite_conf:
|
|
349
|
+
console.print(
|
|
350
|
+
"[dim yellow]Preserved existing conf.py on disk. Pass --force-conf to overwrite.[/dim yellow]"
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
console.print(
|
|
354
|
+
"[bold red]Overwrote existing conf.py due to --force-conf flag.[/bold red]"
|
|
355
|
+
)
|
|
356
|
+
elif report.conf_py_status.value == "CREATED":
|
|
357
|
+
console.print(
|
|
358
|
+
"[green]conf.py Scaffolding:[/green] New Sphinx configuration planned/created."
|
|
359
|
+
)
|
|
360
|
+
elif report.conf_py_status.value == "UNCHANGED":
|
|
361
|
+
console.print(
|
|
362
|
+
"[dim green]conf.py Scaffolding:[/dim green] Existing conf.py on disk is identical to plan."
|
|
363
|
+
)
|
|
364
|
+
console.print()
|
|
365
|
+
|
|
366
|
+
if report.total_stale_actions > 0:
|
|
367
|
+
console.print(
|
|
368
|
+
f"[bold yellow]Stale Actions Detected:[/bold yellow] {report.total_stale_actions} planned transformation(s) were skipped because source files changed."
|
|
369
|
+
)
|
|
370
|
+
for doc in report.transformed_documents:
|
|
371
|
+
for detail in doc.stale_action_details:
|
|
372
|
+
console.print(f" • [yellow]{detail}[/yellow]")
|
|
373
|
+
console.print()
|
|
374
|
+
|
|
375
|
+
if show_diff:
|
|
376
|
+
for doc in report.transformed_documents:
|
|
377
|
+
if doc.diff:
|
|
378
|
+
console.print(f"[bold yellow]Diff for {doc.source_file}:[/bold yellow]")
|
|
379
|
+
syntax = Syntax(doc.diff, "diff", theme="monokai", line_numbers=False)
|
|
380
|
+
console.print(syntax)
|
|
381
|
+
console.print()
|
|
382
|
+
|
|
383
|
+
# 3. Validation Step
|
|
384
|
+
if run_validation:
|
|
385
|
+
validator = TransformationValidator()
|
|
386
|
+
v_report = validator.validate_transformation_report(
|
|
387
|
+
report, run_sphinx_build=run_sphinx_build, strict_warnings=strict_warnings
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if v_report.passed:
|
|
391
|
+
build_str = (
|
|
392
|
+
" (with isolated Sphinx build verified)"
|
|
393
|
+
if run_sphinx_build
|
|
394
|
+
else " (structural validation only)"
|
|
395
|
+
)
|
|
396
|
+
console.print(
|
|
397
|
+
f"[bold green]✔ Validation Passed:[/bold green] All transformed directives and Sphinx conf.py are valid{build_str}."
|
|
398
|
+
)
|
|
399
|
+
if run_sphinx_build and v_report.sphinx_warning_count > 0:
|
|
400
|
+
console.print(
|
|
401
|
+
f"[dim yellow]Sphinx Warnings ({v_report.sphinx_warning_count}): Run with --strict to treat as errors.[/dim yellow]"
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
console.print(
|
|
405
|
+
f"[bold red]✖ Validation Failed:[/bold red] {v_report.errors_count} error(s) detected."
|
|
406
|
+
)
|
|
407
|
+
for issue in v_report.issues:
|
|
408
|
+
console.print(
|
|
409
|
+
f" • [{issue.severity.value}] {issue.file_path}:{issue.line_number or ''} - {issue.message}"
|
|
410
|
+
)
|
|
411
|
+
console.print()
|
|
412
|
+
|
|
413
|
+
console.print("[bold green]Transformation Execution Complete:[/bold green]")
|
|
414
|
+
console.print(f" • Documents Examined: {report.documents_examined}")
|
|
415
|
+
console.print(f" • Documents Changed: {report.documents_changed}")
|
|
416
|
+
console.print(f" • Files Written to Disk: {report.files_written_to_disk}")
|
|
417
|
+
console.print(
|
|
418
|
+
f" • Total Transformations Executed: {report.total_transforms_executed}"
|
|
419
|
+
)
|
|
420
|
+
console.print(f" • Stale Transformations Skipped: {report.total_stale_actions}")
|
|
421
|
+
if report.cleaned_files:
|
|
422
|
+
console.print(f" • Obsolete MkDocs Files Removed: {len(report.cleaned_files)}")
|
|
423
|
+
for cf in report.cleaned_files:
|
|
424
|
+
console.print(f" - [yellow]{cf}[/yellow]")
|
|
425
|
+
console.print()
|
|
426
|
+
if not write_to_disk:
|
|
427
|
+
console.print(
|
|
428
|
+
"[dim]Dry-run mode: files were not modified. Pass --apply to write changes to disk.[/dim]"
|
|
429
|
+
)
|
|
430
|
+
console.print()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@main.command()
|
|
434
|
+
@click.argument(
|
|
435
|
+
"project_path",
|
|
436
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
437
|
+
default=".",
|
|
438
|
+
)
|
|
439
|
+
@click.option(
|
|
440
|
+
"--build/--no-build",
|
|
441
|
+
"run_sphinx_build",
|
|
442
|
+
default=True,
|
|
443
|
+
help="Run an actual isolated Sphinx HTML build during validation.",
|
|
444
|
+
)
|
|
445
|
+
@click.option(
|
|
446
|
+
"--strict",
|
|
447
|
+
"strict_warnings",
|
|
448
|
+
is_flag=True,
|
|
449
|
+
default=False,
|
|
450
|
+
help="Fail validation if Sphinx produces any warnings (-W).",
|
|
451
|
+
)
|
|
452
|
+
@click.option(
|
|
453
|
+
"--show-warnings",
|
|
454
|
+
"show_warnings",
|
|
455
|
+
is_flag=True,
|
|
456
|
+
default=False,
|
|
457
|
+
help="Display detailed Sphinx warnings in validation report.",
|
|
458
|
+
)
|
|
459
|
+
def validate(
|
|
460
|
+
project_path: Path,
|
|
461
|
+
run_sphinx_build: bool,
|
|
462
|
+
strict_warnings: bool,
|
|
463
|
+
show_warnings: bool,
|
|
464
|
+
):
|
|
465
|
+
"""Validates existing or migrated Sphinx configuration and documents."""
|
|
466
|
+
console.print(
|
|
467
|
+
f"[bold blue]Validating documentation structure for:[/bold blue] {project_path.resolve()}"
|
|
468
|
+
)
|
|
469
|
+
console.print()
|
|
470
|
+
planner = MigrationPlanner(project_path)
|
|
471
|
+
plan = planner.create_plan()
|
|
472
|
+
engine = TransformationEngine(plan)
|
|
473
|
+
report = engine.execute(write_to_disk=False)
|
|
474
|
+
validator = TransformationValidator()
|
|
475
|
+
v_report = validator.validate_transformation_report(
|
|
476
|
+
report, run_sphinx_build=run_sphinx_build, strict_warnings=strict_warnings
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if v_report.passed:
|
|
480
|
+
build_str = (
|
|
481
|
+
" (with isolated Sphinx build verified)"
|
|
482
|
+
if run_sphinx_build
|
|
483
|
+
else " (structural validation only)"
|
|
484
|
+
)
|
|
485
|
+
console.print(
|
|
486
|
+
f"[bold green]✔ Validation Passed:[/bold green] All directives and conf.py are valid{build_str}."
|
|
487
|
+
)
|
|
488
|
+
if run_sphinx_build and v_report.sphinx_warning_count > 0:
|
|
489
|
+
console.print(
|
|
490
|
+
f"[dim yellow]Sphinx Warnings ({v_report.sphinx_warning_count}): Run with --strict to treat as errors.[/dim yellow]"
|
|
491
|
+
)
|
|
492
|
+
if show_warnings:
|
|
493
|
+
for idx, w in enumerate(v_report.sphinx_warnings, 1):
|
|
494
|
+
console.print(f" [yellow]•[/yellow] [dim]{w.strip()}[/dim]")
|
|
495
|
+
else:
|
|
496
|
+
console.print(
|
|
497
|
+
f"[bold red]✖ Validation Failed:[/bold red] {v_report.errors_count} error(s) detected."
|
|
498
|
+
)
|
|
499
|
+
for issue in v_report.issues:
|
|
500
|
+
console.print(
|
|
501
|
+
f" • [{issue.severity.value}] {issue.file_path}:{issue.line_number or ''} - {issue.message}"
|
|
502
|
+
)
|
|
503
|
+
sys.exit(1)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
if __name__ == "__main__":
|
|
507
|
+
main()
|