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.
Files changed (192) hide show
  1. codealmanac/__init__.py +13 -0
  2. codealmanac/app.py +175 -0
  3. codealmanac/cli/__init__.py +1 -0
  4. codealmanac/cli/dispatch/__init__.py +0 -0
  5. codealmanac/cli/dispatch/admin.py +124 -0
  6. codealmanac/cli/dispatch/config.py +50 -0
  7. codealmanac/cli/dispatch/root.py +328 -0
  8. codealmanac/cli/main.py +28 -0
  9. codealmanac/cli/parser/__init__.py +0 -0
  10. codealmanac/cli/parser/admin.py +81 -0
  11. codealmanac/cli/parser/lifecycle.py +57 -0
  12. codealmanac/cli/parser/root.py +19 -0
  13. codealmanac/cli/parser/wiki.py +87 -0
  14. codealmanac/cli/render/__init__.py +0 -0
  15. codealmanac/cli/render/admin.py +191 -0
  16. codealmanac/cli/render/root.py +290 -0
  17. codealmanac/core/__init__.py +1 -0
  18. codealmanac/core/errors.py +45 -0
  19. codealmanac/core/models.py +14 -0
  20. codealmanac/core/paths.py +25 -0
  21. codealmanac/core/slug.py +7 -0
  22. codealmanac/core/text.py +5 -0
  23. codealmanac/database/__init__.py +15 -0
  24. codealmanac/database/sqlite.py +54 -0
  25. codealmanac/integrations/__init__.py +1 -0
  26. codealmanac/integrations/automation/__init__.py +3 -0
  27. codealmanac/integrations/automation/scheduler/__init__.py +5 -0
  28. codealmanac/integrations/automation/scheduler/launchd.py +163 -0
  29. codealmanac/integrations/command.py +56 -0
  30. codealmanac/integrations/harnesses/__init__.py +7 -0
  31. codealmanac/integrations/harnesses/claude/__init__.py +1 -0
  32. codealmanac/integrations/harnesses/claude/adapter.py +217 -0
  33. codealmanac/integrations/harnesses/codex/__init__.py +3 -0
  34. codealmanac/integrations/harnesses/codex/adapter.py +221 -0
  35. codealmanac/integrations/harnesses/git_status.py +49 -0
  36. codealmanac/integrations/sources/__init__.py +29 -0
  37. codealmanac/integrations/sources/filesystem/__init__.py +5 -0
  38. codealmanac/integrations/sources/filesystem/adapter.py +685 -0
  39. codealmanac/integrations/sources/filesystem/selection.py +209 -0
  40. codealmanac/integrations/sources/git/__init__.py +3 -0
  41. codealmanac/integrations/sources/git/adapter.py +132 -0
  42. codealmanac/integrations/sources/github/__init__.py +3 -0
  43. codealmanac/integrations/sources/github/adapter.py +413 -0
  44. codealmanac/integrations/sources/runtime.py +22 -0
  45. codealmanac/integrations/sources/transcripts/__init__.py +33 -0
  46. codealmanac/integrations/sources/transcripts/claude.py +61 -0
  47. codealmanac/integrations/sources/transcripts/codex.py +69 -0
  48. codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
  49. codealmanac/integrations/sources/transcripts/runtime.py +387 -0
  50. codealmanac/integrations/sources/web/__init__.py +3 -0
  51. codealmanac/integrations/sources/web/adapter.py +303 -0
  52. codealmanac/integrations/updates/__init__.py +7 -0
  53. codealmanac/integrations/updates/package.py +85 -0
  54. codealmanac/integrations/workspaces/__init__.py +1 -0
  55. codealmanac/integrations/workspaces/git/__init__.py +3 -0
  56. codealmanac/integrations/workspaces/git/probe.py +128 -0
  57. codealmanac/manual/README.md +24 -0
  58. codealmanac/manual/__init__.py +19 -0
  59. codealmanac/manual/build.md +20 -0
  60. codealmanac/manual/evidence.md +23 -0
  61. codealmanac/manual/garden.md +20 -0
  62. codealmanac/manual/ingest.md +17 -0
  63. codealmanac/manual/library.py +84 -0
  64. codealmanac/manual/models.py +83 -0
  65. codealmanac/manual/pages.md +28 -0
  66. codealmanac/manual/requests.py +6 -0
  67. codealmanac/manual/sources.md +18 -0
  68. codealmanac/manual/style.md +19 -0
  69. codealmanac/prompts/__init__.py +5 -0
  70. codealmanac/prompts/base/notability.md +14 -0
  71. codealmanac/prompts/base/purpose.md +23 -0
  72. codealmanac/prompts/base/syntax.md +19 -0
  73. codealmanac/prompts/models.py +9 -0
  74. codealmanac/prompts/operations/garden.md +26 -0
  75. codealmanac/prompts/operations/ingest.md +18 -0
  76. codealmanac/prompts/renderer.py +24 -0
  77. codealmanac/prompts/requests.py +22 -0
  78. codealmanac/server/__init__.py +1 -0
  79. codealmanac/server/app.py +202 -0
  80. codealmanac/server/assets/__init__.py +1 -0
  81. codealmanac/server/assets/app.css +865 -0
  82. codealmanac/server/assets/app.js +3 -0
  83. codealmanac/server/assets/index.html +80 -0
  84. codealmanac/server/assets/viewer/api.js +30 -0
  85. codealmanac/server/assets/viewer/components.js +197 -0
  86. codealmanac/server/assets/viewer/main.js +126 -0
  87. codealmanac/server/assets/viewer/renderers.js +122 -0
  88. codealmanac/server/assets/viewer/routes.js +36 -0
  89. codealmanac/services/__init__.py +1 -0
  90. codealmanac/services/automation/__init__.py +3 -0
  91. codealmanac/services/automation/models.py +83 -0
  92. codealmanac/services/automation/ports.py +14 -0
  93. codealmanac/services/automation/requests.py +40 -0
  94. codealmanac/services/automation/service.py +294 -0
  95. codealmanac/services/config/__init__.py +17 -0
  96. codealmanac/services/config/models.py +61 -0
  97. codealmanac/services/config/requests.py +21 -0
  98. codealmanac/services/config/service.py +55 -0
  99. codealmanac/services/config/store.py +26 -0
  100. codealmanac/services/diagnostics/__init__.py +1 -0
  101. codealmanac/services/diagnostics/models.py +22 -0
  102. codealmanac/services/diagnostics/requests.py +8 -0
  103. codealmanac/services/diagnostics/service.py +283 -0
  104. codealmanac/services/harnesses/__init__.py +1 -0
  105. codealmanac/services/harnesses/models.py +104 -0
  106. codealmanac/services/harnesses/ports.py +18 -0
  107. codealmanac/services/harnesses/requests.py +19 -0
  108. codealmanac/services/harnesses/service.py +38 -0
  109. codealmanac/services/health/__init__.py +1 -0
  110. codealmanac/services/health/requests.py +8 -0
  111. codealmanac/services/health/service.py +20 -0
  112. codealmanac/services/index/__init__.py +1 -0
  113. codealmanac/services/index/models.py +135 -0
  114. codealmanac/services/index/requests.py +26 -0
  115. codealmanac/services/index/service.py +86 -0
  116. codealmanac/services/index/store.py +411 -0
  117. codealmanac/services/index/views.py +524 -0
  118. codealmanac/services/pages/__init__.py +1 -0
  119. codealmanac/services/pages/requests.py +17 -0
  120. codealmanac/services/pages/service.py +26 -0
  121. codealmanac/services/runs/__init__.py +1 -0
  122. codealmanac/services/runs/models.py +91 -0
  123. codealmanac/services/runs/requests.py +76 -0
  124. codealmanac/services/runs/service.py +86 -0
  125. codealmanac/services/runs/store.py +256 -0
  126. codealmanac/services/search/__init__.py +1 -0
  127. codealmanac/services/search/requests.py +23 -0
  128. codealmanac/services/search/service.py +31 -0
  129. codealmanac/services/sources/__init__.py +1 -0
  130. codealmanac/services/sources/models.py +126 -0
  131. codealmanac/services/sources/ports.py +30 -0
  132. codealmanac/services/sources/requests.py +76 -0
  133. codealmanac/services/sources/service.py +351 -0
  134. codealmanac/services/tagging/__init__.py +1 -0
  135. codealmanac/services/tagging/models.py +9 -0
  136. codealmanac/services/tagging/requests.py +35 -0
  137. codealmanac/services/tagging/service.py +43 -0
  138. codealmanac/services/topics/__init__.py +1 -0
  139. codealmanac/services/topics/models.py +36 -0
  140. codealmanac/services/topics/requests.py +115 -0
  141. codealmanac/services/topics/service.py +297 -0
  142. codealmanac/services/updates/__init__.py +4 -0
  143. codealmanac/services/updates/models.py +83 -0
  144. codealmanac/services/updates/ports.py +17 -0
  145. codealmanac/services/updates/requests.py +10 -0
  146. codealmanac/services/updates/service.py +113 -0
  147. codealmanac/services/viewer/__init__.py +1 -0
  148. codealmanac/services/viewer/models.py +80 -0
  149. codealmanac/services/viewer/renderer.py +89 -0
  150. codealmanac/services/viewer/requests.py +86 -0
  151. codealmanac/services/viewer/service.py +211 -0
  152. codealmanac/services/wiki/__init__.py +1 -0
  153. codealmanac/services/wiki/documents.py +83 -0
  154. codealmanac/services/wiki/frontmatter.py +94 -0
  155. codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
  156. codealmanac/services/wiki/models.py +69 -0
  157. codealmanac/services/wiki/paths.py +42 -0
  158. codealmanac/services/wiki/service.py +57 -0
  159. codealmanac/services/wiki/templates.py +73 -0
  160. codealmanac/services/wiki/topics.py +266 -0
  161. codealmanac/services/wiki/wikilinks.py +58 -0
  162. codealmanac/services/workspaces/__init__.py +1 -0
  163. codealmanac/services/workspaces/models.py +124 -0
  164. codealmanac/services/workspaces/ports.py +9 -0
  165. codealmanac/services/workspaces/requests.py +82 -0
  166. codealmanac/services/workspaces/roots.py +74 -0
  167. codealmanac/services/workspaces/service.py +303 -0
  168. codealmanac/services/workspaces/store.py +127 -0
  169. codealmanac/workflows/__init__.py +1 -0
  170. codealmanac/workflows/build/__init__.py +1 -0
  171. codealmanac/workflows/build/models.py +8 -0
  172. codealmanac/workflows/build/service.py +45 -0
  173. codealmanac/workflows/garden/__init__.py +3 -0
  174. codealmanac/workflows/garden/models.py +30 -0
  175. codealmanac/workflows/garden/requests.py +22 -0
  176. codealmanac/workflows/garden/service.py +239 -0
  177. codealmanac/workflows/ingest/__init__.py +1 -0
  178. codealmanac/workflows/ingest/models.py +26 -0
  179. codealmanac/workflows/ingest/requests.py +39 -0
  180. codealmanac/workflows/ingest/service.py +302 -0
  181. codealmanac/workflows/lifecycle.py +197 -0
  182. codealmanac/workflows/sync/__init__.py +3 -0
  183. codealmanac/workflows/sync/models.py +157 -0
  184. codealmanac/workflows/sync/requests.py +63 -0
  185. codealmanac/workflows/sync/service.py +651 -0
  186. codealmanac/workflows/sync/store.py +51 -0
  187. codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
  188. codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
  189. codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
  190. codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  191. codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
  192. codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,26 @@
1
+ # Garden Operation
2
+
3
+ Garden improves the existing configured Almanac wiki as a whole graph.
4
+
5
+ Before editing, read `manual/README.md`, `manual/pages.md`,
6
+ `manual/evidence.md`, `manual/style.md`, `manual/sources.md`, and
7
+ `manual/garden.md` under the configured Almanac root.
8
+
9
+ Garden is cultivation. The goal is not to add activity; the goal is to make the
10
+ project memory more coherent, navigable, current, and trustworthy.
11
+
12
+ Inspect pages, topics, links, referenced files, and health issues where useful.
13
+ Look for duplicate pages, stale claims, missing anchors, missing links, bloated
14
+ pages, confusing topics, unsupported claims, disconnected temporal notes, and
15
+ clusters that need hubs.
16
+
17
+ Prefer synthesis over logs. Fold fragments into evolving pages when chronology
18
+ is not part of the meaning. Split pages that contain independent concepts.
19
+ Merge overlapping pages when one page is the better home. Archive or supersede
20
+ stale pages only when preserving history helps future agents.
21
+
22
+ Improve topic neighborhoods, leads, links, source notes, and page boundaries.
23
+ Create or revise hub pages when a dense cluster needs reading order.
24
+
25
+ No-op is valid when the wiki is already coherent enough for the current pass.
26
+ Do not churn unrelated pages to show activity.
@@ -0,0 +1,18 @@
1
+ # Ingest Operation
2
+
3
+ Ingest starts from bounded selected material and distills reusable project
4
+ understanding into the existing configured Almanac graph.
5
+
6
+ Before editing, read `manual/README.md`, `manual/pages.md`,
7
+ `manual/evidence.md`, `manual/style.md`, `manual/sources.md`, and
8
+ `manual/ingest.md` under the configured Almanac root.
9
+
10
+ Use the source briefs and source runtime snapshots in the runtime context as
11
+ operation input. The brief identifies the selected source and its provenance
12
+ hint. The runtime snapshot is readable source material gathered before the
13
+ agent run. Update existing synthesis pages when they are the right home. Create
14
+ new pages only when the material names a durable concept, decision, flow,
15
+ invariant, incident, gotcha, or project-world fact that deserves its own page.
16
+
17
+ Preserve the source-selection boundary. The selected material is raw material,
18
+ not the source of truth for every claim.
@@ -0,0 +1,24 @@
1
+ from importlib.resources import files
2
+
3
+ from codealmanac.prompts.models import PromptName
4
+ from codealmanac.prompts.requests import RenderPromptRequest
5
+
6
+ PROMPTS_PACKAGE = "codealmanac.prompts"
7
+
8
+
9
+ class PromptRenderer:
10
+ def render(self, request: RenderPromptRequest) -> str:
11
+ sections = [prompt_text(section) for section in request.sections]
12
+ sections.extend(request.context)
13
+ return join_prompt_sections(tuple(sections))
14
+
15
+
16
+ def prompt_text(name: PromptName) -> str:
17
+ resource = files(PROMPTS_PACKAGE).joinpath(*name.value.split("/"))
18
+ return resource.read_text(encoding="utf-8").strip()
19
+
20
+
21
+ def join_prompt_sections(sections: tuple[str, ...]) -> str:
22
+ return "\n\n---\n\n".join(
23
+ section.strip() for section in sections if section.strip()
24
+ )
@@ -0,0 +1,22 @@
1
+ from pydantic import field_validator
2
+
3
+ from codealmanac.core.models import CodeAlmanacModel
4
+ from codealmanac.core.text import required_text
5
+ from codealmanac.prompts.models import PromptName
6
+
7
+
8
+ class RenderPromptRequest(CodeAlmanacModel):
9
+ sections: tuple[PromptName, ...]
10
+ context: tuple[str, ...] = ()
11
+
12
+ @field_validator("sections")
13
+ @classmethod
14
+ def require_sections(cls, value: tuple[PromptName, ...]) -> tuple[PromptName, ...]:
15
+ if len(value) == 0:
16
+ raise ValueError("at least one prompt section is required")
17
+ return value
18
+
19
+ @field_validator("context")
20
+ @classmethod
21
+ def require_context_text(cls, value: tuple[str, ...]) -> tuple[str, ...]:
22
+ return tuple(required_text(item, "prompt context") for item in value)
@@ -0,0 +1 @@
1
+ """Local read-only web server adapter."""
@@ -0,0 +1,202 @@
1
+ from enum import StrEnum
2
+ from importlib import resources
3
+ from pathlib import Path
4
+
5
+ from fastapi import FastAPI, HTTPException
6
+ from fastapi.responses import HTMLResponse, Response
7
+ from pydantic import BaseModel, ConfigDict, ValidationError, field_validator
8
+
9
+ from codealmanac.app import CodeAlmanac
10
+ from codealmanac.core.errors import CodeAlmanacError, ConflictError, NotFoundError
11
+ from codealmanac.services.viewer.models import (
12
+ ViewerFile,
13
+ ViewerOverview,
14
+ ViewerPage,
15
+ ViewerSearch,
16
+ ViewerTopic,
17
+ )
18
+ from codealmanac.services.viewer.requests import (
19
+ ViewerFileRequest,
20
+ ViewerOverviewRequest,
21
+ ViewerPageRequest,
22
+ ViewerSearchRequest,
23
+ ViewerTopicRequest,
24
+ )
25
+
26
+
27
+ class ServerAssetSuffix(StrEnum):
28
+ HTML = ".html"
29
+ CSS = ".css"
30
+ JAVASCRIPT = ".js"
31
+
32
+
33
+ ASSET_MEDIA_TYPES: dict[ServerAssetSuffix, str] = {
34
+ ServerAssetSuffix.HTML: "text/html",
35
+ ServerAssetSuffix.CSS: "text/css",
36
+ ServerAssetSuffix.JAVASCRIPT: "text/javascript",
37
+ }
38
+
39
+
40
+ class ServerAssetRequest(BaseModel):
41
+ model_config = ConfigDict(frozen=True)
42
+
43
+ path: str
44
+
45
+ @field_validator("path")
46
+ @classmethod
47
+ def validate_path(cls, value: str) -> str:
48
+ if value != value.strip():
49
+ raise ValueError("asset path must not contain surrounding whitespace")
50
+ if not value:
51
+ raise ValueError("asset path is required")
52
+ if value.startswith("/") or "\\" in value:
53
+ raise ValueError("asset path must be relative")
54
+ parts = value.split("/")
55
+ if any(part in {"", ".", ".."} for part in parts):
56
+ raise ValueError("asset path contains an invalid segment")
57
+ suffix = Path(value).suffix
58
+ if suffix not in {item.value for item in ServerAssetSuffix}:
59
+ raise ValueError("asset path has an unsupported extension")
60
+ return value
61
+
62
+ @property
63
+ def media_type(self) -> str:
64
+ return ASSET_MEDIA_TYPES[ServerAssetSuffix(Path(self.path).suffix)]
65
+
66
+ @property
67
+ def parts(self) -> list[str]:
68
+ return self.path.split("/")
69
+
70
+
71
+ def create_server_app(
72
+ codealmanac: CodeAlmanac,
73
+ cwd: Path,
74
+ wiki: str | None = None,
75
+ ) -> FastAPI:
76
+ server = FastAPI(title="CodeAlmanac Local Viewer")
77
+
78
+ @server.get("/api/overview", response_model=ViewerOverview)
79
+ def overview() -> ViewerOverview:
80
+ try:
81
+ return codealmanac.viewer.overview(
82
+ ViewerOverviewRequest(cwd=cwd, wiki=wiki)
83
+ )
84
+ except ValidationError as error:
85
+ raise validation_error(error) from error
86
+ except CodeAlmanacError as error:
87
+ raise http_error(error) from error
88
+
89
+ @server.get("/api/page/{slug}", response_model=ViewerPage)
90
+ def page(slug: str) -> ViewerPage:
91
+ try:
92
+ return codealmanac.viewer.page(
93
+ ViewerPageRequest(cwd=cwd, wiki=wiki, slug=slug)
94
+ )
95
+ except ValidationError as error:
96
+ raise validation_error(error) from error
97
+ except CodeAlmanacError as error:
98
+ raise http_error(error) from error
99
+
100
+ @server.get("/api/search", response_model=ViewerSearch)
101
+ def search(q: str | None = None, limit: int = 50) -> ViewerSearch:
102
+ try:
103
+ return codealmanac.viewer.search(
104
+ ViewerSearchRequest(cwd=cwd, wiki=wiki, query=q, limit=limit)
105
+ )
106
+ except ValidationError as error:
107
+ raise validation_error(error) from error
108
+ except CodeAlmanacError as error:
109
+ raise http_error(error) from error
110
+
111
+ @server.get("/api/file", response_model=ViewerFile)
112
+ def file(path: str, limit: int = 50) -> ViewerFile:
113
+ try:
114
+ return codealmanac.viewer.file(
115
+ ViewerFileRequest(cwd=cwd, wiki=wiki, path=path, limit=limit)
116
+ )
117
+ except ValidationError as error:
118
+ raise validation_error(error) from error
119
+ except CodeAlmanacError as error:
120
+ raise http_error(error) from error
121
+
122
+ @server.get("/api/topic/{slug}", response_model=ViewerTopic)
123
+ def topic(slug: str, descendants: bool = False) -> ViewerTopic:
124
+ try:
125
+ return codealmanac.viewer.topic(
126
+ ViewerTopicRequest(
127
+ cwd=cwd,
128
+ wiki=wiki,
129
+ slug=slug,
130
+ include_descendants=descendants,
131
+ )
132
+ )
133
+ except ValidationError as error:
134
+ raise validation_error(error) from error
135
+ except CodeAlmanacError as error:
136
+ raise http_error(error) from error
137
+
138
+ @server.get("/", include_in_schema=False)
139
+ def index() -> HTMLResponse:
140
+ return HTMLResponse(read_asset_text(ServerAssetRequest(path="index.html")))
141
+
142
+ @server.get("/app.js", include_in_schema=False)
143
+ def app_js() -> Response:
144
+ return asset_response("app.js")
145
+
146
+ @server.get("/app.css", include_in_schema=False)
147
+ def app_css() -> Response:
148
+ return asset_response("app.css")
149
+
150
+ @server.get("/assets/{asset_path:path}", include_in_schema=False)
151
+ def static_asset(asset_path: str) -> Response:
152
+ return asset_response(asset_path)
153
+
154
+ @server.get("/{path:path}", include_in_schema=False)
155
+ def fallback(path: str) -> HTMLResponse:
156
+ if path.startswith("api/"):
157
+ raise HTTPException(status_code=404, detail="not found")
158
+ return HTMLResponse(read_asset_text(ServerAssetRequest(path="index.html")))
159
+
160
+ return server
161
+
162
+
163
+ def asset_response(asset_path: str) -> Response:
164
+ try:
165
+ request = ServerAssetRequest(path=asset_path)
166
+ return Response(
167
+ read_asset_text(request),
168
+ media_type=request.media_type,
169
+ )
170
+ except ValidationError as error:
171
+ raise validation_error(error) from error
172
+ except FileNotFoundError as error:
173
+ raise HTTPException(
174
+ status_code=404,
175
+ detail={"code": "not_found", "message": f"asset not found: {asset_path}"},
176
+ ) from error
177
+
178
+
179
+ def read_asset_text(request: ServerAssetRequest) -> str:
180
+ asset = resources.files("codealmanac.server.assets").joinpath(*request.parts)
181
+ if not asset.is_file():
182
+ raise FileNotFoundError(request.path)
183
+ return asset.read_text(encoding="utf-8")
184
+
185
+
186
+ def http_error(error: CodeAlmanacError) -> HTTPException:
187
+ status_code = 400
188
+ if isinstance(error, NotFoundError):
189
+ status_code = 404
190
+ elif isinstance(error, ConflictError):
191
+ status_code = 409
192
+ return HTTPException(
193
+ status_code=status_code,
194
+ detail={"code": error.code, "message": str(error)},
195
+ )
196
+
197
+
198
+ def validation_error(error: ValidationError) -> HTTPException:
199
+ return HTTPException(
200
+ status_code=422,
201
+ detail={"code": "validation_failed", "message": str(error)},
202
+ )
@@ -0,0 +1 @@
1
+ """Static assets bundled with the local viewer."""