codealmanac 0.1.0.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.
- codealmanac/__init__.py +13 -0
- codealmanac/app.py +175 -0
- codealmanac/cli/__init__.py +1 -0
- codealmanac/cli/dispatch/__init__.py +0 -0
- codealmanac/cli/dispatch/admin.py +124 -0
- codealmanac/cli/dispatch/config.py +50 -0
- codealmanac/cli/dispatch/root.py +328 -0
- codealmanac/cli/main.py +28 -0
- codealmanac/cli/parser/__init__.py +0 -0
- codealmanac/cli/parser/admin.py +81 -0
- codealmanac/cli/parser/lifecycle.py +57 -0
- codealmanac/cli/parser/root.py +19 -0
- codealmanac/cli/parser/wiki.py +87 -0
- codealmanac/cli/render/__init__.py +0 -0
- codealmanac/cli/render/admin.py +191 -0
- codealmanac/cli/render/root.py +290 -0
- codealmanac/core/__init__.py +1 -0
- codealmanac/core/errors.py +45 -0
- codealmanac/core/models.py +14 -0
- codealmanac/core/paths.py +25 -0
- codealmanac/core/slug.py +7 -0
- codealmanac/core/text.py +5 -0
- codealmanac/database/__init__.py +15 -0
- codealmanac/database/sqlite.py +54 -0
- codealmanac/integrations/__init__.py +1 -0
- codealmanac/integrations/automation/__init__.py +3 -0
- codealmanac/integrations/automation/scheduler/__init__.py +5 -0
- codealmanac/integrations/automation/scheduler/launchd.py +163 -0
- codealmanac/integrations/command.py +56 -0
- codealmanac/integrations/harnesses/__init__.py +7 -0
- codealmanac/integrations/harnesses/claude/__init__.py +1 -0
- codealmanac/integrations/harnesses/claude/adapter.py +217 -0
- codealmanac/integrations/harnesses/codex/__init__.py +3 -0
- codealmanac/integrations/harnesses/codex/adapter.py +221 -0
- codealmanac/integrations/harnesses/git_status.py +49 -0
- codealmanac/integrations/sources/__init__.py +29 -0
- codealmanac/integrations/sources/filesystem/__init__.py +5 -0
- codealmanac/integrations/sources/filesystem/adapter.py +685 -0
- codealmanac/integrations/sources/filesystem/selection.py +209 -0
- codealmanac/integrations/sources/git/__init__.py +3 -0
- codealmanac/integrations/sources/git/adapter.py +132 -0
- codealmanac/integrations/sources/github/__init__.py +3 -0
- codealmanac/integrations/sources/github/adapter.py +413 -0
- codealmanac/integrations/sources/runtime.py +22 -0
- codealmanac/integrations/sources/transcripts/__init__.py +33 -0
- codealmanac/integrations/sources/transcripts/claude.py +61 -0
- codealmanac/integrations/sources/transcripts/codex.py +69 -0
- codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
- codealmanac/integrations/sources/transcripts/runtime.py +387 -0
- codealmanac/integrations/sources/web/__init__.py +3 -0
- codealmanac/integrations/sources/web/adapter.py +303 -0
- codealmanac/integrations/updates/__init__.py +7 -0
- codealmanac/integrations/updates/package.py +85 -0
- codealmanac/integrations/workspaces/__init__.py +1 -0
- codealmanac/integrations/workspaces/git/__init__.py +3 -0
- codealmanac/integrations/workspaces/git/probe.py +128 -0
- codealmanac/manual/README.md +24 -0
- codealmanac/manual/__init__.py +19 -0
- codealmanac/manual/build.md +20 -0
- codealmanac/manual/evidence.md +23 -0
- codealmanac/manual/garden.md +20 -0
- codealmanac/manual/ingest.md +17 -0
- codealmanac/manual/library.py +84 -0
- codealmanac/manual/models.py +83 -0
- codealmanac/manual/pages.md +28 -0
- codealmanac/manual/requests.py +6 -0
- codealmanac/manual/sources.md +18 -0
- codealmanac/manual/style.md +19 -0
- codealmanac/prompts/__init__.py +5 -0
- codealmanac/prompts/base/notability.md +14 -0
- codealmanac/prompts/base/purpose.md +23 -0
- codealmanac/prompts/base/syntax.md +19 -0
- codealmanac/prompts/models.py +9 -0
- codealmanac/prompts/operations/garden.md +26 -0
- codealmanac/prompts/operations/ingest.md +18 -0
- codealmanac/prompts/renderer.py +24 -0
- codealmanac/prompts/requests.py +22 -0
- codealmanac/server/__init__.py +1 -0
- codealmanac/server/app.py +202 -0
- codealmanac/server/assets/__init__.py +1 -0
- codealmanac/server/assets/app.css +865 -0
- codealmanac/server/assets/app.js +3 -0
- codealmanac/server/assets/index.html +80 -0
- codealmanac/server/assets/viewer/api.js +30 -0
- codealmanac/server/assets/viewer/components.js +197 -0
- codealmanac/server/assets/viewer/main.js +126 -0
- codealmanac/server/assets/viewer/renderers.js +122 -0
- codealmanac/server/assets/viewer/routes.js +36 -0
- codealmanac/services/__init__.py +1 -0
- codealmanac/services/automation/__init__.py +3 -0
- codealmanac/services/automation/models.py +83 -0
- codealmanac/services/automation/ports.py +14 -0
- codealmanac/services/automation/requests.py +40 -0
- codealmanac/services/automation/service.py +294 -0
- codealmanac/services/config/__init__.py +17 -0
- codealmanac/services/config/models.py +61 -0
- codealmanac/services/config/requests.py +21 -0
- codealmanac/services/config/service.py +55 -0
- codealmanac/services/config/store.py +26 -0
- codealmanac/services/diagnostics/__init__.py +1 -0
- codealmanac/services/diagnostics/models.py +22 -0
- codealmanac/services/diagnostics/requests.py +8 -0
- codealmanac/services/diagnostics/service.py +283 -0
- codealmanac/services/harnesses/__init__.py +1 -0
- codealmanac/services/harnesses/models.py +104 -0
- codealmanac/services/harnesses/ports.py +18 -0
- codealmanac/services/harnesses/requests.py +19 -0
- codealmanac/services/harnesses/service.py +38 -0
- codealmanac/services/health/__init__.py +1 -0
- codealmanac/services/health/requests.py +8 -0
- codealmanac/services/health/service.py +20 -0
- codealmanac/services/index/__init__.py +1 -0
- codealmanac/services/index/models.py +135 -0
- codealmanac/services/index/requests.py +26 -0
- codealmanac/services/index/service.py +86 -0
- codealmanac/services/index/store.py +411 -0
- codealmanac/services/index/views.py +524 -0
- codealmanac/services/pages/__init__.py +1 -0
- codealmanac/services/pages/requests.py +17 -0
- codealmanac/services/pages/service.py +26 -0
- codealmanac/services/runs/__init__.py +1 -0
- codealmanac/services/runs/models.py +91 -0
- codealmanac/services/runs/requests.py +76 -0
- codealmanac/services/runs/service.py +86 -0
- codealmanac/services/runs/store.py +256 -0
- codealmanac/services/search/__init__.py +1 -0
- codealmanac/services/search/requests.py +23 -0
- codealmanac/services/search/service.py +31 -0
- codealmanac/services/sources/__init__.py +1 -0
- codealmanac/services/sources/models.py +126 -0
- codealmanac/services/sources/ports.py +30 -0
- codealmanac/services/sources/requests.py +76 -0
- codealmanac/services/sources/service.py +351 -0
- codealmanac/services/tagging/__init__.py +1 -0
- codealmanac/services/tagging/models.py +9 -0
- codealmanac/services/tagging/requests.py +35 -0
- codealmanac/services/tagging/service.py +43 -0
- codealmanac/services/topics/__init__.py +1 -0
- codealmanac/services/topics/models.py +36 -0
- codealmanac/services/topics/requests.py +115 -0
- codealmanac/services/topics/service.py +297 -0
- codealmanac/services/updates/__init__.py +4 -0
- codealmanac/services/updates/models.py +83 -0
- codealmanac/services/updates/ports.py +17 -0
- codealmanac/services/updates/requests.py +10 -0
- codealmanac/services/updates/service.py +113 -0
- codealmanac/services/viewer/__init__.py +1 -0
- codealmanac/services/viewer/models.py +80 -0
- codealmanac/services/viewer/renderer.py +89 -0
- codealmanac/services/viewer/requests.py +86 -0
- codealmanac/services/viewer/service.py +211 -0
- codealmanac/services/wiki/__init__.py +1 -0
- codealmanac/services/wiki/documents.py +83 -0
- codealmanac/services/wiki/frontmatter.py +94 -0
- codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
- codealmanac/services/wiki/models.py +69 -0
- codealmanac/services/wiki/paths.py +42 -0
- codealmanac/services/wiki/service.py +57 -0
- codealmanac/services/wiki/templates.py +73 -0
- codealmanac/services/wiki/topics.py +266 -0
- codealmanac/services/wiki/wikilinks.py +58 -0
- codealmanac/services/workspaces/__init__.py +1 -0
- codealmanac/services/workspaces/models.py +124 -0
- codealmanac/services/workspaces/ports.py +9 -0
- codealmanac/services/workspaces/requests.py +82 -0
- codealmanac/services/workspaces/roots.py +74 -0
- codealmanac/services/workspaces/service.py +303 -0
- codealmanac/services/workspaces/store.py +127 -0
- codealmanac/workflows/__init__.py +1 -0
- codealmanac/workflows/build/__init__.py +1 -0
- codealmanac/workflows/build/models.py +8 -0
- codealmanac/workflows/build/service.py +45 -0
- codealmanac/workflows/garden/__init__.py +3 -0
- codealmanac/workflows/garden/models.py +30 -0
- codealmanac/workflows/garden/requests.py +22 -0
- codealmanac/workflows/garden/service.py +239 -0
- codealmanac/workflows/ingest/__init__.py +1 -0
- codealmanac/workflows/ingest/models.py +26 -0
- codealmanac/workflows/ingest/requests.py +39 -0
- codealmanac/workflows/ingest/service.py +302 -0
- codealmanac/workflows/lifecycle.py +197 -0
- codealmanac/workflows/sync/__init__.py +3 -0
- codealmanac/workflows/sync/models.py +157 -0
- codealmanac/workflows/sync/requests.py +63 -0
- codealmanac/workflows/sync/service.py +651 -0
- codealmanac/workflows/sync/store.py +51 -0
- codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
- codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
- codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
- codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
- codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.errors import CodeAlmanacError, NotFoundError
|
|
6
|
+
from codealmanac.manual import ManualLibrary
|
|
7
|
+
from codealmanac.services.diagnostics.models import (
|
|
8
|
+
DoctorCheck,
|
|
9
|
+
DoctorReport,
|
|
10
|
+
DoctorStatus,
|
|
11
|
+
)
|
|
12
|
+
from codealmanac.services.diagnostics.requests import DoctorRequest
|
|
13
|
+
from codealmanac.services.index.models import HealthReport, IndexSummary
|
|
14
|
+
from codealmanac.services.index.service import IndexService
|
|
15
|
+
from codealmanac.services.workspaces.models import Workspace, WorkspaceRegistryStatus
|
|
16
|
+
from codealmanac.services.workspaces.requests import SelectWorkspaceRequest
|
|
17
|
+
from codealmanac.services.workspaces.service import (
|
|
18
|
+
WorkspacesService,
|
|
19
|
+
workspace_registry_status,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DiagnosticsService:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
workspaces: WorkspacesService,
|
|
27
|
+
index: IndexService,
|
|
28
|
+
manual: ManualLibrary,
|
|
29
|
+
version: str,
|
|
30
|
+
python_version: str | None = None,
|
|
31
|
+
python_supported: bool | None = None,
|
|
32
|
+
):
|
|
33
|
+
self.workspaces = workspaces
|
|
34
|
+
self.index = index
|
|
35
|
+
self.manual = manual
|
|
36
|
+
self.version = version
|
|
37
|
+
self.python_version = python_version or platform.python_version()
|
|
38
|
+
if python_supported is None:
|
|
39
|
+
self.python_supported = sys.version_info >= (3, 12)
|
|
40
|
+
else:
|
|
41
|
+
self.python_supported = python_supported
|
|
42
|
+
|
|
43
|
+
def check(self, request: DoctorRequest) -> DoctorReport:
|
|
44
|
+
return DoctorReport(
|
|
45
|
+
version=self.version,
|
|
46
|
+
install=self._install_checks(),
|
|
47
|
+
wiki=self._wiki_checks(request),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _install_checks(self) -> tuple[DoctorCheck, ...]:
|
|
51
|
+
return (
|
|
52
|
+
DoctorCheck(
|
|
53
|
+
key="install.version",
|
|
54
|
+
status=DoctorStatus.OK,
|
|
55
|
+
message=f"codealmanac {self.version}",
|
|
56
|
+
),
|
|
57
|
+
DoctorCheck(
|
|
58
|
+
key="install.python",
|
|
59
|
+
status=(
|
|
60
|
+
DoctorStatus.OK if self.python_supported else DoctorStatus.PROBLEM
|
|
61
|
+
),
|
|
62
|
+
message=f"python {self.python_version}",
|
|
63
|
+
fix=None
|
|
64
|
+
if self.python_supported
|
|
65
|
+
else "install Python 3.12 or newer",
|
|
66
|
+
),
|
|
67
|
+
DoctorCheck(
|
|
68
|
+
key="install.registry",
|
|
69
|
+
status=DoctorStatus.INFO,
|
|
70
|
+
message=f"registry: {self.workspaces.registry_path}",
|
|
71
|
+
),
|
|
72
|
+
self._manual_package_check(),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _wiki_checks(self, request: DoctorRequest) -> tuple[DoctorCheck, ...]:
|
|
76
|
+
workspace = self._select_workspace(request)
|
|
77
|
+
if isinstance(workspace, DoctorCheck):
|
|
78
|
+
return (workspace,)
|
|
79
|
+
registry_status = workspace_registry_status(workspace)
|
|
80
|
+
checks: list[DoctorCheck] = [
|
|
81
|
+
DoctorCheck(
|
|
82
|
+
key="wiki.repo",
|
|
83
|
+
status=DoctorStatus.INFO,
|
|
84
|
+
message=f"repo: {workspace.root_path}",
|
|
85
|
+
),
|
|
86
|
+
registered_check(workspace, registry_status),
|
|
87
|
+
]
|
|
88
|
+
if registry_status != WorkspaceRegistryStatus.AVAILABLE:
|
|
89
|
+
return tuple(checks)
|
|
90
|
+
checks.extend(self._index_checks(workspace))
|
|
91
|
+
checks.append(self._manual_workspace_check(workspace))
|
|
92
|
+
checks.append(self._health_check(workspace))
|
|
93
|
+
return tuple(checks)
|
|
94
|
+
|
|
95
|
+
def _manual_package_check(self) -> DoctorCheck:
|
|
96
|
+
try:
|
|
97
|
+
inventory = self.manual.inventory()
|
|
98
|
+
except CodeAlmanacError as error:
|
|
99
|
+
return DoctorCheck(
|
|
100
|
+
key="install.manual",
|
|
101
|
+
status=DoctorStatus.PROBLEM,
|
|
102
|
+
message=f"manual package unavailable: {first_line(str(error))}",
|
|
103
|
+
fix="reinstall codealmanac",
|
|
104
|
+
)
|
|
105
|
+
return DoctorCheck(
|
|
106
|
+
key="install.manual",
|
|
107
|
+
status=DoctorStatus.OK,
|
|
108
|
+
message=f"manual: {len(inventory.documents)} bundled docs",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _manual_workspace_check(self, workspace: Workspace) -> DoctorCheck:
|
|
112
|
+
try:
|
|
113
|
+
status = self.manual.workspace_status(workspace.almanac_path / "manual")
|
|
114
|
+
except CodeAlmanacError as error:
|
|
115
|
+
return DoctorCheck(
|
|
116
|
+
key="wiki.manual",
|
|
117
|
+
status=DoctorStatus.PROBLEM,
|
|
118
|
+
message=f"manual unavailable: {first_line(str(error))}",
|
|
119
|
+
fix="run: codealmanac build",
|
|
120
|
+
)
|
|
121
|
+
if status.complete:
|
|
122
|
+
if len(status.changed) > 0:
|
|
123
|
+
changed = ", ".join(status.changed)
|
|
124
|
+
return DoctorCheck(
|
|
125
|
+
key="wiki.manual",
|
|
126
|
+
status=DoctorStatus.INFO,
|
|
127
|
+
message=f"manual differs: {changed}",
|
|
128
|
+
fix=(
|
|
129
|
+
"review local manual files; "
|
|
130
|
+
"codealmanac build preserves existing files"
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
return DoctorCheck(
|
|
134
|
+
key="wiki.manual",
|
|
135
|
+
status=DoctorStatus.OK,
|
|
136
|
+
message=f"manual: {len(status.present)} docs",
|
|
137
|
+
)
|
|
138
|
+
missing = ", ".join(status.missing)
|
|
139
|
+
return DoctorCheck(
|
|
140
|
+
key="wiki.manual",
|
|
141
|
+
status=DoctorStatus.PROBLEM,
|
|
142
|
+
message=f"manual missing: {missing}",
|
|
143
|
+
fix="run: codealmanac build",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _select_workspace(self, request: DoctorRequest) -> Workspace | DoctorCheck:
|
|
147
|
+
try:
|
|
148
|
+
if request.wiki is None:
|
|
149
|
+
return self.workspaces.resolve(request.cwd)
|
|
150
|
+
return self.workspaces.select(
|
|
151
|
+
SelectWorkspaceRequest(selector=request.wiki, base_path=request.cwd)
|
|
152
|
+
)
|
|
153
|
+
except NotFoundError as error:
|
|
154
|
+
if request.wiki is None:
|
|
155
|
+
return DoctorCheck(
|
|
156
|
+
key="wiki.none",
|
|
157
|
+
status=DoctorStatus.INFO,
|
|
158
|
+
message="no Almanac wiki found from current directory",
|
|
159
|
+
fix="run: codealmanac init",
|
|
160
|
+
)
|
|
161
|
+
return DoctorCheck(
|
|
162
|
+
key="wiki.selected",
|
|
163
|
+
status=DoctorStatus.PROBLEM,
|
|
164
|
+
message=first_line(str(error)),
|
|
165
|
+
fix="run: codealmanac list",
|
|
166
|
+
)
|
|
167
|
+
except CodeAlmanacError as error:
|
|
168
|
+
return DoctorCheck(
|
|
169
|
+
key="wiki.selected",
|
|
170
|
+
status=DoctorStatus.PROBLEM,
|
|
171
|
+
message=first_line(str(error)),
|
|
172
|
+
fix="run: codealmanac list",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def _index_checks(self, workspace: Workspace) -> Sequence[DoctorCheck]:
|
|
176
|
+
try:
|
|
177
|
+
summary = self.index.summary(workspace.workspace_id)
|
|
178
|
+
except Exception as error:
|
|
179
|
+
return (
|
|
180
|
+
DoctorCheck(
|
|
181
|
+
key="wiki.index",
|
|
182
|
+
status=DoctorStatus.PROBLEM,
|
|
183
|
+
message=f"could not rebuild index: {first_line(str(error))}",
|
|
184
|
+
fix="run: codealmanac reindex",
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
return (
|
|
188
|
+
DoctorCheck(
|
|
189
|
+
key="wiki.index",
|
|
190
|
+
status=DoctorStatus.OK,
|
|
191
|
+
message=index_message(summary),
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _health_check(self, workspace: Workspace) -> DoctorCheck:
|
|
196
|
+
try:
|
|
197
|
+
report = self.index.health_report(workspace.workspace_id)
|
|
198
|
+
except Exception as error:
|
|
199
|
+
return DoctorCheck(
|
|
200
|
+
key="wiki.health",
|
|
201
|
+
status=DoctorStatus.PROBLEM,
|
|
202
|
+
message=f"could not run health: {first_line(str(error))}",
|
|
203
|
+
fix="run: codealmanac health",
|
|
204
|
+
)
|
|
205
|
+
problems = health_problem_count(report)
|
|
206
|
+
if problems == 0:
|
|
207
|
+
return DoctorCheck(
|
|
208
|
+
key="wiki.health",
|
|
209
|
+
status=DoctorStatus.OK,
|
|
210
|
+
message="health: 0 problems",
|
|
211
|
+
)
|
|
212
|
+
return DoctorCheck(
|
|
213
|
+
key="wiki.health",
|
|
214
|
+
status=DoctorStatus.PROBLEM,
|
|
215
|
+
message=f"health: {problems} {problem_word(problems)}",
|
|
216
|
+
fix="run: codealmanac health",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def index_message(summary: IndexSummary) -> str:
|
|
221
|
+
skip_suffix = (
|
|
222
|
+
f", {summary.files_skipped} skipped" if summary.files_skipped > 0 else ""
|
|
223
|
+
)
|
|
224
|
+
return (
|
|
225
|
+
f"index: {summary.pages} {page_word(summary.pages)}, "
|
|
226
|
+
f"{summary.topics} {topic_word(summary.topics)} "
|
|
227
|
+
f"({summary.files_seen} files seen{skip_suffix})"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def health_problem_count(report: HealthReport) -> int:
|
|
232
|
+
return (
|
|
233
|
+
len(report.orphans)
|
|
234
|
+
+ len(report.dead_refs)
|
|
235
|
+
+ len(report.broken_links)
|
|
236
|
+
+ len(report.broken_xwiki)
|
|
237
|
+
+ len(report.empty_topics)
|
|
238
|
+
+ len(report.empty_pages)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def first_line(value: str) -> str:
|
|
243
|
+
return value.splitlines()[0] if value.splitlines() else value
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def registered_check(
|
|
247
|
+
workspace: Workspace,
|
|
248
|
+
status: WorkspaceRegistryStatus,
|
|
249
|
+
) -> DoctorCheck:
|
|
250
|
+
registered = (
|
|
251
|
+
f"registered as '{workspace.name}' ({workspace.almanac_root.as_posix()})"
|
|
252
|
+
)
|
|
253
|
+
if status == WorkspaceRegistryStatus.AVAILABLE:
|
|
254
|
+
return DoctorCheck(
|
|
255
|
+
key="wiki.registered",
|
|
256
|
+
status=DoctorStatus.OK,
|
|
257
|
+
message=registered,
|
|
258
|
+
)
|
|
259
|
+
if status == WorkspaceRegistryStatus.MISSING_REPO:
|
|
260
|
+
return DoctorCheck(
|
|
261
|
+
key="wiki.registered",
|
|
262
|
+
status=DoctorStatus.PROBLEM,
|
|
263
|
+
message=f"{registered}, but repo path is missing",
|
|
264
|
+
fix=f"run: codealmanac list --drop {workspace.workspace_id}",
|
|
265
|
+
)
|
|
266
|
+
return DoctorCheck(
|
|
267
|
+
key="wiki.registered",
|
|
268
|
+
status=DoctorStatus.PROBLEM,
|
|
269
|
+
message=f"{registered}, but Almanac root is missing: {workspace.almanac_path}",
|
|
270
|
+
fix="run: codealmanac build",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def page_word(count: int) -> str:
|
|
275
|
+
return "page" if count == 1 else "pages"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def topic_word(count: int) -> str:
|
|
279
|
+
return "topic" if count == 1 else "topics"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def problem_word(count: int) -> str:
|
|
283
|
+
return "problem" if count == 1 else "problems"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Normalized AI harness contracts for lifecycle workflows."""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import field_validator
|
|
5
|
+
|
|
6
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
7
|
+
from codealmanac.core.text import required_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HarnessKind(StrEnum):
|
|
11
|
+
CODEX = "codex"
|
|
12
|
+
CLAUDE = "claude"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HarnessRunStatus(StrEnum):
|
|
16
|
+
SUCCEEDED = "succeeded"
|
|
17
|
+
FAILED = "failed"
|
|
18
|
+
CANCELLED = "cancelled"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HarnessEventKind(StrEnum):
|
|
22
|
+
TEXT = "text"
|
|
23
|
+
TOOL_USE = "tool_use"
|
|
24
|
+
TOOL_RESULT = "tool_result"
|
|
25
|
+
TOOL_SUMMARY = "tool_summary"
|
|
26
|
+
CONTEXT_USAGE = "context_usage"
|
|
27
|
+
WARNING = "warning"
|
|
28
|
+
ERROR = "error"
|
|
29
|
+
DONE = "done"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HarnessReadiness(CodeAlmanacModel):
|
|
33
|
+
kind: HarnessKind
|
|
34
|
+
available: bool
|
|
35
|
+
message: str
|
|
36
|
+
|
|
37
|
+
@field_validator("message")
|
|
38
|
+
@classmethod
|
|
39
|
+
def require_message(cls, value: str) -> str:
|
|
40
|
+
return required_text(value, "harness readiness message")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HarnessTranscriptRef(CodeAlmanacModel):
|
|
44
|
+
kind: HarnessKind
|
|
45
|
+
session_id: str
|
|
46
|
+
transcript_path: Path | None = None
|
|
47
|
+
|
|
48
|
+
@field_validator("session_id")
|
|
49
|
+
@classmethod
|
|
50
|
+
def require_session_id(cls, value: str) -> str:
|
|
51
|
+
return required_text(value, "harness transcript session id")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class HarnessEvent(CodeAlmanacModel):
|
|
55
|
+
kind: HarnessEventKind
|
|
56
|
+
message: str
|
|
57
|
+
status: HarnessRunStatus | None = None
|
|
58
|
+
|
|
59
|
+
@field_validator("message")
|
|
60
|
+
@classmethod
|
|
61
|
+
def require_message(cls, value: str) -> str:
|
|
62
|
+
return required_text(value, "harness event message")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class HarnessRunResult(CodeAlmanacModel):
|
|
66
|
+
kind: HarnessKind
|
|
67
|
+
status: HarnessRunStatus
|
|
68
|
+
output_text: str
|
|
69
|
+
summary: str | None = None
|
|
70
|
+
changed_files: tuple[Path, ...] = ()
|
|
71
|
+
transcript: HarnessTranscriptRef | None = None
|
|
72
|
+
events: tuple[HarnessEvent, ...] = ()
|
|
73
|
+
|
|
74
|
+
@field_validator("output_text")
|
|
75
|
+
@classmethod
|
|
76
|
+
def require_output_text(cls, value: str) -> str:
|
|
77
|
+
return required_text(value, "harness output")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def terminal_harness_event(
|
|
81
|
+
kind: HarnessKind,
|
|
82
|
+
status: HarnessRunStatus,
|
|
83
|
+
output_text: str,
|
|
84
|
+
) -> HarnessEvent:
|
|
85
|
+
return HarnessEvent(
|
|
86
|
+
kind=HarnessEventKind.DONE,
|
|
87
|
+
status=status,
|
|
88
|
+
message=terminal_harness_message(kind, status, output_text),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def terminal_harness_message(
|
|
93
|
+
kind: HarnessKind,
|
|
94
|
+
status: HarnessRunStatus,
|
|
95
|
+
output_text: str,
|
|
96
|
+
) -> str:
|
|
97
|
+
suffix = first_line(output_text)
|
|
98
|
+
details = f": {suffix}" if suffix else ""
|
|
99
|
+
return f"{kind.value} {status.value}{details}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def first_line(value: str) -> str:
|
|
103
|
+
lines = value.splitlines()
|
|
104
|
+
return lines[0] if lines else value
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from codealmanac.services.harnesses.models import (
|
|
4
|
+
HarnessKind,
|
|
5
|
+
HarnessReadiness,
|
|
6
|
+
HarnessRunResult,
|
|
7
|
+
)
|
|
8
|
+
from codealmanac.services.harnesses.requests import RunHarnessRequest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HarnessAdapter(Protocol):
|
|
12
|
+
kind: HarnessKind
|
|
13
|
+
|
|
14
|
+
def check(self) -> HarnessReadiness:
|
|
15
|
+
"""Return local readiness without starting an agent run."""
|
|
16
|
+
|
|
17
|
+
def run(self, request: RunHarnessRequest) -> HarnessRunResult:
|
|
18
|
+
"""Run one normalized agent task."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
6
|
+
from codealmanac.core.text import required_text
|
|
7
|
+
from codealmanac.services.harnesses.models import HarnessKind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunHarnessRequest(CodeAlmanacModel):
|
|
11
|
+
kind: HarnessKind
|
|
12
|
+
cwd: Path
|
|
13
|
+
prompt: str
|
|
14
|
+
title: str | None = None
|
|
15
|
+
|
|
16
|
+
@field_validator("prompt")
|
|
17
|
+
@classmethod
|
|
18
|
+
def require_prompt(cls, value: str) -> str:
|
|
19
|
+
return required_text(value, "harness prompt")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from codealmanac.core.errors import ConflictError, NotFoundError
|
|
4
|
+
from codealmanac.services.harnesses.models import (
|
|
5
|
+
HarnessKind,
|
|
6
|
+
HarnessReadiness,
|
|
7
|
+
HarnessRunResult,
|
|
8
|
+
)
|
|
9
|
+
from codealmanac.services.harnesses.ports import HarnessAdapter
|
|
10
|
+
from codealmanac.services.harnesses.requests import RunHarnessRequest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HarnessesService:
|
|
14
|
+
def __init__(self, adapters: Sequence[HarnessAdapter] = ()):
|
|
15
|
+
self.adapters = adapters_by_kind(adapters)
|
|
16
|
+
|
|
17
|
+
def check(self) -> tuple[HarnessReadiness, ...]:
|
|
18
|
+
return tuple(adapter.check() for adapter in self.adapters.values())
|
|
19
|
+
|
|
20
|
+
def run(self, request: RunHarnessRequest) -> HarnessRunResult:
|
|
21
|
+
return self.adapter_for(request.kind).run(request)
|
|
22
|
+
|
|
23
|
+
def adapter_for(self, kind: HarnessKind) -> HarnessAdapter:
|
|
24
|
+
try:
|
|
25
|
+
return self.adapters[kind]
|
|
26
|
+
except KeyError as error:
|
|
27
|
+
raise NotFoundError("harness", kind.value) from error
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def adapters_by_kind(
|
|
31
|
+
adapters: Sequence[HarnessAdapter],
|
|
32
|
+
) -> dict[HarnessKind, HarnessAdapter]:
|
|
33
|
+
indexed: dict[HarnessKind, HarnessAdapter] = {}
|
|
34
|
+
for adapter in adapters:
|
|
35
|
+
if adapter.kind in indexed:
|
|
36
|
+
raise ConflictError(f"duplicate harness adapter: {adapter.kind.value}")
|
|
37
|
+
indexed[adapter.kind] = adapter
|
|
38
|
+
return indexed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from codealmanac.services.health.requests import HealthCheckRequest
|
|
2
|
+
from codealmanac.services.index.models import HealthReport
|
|
3
|
+
from codealmanac.services.index.service import IndexService
|
|
4
|
+
from codealmanac.services.workspaces.requests import SelectWorkspaceRequest
|
|
5
|
+
from codealmanac.services.workspaces.service import WorkspacesService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HealthService:
|
|
9
|
+
def __init__(self, workspaces: WorkspacesService, index: IndexService):
|
|
10
|
+
self.workspaces = workspaces
|
|
11
|
+
self.index = index
|
|
12
|
+
|
|
13
|
+
def check(self, request: HealthCheckRequest) -> HealthReport:
|
|
14
|
+
if request.wiki is None:
|
|
15
|
+
workspace = self.workspaces.resolve(request.cwd)
|
|
16
|
+
else:
|
|
17
|
+
workspace = self.workspaces.select(
|
|
18
|
+
SelectWorkspaceRequest(selector=request.wiki, base_path=request.cwd)
|
|
19
|
+
)
|
|
20
|
+
return self.index.health_report(workspace.workspace_id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HealthCategory(StrEnum):
|
|
8
|
+
ORPHANS = "orphans"
|
|
9
|
+
DEAD_REFS = "dead_refs"
|
|
10
|
+
BROKEN_LINKS = "broken_links"
|
|
11
|
+
BROKEN_XWIKI = "broken_xwiki"
|
|
12
|
+
EMPTY_TOPICS = "empty_topics"
|
|
13
|
+
EMPTY_PAGES = "empty_pages"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SearchPageResult(CodeAlmanacModel):
|
|
17
|
+
slug: str
|
|
18
|
+
title: str | None
|
|
19
|
+
summary: str | None
|
|
20
|
+
updated_at: int
|
|
21
|
+
archived_at: int | None
|
|
22
|
+
superseded_by: str | None
|
|
23
|
+
topics: tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PageFileReference(CodeAlmanacModel):
|
|
27
|
+
path: str
|
|
28
|
+
is_dir: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CrossWikiReference(CodeAlmanacModel):
|
|
32
|
+
wiki: str
|
|
33
|
+
target: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PageView(CodeAlmanacModel):
|
|
37
|
+
slug: str
|
|
38
|
+
title: str | None
|
|
39
|
+
summary: str | None
|
|
40
|
+
file_path: Path
|
|
41
|
+
updated_at: int
|
|
42
|
+
archived_at: int | None
|
|
43
|
+
superseded_by: str | None
|
|
44
|
+
topics: tuple[str, ...]
|
|
45
|
+
file_refs: tuple[PageFileReference, ...]
|
|
46
|
+
wikilinks_out: tuple[str, ...]
|
|
47
|
+
wikilinks_in: tuple[str, ...]
|
|
48
|
+
cross_wiki_links: tuple[CrossWikiReference, ...]
|
|
49
|
+
body: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class IndexRefreshResult(CodeAlmanacModel):
|
|
53
|
+
changed: int
|
|
54
|
+
removed: int
|
|
55
|
+
pages_indexed: int
|
|
56
|
+
files_seen: int
|
|
57
|
+
files_skipped: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class IndexCounts(CodeAlmanacModel):
|
|
61
|
+
pages: int
|
|
62
|
+
topics: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class IndexSummary(CodeAlmanacModel):
|
|
66
|
+
pages: int
|
|
67
|
+
topics: int
|
|
68
|
+
files_seen: int
|
|
69
|
+
files_skipped: int
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class IndexedPageFingerprint(CodeAlmanacModel):
|
|
73
|
+
slug: str
|
|
74
|
+
relative_path: str
|
|
75
|
+
content_hash: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class IndexSourceSignature(CodeAlmanacModel):
|
|
79
|
+
pages: tuple[IndexedPageFingerprint, ...]
|
|
80
|
+
topics_hash: str
|
|
81
|
+
files_seen: int
|
|
82
|
+
files_skipped: int
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TopicSummary(CodeAlmanacModel):
|
|
86
|
+
slug: str
|
|
87
|
+
title: str | None
|
|
88
|
+
description: str | None
|
|
89
|
+
page_count: int
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TopicDetail(CodeAlmanacModel):
|
|
93
|
+
slug: str
|
|
94
|
+
title: str | None
|
|
95
|
+
description: str | None
|
|
96
|
+
parents: tuple[str, ...]
|
|
97
|
+
children: tuple[str, ...]
|
|
98
|
+
pages: tuple[str, ...]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class OrphanPage(CodeAlmanacModel):
|
|
102
|
+
slug: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DeadFileReference(CodeAlmanacModel):
|
|
106
|
+
slug: str
|
|
107
|
+
path: str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class BrokenPageLink(CodeAlmanacModel):
|
|
111
|
+
source_slug: str
|
|
112
|
+
target_slug: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class BrokenCrossWikiLink(CodeAlmanacModel):
|
|
116
|
+
source_slug: str
|
|
117
|
+
target_wiki: str
|
|
118
|
+
target_slug: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class EmptyTopic(CodeAlmanacModel):
|
|
122
|
+
slug: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class EmptyPage(CodeAlmanacModel):
|
|
126
|
+
slug: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class HealthReport(CodeAlmanacModel):
|
|
130
|
+
orphans: tuple[OrphanPage, ...]
|
|
131
|
+
dead_refs: tuple[DeadFileReference, ...]
|
|
132
|
+
broken_links: tuple[BrokenPageLink, ...]
|
|
133
|
+
broken_xwiki: tuple[BrokenCrossWikiLink, ...]
|
|
134
|
+
empty_topics: tuple[EmptyTopic, ...]
|
|
135
|
+
empty_pages: tuple[EmptyPage, ...]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SearchIndexRequest(CodeAlmanacModel):
|
|
9
|
+
query: str | None = None
|
|
10
|
+
topics: tuple[str, ...] = ()
|
|
11
|
+
mentions: str | None = None
|
|
12
|
+
include_archive: bool = False
|
|
13
|
+
archived: bool = False
|
|
14
|
+
limit: int | None = None
|
|
15
|
+
|
|
16
|
+
@field_validator("limit")
|
|
17
|
+
@classmethod
|
|
18
|
+
def non_negative_limit(cls, value: int | None) -> int | None:
|
|
19
|
+
if value is not None and value < 0:
|
|
20
|
+
raise ValueError("limit must be non-negative")
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReindexRequest(CodeAlmanacModel):
|
|
25
|
+
cwd: Path
|
|
26
|
+
wiki: str | None = None
|