ai-docs-toolkit 0.1.0b1__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.
- ai_docs_toolkit/__init__.py +3 -0
- ai_docs_toolkit/__main__.py +4 -0
- ai_docs_toolkit/cli/__init__.py +232 -0
- ai_docs_toolkit/context/__init__.py +13 -0
- ai_docs_toolkit/context/markdown_output.py +325 -0
- ai_docs_toolkit/core/__init__.py +154 -0
- ai_docs_toolkit/core/changed_files.py +138 -0
- ai_docs_toolkit/core/config.py +201 -0
- ai_docs_toolkit/core/context.py +347 -0
- ai_docs_toolkit/core/frontmatter.py +139 -0
- ai_docs_toolkit/core/graph.py +150 -0
- ai_docs_toolkit/core/impact.py +273 -0
- ai_docs_toolkit/core/registry.py +108 -0
- ai_docs_toolkit/core/scanner.py +38 -0
- ai_docs_toolkit/core/schema_validation.py +214 -0
- ai_docs_toolkit/core/structure_validation.py +751 -0
- ai_docs_toolkit/graph/__init__.py +13 -0
- ai_docs_toolkit/graph/json_output.py +234 -0
- ai_docs_toolkit/impact/__init__.py +15 -0
- ai_docs_toolkit/impact/json_output.py +341 -0
- ai_docs_toolkit/mcp/__init__.py +8 -0
- ai_docs_toolkit/mcp/__main__.py +5 -0
- ai_docs_toolkit/mcp/server.py +224 -0
- ai_docs_toolkit/validation/__init__.py +16 -0
- ai_docs_toolkit/validation/human.py +246 -0
- ai_docs_toolkit-0.1.0b1.dist-info/METADATA +186 -0
- ai_docs_toolkit-0.1.0b1.dist-info/RECORD +30 -0
- ai_docs_toolkit-0.1.0b1.dist-info/WHEEL +5 -0
- ai_docs_toolkit-0.1.0b1.dist-info/entry_points.txt +3 -0
- ai_docs_toolkit-0.1.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from ai_docs_toolkit import __version__
|
|
2
|
+
from ai_docs_toolkit.context import (
|
|
3
|
+
context_project,
|
|
4
|
+
format_context_json,
|
|
5
|
+
format_context_markdown,
|
|
6
|
+
)
|
|
7
|
+
from ai_docs_toolkit.graph import GraphFilters, format_graph_json, graph_project
|
|
8
|
+
from ai_docs_toolkit.impact import (
|
|
9
|
+
format_impact_human,
|
|
10
|
+
format_impact_json,
|
|
11
|
+
impact_changed_project,
|
|
12
|
+
impact_project,
|
|
13
|
+
)
|
|
14
|
+
from ai_docs_toolkit.validation import (
|
|
15
|
+
format_human_validation_result,
|
|
16
|
+
format_json_validation_result,
|
|
17
|
+
validate_project_human,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _help_text() -> str:
|
|
22
|
+
return "\n".join(
|
|
23
|
+
[
|
|
24
|
+
f"ai-docs {__version__}",
|
|
25
|
+
"",
|
|
26
|
+
"Usage:",
|
|
27
|
+
" ai-docs --help",
|
|
28
|
+
" ai-docs --version",
|
|
29
|
+
" ai-docs validate",
|
|
30
|
+
" ai-docs validate --json",
|
|
31
|
+
" ai-docs graph --format json [--type TYPE] [--module MODULE] [--id ID]",
|
|
32
|
+
" ai-docs impact (--id ID | --changed) [--format json]",
|
|
33
|
+
" ai-docs context (--id ID | --module MODULE | --feature FEATURE | --changed) [--format json]",
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv: list[str] | None = None) -> int:
|
|
39
|
+
args = list(argv) if argv is not None else None
|
|
40
|
+
|
|
41
|
+
if args is None:
|
|
42
|
+
import sys
|
|
43
|
+
|
|
44
|
+
args = sys.argv[1:]
|
|
45
|
+
|
|
46
|
+
if not args or "--help" in args or "-h" in args:
|
|
47
|
+
print(_help_text())
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
if "--version" in args or "-V" in args:
|
|
51
|
+
print(__version__)
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
if args == ["validate"]:
|
|
55
|
+
result = validate_project_human(".")
|
|
56
|
+
print(format_human_validation_result(result))
|
|
57
|
+
return result.exit_code
|
|
58
|
+
|
|
59
|
+
if args == ["validate", "--json"] or args == ["--json", "validate"]:
|
|
60
|
+
result = validate_project_human(".")
|
|
61
|
+
print(format_json_validation_result(result))
|
|
62
|
+
return result.exit_code
|
|
63
|
+
|
|
64
|
+
graph_filters = _parse_graph_args(args)
|
|
65
|
+
if graph_filters is not None:
|
|
66
|
+
result = graph_project(".", filters=graph_filters)
|
|
67
|
+
print(format_graph_json(result))
|
|
68
|
+
return result.exit_code
|
|
69
|
+
|
|
70
|
+
impact_args = _parse_impact_args(args)
|
|
71
|
+
if impact_args is not None:
|
|
72
|
+
impact_mode, impact_source_ids, impact_format = impact_args
|
|
73
|
+
result = (
|
|
74
|
+
impact_changed_project(".")
|
|
75
|
+
if impact_mode == "changed"
|
|
76
|
+
else impact_project(impact_source_ids, ".")
|
|
77
|
+
)
|
|
78
|
+
print(
|
|
79
|
+
format_impact_json(result)
|
|
80
|
+
if impact_format == "json"
|
|
81
|
+
else format_impact_human(result)
|
|
82
|
+
)
|
|
83
|
+
return result.exit_code
|
|
84
|
+
|
|
85
|
+
context_args = _parse_context_args(args)
|
|
86
|
+
if context_args is not None:
|
|
87
|
+
context_mode, context_value, context_format = context_args
|
|
88
|
+
result = context_project(mode=context_mode, value=context_value, project_root=".")
|
|
89
|
+
print(
|
|
90
|
+
format_context_json(result)
|
|
91
|
+
if context_format == "json"
|
|
92
|
+
else format_context_markdown(result)
|
|
93
|
+
)
|
|
94
|
+
return result.exit_code
|
|
95
|
+
|
|
96
|
+
if args and args[0] == "graph":
|
|
97
|
+
import sys
|
|
98
|
+
|
|
99
|
+
print("Usage: ai-docs graph --format json", file=sys.stderr)
|
|
100
|
+
return 3
|
|
101
|
+
|
|
102
|
+
if args and args[0] == "impact":
|
|
103
|
+
import sys
|
|
104
|
+
|
|
105
|
+
print("Usage: ai-docs impact (--id ID | --changed) [--format json]", file=sys.stderr)
|
|
106
|
+
return 3
|
|
107
|
+
|
|
108
|
+
if args and args[0] == "context":
|
|
109
|
+
import sys
|
|
110
|
+
|
|
111
|
+
print(
|
|
112
|
+
"Usage: ai-docs context (--id ID | --module MODULE | --feature FEATURE | --changed) [--format json]",
|
|
113
|
+
file=sys.stderr,
|
|
114
|
+
)
|
|
115
|
+
return 3
|
|
116
|
+
|
|
117
|
+
import sys
|
|
118
|
+
|
|
119
|
+
print(f"Unknown command or option: {' '.join(args)}", file=sys.stderr)
|
|
120
|
+
print("Run `ai-docs --help` for usage.", file=sys.stderr)
|
|
121
|
+
return 3
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _parse_graph_args(args: list[str]) -> GraphFilters | None:
|
|
125
|
+
if not args or args[0] != "graph":
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
values = args[1:]
|
|
129
|
+
if len(values) < 2 or values[:2] != ["--format", "json"]:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
document_type: str | None = None
|
|
133
|
+
module: str | None = None
|
|
134
|
+
document_id: str | None = None
|
|
135
|
+
index = 2
|
|
136
|
+
while index < len(values):
|
|
137
|
+
option = values[index]
|
|
138
|
+
if index + 1 >= len(values):
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
value = values[index + 1]
|
|
142
|
+
if option == "--type":
|
|
143
|
+
document_type = value
|
|
144
|
+
elif option == "--module":
|
|
145
|
+
module = value
|
|
146
|
+
elif option == "--id":
|
|
147
|
+
document_id = value
|
|
148
|
+
else:
|
|
149
|
+
return None
|
|
150
|
+
index += 2
|
|
151
|
+
|
|
152
|
+
return GraphFilters(
|
|
153
|
+
document_type=document_type,
|
|
154
|
+
module=module,
|
|
155
|
+
document_id=document_id,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _parse_impact_args(args: list[str]) -> tuple[str, tuple[str, ...], str] | None:
|
|
160
|
+
if not args or args[0] != "impact":
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
values = args[1:]
|
|
164
|
+
if len(values) not in (1, 2, 3, 4):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
if len(values) == 2 and values[0] == "--id":
|
|
168
|
+
return ("id", (values[1],), "human")
|
|
169
|
+
|
|
170
|
+
if len(values) == 4 and values[0] == "--id" and values[2:] == ["--format", "json"]:
|
|
171
|
+
return ("id", (values[1],), "json")
|
|
172
|
+
|
|
173
|
+
if len(values) == 4 and values[:2] == ["--format", "json"] and values[2] == "--id":
|
|
174
|
+
return ("id", (values[3],), "json")
|
|
175
|
+
|
|
176
|
+
if values == ["--changed", "--format", "json"]:
|
|
177
|
+
return ("changed", (), "json")
|
|
178
|
+
|
|
179
|
+
if values == ["--format", "json", "--changed"]:
|
|
180
|
+
return ("changed", (), "json")
|
|
181
|
+
|
|
182
|
+
if values == ["--changed"]:
|
|
183
|
+
return ("changed", (), "human")
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_context_args(args: list[str]) -> tuple[str, str, str] | None:
|
|
189
|
+
if not args or args[0] != "context":
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
values = args[1:]
|
|
193
|
+
if len(values) not in (1, 2, 3, 4):
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
if values == ["--changed"]:
|
|
197
|
+
return ("changed", "", "markdown")
|
|
198
|
+
|
|
199
|
+
if values == ["--changed", "--format", "json"]:
|
|
200
|
+
return ("changed", "", "json")
|
|
201
|
+
|
|
202
|
+
if values == ["--format", "json", "--changed"]:
|
|
203
|
+
return ("changed", "", "json")
|
|
204
|
+
|
|
205
|
+
if len(values) == 2:
|
|
206
|
+
option, value = values
|
|
207
|
+
mode = _context_mode(option)
|
|
208
|
+
if mode is not None and value:
|
|
209
|
+
return (mode, value, "markdown")
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
if values[2:] == ["--format", "json"]:
|
|
213
|
+
mode = _context_mode(values[0])
|
|
214
|
+
if mode is not None and values[1]:
|
|
215
|
+
return (mode, values[1], "json")
|
|
216
|
+
|
|
217
|
+
if values[:2] == ["--format", "json"]:
|
|
218
|
+
mode = _context_mode(values[2])
|
|
219
|
+
if mode is not None and values[3]:
|
|
220
|
+
return (mode, values[3], "json")
|
|
221
|
+
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _context_mode(option: str) -> str | None:
|
|
226
|
+
if option == "--id":
|
|
227
|
+
return "id"
|
|
228
|
+
if option == "--module":
|
|
229
|
+
return "module"
|
|
230
|
+
if option == "--feature":
|
|
231
|
+
return "feature"
|
|
232
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ai_docs_toolkit.context.markdown_output import (
|
|
2
|
+
ContextBuildResult,
|
|
3
|
+
context_project,
|
|
4
|
+
format_context_json,
|
|
5
|
+
format_context_markdown,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ContextBuildResult",
|
|
10
|
+
"context_project",
|
|
11
|
+
"format_context_json",
|
|
12
|
+
"format_context_markdown",
|
|
13
|
+
]
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ai_docs_toolkit.core.changed_files import (
|
|
9
|
+
ChangedFileIssue,
|
|
10
|
+
read_git_changed_files,
|
|
11
|
+
resolve_changed_document_sources,
|
|
12
|
+
)
|
|
13
|
+
from ai_docs_toolkit.core import (
|
|
14
|
+
ContextSelectionResult,
|
|
15
|
+
DocumentRegistry,
|
|
16
|
+
RegistryDocument,
|
|
17
|
+
ToolkitConfig,
|
|
18
|
+
build_document_graph,
|
|
19
|
+
build_document_registry,
|
|
20
|
+
load_project_config,
|
|
21
|
+
parse_markdown_file,
|
|
22
|
+
registry_document_from_parsed,
|
|
23
|
+
scan_markdown_files,
|
|
24
|
+
select_context_by_changed_ids,
|
|
25
|
+
select_context_by_feature,
|
|
26
|
+
select_context_by_ids,
|
|
27
|
+
select_context_by_module,
|
|
28
|
+
)
|
|
29
|
+
from ai_docs_toolkit.validation import ValidationMessage
|
|
30
|
+
|
|
31
|
+
RULE_CHANGED_FILE_NOT_DOCUMENT = "document.context.changed_file_not_document"
|
|
32
|
+
RULE_CHANGED_FILE_OUTSIDE_DOCS = "document.context.changed_file_outside_docs"
|
|
33
|
+
RULE_GIT_CHANGED_FILES_UNAVAILABLE = "document.context.git_changed_files_unavailable"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class ContextBuildResult:
|
|
38
|
+
exit_code: int
|
|
39
|
+
selection: ContextSelectionResult | None
|
|
40
|
+
registry: DocumentRegistry | None = None
|
|
41
|
+
warnings: tuple[ValidationMessage, ...] = ()
|
|
42
|
+
errors: tuple[ValidationMessage, ...] = ()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def ok(self) -> bool:
|
|
46
|
+
return self.exit_code == 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class _ProjectContext:
|
|
51
|
+
root: Path
|
|
52
|
+
config: ToolkitConfig
|
|
53
|
+
documents: tuple[RegistryDocument, ...]
|
|
54
|
+
errors: tuple[ValidationMessage, ...]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def context_project(
|
|
58
|
+
*,
|
|
59
|
+
mode: str,
|
|
60
|
+
value: str,
|
|
61
|
+
project_root: str | Path = ".",
|
|
62
|
+
) -> ContextBuildResult:
|
|
63
|
+
context_result = _load_project_context(project_root)
|
|
64
|
+
if isinstance(context_result, ContextBuildResult):
|
|
65
|
+
return context_result
|
|
66
|
+
|
|
67
|
+
registry = build_document_registry(context_result.documents)
|
|
68
|
+
errors = [*context_result.errors, *_registry_errors(registry)]
|
|
69
|
+
graph = build_document_graph(registry, project_root=context_result.root)
|
|
70
|
+
|
|
71
|
+
if mode == "id":
|
|
72
|
+
selection = select_context_by_ids(graph, registry, [value])
|
|
73
|
+
elif mode == "module":
|
|
74
|
+
selection = select_context_by_module(graph, registry, value)
|
|
75
|
+
elif mode == "feature":
|
|
76
|
+
selection = select_context_by_feature(graph, registry, value)
|
|
77
|
+
elif mode == "changed":
|
|
78
|
+
changed_result = _git_changed_files(context_result.root)
|
|
79
|
+
errors.extend(_changed_file_errors(changed_result.issues))
|
|
80
|
+
source_result = resolve_changed_document_sources(
|
|
81
|
+
changed_result.changed_files,
|
|
82
|
+
config=context_result.config,
|
|
83
|
+
registry=registry,
|
|
84
|
+
root=context_result.root,
|
|
85
|
+
outside_docs_rule_id=RULE_CHANGED_FILE_OUTSIDE_DOCS,
|
|
86
|
+
not_document_rule_id=RULE_CHANGED_FILE_NOT_DOCUMENT,
|
|
87
|
+
)
|
|
88
|
+
source_ids = source_result.source_ids
|
|
89
|
+
changed_files_by_id = source_result.changed_files_by_id
|
|
90
|
+
warnings = [*_changed_file_warnings((*changed_result.issues, *source_result.issues))]
|
|
91
|
+
selection = select_context_by_changed_ids(
|
|
92
|
+
graph,
|
|
93
|
+
registry,
|
|
94
|
+
{document_id: changed_files_by_id[document_id] for document_id in source_ids},
|
|
95
|
+
)
|
|
96
|
+
errors.extend(_selection_errors(selection))
|
|
97
|
+
warnings.extend(_selection_warnings(selection))
|
|
98
|
+
return ContextBuildResult(
|
|
99
|
+
exit_code=0 if not errors else 1,
|
|
100
|
+
selection=selection,
|
|
101
|
+
registry=registry,
|
|
102
|
+
warnings=tuple(warnings),
|
|
103
|
+
errors=tuple(errors),
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Unsupported context mode: {mode}")
|
|
107
|
+
|
|
108
|
+
errors.extend(_selection_errors(selection))
|
|
109
|
+
warnings = _selection_warnings(selection)
|
|
110
|
+
return ContextBuildResult(
|
|
111
|
+
exit_code=0 if not errors else 1,
|
|
112
|
+
selection=selection,
|
|
113
|
+
registry=registry,
|
|
114
|
+
warnings=warnings,
|
|
115
|
+
errors=tuple(errors),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def format_context_markdown(result: ContextBuildResult) -> str:
|
|
120
|
+
lines: list[str] = ["# AI Docs Context Bundle", ""]
|
|
121
|
+
lines.append("Status: completed" if result.exit_code == 0 else "Status: failed")
|
|
122
|
+
|
|
123
|
+
selection = result.selection
|
|
124
|
+
sources = selection.sources if selection is not None else ()
|
|
125
|
+
documents = selection.documents if selection is not None else ()
|
|
126
|
+
lines.append(f"Sources: {len(sources)}")
|
|
127
|
+
lines.append(f"Documents: {len(documents)}")
|
|
128
|
+
|
|
129
|
+
if sources:
|
|
130
|
+
lines.append("")
|
|
131
|
+
lines.append("## Sources")
|
|
132
|
+
for source in sources:
|
|
133
|
+
changed = f" changed_file={source.changed_file}" if source.changed_file else ""
|
|
134
|
+
lines.append(
|
|
135
|
+
f"- {source.document_id} ({source.type}) {source.path} "
|
|
136
|
+
f"source={source.source}{changed}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if result.warnings:
|
|
140
|
+
lines.append("")
|
|
141
|
+
lines.append("## Warnings")
|
|
142
|
+
for warning in result.warnings:
|
|
143
|
+
lines.append(f"- {_message_line(warning)}")
|
|
144
|
+
|
|
145
|
+
if result.errors:
|
|
146
|
+
lines.append("")
|
|
147
|
+
lines.append("## Errors")
|
|
148
|
+
for error in result.errors:
|
|
149
|
+
lines.append(f"- {_message_line(error)}")
|
|
150
|
+
|
|
151
|
+
if documents:
|
|
152
|
+
lines.append("")
|
|
153
|
+
lines.append("## Documents")
|
|
154
|
+
for document in documents:
|
|
155
|
+
lines.append("")
|
|
156
|
+
lines.append(f"### {document.document_id}")
|
|
157
|
+
lines.append("")
|
|
158
|
+
lines.append("```yaml")
|
|
159
|
+
lines.append(f"type: {document.type}")
|
|
160
|
+
lines.append(f"path: {document.path}")
|
|
161
|
+
lines.append(f"group: {document.group}")
|
|
162
|
+
lines.append(f"distance: {document.distance}")
|
|
163
|
+
lines.append(f"priority: {document.priority}")
|
|
164
|
+
lines.append(f"context_role: {document.context_role}")
|
|
165
|
+
lines.append("reasons:")
|
|
166
|
+
for reason in document.reasons:
|
|
167
|
+
lines.append(f" - {reason.message}")
|
|
168
|
+
lines.append("```")
|
|
169
|
+
body = _document_body(result.registry, document.document_id)
|
|
170
|
+
if body:
|
|
171
|
+
lines.append("")
|
|
172
|
+
lines.extend(body.splitlines())
|
|
173
|
+
|
|
174
|
+
return "\n".join(lines)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def format_context_json(result: ContextBuildResult) -> str:
|
|
178
|
+
selection = result.selection
|
|
179
|
+
payload = {
|
|
180
|
+
"sources": [asdict(source) for source in selection.sources] if selection else [],
|
|
181
|
+
"documents": [_document_payload(document) for document in selection.documents]
|
|
182
|
+
if selection
|
|
183
|
+
else [],
|
|
184
|
+
"warnings": [_message_payload(message) for message in result.warnings],
|
|
185
|
+
}
|
|
186
|
+
if result.errors:
|
|
187
|
+
payload["errors"] = [_message_payload(message) for message in result.errors]
|
|
188
|
+
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _load_project_context(project_root: str | Path) -> _ProjectContext | ContextBuildResult:
|
|
192
|
+
root = Path(project_root).resolve()
|
|
193
|
+
config_result = load_project_config(root)
|
|
194
|
+
if not config_result.ok or config_result.config is None:
|
|
195
|
+
return ContextBuildResult(
|
|
196
|
+
exit_code=2,
|
|
197
|
+
selection=None,
|
|
198
|
+
errors=tuple(
|
|
199
|
+
ValidationMessage(
|
|
200
|
+
rule_id=error.code,
|
|
201
|
+
message=error.message,
|
|
202
|
+
path=error.path,
|
|
203
|
+
)
|
|
204
|
+
for error in config_result.errors
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
documents: list[RegistryDocument] = []
|
|
209
|
+
errors: list[ValidationMessage] = []
|
|
210
|
+
for path in scan_markdown_files(config_result.config):
|
|
211
|
+
parse_result = parse_markdown_file(path)
|
|
212
|
+
if parse_result.document is None:
|
|
213
|
+
errors.extend(
|
|
214
|
+
ValidationMessage(
|
|
215
|
+
rule_id=f"frontmatter.{error.code}",
|
|
216
|
+
message=error.message,
|
|
217
|
+
path=error.path,
|
|
218
|
+
)
|
|
219
|
+
for error in parse_result.errors
|
|
220
|
+
)
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
documents.append(registry_document_from_parsed(parse_result.document))
|
|
224
|
+
|
|
225
|
+
return _ProjectContext(
|
|
226
|
+
root=root,
|
|
227
|
+
config=config_result.config,
|
|
228
|
+
documents=tuple(documents),
|
|
229
|
+
errors=tuple(errors),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _registry_errors(registry: DocumentRegistry) -> tuple[ValidationMessage, ...]:
|
|
234
|
+
return tuple(
|
|
235
|
+
ValidationMessage(
|
|
236
|
+
rule_id=issue.rule_id,
|
|
237
|
+
message=issue.message,
|
|
238
|
+
path=", ".join(str(path) for path in issue.paths),
|
|
239
|
+
document_id=issue.document_id,
|
|
240
|
+
severity=issue.severity,
|
|
241
|
+
)
|
|
242
|
+
for issue in registry.issues
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _selection_errors(selection: ContextSelectionResult) -> tuple[ValidationMessage, ...]:
|
|
247
|
+
return tuple(
|
|
248
|
+
ValidationMessage(
|
|
249
|
+
rule_id=issue.rule_id,
|
|
250
|
+
message=issue.message,
|
|
251
|
+
document_id=issue.document_id,
|
|
252
|
+
severity=issue.severity,
|
|
253
|
+
)
|
|
254
|
+
for issue in selection.issues
|
|
255
|
+
if issue.severity == "error"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _selection_warnings(selection: ContextSelectionResult) -> tuple[ValidationMessage, ...]:
|
|
260
|
+
return tuple(
|
|
261
|
+
ValidationMessage(
|
|
262
|
+
rule_id=issue.rule_id,
|
|
263
|
+
message=issue.message,
|
|
264
|
+
document_id=issue.document_id,
|
|
265
|
+
severity=issue.severity,
|
|
266
|
+
)
|
|
267
|
+
for issue in selection.issues
|
|
268
|
+
if issue.severity != "error"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _changed_file_errors(issues: tuple[ChangedFileIssue, ...]) -> tuple[ValidationMessage, ...]:
|
|
273
|
+
return tuple(_changed_file_message(issue) for issue in issues if issue.severity == "error")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _changed_file_warnings(issues: tuple[ChangedFileIssue, ...]) -> tuple[ValidationMessage, ...]:
|
|
277
|
+
return tuple(_changed_file_message(issue) for issue in issues if issue.severity != "error")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _changed_file_message(issue: ChangedFileIssue) -> ValidationMessage:
|
|
281
|
+
return ValidationMessage(
|
|
282
|
+
rule_id=issue.rule_id,
|
|
283
|
+
message=issue.message,
|
|
284
|
+
path=issue.path,
|
|
285
|
+
document_id=issue.document_id,
|
|
286
|
+
severity=issue.severity,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _document_body(registry: DocumentRegistry | None, document_id: str) -> str:
|
|
291
|
+
if registry is None:
|
|
292
|
+
return ""
|
|
293
|
+
document = registry.get_by_id(document_id)
|
|
294
|
+
if document is None:
|
|
295
|
+
return ""
|
|
296
|
+
return document.body.strip()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _message_line(message: ValidationMessage) -> str:
|
|
300
|
+
path = f"{message.path} " if message.path else ""
|
|
301
|
+
document = f"[{message.document_id}] " if message.document_id else ""
|
|
302
|
+
return f"{path}{document}{message.rule_id}: {message.message}"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _document_payload(document: object) -> dict[str, object]:
|
|
306
|
+
payload = asdict(document)
|
|
307
|
+
payload["reasons"] = [asdict(reason) for reason in document.reasons]
|
|
308
|
+
return payload
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _message_payload(message: ValidationMessage) -> dict[str, str | None]:
|
|
312
|
+
return {
|
|
313
|
+
"path": message.path,
|
|
314
|
+
"document_id": message.document_id,
|
|
315
|
+
"rule_id": message.rule_id,
|
|
316
|
+
"severity": message.severity,
|
|
317
|
+
"message": message.message,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _git_changed_files(root: Path):
|
|
322
|
+
return read_git_changed_files(
|
|
323
|
+
root,
|
|
324
|
+
unavailable_rule_id=RULE_GIT_CHANGED_FILES_UNAVAILABLE,
|
|
325
|
+
)
|