ai-docs-toolkit 0.1.0__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 +1153 -0
- ai_docs_toolkit/context/__init__.py +13 -0
- ai_docs_toolkit/context/markdown_output.py +470 -0
- ai_docs_toolkit/core/__init__.py +360 -0
- ai_docs_toolkit/core/azure_devops_provider.py +569 -0
- ai_docs_toolkit/core/changed_files.py +235 -0
- ai_docs_toolkit/core/config.py +515 -0
- ai_docs_toolkit/core/context.py +666 -0
- ai_docs_toolkit/core/doctor.py +362 -0
- ai_docs_toolkit/core/file_lookup.py +656 -0
- ai_docs_toolkit/core/frontmatter.py +143 -0
- ai_docs_toolkit/core/graph.py +153 -0
- ai_docs_toolkit/core/impact.py +291 -0
- ai_docs_toolkit/core/init.py +837 -0
- ai_docs_toolkit/core/planning_operations.py +792 -0
- ai_docs_toolkit/core/planning_scaffold.py +334 -0
- ai_docs_toolkit/core/registry.py +161 -0
- ai_docs_toolkit/core/requirement_capture.py +152 -0
- ai_docs_toolkit/core/scanner.py +38 -0
- ai_docs_toolkit/core/schema_validation.py +222 -0
- ai_docs_toolkit/core/section_fix.py +169 -0
- ai_docs_toolkit/core/structure_validation.py +1155 -0
- ai_docs_toolkit/core/task_export.py +639 -0
- ai_docs_toolkit/docs/agent-installation.md +443 -0
- ai_docs_toolkit/graph/__init__.py +13 -0
- ai_docs_toolkit/graph/json_output.py +238 -0
- ai_docs_toolkit/impact/__init__.py +15 -0
- ai_docs_toolkit/impact/json_output.py +458 -0
- ai_docs_toolkit/mcp/__init__.py +8 -0
- ai_docs_toolkit/mcp/__main__.py +5 -0
- ai_docs_toolkit/mcp/server.py +345 -0
- ai_docs_toolkit/mcp/setup.py +107 -0
- ai_docs_toolkit/templates/project/.ai-docs/examples/skills/release-publication/SKILL.md +85 -0
- ai_docs_toolkit/templates/project/.ai-docs/reference/documentation-cascade-authoring-skill.md +157 -0
- ai_docs_toolkit/templates/project/.ai-docs/reference/documentation-change-review-skill.md +128 -0
- ai_docs_toolkit/templates/project/.ai-docs/reference/incoming-request-model.md +332 -0
- ai_docs_toolkit/templates/project/.ai-docs/reference/incoming-request-triage-skill.md +158 -0
- ai_docs_toolkit/templates/project/.ai-docs/reference/init-command-contract.md +262 -0
- ai_docs_toolkit/templates/project/.ai-docs/reference/planning-document-model.md +1483 -0
- ai_docs_toolkit/templates/project/.ai-docs/reference/toolkit-maintenance-context-routing.md +373 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/acceptance.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/architecture-decision.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/asset.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/business-rule.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/contract.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/guide.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/implementation-task.schema.yaml +97 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/index.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/inventory.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/module.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/operation.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/plan-stage.schema.yaml +89 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/plan.schema.yaml +86 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/runbook.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/scenario.schema.yaml +17 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/standalone-task.schema.yaml +90 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/task-registry.schema.yaml +114 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/document-types/test-case.schema.yaml +23 -0
- ai_docs_toolkit/templates/project/.ai-docs/schemas/frontmatter.schema.yaml +181 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/documentation-cascade-authoring/SKILL.md +84 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/documentation-change-review/SKILL.md +76 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/incoming-request-triage/SKILL.md +90 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/index.md +51 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/planning-record-generation/SKILL.md +77 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/planning-task-execution/SKILL.md +106 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/toolkit-consumer-policy/SKILL.md +44 -0
- ai_docs_toolkit/templates/project/.ai-docs/skills/update-ai-docs-toolkit/SKILL.md +100 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/agent-context-architecture-beta-18.md +123 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/archive-validation-beta-12.md +123 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/cascade-impact-beta-14.md +69 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/consumer-migration-hardening.md +128 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/decision-visibility-beta-13.md +132 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/doctor-filesystem-traversal-beta-19.md +98 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/doctor-json-provenance-beta-20.md +107 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/documentation-cascade-authoring-beta-23.md +54 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/documentation-change-review-beta-21.md +105 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/incoming-request-storage-beta-17.md +70 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/incoming-request-triage-beta-22.md +52 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/migration-note.md +90 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/post-publication-checks.md +9 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/release-task.md +123 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/routing-restoration-beta-15.md +39 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/task-retirement-beta-16.md +54 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/validation-evidence.md +18 -0
- ai_docs_toolkit/templates/project/.ai-docs/templates/release/version-change-checklist.md +11 -0
- ai_docs_toolkit/templates/project/.ai-docs/workflows/documentation-cascade-adoption.md +581 -0
- ai_docs_toolkit/templates/project/.ai-docs/workflows/plan-stage-workflow.md +752 -0
- ai_docs_toolkit/templates/project/.ai-docs/workflows/toolkit-migration-workflow.md +150 -0
- ai_docs_toolkit/templates/project/AGENTS.md +177 -0
- ai_docs_toolkit/templates/project/ai-docs-index.md +39 -0
- ai_docs_toolkit/templates/project/ai-docs.config.yaml +56 -0
- ai_docs_toolkit/validation/__init__.py +16 -0
- ai_docs_toolkit/validation/human.py +313 -0
- ai_docs_toolkit-0.1.0.dist-info/METADATA +269 -0
- ai_docs_toolkit-0.1.0.dist-info/RECORD +100 -0
- ai_docs_toolkit-0.1.0.dist-info/WHEEL +5 -0
- ai_docs_toolkit-0.1.0.dist-info/entry_points.txt +3 -0
- ai_docs_toolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,470 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ai_docs_toolkit.core import (
|
|
9
|
+
ContextSelectionResult,
|
|
10
|
+
DocumentRegistry,
|
|
11
|
+
RegistryDocument,
|
|
12
|
+
ToolkitConfig,
|
|
13
|
+
build_document_graph,
|
|
14
|
+
build_document_registry,
|
|
15
|
+
load_project_config,
|
|
16
|
+
parse_markdown_text,
|
|
17
|
+
registry_document_from_parsed,
|
|
18
|
+
scan_markdown_files,
|
|
19
|
+
select_context_by_changed_ids,
|
|
20
|
+
select_context_by_feature,
|
|
21
|
+
select_context_by_ids,
|
|
22
|
+
select_context_by_module,
|
|
23
|
+
)
|
|
24
|
+
from ai_docs_toolkit.core.changed_files import (
|
|
25
|
+
ChangedFileIssue,
|
|
26
|
+
read_git_changed_files,
|
|
27
|
+
resolve_changed_document_sources,
|
|
28
|
+
)
|
|
29
|
+
from ai_docs_toolkit.core.config import context_settings_error
|
|
30
|
+
from ai_docs_toolkit.core.context import ContextPolicy
|
|
31
|
+
from ai_docs_toolkit.validation import ValidationMessage
|
|
32
|
+
|
|
33
|
+
RULE_CHANGED_FILE_NOT_DOCUMENT = "document.context.changed_file_not_document"
|
|
34
|
+
RULE_CHANGED_FILE_OUTSIDE_DOCS = "document.context.changed_file_outside_docs"
|
|
35
|
+
RULE_GIT_CHANGED_FILES_UNAVAILABLE = "document.context.git_changed_files_unavailable"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ContextBuildResult:
|
|
40
|
+
exit_code: int
|
|
41
|
+
selection: ContextSelectionResult | None
|
|
42
|
+
registry: DocumentRegistry | None = None
|
|
43
|
+
warnings: tuple[ValidationMessage, ...] = ()
|
|
44
|
+
errors: tuple[ValidationMessage, ...] = ()
|
|
45
|
+
snapshot: dict | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def ok(self) -> bool:
|
|
49
|
+
return self.exit_code == 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class _ProjectContext:
|
|
54
|
+
root: Path
|
|
55
|
+
config: ToolkitConfig
|
|
56
|
+
documents: tuple[RegistryDocument, ...]
|
|
57
|
+
errors: tuple[ValidationMessage, ...]
|
|
58
|
+
file_hashes: dict[str, str]
|
|
59
|
+
config_hash: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def context_project(
|
|
63
|
+
*,
|
|
64
|
+
mode: str,
|
|
65
|
+
value: str,
|
|
66
|
+
project_root: str | Path = ".",
|
|
67
|
+
history: str | None = None,
|
|
68
|
+
body_budget_bytes: int | None = None,
|
|
69
|
+
max_depth: int | None = None,
|
|
70
|
+
) -> ContextBuildResult:
|
|
71
|
+
context_result = _load_project_context(project_root)
|
|
72
|
+
if isinstance(context_result, ContextBuildResult):
|
|
73
|
+
return context_result
|
|
74
|
+
config = context_result.config
|
|
75
|
+
effective_history = history if history is not None else config.context.history
|
|
76
|
+
effective_budget = (
|
|
77
|
+
body_budget_bytes
|
|
78
|
+
if body_budget_bytes is not None
|
|
79
|
+
else config.context.body_budget_bytes
|
|
80
|
+
)
|
|
81
|
+
effective_depth = max_depth if max_depth is not None else config.context.max_depth
|
|
82
|
+
if error := context_settings_error(
|
|
83
|
+
effective_history, effective_budget, effective_depth
|
|
84
|
+
):
|
|
85
|
+
return ContextBuildResult(
|
|
86
|
+
exit_code=2,
|
|
87
|
+
selection=None,
|
|
88
|
+
errors=(
|
|
89
|
+
ValidationMessage(rule_id="invalid_context_options", message=error),
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
policy = ContextPolicy(
|
|
93
|
+
effective_history,
|
|
94
|
+
config.work_history.archive_root,
|
|
95
|
+
effective_budget or None,
|
|
96
|
+
effective_depth,
|
|
97
|
+
)
|
|
98
|
+
registry = build_document_registry(context_result.documents)
|
|
99
|
+
errors = [*context_result.errors, *_registry_errors(registry)]
|
|
100
|
+
warnings = []
|
|
101
|
+
graph = build_document_graph(registry, project_root=context_result.root)
|
|
102
|
+
if mode == "id":
|
|
103
|
+
selection = select_context_by_ids(graph, registry, [value], policy=policy)
|
|
104
|
+
elif mode == "module":
|
|
105
|
+
selection = select_context_by_module(graph, registry, value, policy=policy)
|
|
106
|
+
elif mode == "feature":
|
|
107
|
+
selection = select_context_by_feature(graph, registry, value, policy=policy)
|
|
108
|
+
elif mode == "changed":
|
|
109
|
+
changed = _git_changed_files(context_result.root)
|
|
110
|
+
source_result = resolve_changed_document_sources(
|
|
111
|
+
changed.changed_files,
|
|
112
|
+
config=config,
|
|
113
|
+
registry=registry,
|
|
114
|
+
root=context_result.root,
|
|
115
|
+
outside_docs_rule_id=RULE_CHANGED_FILE_OUTSIDE_DOCS,
|
|
116
|
+
not_document_rule_id=RULE_CHANGED_FILE_NOT_DOCUMENT,
|
|
117
|
+
)
|
|
118
|
+
errors.extend(_changed_file_errors(changed.issues))
|
|
119
|
+
warnings.extend(
|
|
120
|
+
_changed_file_warnings((*changed.issues, *source_result.issues))
|
|
121
|
+
)
|
|
122
|
+
selection = select_context_by_changed_ids(
|
|
123
|
+
graph,
|
|
124
|
+
registry,
|
|
125
|
+
{
|
|
126
|
+
id: source_result.changed_files_by_id[id]
|
|
127
|
+
for id in source_result.source_ids
|
|
128
|
+
},
|
|
129
|
+
policy=policy,
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError(f"Unsupported context mode: {mode}")
|
|
133
|
+
errors.extend(_selection_errors(selection))
|
|
134
|
+
warnings.extend(_selection_warnings(selection))
|
|
135
|
+
fingerprint_inputs = {
|
|
136
|
+
"files": context_result.file_hashes,
|
|
137
|
+
"config": context_result.config_hash,
|
|
138
|
+
"mode": mode,
|
|
139
|
+
"value": value,
|
|
140
|
+
"history": policy.history,
|
|
141
|
+
"body_budget_bytes": policy.body_budget_bytes,
|
|
142
|
+
"max_depth": policy.max_depth,
|
|
143
|
+
"sources": [asdict(s) for s in selection.sources],
|
|
144
|
+
}
|
|
145
|
+
snapshot = {
|
|
146
|
+
"fingerprint": hashlib.sha256(
|
|
147
|
+
json.dumps(fingerprint_inputs, sort_keys=True, ensure_ascii=False).encode()
|
|
148
|
+
).hexdigest(),
|
|
149
|
+
"files": {
|
|
150
|
+
d.path: context_result.file_hashes[d.path] for d in selection.documents
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
return ContextBuildResult(
|
|
154
|
+
exit_code=0 if not errors else 1,
|
|
155
|
+
selection=selection,
|
|
156
|
+
registry=registry,
|
|
157
|
+
warnings=tuple(warnings),
|
|
158
|
+
errors=tuple(errors),
|
|
159
|
+
snapshot=snapshot,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def format_context_markdown(result: ContextBuildResult) -> str:
|
|
164
|
+
lines: list[str] = ["# AI Docs Context Bundle", ""]
|
|
165
|
+
lines.append("Status: completed" if result.exit_code == 0 else "Status: failed")
|
|
166
|
+
|
|
167
|
+
selection = result.selection
|
|
168
|
+
sources = selection.sources if selection is not None else ()
|
|
169
|
+
documents = selection.documents if selection is not None else ()
|
|
170
|
+
lines.append(f"Sources: {len(sources)}")
|
|
171
|
+
lines.append(f"Documents: {len(documents)}")
|
|
172
|
+
|
|
173
|
+
if selection is not None:
|
|
174
|
+
budget = selection.policy.body_budget_bytes
|
|
175
|
+
lines.extend(
|
|
176
|
+
[
|
|
177
|
+
f"History: {selection.policy.history}",
|
|
178
|
+
f"Depth: {selection.policy.max_depth}; boundary documents: {selection.boundary_count}",
|
|
179
|
+
f"Body budget (UTF-8 bytes): {budget if budget else 'unlimited'} (soft)",
|
|
180
|
+
f"Selected body bytes: {selection.body_bytes}; excess: {selection.excess_bytes}",
|
|
181
|
+
"Coverage: explicit graph only; read applicable agent/local instructions separately.",
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
if selection.excluded:
|
|
185
|
+
lines.extend(["", "## Excluded"])
|
|
186
|
+
lines.extend(
|
|
187
|
+
f"- {e.document_id} ({e.path}): {', '.join(e.reasons)}"
|
|
188
|
+
for e in selection.excluded
|
|
189
|
+
)
|
|
190
|
+
if selection.historical:
|
|
191
|
+
lines.extend(["", "## Historical Documents"])
|
|
192
|
+
lines.extend(
|
|
193
|
+
f"- {e.document_id}: {', '.join(e.reasons)}"
|
|
194
|
+
for e in selection.historical
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if sources:
|
|
198
|
+
lines.append("")
|
|
199
|
+
lines.append("## Sources")
|
|
200
|
+
for source in sources:
|
|
201
|
+
changed = (
|
|
202
|
+
f" changed_file={source.changed_file}" if source.changed_file else ""
|
|
203
|
+
)
|
|
204
|
+
lines.append(
|
|
205
|
+
f"- {source.document_id} ({source.type}) {source.path} "
|
|
206
|
+
f"source={source.source}{changed}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if result.warnings:
|
|
210
|
+
lines.append("")
|
|
211
|
+
lines.append("## Warnings")
|
|
212
|
+
for warning in result.warnings:
|
|
213
|
+
lines.append(f"- {_message_line(warning)}")
|
|
214
|
+
|
|
215
|
+
if result.errors:
|
|
216
|
+
lines.append("")
|
|
217
|
+
lines.append("## Errors")
|
|
218
|
+
for error in result.errors:
|
|
219
|
+
lines.append(f"- {_message_line(error)}")
|
|
220
|
+
|
|
221
|
+
if documents:
|
|
222
|
+
lines.append("")
|
|
223
|
+
lines.append("## Documents")
|
|
224
|
+
for document in documents:
|
|
225
|
+
lines.append("")
|
|
226
|
+
lines.append(f"### {document.document_id}")
|
|
227
|
+
lines.append("")
|
|
228
|
+
lines.append("```yaml")
|
|
229
|
+
lines.append(f"type: {document.type}")
|
|
230
|
+
lines.append(f"path: {document.path}")
|
|
231
|
+
lines.append(f"group: {document.group}")
|
|
232
|
+
lines.append(f"distance: {document.distance}")
|
|
233
|
+
lines.append(f"priority: {document.priority}")
|
|
234
|
+
lines.append(f"context_role: {document.context_role}")
|
|
235
|
+
lines.append("reasons:")
|
|
236
|
+
for reason in document.reasons:
|
|
237
|
+
path_hint = f" Path {' -> '.join(reason.path)}." if reason.path else ""
|
|
238
|
+
lines.append(f" - {reason.message}{path_hint}")
|
|
239
|
+
lines.append("```")
|
|
240
|
+
body = _document_body(result.registry, document.document_id)
|
|
241
|
+
if body:
|
|
242
|
+
lines.append("")
|
|
243
|
+
lines.extend(body.splitlines())
|
|
244
|
+
|
|
245
|
+
return "\n".join(lines)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def format_context_json(result: ContextBuildResult) -> str:
|
|
249
|
+
selection = result.selection
|
|
250
|
+
payload = {
|
|
251
|
+
"sources": [asdict(source) for source in selection.sources]
|
|
252
|
+
if selection
|
|
253
|
+
else [],
|
|
254
|
+
"documents": [_document_payload(document) for document in selection.documents]
|
|
255
|
+
if selection
|
|
256
|
+
else [],
|
|
257
|
+
"warnings": [_message_payload(message) for message in result.warnings],
|
|
258
|
+
}
|
|
259
|
+
if selection is not None:
|
|
260
|
+
payload.update(
|
|
261
|
+
{
|
|
262
|
+
"selection": {
|
|
263
|
+
"history": selection.policy.history,
|
|
264
|
+
"max_depth": selection.policy.max_depth,
|
|
265
|
+
"depth_limited": selection.boundary_count > 0,
|
|
266
|
+
"boundary_count": selection.boundary_count,
|
|
267
|
+
"body_budget_bytes": selection.policy.body_budget_bytes,
|
|
268
|
+
"body_bytes": selection.body_bytes,
|
|
269
|
+
"excess_bytes": selection.excess_bytes,
|
|
270
|
+
"protected_ids": list(selection.protected_ids),
|
|
271
|
+
"candidate_count": selection.candidate_count,
|
|
272
|
+
"coverage": "explicit_graph_only",
|
|
273
|
+
},
|
|
274
|
+
"excluded": [asdict(e) for e in selection.excluded],
|
|
275
|
+
"historical": [asdict(e) for e in selection.historical],
|
|
276
|
+
"gaps": [asdict(g) for g in selection.gaps],
|
|
277
|
+
"snapshot": result.snapshot,
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
if result.errors:
|
|
281
|
+
payload["errors"] = [_message_payload(message) for message in result.errors]
|
|
282
|
+
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _load_project_context(
|
|
286
|
+
project_root: str | Path,
|
|
287
|
+
) -> _ProjectContext | ContextBuildResult:
|
|
288
|
+
root = Path(project_root).resolve()
|
|
289
|
+
config_path = root / "ai-docs.config.yaml"
|
|
290
|
+
config_bytes = config_path.read_bytes() if config_path.exists() else b""
|
|
291
|
+
config_result = load_project_config(root)
|
|
292
|
+
if not config_result.ok or config_result.config is None:
|
|
293
|
+
return ContextBuildResult(
|
|
294
|
+
exit_code=2,
|
|
295
|
+
selection=None,
|
|
296
|
+
errors=tuple(
|
|
297
|
+
ValidationMessage(
|
|
298
|
+
rule_id=error.code,
|
|
299
|
+
message=error.message,
|
|
300
|
+
path=error.path,
|
|
301
|
+
)
|
|
302
|
+
for error in config_result.errors
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if config_bytes != (config_path.read_bytes() if config_path.exists() else b""):
|
|
307
|
+
return ContextBuildResult(
|
|
308
|
+
exit_code=1,
|
|
309
|
+
selection=None,
|
|
310
|
+
errors=(
|
|
311
|
+
ValidationMessage(
|
|
312
|
+
rule_id="document.context.snapshot_changed",
|
|
313
|
+
message="Configuration changed during selection; rerun context.",
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
documents: list[RegistryDocument] = []
|
|
318
|
+
file_hashes = {}
|
|
319
|
+
errors: list[ValidationMessage] = []
|
|
320
|
+
for path in scan_markdown_files(config_result.config):
|
|
321
|
+
display_path = (
|
|
322
|
+
path.relative_to(root).as_posix()
|
|
323
|
+
if path.is_relative_to(root)
|
|
324
|
+
else str(path)
|
|
325
|
+
)
|
|
326
|
+
try:
|
|
327
|
+
data = path.read_bytes()
|
|
328
|
+
file_hashes[display_path] = hashlib.sha256(data).hexdigest()
|
|
329
|
+
text = data.decode("utf-8")
|
|
330
|
+
except (OSError, UnicodeError) as exc:
|
|
331
|
+
file_hashes[display_path] = "unreadable"
|
|
332
|
+
errors.append(
|
|
333
|
+
ValidationMessage(
|
|
334
|
+
rule_id="document.context.read_failed",
|
|
335
|
+
message=str(exc),
|
|
336
|
+
path=str(path),
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
continue
|
|
340
|
+
parse_result = parse_markdown_text(text, path=str(path))
|
|
341
|
+
if parse_result.document is None:
|
|
342
|
+
errors.extend(
|
|
343
|
+
ValidationMessage(
|
|
344
|
+
rule_id=f"frontmatter.{error.code}",
|
|
345
|
+
message=error.message,
|
|
346
|
+
path=error.path,
|
|
347
|
+
)
|
|
348
|
+
for error in parse_result.errors
|
|
349
|
+
)
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
documents.append(registry_document_from_parsed(parse_result.document))
|
|
353
|
+
|
|
354
|
+
return _ProjectContext(
|
|
355
|
+
root=root,
|
|
356
|
+
config=config_result.config,
|
|
357
|
+
documents=tuple(documents),
|
|
358
|
+
errors=tuple(errors),
|
|
359
|
+
file_hashes=file_hashes,
|
|
360
|
+
config_hash=hashlib.sha256(config_bytes).hexdigest(),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _registry_errors(registry: DocumentRegistry) -> tuple[ValidationMessage, ...]:
|
|
365
|
+
return tuple(
|
|
366
|
+
ValidationMessage(
|
|
367
|
+
rule_id=issue.rule_id,
|
|
368
|
+
message=issue.message,
|
|
369
|
+
path=", ".join(str(path) for path in issue.paths),
|
|
370
|
+
document_id=issue.document_id,
|
|
371
|
+
severity=issue.severity,
|
|
372
|
+
)
|
|
373
|
+
for issue in registry.issues
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _selection_errors(
|
|
378
|
+
selection: ContextSelectionResult,
|
|
379
|
+
) -> tuple[ValidationMessage, ...]:
|
|
380
|
+
return tuple(
|
|
381
|
+
ValidationMessage(
|
|
382
|
+
rule_id=issue.rule_id,
|
|
383
|
+
message=issue.message,
|
|
384
|
+
document_id=issue.document_id,
|
|
385
|
+
severity=issue.severity,
|
|
386
|
+
)
|
|
387
|
+
for issue in selection.issues
|
|
388
|
+
if issue.severity == "error"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _selection_warnings(
|
|
393
|
+
selection: ContextSelectionResult,
|
|
394
|
+
) -> tuple[ValidationMessage, ...]:
|
|
395
|
+
return tuple(
|
|
396
|
+
ValidationMessage(
|
|
397
|
+
rule_id=issue.rule_id,
|
|
398
|
+
message=issue.message,
|
|
399
|
+
document_id=issue.document_id,
|
|
400
|
+
severity=issue.severity,
|
|
401
|
+
)
|
|
402
|
+
for issue in selection.issues
|
|
403
|
+
if issue.severity != "error"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _changed_file_errors(
|
|
408
|
+
issues: tuple[ChangedFileIssue, ...],
|
|
409
|
+
) -> tuple[ValidationMessage, ...]:
|
|
410
|
+
return tuple(
|
|
411
|
+
_changed_file_message(issue) for issue in issues if issue.severity == "error"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _changed_file_warnings(
|
|
416
|
+
issues: tuple[ChangedFileIssue, ...],
|
|
417
|
+
) -> tuple[ValidationMessage, ...]:
|
|
418
|
+
return tuple(
|
|
419
|
+
_changed_file_message(issue) for issue in issues if issue.severity != "error"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _changed_file_message(issue: ChangedFileIssue) -> ValidationMessage:
|
|
424
|
+
return ValidationMessage(
|
|
425
|
+
rule_id=issue.rule_id,
|
|
426
|
+
message=issue.message,
|
|
427
|
+
path=issue.path,
|
|
428
|
+
document_id=issue.document_id,
|
|
429
|
+
severity=issue.severity,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _document_body(registry: DocumentRegistry | None, document_id: str) -> str:
|
|
434
|
+
if registry is None:
|
|
435
|
+
return ""
|
|
436
|
+
document = registry.get_by_id(document_id)
|
|
437
|
+
if document is None:
|
|
438
|
+
return ""
|
|
439
|
+
return document.body.strip()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _message_line(message: ValidationMessage) -> str:
|
|
443
|
+
path = f"{message.path} " if message.path else ""
|
|
444
|
+
document = f"[{message.document_id}] " if message.document_id else ""
|
|
445
|
+
return f"{path}{document}{message.rule_id}: {message.message}"
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _document_payload(document: object) -> dict[str, object]:
|
|
449
|
+
payload = asdict(document)
|
|
450
|
+
for reason in payload["reasons"]:
|
|
451
|
+
if not reason["path"]:
|
|
452
|
+
reason.pop("path")
|
|
453
|
+
return payload
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _message_payload(message: ValidationMessage) -> dict[str, str | None]:
|
|
457
|
+
return {
|
|
458
|
+
"path": message.path,
|
|
459
|
+
"document_id": message.document_id,
|
|
460
|
+
"rule_id": message.rule_id,
|
|
461
|
+
"severity": message.severity,
|
|
462
|
+
"message": message.message,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _git_changed_files(root: Path):
|
|
467
|
+
return read_git_changed_files(
|
|
468
|
+
root,
|
|
469
|
+
unavailable_rule_id=RULE_GIT_CHANGED_FILES_UNAVAILABLE,
|
|
470
|
+
)
|