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,533 @@
|
|
|
1
|
+
"""Semantic Documentation Intermediate Representation (IR) Models.
|
|
2
|
+
|
|
3
|
+
Schema Version: 1.0.0
|
|
4
|
+
Invariants:
|
|
5
|
+
1. Documentation IR is the migration authority.
|
|
6
|
+
2. Document flow is an ordered, heterogeneous, strongly-typed sequence with provenance (SourceSpan).
|
|
7
|
+
3. Discriminated unions prevent element_type and content drift.
|
|
8
|
+
4. Rendered HTML is verification evidence, not the migration source of truth.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import List, Dict, Any, Optional, Union, Set
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
DOCUMENTATION_IR_SCHEMA_VERSION = "1.0.0"
|
|
18
|
+
|
|
19
|
+
# --- 1. Provenance & Source Spans ---
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SourceSpan(BaseModel):
|
|
23
|
+
file: str
|
|
24
|
+
start_line: Optional[int] = None
|
|
25
|
+
end_line: Optional[int] = None
|
|
26
|
+
start_column: Optional[int] = None
|
|
27
|
+
end_column: Optional[int] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- 2. API Documentation Request Models ---
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApiObjectKind(str, Enum):
|
|
34
|
+
MODULE = "module"
|
|
35
|
+
PACKAGE = "package"
|
|
36
|
+
CLASS = "class"
|
|
37
|
+
FUNCTION = "function"
|
|
38
|
+
METHOD = "method"
|
|
39
|
+
ATTRIBUTE = "attribute"
|
|
40
|
+
EXCEPTION = "exception"
|
|
41
|
+
UNKNOWN = "unknown"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MemberSelection(str, Enum):
|
|
45
|
+
NOT_SPECIFIED = "NOT_SPECIFIED"
|
|
46
|
+
ALL_PUBLIC = "ALL_PUBLIC"
|
|
47
|
+
EXPLICIT = "EXPLICIT"
|
|
48
|
+
DOCSTRING_ONLY = "DOCSTRING_ONLY"
|
|
49
|
+
NONE = "NONE"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SummaryMode(str, Enum):
|
|
53
|
+
NOT_REQUESTED = "NOT_REQUESTED"
|
|
54
|
+
EXPLICIT = "EXPLICIT"
|
|
55
|
+
HANDLER_DEFAULT = "HANDLER_DEFAULT"
|
|
56
|
+
INFERRED = "INFERRED"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ResolutionStatus(str, Enum):
|
|
60
|
+
RESOLVED = "RESOLVED"
|
|
61
|
+
UNRESOLVED = "UNRESOLVED"
|
|
62
|
+
AMBIGUOUS = "AMBIGUOUS"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ApiDocumentationRequest(BaseModel):
|
|
66
|
+
construct_id: str
|
|
67
|
+
source_span: SourceSpan
|
|
68
|
+
object_path: str
|
|
69
|
+
object_kind: ApiObjectKind = ApiObjectKind.UNKNOWN
|
|
70
|
+
handler: str = "mkdocstrings.python"
|
|
71
|
+
raw_options: Dict[str, Any] = Field(default_factory=dict)
|
|
72
|
+
normalized_options: Dict[str, Any] = Field(default_factory=dict)
|
|
73
|
+
include_docstring: bool = True
|
|
74
|
+
member_selection: MemberSelection = MemberSelection.NOT_SPECIFIED
|
|
75
|
+
explicit_members: List[str] = Field(default_factory=list)
|
|
76
|
+
summary_mode: SummaryMode = SummaryMode.NOT_REQUESTED
|
|
77
|
+
resolution_status: ResolutionStatus = ResolutionStatus.RESOLVED
|
|
78
|
+
resolved_file: Optional[str] = None
|
|
79
|
+
resolved_symbol: Optional[str] = None
|
|
80
|
+
inheritance: List[str] = Field(default_factory=list)
|
|
81
|
+
declared_members: List[str] = Field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- 3. Strongly-Typed Content Elements ---
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class InlineElementKind(str, Enum):
|
|
88
|
+
TEXT = "TEXT"
|
|
89
|
+
CODE = "CODE"
|
|
90
|
+
EMPHASIS = "EMPHASIS"
|
|
91
|
+
STRONG = "STRONG"
|
|
92
|
+
LINK = "LINK"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class InlineElement(BaseModel):
|
|
96
|
+
kind: InlineElementKind
|
|
97
|
+
text: str
|
|
98
|
+
target: Optional[str] = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class HeadingElement(BaseModel):
|
|
102
|
+
level: int
|
|
103
|
+
text: str
|
|
104
|
+
anchor_id: Optional[str] = None
|
|
105
|
+
inlines: List[InlineElement] = Field(default_factory=list)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ParagraphElement(BaseModel):
|
|
109
|
+
text: str
|
|
110
|
+
inlines: List[InlineElement] = Field(default_factory=list)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ListItemElement(BaseModel):
|
|
114
|
+
text: str
|
|
115
|
+
inlines: List[InlineElement] = Field(default_factory=list)
|
|
116
|
+
children: List["ListItemElement"] = Field(default_factory=list)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ListElement(BaseModel):
|
|
120
|
+
ordered: bool = False
|
|
121
|
+
items: List[ListItemElement] = Field(default_factory=list)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TableCellElement(BaseModel):
|
|
125
|
+
text: str
|
|
126
|
+
inlines: List[InlineElement] = Field(default_factory=list)
|
|
127
|
+
colspan: int = 1
|
|
128
|
+
rowspan: int = 1
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TableElement(BaseModel):
|
|
132
|
+
caption: Optional[str] = None
|
|
133
|
+
headers: List[TableCellElement] = Field(default_factory=list)
|
|
134
|
+
rows: List[List[TableCellElement]] = Field(default_factory=list)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CodeBlockElement(BaseModel):
|
|
138
|
+
language: Optional[str] = None
|
|
139
|
+
code: str
|
|
140
|
+
title: Optional[str] = None
|
|
141
|
+
linenums: bool = False
|
|
142
|
+
highlight_lines: List[int] = Field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class AdmonitionElement(BaseModel):
|
|
146
|
+
kind: str
|
|
147
|
+
title: Optional[str] = None
|
|
148
|
+
content_text: str
|
|
149
|
+
collapsible: bool = False
|
|
150
|
+
inlines: List[InlineElement] = Field(default_factory=list)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ImageElement(BaseModel):
|
|
154
|
+
src: str
|
|
155
|
+
alt: Optional[str] = None
|
|
156
|
+
title: Optional[str] = None
|
|
157
|
+
resolved_path: Optional[str] = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class LinkBlockElement(BaseModel):
|
|
161
|
+
text: str
|
|
162
|
+
target: str
|
|
163
|
+
title: Optional[str] = None
|
|
164
|
+
is_internal: bool = False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class SnippetElement(BaseModel):
|
|
168
|
+
snippet_path: str
|
|
169
|
+
resolved_path: Optional[str] = None
|
|
170
|
+
raw_content: Optional[str] = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class RawHtmlElement(BaseModel):
|
|
174
|
+
raw_html: str
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class UnknownElement(BaseModel):
|
|
178
|
+
tag_or_type: str
|
|
179
|
+
raw_content: str
|
|
180
|
+
rationale: str
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --- 4. Discriminated Union for Document Elements ---
|
|
184
|
+
|
|
185
|
+
DocumentElementContent = Union[
|
|
186
|
+
HeadingElement,
|
|
187
|
+
ParagraphElement,
|
|
188
|
+
ListElement,
|
|
189
|
+
TableElement,
|
|
190
|
+
CodeBlockElement,
|
|
191
|
+
AdmonitionElement,
|
|
192
|
+
ImageElement,
|
|
193
|
+
LinkBlockElement,
|
|
194
|
+
SnippetElement,
|
|
195
|
+
ApiDocumentationRequest,
|
|
196
|
+
RawHtmlElement,
|
|
197
|
+
UnknownElement,
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class DocumentElementType(str, Enum):
|
|
202
|
+
HEADING = "HEADING"
|
|
203
|
+
PARAGRAPH = "PARAGRAPH"
|
|
204
|
+
LIST = "LIST"
|
|
205
|
+
TABLE = "TABLE"
|
|
206
|
+
CODE_BLOCK = "CODE_BLOCK"
|
|
207
|
+
ADMONITION = "ADMONITION"
|
|
208
|
+
IMAGE = "IMAGE"
|
|
209
|
+
LINK_BLOCK = "LINK_BLOCK"
|
|
210
|
+
SNIPPET = "SNIPPET"
|
|
211
|
+
API_REQUEST = "API_REQUEST"
|
|
212
|
+
RAW_HTML = "RAW_HTML"
|
|
213
|
+
UNKNOWN = "UNKNOWN"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class DocumentElement(BaseModel):
|
|
217
|
+
construct_id: str
|
|
218
|
+
element_type: DocumentElementType
|
|
219
|
+
source_order_index: int
|
|
220
|
+
source_span: SourceSpan
|
|
221
|
+
content: DocumentElementContent
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class DocumentFlowSpec(BaseModel):
|
|
225
|
+
source_file: str
|
|
226
|
+
elements: List[DocumentElement] = Field(default_factory=list)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# --- 5. Relationships, Links, & Navigation Graph ---
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class DocumentationLink(BaseModel):
|
|
233
|
+
source_construct_id: str
|
|
234
|
+
target: str
|
|
235
|
+
fragment: Optional[str] = None
|
|
236
|
+
link_kind: str # internal_page, internal_anchor, external_url, asset
|
|
237
|
+
resolved: bool = False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class AssetReference(BaseModel):
|
|
241
|
+
source_construct_id: str
|
|
242
|
+
source_path: str
|
|
243
|
+
resolved_path: Optional[str] = None
|
|
244
|
+
asset_kind: str
|
|
245
|
+
exists: bool = False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class DocumentationPage(BaseModel):
|
|
249
|
+
source_file: str
|
|
250
|
+
logical_route: str
|
|
251
|
+
title: Optional[str] = None
|
|
252
|
+
flow: DocumentFlowSpec
|
|
253
|
+
outgoing_links: List[DocumentationLink] = Field(default_factory=list)
|
|
254
|
+
referenced_assets: List[AssetReference] = Field(default_factory=list)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class NavigationNode(BaseModel):
|
|
258
|
+
construct_id: str
|
|
259
|
+
label: str
|
|
260
|
+
page_route: Optional[str] = None
|
|
261
|
+
children: List["NavigationNode"] = Field(default_factory=list)
|
|
262
|
+
order_index: int = 0
|
|
263
|
+
hidden: bool = False
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class GeneratedPipeline(BaseModel):
|
|
267
|
+
construct_id: str
|
|
268
|
+
source_plugin: str
|
|
269
|
+
source_configuration: Dict[str, Any] = Field(default_factory=dict)
|
|
270
|
+
inputs: List[str] = Field(default_factory=list)
|
|
271
|
+
generated_outputs: List[str] = Field(default_factory=list)
|
|
272
|
+
navigation_effects: List[str] = Field(default_factory=list)
|
|
273
|
+
generation_strategy: str
|
|
274
|
+
verification_status: str
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# --- 6. Generic Documentation Build Graph Models ---
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class BuildStageType(str, Enum):
|
|
281
|
+
SOURCE_PREPROCESSING = "SOURCE_PREPROCESSING"
|
|
282
|
+
GENERATED_SOURCE_VARIANT = "GENERATED_SOURCE_VARIANT"
|
|
283
|
+
DATA_DRIVEN_ARTIFACT = "DATA_DRIVEN_ARTIFACT"
|
|
284
|
+
LOCALE_OVERLAY = "LOCALE_OVERLAY"
|
|
285
|
+
CONFIG_SYNTHESIS = "CONFIG_SYNTHESIS"
|
|
286
|
+
DOCUMENTATION_RENDERER = "DOCUMENTATION_RENDERER"
|
|
287
|
+
SITE_ASSEMBLY = "SITE_ASSEMBLY"
|
|
288
|
+
DEPLOYMENT_ASSEMBLY = "DEPLOYMENT_ASSEMBLY"
|
|
289
|
+
ARTIFACT_COPY = "ARTIFACT_COPY"
|
|
290
|
+
VALIDATION = "VALIDATION"
|
|
291
|
+
SIDE_OUTPUT = "SIDE_OUTPUT"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class ArtifactScope(str, Enum):
|
|
295
|
+
GLOBAL = "GLOBAL"
|
|
296
|
+
LOCALE = "LOCALE"
|
|
297
|
+
DOCUMENT = "DOCUMENT"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class ArtifactConsumerKind(str, Enum):
|
|
301
|
+
SITE_ARTIFACT = "SITE_ARTIFACT"
|
|
302
|
+
REPOSITORY_ARTIFACT = "REPOSITORY_ARTIFACT"
|
|
303
|
+
DEPLOYMENT_ARTIFACT = "DEPLOYMENT_ARTIFACT"
|
|
304
|
+
TEST_ARTIFACT = "TEST_ARTIFACT"
|
|
305
|
+
TOOLING_ARTIFACT = "TOOLING_ARTIFACT"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class BuildArtifact(BaseModel):
|
|
309
|
+
artifact_id: str
|
|
310
|
+
path: str
|
|
311
|
+
logical_role: Optional[str] = (
|
|
312
|
+
None # e.g., "sponsor_banner_partial", "python310_syntax_variant"
|
|
313
|
+
)
|
|
314
|
+
materialized_path: Optional[str] = (
|
|
315
|
+
None # e.g., "docs/en/overrides/partials/banner-sponsors.html"
|
|
316
|
+
)
|
|
317
|
+
artifact_kind: str # source_file, generated_variant, data_partial, config_artifact, html_page, asset
|
|
318
|
+
producer_stage: str
|
|
319
|
+
consumer_stages: List[str] = Field(default_factory=list)
|
|
320
|
+
external_consumers: List[str] = Field(
|
|
321
|
+
default_factory=list
|
|
322
|
+
) # e.g., ["documentation_source_tree", "test_suite"]
|
|
323
|
+
consumer_kinds: Set[ArtifactConsumerKind] = Field(
|
|
324
|
+
default_factory=lambda: {ArtifactConsumerKind.SITE_ARTIFACT}
|
|
325
|
+
)
|
|
326
|
+
scope: ArtifactScope = ArtifactScope.GLOBAL
|
|
327
|
+
locale: Optional[str] = None
|
|
328
|
+
is_intermediate: bool = False
|
|
329
|
+
provenance_source: Optional[str] = None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class StageWorkflowKind(str, Enum):
|
|
333
|
+
PREPARATION_WORKFLOW = "PREPARATION_WORKFLOW" # External / static preparation step (e.g. generate-docs-src-versions)
|
|
334
|
+
BUILD_PIPELINE = "BUILD_PIPELINE" # Core build DAG executed during site compilation
|
|
335
|
+
MAINTENANCE_WORKFLOW = "MAINTENANCE_WORKFLOW" # Auxiliary scripts (e.g. notify_translations, translation_fixer)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class DependencyJustificationKind(str, Enum):
|
|
339
|
+
ARTIFACT_DEPENDENCY = "ARTIFACT_DEPENDENCY"
|
|
340
|
+
EXTERNAL_DEPENDENCY = "EXTERNAL_DEPENDENCY"
|
|
341
|
+
CONTROL_DEPENDENCY = "CONTROL_DEPENDENCY"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class StageDependency(BaseModel):
|
|
345
|
+
antecedent_stage_id: str
|
|
346
|
+
justification_kind: DependencyJustificationKind
|
|
347
|
+
artifact_id: Optional[str] = None
|
|
348
|
+
description: Optional[str] = None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class BuildCondition(BaseModel):
|
|
352
|
+
condition_id: str
|
|
353
|
+
expression: str # e.g., "locale != 'en'", "is_fallback == True"
|
|
354
|
+
description: str
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class BuildStage(BaseModel):
|
|
358
|
+
stage_id: str
|
|
359
|
+
stage_type: BuildStageType
|
|
360
|
+
workflow_kind: StageWorkflowKind = StageWorkflowKind.BUILD_PIPELINE
|
|
361
|
+
scope: ArtifactScope = ArtifactScope.GLOBAL
|
|
362
|
+
locale: Optional[str] = None
|
|
363
|
+
inputs: List[str] = Field(default_factory=list)
|
|
364
|
+
outputs: List[str] = Field(default_factory=list)
|
|
365
|
+
depends_on: List[str] = Field(
|
|
366
|
+
default_factory=list
|
|
367
|
+
) # stage_ids that must complete before this
|
|
368
|
+
stage_dependencies: List[StageDependency] = Field(default_factory=list)
|
|
369
|
+
conditions: List[BuildCondition] = Field(default_factory=list)
|
|
370
|
+
strategy: str
|
|
371
|
+
consumer_kinds: Set[ArtifactConsumerKind] = Field(
|
|
372
|
+
default_factory=lambda: {ArtifactConsumerKind.SITE_ARTIFACT}
|
|
373
|
+
)
|
|
374
|
+
source_provenance_symbol: Optional[str] = None # e.g., "scripts/docs.py:build_all"
|
|
375
|
+
rationale: str
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class LocaleDocumentSpec(BaseModel):
|
|
379
|
+
logical_route: str
|
|
380
|
+
locale: str
|
|
381
|
+
canonical_source: str
|
|
382
|
+
localized_source: Optional[str] = None
|
|
383
|
+
effective_source: str
|
|
384
|
+
is_fallback: bool = False
|
|
385
|
+
translation_notice_required: bool = False
|
|
386
|
+
translation_status: str = "CANONICAL" # CANONICAL, TRANSLATED, FALLBACK, EXCLUDED
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class LocaleOverlaySpec(BaseModel):
|
|
390
|
+
locale: str
|
|
391
|
+
is_canonical: bool = False
|
|
392
|
+
canonical_source_dir: str
|
|
393
|
+
localized_source_dir: Optional[str] = None
|
|
394
|
+
fallback_locale: Optional[str] = "en"
|
|
395
|
+
inject_missing_notice: bool = True
|
|
396
|
+
excluded_paths: List[str] = Field(default_factory=list)
|
|
397
|
+
documents: Dict[str, LocaleDocumentSpec] = Field(default_factory=dict)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class DocumentationBuildGraph(BaseModel):
|
|
401
|
+
schema_version: str = DOCUMENTATION_IR_SCHEMA_VERSION
|
|
402
|
+
stages: Dict[str, BuildStage] = Field(default_factory=dict)
|
|
403
|
+
artifacts: Dict[str, BuildArtifact] = Field(default_factory=dict)
|
|
404
|
+
locales: Dict[str, LocaleOverlaySpec] = Field(default_factory=dict)
|
|
405
|
+
|
|
406
|
+
def execution_order(self) -> List[str]:
|
|
407
|
+
"""Derives topological sort of build stages dynamically based on depends_on DAG."""
|
|
408
|
+
visited: Set[str] = set()
|
|
409
|
+
order: List[str] = []
|
|
410
|
+
|
|
411
|
+
def visit(node_id: str, path: Set[str]):
|
|
412
|
+
if node_id in path:
|
|
413
|
+
raise ValueError(
|
|
414
|
+
f"Cyclic dependency detected in build stage: {node_id}"
|
|
415
|
+
)
|
|
416
|
+
if node_id not in visited:
|
|
417
|
+
path.add(node_id)
|
|
418
|
+
stage = self.stages.get(node_id)
|
|
419
|
+
if stage:
|
|
420
|
+
for dep in stage.depends_on:
|
|
421
|
+
visit(dep, path)
|
|
422
|
+
path.remove(node_id)
|
|
423
|
+
visited.add(node_id)
|
|
424
|
+
order.append(node_id)
|
|
425
|
+
|
|
426
|
+
for s_id in self.stages:
|
|
427
|
+
visit(s_id, set())
|
|
428
|
+
return order
|
|
429
|
+
|
|
430
|
+
def validate_graph_invariants(self) -> List[str]:
|
|
431
|
+
"""Validates structural integrity of the DAG, artifacts, and dependencies.
|
|
432
|
+
|
|
433
|
+
Invariants:
|
|
434
|
+
1. Every depends_on stage exists in stages.
|
|
435
|
+
2. No stage depends on itself.
|
|
436
|
+
3. Every BuildArtifact producer_stage exists.
|
|
437
|
+
4. Every BuildArtifact consumer_stage exists.
|
|
438
|
+
5. For every stage dependency A depends_on B, A != B.
|
|
439
|
+
6. Bi-directional fidelity: If artifact X is produced by P and consumed by C, C must declare depends_on P (or external source).
|
|
440
|
+
"""
|
|
441
|
+
errors: List[str] = []
|
|
442
|
+
|
|
443
|
+
# 1. Validate Stage DAG
|
|
444
|
+
for s_id, stage in self.stages.items():
|
|
445
|
+
for dep in stage.depends_on:
|
|
446
|
+
if dep not in self.stages:
|
|
447
|
+
errors.append(
|
|
448
|
+
f"Stage '{s_id}' depends on non-existent stage '{dep}'."
|
|
449
|
+
)
|
|
450
|
+
if dep == s_id:
|
|
451
|
+
errors.append(f"Stage '{s_id}' cannot depend on itself.")
|
|
452
|
+
|
|
453
|
+
# 2. Validate Artifact Producer / Consumer Consistency
|
|
454
|
+
for a_id, art in self.artifacts.items():
|
|
455
|
+
if art.producer_stage not in self.stages:
|
|
456
|
+
errors.append(
|
|
457
|
+
f"Artifact '{a_id}' producer_stage '{art.producer_stage}' does not exist."
|
|
458
|
+
)
|
|
459
|
+
for c_stage in art.consumer_stages:
|
|
460
|
+
if c_stage not in self.stages:
|
|
461
|
+
errors.append(
|
|
462
|
+
f"Artifact '{a_id}' consumer_stage '{c_stage}' does not exist."
|
|
463
|
+
)
|
|
464
|
+
else:
|
|
465
|
+
# Bi-directional check: consuming stage must depend on producing stage
|
|
466
|
+
consumer_obj = self.stages[c_stage]
|
|
467
|
+
if (
|
|
468
|
+
art.producer_stage != c_stage
|
|
469
|
+
and art.producer_stage not in consumer_obj.depends_on
|
|
470
|
+
):
|
|
471
|
+
errors.append(
|
|
472
|
+
f"Consuming stage '{c_stage}' consumes artifact '{a_id}' produced by '{art.producer_stage}', "
|
|
473
|
+
f"but '{c_stage}' does not declare depends_on '{art.producer_stage}'."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# 3. Validate Cycle-free DAG
|
|
477
|
+
try:
|
|
478
|
+
self.execution_order()
|
|
479
|
+
except ValueError as e:
|
|
480
|
+
errors.append(str(e))
|
|
481
|
+
|
|
482
|
+
return errors
|
|
483
|
+
|
|
484
|
+
def validate_locale_invariants(self) -> List[str]:
|
|
485
|
+
"""Validates locale document lineage and fallback invariants.
|
|
486
|
+
|
|
487
|
+
Invariants:
|
|
488
|
+
1. TRANSLATED: effective_source == localized_source and is_fallback is False.
|
|
489
|
+
2. FALLBACK: effective_source == canonical_source, is_fallback is True, translation_notice_required is True.
|
|
490
|
+
3. CANONICAL: effective_source == canonical_source, is_fallback is False, translation_notice_required is False.
|
|
491
|
+
"""
|
|
492
|
+
errors: List[str] = []
|
|
493
|
+
for loc_id, spec in self.locales.items():
|
|
494
|
+
for route, doc in spec.documents.items():
|
|
495
|
+
if doc.translation_status == "TRANSLATED":
|
|
496
|
+
if doc.effective_source != doc.localized_source or doc.is_fallback:
|
|
497
|
+
errors.append(
|
|
498
|
+
f"Locale '{loc_id}' route '{route}' has TRANSLATED status but invalid effective_source or is_fallback."
|
|
499
|
+
)
|
|
500
|
+
elif doc.translation_status == "FALLBACK":
|
|
501
|
+
if (
|
|
502
|
+
doc.effective_source != doc.canonical_source
|
|
503
|
+
or not doc.is_fallback
|
|
504
|
+
or not doc.translation_notice_required
|
|
505
|
+
):
|
|
506
|
+
errors.append(
|
|
507
|
+
f"Locale '{loc_id}' route '{route}' has FALLBACK status but invalid canonical mapping or missing notice."
|
|
508
|
+
)
|
|
509
|
+
elif doc.translation_status == "CANONICAL":
|
|
510
|
+
if (
|
|
511
|
+
doc.effective_source != doc.canonical_source
|
|
512
|
+
or doc.is_fallback
|
|
513
|
+
or doc.translation_notice_required
|
|
514
|
+
):
|
|
515
|
+
errors.append(
|
|
516
|
+
f"Locale '{loc_id}' route '{route}' has CANONICAL status but invalid effective_source or notice flag."
|
|
517
|
+
)
|
|
518
|
+
return errors
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class DocumentationSiteGraph(BaseModel):
|
|
522
|
+
schema_version: str = DOCUMENTATION_IR_SCHEMA_VERSION
|
|
523
|
+
pages: Dict[str, DocumentationPage] = Field(default_factory=dict)
|
|
524
|
+
navigation: Optional[NavigationNode] = None
|
|
525
|
+
generated_pipelines: List[GeneratedPipeline] = Field(default_factory=list)
|
|
526
|
+
build_graph: Optional[DocumentationBuildGraph] = None
|
|
527
|
+
|
|
528
|
+
def canonical_dict(self) -> Dict[str, Any]:
|
|
529
|
+
return self.model_dump()
|
|
530
|
+
|
|
531
|
+
def canonical_hash(self) -> str:
|
|
532
|
+
canonical_json = json.dumps(self.canonical_dict(), sort_keys=True, default=str)
|
|
533
|
+
return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
|