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,263 @@
|
|
|
1
|
+
"""Subsystem analyzer for mkdocs.yml configuration with robust YAML tag handling."""
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List, Any, Dict
|
|
6
|
+
from .models import ConfigAnalysis, ThemePalette, ThemeFont
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SafeMkDocsLoader(yaml.SafeLoader):
|
|
10
|
+
"""Custom YAML loader ignoring python-specific tags and environment constructors in mkdocs.yml."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Ignore all unknown custom YAML tags like !!python/name, !ENV, !relative, etc.
|
|
16
|
+
def _ignore_unknown_tags(loader: yaml.SafeLoader, tag_suffix: str, node: yaml.Node):
|
|
17
|
+
if isinstance(node, yaml.ScalarNode):
|
|
18
|
+
return loader.construct_scalar(node)
|
|
19
|
+
elif isinstance(node, yaml.SequenceNode):
|
|
20
|
+
return loader.construct_sequence(node)
|
|
21
|
+
elif isinstance(node, yaml.MappingNode):
|
|
22
|
+
return loader.construct_mapping(node)
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SafeMkDocsLoader.add_multi_constructor("!", _ignore_unknown_tags)
|
|
27
|
+
SafeMkDocsLoader.add_multi_constructor(
|
|
28
|
+
"tag:yaml.org,2002:python/", _ignore_unknown_tags
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MkDocsConfigAnalyzer:
|
|
33
|
+
def __init__(self, project_root: Path):
|
|
34
|
+
self.project_root = project_root
|
|
35
|
+
|
|
36
|
+
def _find_config_file(self) -> Optional[Path]:
|
|
37
|
+
"""Discovers mkdocs.yml in root or common documentation subdirectories."""
|
|
38
|
+
for candidate in [
|
|
39
|
+
"mkdocs.yml",
|
|
40
|
+
"mkdocs.yaml",
|
|
41
|
+
"docs/en/mkdocs.yml",
|
|
42
|
+
"docs/mkdocs.yml",
|
|
43
|
+
".mkdocs.yml",
|
|
44
|
+
"mkdocs.template.yml",
|
|
45
|
+
]:
|
|
46
|
+
path = self.project_root / candidate
|
|
47
|
+
if path.exists():
|
|
48
|
+
return path
|
|
49
|
+
# Fallback search
|
|
50
|
+
all_ymls = list(self.project_root.glob("*mkdocs*.yml")) + list(
|
|
51
|
+
self.project_root.glob("*mkdocs*.yaml")
|
|
52
|
+
)
|
|
53
|
+
if all_ymls:
|
|
54
|
+
return all_ymls[0]
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def analyze(self) -> ConfigAnalysis:
|
|
58
|
+
mkdocs_file = self._find_config_file()
|
|
59
|
+
if not mkdocs_file or not mkdocs_file.exists():
|
|
60
|
+
return ConfigAnalysis()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
content = mkdocs_file.read_text(encoding="utf-8")
|
|
64
|
+
data = yaml.load(content, Loader=SafeMkDocsLoader) or {}
|
|
65
|
+
except Exception:
|
|
66
|
+
try:
|
|
67
|
+
# Fallback simple load
|
|
68
|
+
data = yaml.safe_load(mkdocs_file.read_text(encoding="utf-8")) or {}
|
|
69
|
+
except Exception:
|
|
70
|
+
return ConfigAnalysis()
|
|
71
|
+
|
|
72
|
+
theme_data = data.get("theme", {})
|
|
73
|
+
theme_logo = None
|
|
74
|
+
theme_icon = None
|
|
75
|
+
theme_favicon = None
|
|
76
|
+
theme_language = None
|
|
77
|
+
theme_font = None
|
|
78
|
+
theme_palette: List[ThemePalette] = []
|
|
79
|
+
|
|
80
|
+
if isinstance(theme_data, str):
|
|
81
|
+
theme_name = theme_data
|
|
82
|
+
features = []
|
|
83
|
+
elif isinstance(theme_data, dict):
|
|
84
|
+
theme_name = theme_data.get("name", "mkdocs")
|
|
85
|
+
features = theme_data.get("features", [])
|
|
86
|
+
theme_logo = theme_data.get("logo")
|
|
87
|
+
theme_icon = (
|
|
88
|
+
theme_data.get("icon")
|
|
89
|
+
if isinstance(theme_data.get("icon"), dict)
|
|
90
|
+
else None
|
|
91
|
+
)
|
|
92
|
+
theme_favicon = theme_data.get("favicon")
|
|
93
|
+
theme_language = theme_data.get("language")
|
|
94
|
+
|
|
95
|
+
# Palette parsing (can be a dict or list of dicts in Material for MkDocs)
|
|
96
|
+
pal = theme_data.get("palette")
|
|
97
|
+
if isinstance(pal, dict):
|
|
98
|
+
theme_palette.append(
|
|
99
|
+
ThemePalette(
|
|
100
|
+
scheme=pal.get("scheme"),
|
|
101
|
+
primary=pal.get("primary"),
|
|
102
|
+
accent=pal.get("accent"),
|
|
103
|
+
toggle_icon=pal.get("toggle", {}).get("icon")
|
|
104
|
+
if isinstance(pal.get("toggle"), dict)
|
|
105
|
+
else None,
|
|
106
|
+
toggle_name=pal.get("toggle", {}).get("name")
|
|
107
|
+
if isinstance(pal.get("toggle"), dict)
|
|
108
|
+
else None,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
elif isinstance(pal, list):
|
|
112
|
+
for p in pal:
|
|
113
|
+
if isinstance(p, dict):
|
|
114
|
+
theme_palette.append(
|
|
115
|
+
ThemePalette(
|
|
116
|
+
scheme=p.get("scheme"),
|
|
117
|
+
primary=p.get("primary"),
|
|
118
|
+
accent=p.get("accent"),
|
|
119
|
+
toggle_icon=p.get("toggle", {}).get("icon")
|
|
120
|
+
if isinstance(p.get("toggle"), dict)
|
|
121
|
+
else None,
|
|
122
|
+
toggle_name=p.get("toggle", {}).get("name")
|
|
123
|
+
if isinstance(p.get("toggle"), dict)
|
|
124
|
+
else None,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Font parsing
|
|
129
|
+
font_data = theme_data.get("font")
|
|
130
|
+
if isinstance(font_data, dict):
|
|
131
|
+
theme_font = ThemeFont(
|
|
132
|
+
text=font_data.get("text"), code=font_data.get("code")
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
theme_name = "mkdocs"
|
|
136
|
+
features = []
|
|
137
|
+
|
|
138
|
+
plugins: List[str] = []
|
|
139
|
+
plugins_config: Dict[str, Any] = {}
|
|
140
|
+
for p in data.get("plugins", []):
|
|
141
|
+
if isinstance(p, str):
|
|
142
|
+
plugins.append(p)
|
|
143
|
+
plugins_config[p] = {}
|
|
144
|
+
elif isinstance(p, dict):
|
|
145
|
+
for k, v in p.items():
|
|
146
|
+
plugins.append(k)
|
|
147
|
+
plugins_config[k] = v if isinstance(v, dict) else {}
|
|
148
|
+
hooks = data.get("hooks", [])
|
|
149
|
+
|
|
150
|
+
# Normalize markdown_extensions list (strings or {name: dict_options})
|
|
151
|
+
raw_md_exts = data.get("markdown_extensions", [])
|
|
152
|
+
normalized_md_exts: List[str] = []
|
|
153
|
+
if isinstance(raw_md_exts, list):
|
|
154
|
+
for ext in raw_md_exts:
|
|
155
|
+
if isinstance(ext, str):
|
|
156
|
+
normalized_md_exts.append(ext)
|
|
157
|
+
elif isinstance(ext, dict):
|
|
158
|
+
normalized_md_exts.extend(list(ext.keys()))
|
|
159
|
+
|
|
160
|
+
# Determine docs_dir: if config is in a subdirectory (e.g. docs/en/mkdocs.yml), adjust relative path
|
|
161
|
+
raw_docs_dir = data.get("docs_dir", "docs")
|
|
162
|
+
if mkdocs_file.parent != self.project_root:
|
|
163
|
+
rel_parent = mkdocs_file.parent.relative_to(self.project_root).as_posix()
|
|
164
|
+
if raw_docs_dir == "docs":
|
|
165
|
+
adjusted_docs_dir = (
|
|
166
|
+
f"{rel_parent}/docs"
|
|
167
|
+
if (self.project_root / rel_parent / "docs").exists()
|
|
168
|
+
else rel_parent
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
adjusted_docs_dir = f"{rel_parent}/{raw_docs_dir}"
|
|
172
|
+
else:
|
|
173
|
+
adjusted_docs_dir = raw_docs_dir
|
|
174
|
+
|
|
175
|
+
site_name = (
|
|
176
|
+
data.get("site_name") or self.project_root.name.replace("-", " ").title()
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Extra CSS & JS
|
|
180
|
+
extra_css = data.get("extra_css", [])
|
|
181
|
+
if isinstance(extra_css, str):
|
|
182
|
+
extra_css = [extra_css]
|
|
183
|
+
extra_js = data.get("extra_javascript", [])
|
|
184
|
+
if isinstance(extra_js, str):
|
|
185
|
+
extra_js = [extra_js]
|
|
186
|
+
extra = data.get("extra", {}) if isinstance(data.get("extra"), dict) else {}
|
|
187
|
+
|
|
188
|
+
return ConfigAnalysis(
|
|
189
|
+
site_name=site_name,
|
|
190
|
+
site_description=data.get("site_description"),
|
|
191
|
+
site_author=data.get("site_author"),
|
|
192
|
+
site_url=data.get("site_url"),
|
|
193
|
+
repo_url=data.get("repo_url"),
|
|
194
|
+
repo_name=data.get("repo_name"),
|
|
195
|
+
edit_uri=data.get("edit_uri"),
|
|
196
|
+
copyright=data.get("copyright"),
|
|
197
|
+
docs_dir=adjusted_docs_dir,
|
|
198
|
+
theme_name=theme_name,
|
|
199
|
+
theme_logo=theme_logo,
|
|
200
|
+
theme_icon=theme_icon,
|
|
201
|
+
theme_favicon=theme_favicon,
|
|
202
|
+
theme_language=theme_language,
|
|
203
|
+
theme_palette=theme_palette,
|
|
204
|
+
theme_font=theme_font,
|
|
205
|
+
theme_features=features,
|
|
206
|
+
plugins=plugins,
|
|
207
|
+
plugins_config=plugins_config,
|
|
208
|
+
markdown_extensions=normalized_md_exts,
|
|
209
|
+
nav_raw=data.get("nav"),
|
|
210
|
+
custom_hooks=hooks,
|
|
211
|
+
extra_css=extra_css,
|
|
212
|
+
extra_javascript=extra_js,
|
|
213
|
+
extra=extra,
|
|
214
|
+
raw_config_keys=list(data.keys()) if isinstance(data, dict) else [],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def detect_obsolete_generator_scripts(
|
|
219
|
+
project_root: Path,
|
|
220
|
+
mkdocs_config: Optional[ConfigAnalysis],
|
|
221
|
+
additional_scripts: Optional[List[str]] = None,
|
|
222
|
+
) -> List[str]:
|
|
223
|
+
"""Detect obsolete MkDocs generator scripts and hooks (e.g. scripts/gen_ref_nav.py) that should be removed.
|
|
224
|
+
|
|
225
|
+
These scripts run at build time under MkDocs (e.g., mkdocs-gen-files) to generate virtual stubs.
|
|
226
|
+
Once Sphinx autodoc/autosummary is configured and MkDocs dependencies are removed, these scripts
|
|
227
|
+
become obsolete, broken, and trigger repo lint failures.
|
|
228
|
+
"""
|
|
229
|
+
if not mkdocs_config:
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
candidate_scripts: set[str] = set()
|
|
233
|
+
gen_cfg = mkdocs_config.plugins_config.get("gen-files", {})
|
|
234
|
+
if isinstance(gen_cfg, dict):
|
|
235
|
+
for s in gen_cfg.get("scripts", []):
|
|
236
|
+
if isinstance(s, str):
|
|
237
|
+
candidate_scripts.add(s)
|
|
238
|
+
|
|
239
|
+
if additional_scripts:
|
|
240
|
+
for s in additional_scripts:
|
|
241
|
+
if isinstance(s, str):
|
|
242
|
+
candidate_scripts.add(s)
|
|
243
|
+
|
|
244
|
+
for hook in mkdocs_config.custom_hooks or []:
|
|
245
|
+
if isinstance(hook, str):
|
|
246
|
+
candidate_scripts.add(hook)
|
|
247
|
+
|
|
248
|
+
obsolete: List[str] = []
|
|
249
|
+
for s_rel in sorted(candidate_scripts):
|
|
250
|
+
s_path = project_root / s_rel
|
|
251
|
+
if s_path.is_file():
|
|
252
|
+
try:
|
|
253
|
+
content = s_path.read_text(encoding="utf-8")
|
|
254
|
+
except Exception:
|
|
255
|
+
content = ""
|
|
256
|
+
if (
|
|
257
|
+
"mkdocs" in content
|
|
258
|
+
or "mkdocstrings" in content
|
|
259
|
+
or (isinstance(gen_cfg, dict) and s_rel in gen_cfg.get("scripts", []))
|
|
260
|
+
):
|
|
261
|
+
obsolete.append(s_rel)
|
|
262
|
+
|
|
263
|
+
return obsolete
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Data models for migration analysis, findings, effective specifications, and migration requirements."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List, Dict, Any, Optional
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from ..parsing.doc_ir import (
|
|
10
|
+
DocumentationSiteGraph,
|
|
11
|
+
DocumentationBuildGraph,
|
|
12
|
+
DocumentFlowSpec,
|
|
13
|
+
ApiDocumentationRequest,
|
|
14
|
+
DOCUMENTATION_IR_SCHEMA_VERSION,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Classification(str, Enum):
|
|
19
|
+
PRESERVE = "PRESERVE"
|
|
20
|
+
TRANSFORM = "TRANSFORM"
|
|
21
|
+
MANUAL = "MANUAL"
|
|
22
|
+
UNSUPPORTED = "UNSUPPORTED"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RequirementDisposition(str, Enum):
|
|
26
|
+
PRESERVE = "PRESERVE"
|
|
27
|
+
TRANSFORM = "TRANSFORM"
|
|
28
|
+
GENERATE = "GENERATE"
|
|
29
|
+
COPY = "COPY"
|
|
30
|
+
CONFIGURE = "CONFIGURE"
|
|
31
|
+
ACCOUNT_NO_DIRECT_EQUIVALENT = "ACCOUNT_NO_DIRECT_EQUIVALENT"
|
|
32
|
+
MANUAL_ACTION = "MANUAL_ACTION"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RequirementCategory(str, Enum):
|
|
36
|
+
FLOW = "FLOW"
|
|
37
|
+
API = "API"
|
|
38
|
+
THEME_FEATURE = "THEME_FEATURE"
|
|
39
|
+
MARKDOWN_EXTENSION = "MARKDOWN_EXTENSION"
|
|
40
|
+
PLUGIN = "PLUGIN"
|
|
41
|
+
NAVIGATION = "NAVIGATION"
|
|
42
|
+
BUILD_GRAPH = "BUILD_GRAPH"
|
|
43
|
+
ASSET = "ASSET"
|
|
44
|
+
CONFIG = "CONFIG"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MigrationRequirement(BaseModel):
|
|
48
|
+
"""Explicit requirement derived during analysis that must be realized by the migration plan."""
|
|
49
|
+
|
|
50
|
+
requirement_id: str
|
|
51
|
+
category: RequirementCategory
|
|
52
|
+
disposition: RequirementDisposition
|
|
53
|
+
source_construct: str
|
|
54
|
+
required_outcome: str
|
|
55
|
+
rationale: str
|
|
56
|
+
verification_check: Optional[str] = None
|
|
57
|
+
provenance_location: Optional[str] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PropertyResolutionState(str, Enum):
|
|
61
|
+
CONFIGURED = "CONFIGURED" # Explicitly declared in mkdocs.yml
|
|
62
|
+
DEFAULTED = "DEFAULTED" # Inferred from package / theme default
|
|
63
|
+
DERIVED = "DERIVED" # Computed from another configuration property
|
|
64
|
+
GENERATED = "GENERATED" # Generated by plugin / build step
|
|
65
|
+
UNRESOLVED = "UNRESOLVED" # Unknown / cannot resolve
|
|
66
|
+
UNSUPPORTED = "UNSUPPORTED" # Not supported in migration target
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ResolutionProvenance(BaseModel):
|
|
70
|
+
source_type: (
|
|
71
|
+
str # e.g., "mkdocs.yml", "package_default", "theme_default", "derived"
|
|
72
|
+
)
|
|
73
|
+
source_location: Optional[str] = None
|
|
74
|
+
package_name: Optional[str] = None
|
|
75
|
+
package_version: Optional[str] = None
|
|
76
|
+
rationale: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ResolvedProperty(BaseModel):
|
|
80
|
+
key: str
|
|
81
|
+
configured_value: Optional[Any] = None
|
|
82
|
+
default_value: Optional[Any] = None
|
|
83
|
+
effective_value: Any
|
|
84
|
+
state: PropertyResolutionState
|
|
85
|
+
provenance: ResolutionProvenance
|
|
86
|
+
description: Optional[str] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ResolvedEffectiveConfig(BaseModel):
|
|
90
|
+
properties: Dict[str, ResolvedProperty] = Field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
93
|
+
prop = self.properties.get(key)
|
|
94
|
+
if prop is not None:
|
|
95
|
+
return prop.effective_value
|
|
96
|
+
return default
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ResolvedApiSymbol(BaseModel):
|
|
100
|
+
fully_qualified_name: str
|
|
101
|
+
symbol_kind: str # module, class, function, method, attribute, exception
|
|
102
|
+
docstring: Optional[str] = None
|
|
103
|
+
signature: Optional[str] = None
|
|
104
|
+
is_public: bool = True
|
|
105
|
+
parent_symbol: Optional[str] = None
|
|
106
|
+
source_file: Optional[str] = None
|
|
107
|
+
source_line: Optional[int] = None
|
|
108
|
+
member_names: List[str] = Field(default_factory=list)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ResolvedApiModule(BaseModel):
|
|
112
|
+
module_path: str
|
|
113
|
+
docstring: Optional[str] = None
|
|
114
|
+
symbols: Dict[str, ResolvedApiSymbol] = Field(default_factory=dict)
|
|
115
|
+
summary_mode: str = "NOT_REQUESTED"
|
|
116
|
+
explicit_members: List[str] = Field(default_factory=list)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ResolvedCapabilityItem(BaseModel):
|
|
120
|
+
capability_name: str
|
|
121
|
+
source_feature: str
|
|
122
|
+
state: PropertyResolutionState
|
|
123
|
+
target_strategy: str
|
|
124
|
+
details: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ResolvedUnresolvedItem(BaseModel):
|
|
128
|
+
item_id: str
|
|
129
|
+
category: str
|
|
130
|
+
raw_snippet: Optional[str] = None
|
|
131
|
+
rationale: str
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ConstructFinding(BaseModel):
|
|
135
|
+
category: str
|
|
136
|
+
construct_type: str
|
|
137
|
+
file_path: str
|
|
138
|
+
line_number: int
|
|
139
|
+
end_line_number: Optional[int] = None
|
|
140
|
+
raw_snippet: str
|
|
141
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
142
|
+
classification: Classification = Classification.TRANSFORM
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class MigrationRule(BaseModel):
|
|
146
|
+
source_construct: str
|
|
147
|
+
target_directive_or_construct: str
|
|
148
|
+
required_sphinx_extensions: List[str] = Field(default_factory=list)
|
|
149
|
+
description: str
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ThemePalette(BaseModel):
|
|
153
|
+
scheme: Optional[str] = None
|
|
154
|
+
primary: Optional[str] = None
|
|
155
|
+
accent: Optional[str] = None
|
|
156
|
+
toggle_icon: Optional[str] = None
|
|
157
|
+
toggle_name: Optional[str] = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ThemeFont(BaseModel):
|
|
161
|
+
text: Optional[str] = None
|
|
162
|
+
code: Optional[str] = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ConfigAnalysis(BaseModel):
|
|
166
|
+
site_name: Optional[str] = None
|
|
167
|
+
site_description: Optional[str] = None
|
|
168
|
+
site_author: Optional[str] = None
|
|
169
|
+
site_url: Optional[str] = None
|
|
170
|
+
repo_url: Optional[str] = None
|
|
171
|
+
repo_name: Optional[str] = None
|
|
172
|
+
edit_uri: Optional[str] = None
|
|
173
|
+
copyright: Optional[str] = None
|
|
174
|
+
docs_dir: str = "docs"
|
|
175
|
+
theme_name: str = "mkdocs"
|
|
176
|
+
theme_logo: Optional[str] = None
|
|
177
|
+
theme_icon: Optional[Dict[str, Any]] = None
|
|
178
|
+
theme_favicon: Optional[str] = None
|
|
179
|
+
theme_language: Optional[str] = None
|
|
180
|
+
theme_palette: List[ThemePalette] = Field(default_factory=list)
|
|
181
|
+
theme_font: Optional[ThemeFont] = None
|
|
182
|
+
theme_features: List[str] = Field(default_factory=list)
|
|
183
|
+
plugins: List[str] = Field(default_factory=list)
|
|
184
|
+
plugins_config: Dict[str, Any] = Field(default_factory=dict)
|
|
185
|
+
markdown_extensions: List[str] = Field(default_factory=list)
|
|
186
|
+
nav_raw: Optional[Any] = None
|
|
187
|
+
custom_hooks: List[str] = Field(default_factory=list)
|
|
188
|
+
extra_css: List[str] = Field(default_factory=list)
|
|
189
|
+
extra_javascript: List[str] = Field(default_factory=list)
|
|
190
|
+
extra: Dict[str, Any] = Field(default_factory=dict)
|
|
191
|
+
raw_config_keys: List[str] = Field(default_factory=list)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class NavigationItem(BaseModel):
|
|
195
|
+
title: str
|
|
196
|
+
path: Optional[str] = None
|
|
197
|
+
children: List["NavigationItem"] = Field(default_factory=list)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class NavigationAnalysis(BaseModel):
|
|
201
|
+
has_nav: bool = False
|
|
202
|
+
total_nav_entries: int = 0
|
|
203
|
+
max_depth: int = 0
|
|
204
|
+
missing_references: List[str] = Field(default_factory=list)
|
|
205
|
+
generated_wildcard_references: List[str] = Field(default_factory=list)
|
|
206
|
+
orphan_documents: List[str] = Field(default_factory=list)
|
|
207
|
+
tree: List[NavigationItem] = Field(default_factory=list)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class PackageVersionInfo(BaseModel):
|
|
211
|
+
package_name: str
|
|
212
|
+
declared_spec: str
|
|
213
|
+
resolved_version: Optional[str] = None
|
|
214
|
+
resolution_status: str = (
|
|
215
|
+
"DECLARED_ONLY" # "INSTALLED_RESOLVED" | "DECLARED_ONLY" | "UNRESOLVED"
|
|
216
|
+
)
|
|
217
|
+
resolution_source: Optional[str] = (
|
|
218
|
+
None # e.g., "CURRENT_PYTHON_ENVIRONMENT" | "MANIFEST_CONSTRAINT"
|
|
219
|
+
)
|
|
220
|
+
satisfies_declared_constraint: Optional[bool] = None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class VersionEnvironment(BaseModel):
|
|
224
|
+
python_version: Optional[str] = None # Deprecated alias for backwards compat
|
|
225
|
+
python_constraint: Optional[str] = None
|
|
226
|
+
python_resolved_version: Optional[str] = None
|
|
227
|
+
python_resolution_status: str = (
|
|
228
|
+
"DECLARED_ONLY" # "INSTALLED_RESOLVED" | "DECLARED_ONLY"
|
|
229
|
+
)
|
|
230
|
+
packages: Dict[str, PackageVersionInfo] = Field(default_factory=dict)
|
|
231
|
+
detected_mkdocs_version: Optional[str] = None
|
|
232
|
+
detected_plugin_versions: Dict[str, str] = Field(default_factory=dict)
|
|
233
|
+
target_sphinx_version: str = ">=7.0"
|
|
234
|
+
target_myst_version: str = ">=2.0"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class DependencyAnalysis(BaseModel):
|
|
238
|
+
manifest_type: Optional[str] = None
|
|
239
|
+
detected_packages_to_remove: List[str] = Field(default_factory=list)
|
|
240
|
+
suggested_packages_to_add: List[str] = Field(default_factory=list)
|
|
241
|
+
source_group_type: Optional[str] = None
|
|
242
|
+
source_group_name: Optional[str] = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class CIAnalysis(BaseModel):
|
|
246
|
+
ci_system: Optional[str] = None
|
|
247
|
+
workflow_files: List[str] = Field(default_factory=list)
|
|
248
|
+
docs_workflow_files: List[str] = Field(default_factory=list)
|
|
249
|
+
has_mkdocs_deploy: bool = False
|
|
250
|
+
readthedocs_detected: bool = False
|
|
251
|
+
rtd_config_file: Optional[str] = None
|
|
252
|
+
github_actions_detected: bool = False
|
|
253
|
+
has_tox_in_ci: bool = False
|
|
254
|
+
has_uv_in_ci: bool = False
|
|
255
|
+
has_gh_pages_action: bool = False
|
|
256
|
+
checkout_action_ref: Optional[str] = None
|
|
257
|
+
setup_uv_action_ref: Optional[str] = None
|
|
258
|
+
checkout_pinned_ref: Optional[str] = None
|
|
259
|
+
setup_uv_pinned_ref: Optional[str] = None
|
|
260
|
+
has_tox: bool = False
|
|
261
|
+
tox_file: Optional[str] = None
|
|
262
|
+
tox_has_docs_env: bool = False
|
|
263
|
+
tox_docs_commands: List[str] = Field(default_factory=list)
|
|
264
|
+
tox_dependency_spec: Optional[str] = None
|
|
265
|
+
has_nox: bool = False
|
|
266
|
+
nox_file: Optional[str] = None
|
|
267
|
+
has_makefile: bool = False
|
|
268
|
+
has_pre_commit: bool = False
|
|
269
|
+
gitlab_ci_detected: bool = False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class SubsystemSummary(BaseModel):
|
|
273
|
+
name: str
|
|
274
|
+
status: str
|
|
275
|
+
details: str
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class ProjectAnalysisReport(BaseModel):
|
|
279
|
+
"""Complete, factual and effective analysis of the source MkDocs documentation repository."""
|
|
280
|
+
|
|
281
|
+
schema_version: str = DOCUMENTATION_IR_SCHEMA_VERSION
|
|
282
|
+
project_root: str
|
|
283
|
+
mkdocs_config: Optional[ConfigAnalysis] = None
|
|
284
|
+
effective_config: ResolvedEffectiveConfig = Field(
|
|
285
|
+
default_factory=ResolvedEffectiveConfig
|
|
286
|
+
)
|
|
287
|
+
version_env: Optional[VersionEnvironment] = None
|
|
288
|
+
dependency_analysis: Optional[DependencyAnalysis] = None
|
|
289
|
+
navigation_analysis: Optional[NavigationAnalysis] = None
|
|
290
|
+
ci_analysis: Optional[CIAnalysis] = None
|
|
291
|
+
total_markdown_files: int = 0
|
|
292
|
+
construct_findings: List[ConstructFinding] = Field(default_factory=list)
|
|
293
|
+
document_flows: Dict[str, DocumentFlowSpec] = Field(default_factory=dict)
|
|
294
|
+
documentation_site_graph: Optional[DocumentationSiteGraph] = None
|
|
295
|
+
build_graph: Optional[DocumentationBuildGraph] = None
|
|
296
|
+
api_requests: List[ApiDocumentationRequest] = Field(default_factory=list)
|
|
297
|
+
resolved_api_modules: Dict[str, ResolvedApiModule] = Field(default_factory=dict)
|
|
298
|
+
capabilities: List[ResolvedCapabilityItem] = Field(default_factory=list)
|
|
299
|
+
migration_requirements: List[MigrationRequirement] = Field(default_factory=list)
|
|
300
|
+
unresolved_items: List[ResolvedUnresolvedItem] = Field(default_factory=list)
|
|
301
|
+
subsystem_summaries: List[SubsystemSummary] = Field(default_factory=list)
|
|
302
|
+
manual_action_items: List[str] = Field(default_factory=list)
|
|
303
|
+
|
|
304
|
+
def source_canonical_dict(self) -> Dict[str, Any]:
|
|
305
|
+
"""Returns deterministic source repository semantics excluding ephemeral environment observations."""
|
|
306
|
+
data = self.model_dump()
|
|
307
|
+
# Exclude environment-specific observation details from source canonical identity
|
|
308
|
+
if "version_env" in data and data["version_env"]:
|
|
309
|
+
v = dict(data["version_env"])
|
|
310
|
+
v.pop("python_resolved_version", None)
|
|
311
|
+
v.pop("python_resolution_status", None)
|
|
312
|
+
if "packages" in v and isinstance(v["packages"], dict):
|
|
313
|
+
cleaned_pkgs = {}
|
|
314
|
+
for name, pkg in v["packages"].items():
|
|
315
|
+
if isinstance(pkg, dict):
|
|
316
|
+
p_copy = dict(pkg)
|
|
317
|
+
p_copy.pop("resolved_version", None)
|
|
318
|
+
p_copy.pop("resolution_status", None)
|
|
319
|
+
p_copy.pop("resolution_source", None)
|
|
320
|
+
p_copy.pop("satisfies_declared_constraint", None)
|
|
321
|
+
cleaned_pkgs[name] = p_copy
|
|
322
|
+
v["packages"] = cleaned_pkgs
|
|
323
|
+
data["version_env"] = v
|
|
324
|
+
return data
|
|
325
|
+
|
|
326
|
+
def canonical_dict(self) -> Dict[str, Any]:
|
|
327
|
+
return self.source_canonical_dict()
|
|
328
|
+
|
|
329
|
+
def canonical_hash(self) -> str:
|
|
330
|
+
canonical_json = json.dumps(self.canonical_dict(), sort_keys=True, default=str)
|
|
331
|
+
return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
|
|
332
|
+
|
|
333
|
+
def environment_fingerprint(self) -> Optional[str]:
|
|
334
|
+
"""Returns stable fingerprint of the observed execution-environment."""
|
|
335
|
+
if not self.version_env:
|
|
336
|
+
return None
|
|
337
|
+
env_dict = {
|
|
338
|
+
"python_resolved_version": self.version_env.python_resolved_version,
|
|
339
|
+
"packages": {
|
|
340
|
+
name: {
|
|
341
|
+
"resolved_version": pkg.resolved_version,
|
|
342
|
+
"resolution_source": pkg.resolution_source,
|
|
343
|
+
}
|
|
344
|
+
for name, pkg in self.version_env.packages.items()
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
env_json = json.dumps(env_dict, sort_keys=True, default=str)
|
|
348
|
+
return hashlib.sha256(env_json.encode("utf-8")).hexdigest()
|