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,209 @@
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 FilesystemDirectoryListingSource(StrEnum):
11
+ GIT = "git"
12
+ WALK = "walk"
13
+
14
+
15
+ class FilesystemDirectoryFileState(StrEnum):
16
+ CHANGED = "changed"
17
+ UNCHANGED = "unchanged"
18
+
19
+
20
+ class FilesystemDirectorySelectionPolicy(StrEnum):
21
+ CHANGED_THEN_DIVERSE = "changed_then_diverse"
22
+ DIVERSE = "diverse"
23
+
24
+
25
+ class FilesystemDirectoryCandidate(CodeAlmanacModel):
26
+ path: Path
27
+ display_path: str
28
+ selection_group: str
29
+ state: FilesystemDirectoryFileState = FilesystemDirectoryFileState.UNCHANGED
30
+ git_status: str | None = None
31
+
32
+ @field_validator("display_path", "selection_group")
33
+ @classmethod
34
+ def require_display_path(cls, value: str) -> str:
35
+ return required_text(value, "filesystem directory candidate")
36
+
37
+ @field_validator("git_status")
38
+ @classmethod
39
+ def validate_git_status(cls, value: str | None) -> str | None:
40
+ if value is None:
41
+ return value
42
+ if len(value) != 2:
43
+ raise ValueError("filesystem directory git status must be two characters")
44
+ return value
45
+
46
+
47
+ SOURCE_SUFFIXES = frozenset(
48
+ {
49
+ ".c",
50
+ ".cc",
51
+ ".cpp",
52
+ ".cs",
53
+ ".css",
54
+ ".go",
55
+ ".h",
56
+ ".hpp",
57
+ ".html",
58
+ ".java",
59
+ ".js",
60
+ ".jsx",
61
+ ".kt",
62
+ ".mjs",
63
+ ".php",
64
+ ".py",
65
+ ".rb",
66
+ ".rs",
67
+ ".scss",
68
+ ".sh",
69
+ ".sql",
70
+ ".swift",
71
+ ".ts",
72
+ ".tsx",
73
+ ".vue",
74
+ }
75
+ )
76
+ STRUCTURED_SUFFIXES = frozenset(
77
+ {
78
+ ".cfg",
79
+ ".ini",
80
+ ".json",
81
+ ".md",
82
+ ".toml",
83
+ ".yaml",
84
+ ".yml",
85
+ }
86
+ )
87
+ LOW_VALUE_FILENAMES = frozenset(
88
+ {
89
+ ".gitkeep",
90
+ "__init__.py",
91
+ }
92
+ )
93
+ PRIMARY_ROLE_FILENAMES = frozenset(
94
+ {
95
+ "adapter.py",
96
+ "app.py",
97
+ "main.py",
98
+ "service.py",
99
+ }
100
+ )
101
+ SECONDARY_ROLE_FILENAMES = frozenset(
102
+ {
103
+ "models.py",
104
+ "ports.py",
105
+ "renderer.py",
106
+ "requests.py",
107
+ "root.py",
108
+ "store.py",
109
+ }
110
+ )
111
+
112
+
113
+ def ranked_directory_candidates(
114
+ candidates: tuple[FilesystemDirectoryCandidate, ...],
115
+ ) -> tuple[FilesystemDirectoryCandidate, ...]:
116
+ changed = tuple(
117
+ candidate
118
+ for candidate in candidates
119
+ if candidate.state == FilesystemDirectoryFileState.CHANGED
120
+ )
121
+ unchanged = tuple(
122
+ candidate
123
+ for candidate in candidates
124
+ if candidate.state == FilesystemDirectoryFileState.UNCHANGED
125
+ )
126
+ return (
127
+ *diverse_directory_candidates(changed),
128
+ *diverse_directory_candidates(unchanged),
129
+ )
130
+
131
+
132
+ def directory_candidate_key(
133
+ candidate: FilesystemDirectoryCandidate,
134
+ ) -> tuple[int, int, int, str]:
135
+ return (
136
+ filename_role_rank(candidate.path),
137
+ unchanged_content_rank(candidate.path),
138
+ path_depth(candidate.display_path),
139
+ candidate.display_path.casefold(),
140
+ )
141
+
142
+
143
+ def diverse_directory_candidates(
144
+ candidates: tuple[FilesystemDirectoryCandidate, ...],
145
+ ) -> tuple[FilesystemDirectoryCandidate, ...]:
146
+ grouped: dict[str, list[FilesystemDirectoryCandidate]] = {}
147
+ for candidate in candidates:
148
+ grouped.setdefault(candidate.selection_group, []).append(candidate)
149
+ for group_candidates in grouped.values():
150
+ group_candidates.sort(key=directory_candidate_key)
151
+ groups = tuple(
152
+ sorted(
153
+ grouped.items(),
154
+ key=lambda item: (directory_candidate_key(item[1][0]), item[0].casefold()),
155
+ )
156
+ )
157
+ ordered: list[FilesystemDirectoryCandidate] = []
158
+ index = 0
159
+ while True:
160
+ added = False
161
+ for _group, group_candidates in groups:
162
+ if index >= len(group_candidates):
163
+ continue
164
+ ordered.append(group_candidates[index])
165
+ added = True
166
+ if not added:
167
+ return tuple(ordered)
168
+ index += 1
169
+
170
+
171
+ def filename_role_rank(path: Path) -> int:
172
+ name = path.name
173
+ if name in PRIMARY_ROLE_FILENAMES:
174
+ return 0
175
+ if name in SECONDARY_ROLE_FILENAMES:
176
+ return 1
177
+ if name in LOW_VALUE_FILENAMES:
178
+ return 4
179
+ return 2
180
+
181
+
182
+ def unchanged_content_rank(path: Path) -> int:
183
+ if path.name in LOW_VALUE_FILENAMES:
184
+ return 3
185
+ suffix = path.suffix.casefold()
186
+ if suffix in SOURCE_SUFFIXES:
187
+ return 0
188
+ if suffix in STRUCTURED_SUFFIXES:
189
+ return 1
190
+ return 2
191
+
192
+
193
+ def path_depth(display_path: str) -> int:
194
+ return display_path.count("/")
195
+
196
+
197
+ def directory_selection_group(path: Path, root: Path) -> str:
198
+ try:
199
+ relative = path.relative_to(root)
200
+ except ValueError:
201
+ return path.name
202
+ parts = relative.parts
203
+ if len(parts) == 0:
204
+ return "."
205
+ if len(parts) == 1:
206
+ return parts[0]
207
+ if Path(parts[1]).suffix:
208
+ return parts[0]
209
+ return "/".join(parts[:2])
@@ -0,0 +1,3 @@
1
+ from codealmanac.integrations.sources.git.adapter import GitSourceRuntimeAdapter
2
+
3
+ __all__ = ["GitSourceRuntimeAdapter"]
@@ -0,0 +1,132 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ from codealmanac.core.errors import ExecutionFailed
5
+ from codealmanac.integrations.command import CommandRunner, SubprocessCommandRunner
6
+ from codealmanac.integrations.sources.runtime import (
7
+ bounded_text,
8
+ source_runtime_section,
9
+ surface_process_error,
10
+ )
11
+ from codealmanac.services.sources.models import (
12
+ SourceKind,
13
+ SourceRef,
14
+ SourceRuntime,
15
+ SourceRuntimeStatus,
16
+ )
17
+ from codealmanac.services.sources.requests import InspectSourceRuntimeRequest
18
+
19
+ GIT_RUNTIME_TIMEOUT_SECONDS = 30
20
+ DEFAULT_MAX_CHARS = 60_000
21
+
22
+
23
+ class GitSourceRuntimeAdapter:
24
+ def __init__(
25
+ self,
26
+ runner: CommandRunner | None = None,
27
+ max_chars: int = DEFAULT_MAX_CHARS,
28
+ timeout_seconds: int = GIT_RUNTIME_TIMEOUT_SECONDS,
29
+ ):
30
+ self.runner = runner or SubprocessCommandRunner()
31
+ self.max_chars = max_chars
32
+ self.timeout_seconds = timeout_seconds
33
+
34
+ def supports(self, ref: SourceRef) -> bool:
35
+ return ref.kind in {SourceKind.GIT_DIFF, SourceKind.GIT_RANGE}
36
+
37
+ def inspect(self, request: InspectSourceRuntimeRequest) -> SourceRuntime:
38
+ if request.ref.kind == SourceKind.GIT_RANGE:
39
+ return self._inspect_range(request.cwd, request.ref)
40
+ if request.ref.kind == SourceKind.GIT_DIFF:
41
+ return self._inspect_diff(request.cwd, request.ref)
42
+ return SourceRuntime(
43
+ ref=request.ref,
44
+ status=SourceRuntimeStatus.SKIPPED,
45
+ title=f"Unsupported Git source {request.ref.identity}",
46
+ )
47
+
48
+ def _inspect_range(self, cwd: Path, ref: SourceRef) -> SourceRuntime:
49
+ revision_range = require_revision_range(ref)
50
+ sections = (
51
+ source_runtime_section(
52
+ "commits",
53
+ self._git(cwd, ("log", "--oneline", "--decorate", revision_range)),
54
+ ),
55
+ source_runtime_section(
56
+ "stat",
57
+ self._git(cwd, ("diff", "--stat", revision_range)),
58
+ ),
59
+ source_runtime_section(
60
+ "diff",
61
+ self._git(cwd, ("diff", "--no-ext-diff", revision_range)),
62
+ ),
63
+ )
64
+ content, truncated = bounded_text("\n\n".join(sections), self.max_chars)
65
+ return SourceRuntime(
66
+ ref=ref,
67
+ status=SourceRuntimeStatus.AVAILABLE,
68
+ title=f"Git range {revision_range}",
69
+ content=content,
70
+ truncated=truncated,
71
+ )
72
+
73
+ def _inspect_diff(self, cwd: Path, ref: SourceRef) -> SourceRuntime:
74
+ target = require_revision_range(ref)
75
+ if target == "working-tree":
76
+ sections = (
77
+ source_runtime_section("status", self._git(cwd, ("status", "--short"))),
78
+ source_runtime_section(
79
+ "unstaged stat",
80
+ self._git(cwd, ("diff", "--stat")),
81
+ ),
82
+ source_runtime_section(
83
+ "unstaged diff",
84
+ self._git(cwd, ("diff", "--no-ext-diff")),
85
+ ),
86
+ source_runtime_section(
87
+ "staged stat",
88
+ self._git(cwd, ("diff", "--cached", "--stat")),
89
+ ),
90
+ source_runtime_section(
91
+ "staged diff",
92
+ self._git(cwd, ("diff", "--cached", "--no-ext-diff")),
93
+ ),
94
+ )
95
+ else:
96
+ sections = (
97
+ source_runtime_section(
98
+ "stat",
99
+ self._git(cwd, ("diff", "--stat", target)),
100
+ ),
101
+ source_runtime_section(
102
+ "diff",
103
+ self._git(cwd, ("diff", "--no-ext-diff", target)),
104
+ ),
105
+ )
106
+ content, truncated = bounded_text("\n\n".join(sections), self.max_chars)
107
+ return SourceRuntime(
108
+ ref=ref,
109
+ status=SourceRuntimeStatus.AVAILABLE,
110
+ title=f"Git diff {target}",
111
+ content=content,
112
+ truncated=truncated,
113
+ )
114
+
115
+ def _git(self, cwd: Path, args: tuple[str, ...]) -> str:
116
+ try:
117
+ result = self.runner.run("git", args, cwd, self.timeout_seconds)
118
+ except FileNotFoundError as error:
119
+ raise ExecutionFailed("git not found on PATH") from error
120
+ except subprocess.TimeoutExpired as error:
121
+ raise ExecutionFailed(f"git {' '.join(args)} timed out") from error
122
+ if result.returncode != 0:
123
+ raise ExecutionFailed(
124
+ f"git {' '.join(args)} failed: {surface_process_error(result)}"
125
+ )
126
+ return result.stdout.strip()
127
+
128
+
129
+ def require_revision_range(ref: SourceRef) -> str:
130
+ if ref.revision_range is None or ref.revision_range.strip() == "":
131
+ raise ExecutionFailed(f"Git source missing revision range: {ref.identity}")
132
+ return ref.revision_range
@@ -0,0 +1,3 @@
1
+ from codealmanac.integrations.sources.github.adapter import GitHubSourceRuntimeAdapter
2
+
3
+ __all__ = ["GitHubSourceRuntimeAdapter"]