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,85 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, distribution
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
6
|
+
from codealmanac.services.updates.models import (
|
|
7
|
+
PACKAGE_NAME,
|
|
8
|
+
PackageCommandResult,
|
|
9
|
+
PackageInstallMetadata,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InstalledPackageMetadataProvider:
|
|
14
|
+
def read(self) -> PackageInstallMetadata:
|
|
15
|
+
try:
|
|
16
|
+
package = distribution(PACKAGE_NAME)
|
|
17
|
+
except PackageNotFoundError:
|
|
18
|
+
return PackageInstallMetadata(
|
|
19
|
+
version="unknown",
|
|
20
|
+
installer=None,
|
|
21
|
+
editable=False,
|
|
22
|
+
source_url=None,
|
|
23
|
+
)
|
|
24
|
+
direct_url = read_direct_url(package.read_text("direct_url.json"))
|
|
25
|
+
return PackageInstallMetadata(
|
|
26
|
+
version=package.version,
|
|
27
|
+
installer=clean_optional_text(package.read_text("INSTALLER")),
|
|
28
|
+
editable=direct_url.editable,
|
|
29
|
+
source_url=direct_url.source_url,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SubprocessPackageCommandRunner:
|
|
34
|
+
def run(self, command: tuple[str, ...]) -> PackageCommandResult:
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
command,
|
|
38
|
+
text=True,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
check=False,
|
|
41
|
+
)
|
|
42
|
+
except OSError as error:
|
|
43
|
+
return PackageCommandResult(
|
|
44
|
+
exit_code=1,
|
|
45
|
+
stderr=f"{error.__class__.__name__}: {error}",
|
|
46
|
+
)
|
|
47
|
+
return PackageCommandResult(
|
|
48
|
+
exit_code=result.returncode,
|
|
49
|
+
stdout=result.stdout,
|
|
50
|
+
stderr=result.stderr,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DirectUrlMetadata(CodeAlmanacModel):
|
|
55
|
+
editable: bool = False
|
|
56
|
+
source_url: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_direct_url(raw: str | None) -> DirectUrlMetadata:
|
|
60
|
+
if raw is None:
|
|
61
|
+
return DirectUrlMetadata()
|
|
62
|
+
try:
|
|
63
|
+
payload = json.loads(raw)
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
return DirectUrlMetadata()
|
|
66
|
+
if not isinstance(payload, dict):
|
|
67
|
+
return DirectUrlMetadata()
|
|
68
|
+
dir_info = payload.get("dir_info")
|
|
69
|
+
editable = False
|
|
70
|
+
if isinstance(dir_info, dict):
|
|
71
|
+
editable = dir_info.get("editable") is True
|
|
72
|
+
url = payload.get("url")
|
|
73
|
+
return DirectUrlMetadata(
|
|
74
|
+
editable=editable,
|
|
75
|
+
source_url=url if isinstance(url, str) and url.strip() else None,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def clean_optional_text(value: str | None) -> str | None:
|
|
80
|
+
if value is None:
|
|
81
|
+
return None
|
|
82
|
+
cleaned = value.strip()
|
|
83
|
+
if cleaned == "":
|
|
84
|
+
return None
|
|
85
|
+
return cleaned
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Concrete workspace-state integrations."""
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from codealmanac.services.workspaces.models import (
|
|
6
|
+
WorkspaceChangeSnapshot,
|
|
7
|
+
WorkspacePathChange,
|
|
8
|
+
WorkspacePathState,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
GIT_STATUS_TIMEOUT_SECONDS = 10
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GitWorkspaceChangeProbe:
|
|
15
|
+
def snapshot(self, root_path: Path) -> WorkspaceChangeSnapshot:
|
|
16
|
+
try:
|
|
17
|
+
completed = subprocess.run(
|
|
18
|
+
(
|
|
19
|
+
"git",
|
|
20
|
+
"-C",
|
|
21
|
+
str(root_path),
|
|
22
|
+
"status",
|
|
23
|
+
"--porcelain=v1",
|
|
24
|
+
"-z",
|
|
25
|
+
"--untracked-files=all",
|
|
26
|
+
),
|
|
27
|
+
text=True,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
timeout=GIT_STATUS_TIMEOUT_SECONDS,
|
|
30
|
+
check=False,
|
|
31
|
+
)
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
return unavailable_snapshot(root_path, "git not found on PATH")
|
|
34
|
+
except subprocess.TimeoutExpired:
|
|
35
|
+
return unavailable_snapshot(root_path, "git status timed out")
|
|
36
|
+
if completed.returncode != 0:
|
|
37
|
+
return unavailable_snapshot(
|
|
38
|
+
root_path,
|
|
39
|
+
first_line(completed.stderr, completed.stdout)
|
|
40
|
+
or f"git status exited {completed.returncode}",
|
|
41
|
+
)
|
|
42
|
+
return WorkspaceChangeSnapshot(
|
|
43
|
+
root_path=root_path,
|
|
44
|
+
available=True,
|
|
45
|
+
changes=tuple(
|
|
46
|
+
change_with_fingerprint(root_path, change)
|
|
47
|
+
for change in parse_git_status(completed.stdout)
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def unavailable_snapshot(root_path: Path, reason: str) -> WorkspaceChangeSnapshot:
|
|
53
|
+
return WorkspaceChangeSnapshot(
|
|
54
|
+
root_path=root_path,
|
|
55
|
+
available=False,
|
|
56
|
+
unavailable_reason=reason,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_git_status(value: str) -> tuple[WorkspacePathChange, ...]:
|
|
61
|
+
changes: list[WorkspacePathChange] = []
|
|
62
|
+
fields = [field for field in value.split("\0") if field]
|
|
63
|
+
skip_next = False
|
|
64
|
+
for field in fields:
|
|
65
|
+
if skip_next:
|
|
66
|
+
skip_next = False
|
|
67
|
+
continue
|
|
68
|
+
if len(field) < 4:
|
|
69
|
+
continue
|
|
70
|
+
status = field[:2]
|
|
71
|
+
path = Path(field[3:])
|
|
72
|
+
changes.append(
|
|
73
|
+
WorkspacePathChange(
|
|
74
|
+
path=path,
|
|
75
|
+
state=state_from_status(status),
|
|
76
|
+
status=status,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
if "R" in status or "C" in status:
|
|
80
|
+
skip_next = True
|
|
81
|
+
return tuple(changes)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def state_from_status(status: str) -> WorkspacePathState:
|
|
85
|
+
if "?" in status:
|
|
86
|
+
return WorkspacePathState.UNTRACKED
|
|
87
|
+
if "U" in status:
|
|
88
|
+
return WorkspacePathState.UNMERGED
|
|
89
|
+
if "R" in status:
|
|
90
|
+
return WorkspacePathState.RENAMED
|
|
91
|
+
if "C" in status:
|
|
92
|
+
return WorkspacePathState.COPIED
|
|
93
|
+
if "D" in status:
|
|
94
|
+
return WorkspacePathState.DELETED
|
|
95
|
+
if "A" in status:
|
|
96
|
+
return WorkspacePathState.ADDED
|
|
97
|
+
if "M" in status:
|
|
98
|
+
return WorkspacePathState.MODIFIED
|
|
99
|
+
if "T" in status:
|
|
100
|
+
return WorkspacePathState.TYPE_CHANGED
|
|
101
|
+
return WorkspacePathState.UNKNOWN
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def change_with_fingerprint(
|
|
105
|
+
root_path: Path,
|
|
106
|
+
change: WorkspacePathChange,
|
|
107
|
+
) -> WorkspacePathChange:
|
|
108
|
+
return change.model_copy(
|
|
109
|
+
update={"fingerprint": file_fingerprint(root_path / change.path)}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def file_fingerprint(path: Path) -> str | None:
|
|
114
|
+
if not path.is_file():
|
|
115
|
+
return None
|
|
116
|
+
digest = hashlib.sha256()
|
|
117
|
+
with path.open("rb") as handle:
|
|
118
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
119
|
+
digest.update(chunk)
|
|
120
|
+
return digest.hexdigest()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def first_line(*values: str) -> str:
|
|
124
|
+
for value in values:
|
|
125
|
+
lines = [line.strip() for line in value.splitlines() if line.strip()]
|
|
126
|
+
if lines:
|
|
127
|
+
return lines[0]
|
|
128
|
+
return ""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Manual Overview
|
|
3
|
+
topics: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manual Overview
|
|
7
|
+
|
|
8
|
+
This manual defines how agents maintain a CodeAlmanac repo-owned wiki.
|
|
9
|
+
|
|
10
|
+
Prompts name the job. The manual defines the writing rules, evidence bar, page
|
|
11
|
+
shape, and operation-specific workflow.
|
|
12
|
+
|
|
13
|
+
Read the relevant page before editing:
|
|
14
|
+
|
|
15
|
+
- `pages.md`: what deserves a page and how pages connect.
|
|
16
|
+
- `evidence.md`: how claims stay grounded and how conflicts are handled.
|
|
17
|
+
- `style.md`: how CodeAlmanac prose should read.
|
|
18
|
+
- `sources.md`: how raw material relates to wiki synthesis.
|
|
19
|
+
- `build.md`: how to create the first useful wiki.
|
|
20
|
+
- `ingest.md`: how to fold new material into an existing wiki.
|
|
21
|
+
- `garden.md`: how to improve an existing wiki graph.
|
|
22
|
+
|
|
23
|
+
Repo-specific conventions live in `README.md` and `topics.yaml` under the
|
|
24
|
+
configured Almanac root.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from codealmanac.manual.library import ManualLibrary
|
|
2
|
+
from codealmanac.manual.models import (
|
|
3
|
+
ManualDocument,
|
|
4
|
+
ManualDocumentName,
|
|
5
|
+
ManualInstallResult,
|
|
6
|
+
ManualInventory,
|
|
7
|
+
ManualWorkspaceStatus,
|
|
8
|
+
)
|
|
9
|
+
from codealmanac.manual.requests import ManualReadRequest
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ManualDocument",
|
|
13
|
+
"ManualDocumentName",
|
|
14
|
+
"ManualInstallResult",
|
|
15
|
+
"ManualInventory",
|
|
16
|
+
"ManualLibrary",
|
|
17
|
+
"ManualReadRequest",
|
|
18
|
+
"ManualWorkspaceStatus",
|
|
19
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build
|
|
3
|
+
topics: [manual]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build
|
|
7
|
+
|
|
8
|
+
Build creates or refreshes the initial scaffold under the configured Almanac
|
|
9
|
+
root for a repo.
|
|
10
|
+
|
|
11
|
+
The goal is a usable local wiki, not a file-tree summary. A thin starter page is
|
|
12
|
+
acceptable at initialization, but the first real build should create a reading
|
|
13
|
+
path through durable subjects.
|
|
14
|
+
|
|
15
|
+
Before writing substantive pages, read `manual/README.md`, `manual/pages.md`,
|
|
16
|
+
`manual/evidence.md`, `manual/style.md`, and the repo-specific `README.md`
|
|
17
|
+
under the configured Almanac root.
|
|
18
|
+
|
|
19
|
+
A build is useful when a future agent can understand the repo faster from the
|
|
20
|
+
wiki than by rediscovering context from raw files and old conversations.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Evidence
|
|
3
|
+
topics: [manual]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Evidence
|
|
7
|
+
|
|
8
|
+
Every durable claim needs grounding in code, docs, transcripts, PRs, local
|
|
9
|
+
commands, or another named source.
|
|
10
|
+
|
|
11
|
+
Authority depends on the claim:
|
|
12
|
+
|
|
13
|
+
- Code is authoritative for runtime behavior.
|
|
14
|
+
- Transcripts are authoritative for what was discussed.
|
|
15
|
+
- PRs are authoritative for review and merge context.
|
|
16
|
+
- The wiki is the maintained synthesis.
|
|
17
|
+
|
|
18
|
+
When evidence conflicts, state the conflict plainly or defer the claim. Do not
|
|
19
|
+
turn a transcript, diff, or note into source of truth for behavior the code
|
|
20
|
+
contradicts.
|
|
21
|
+
|
|
22
|
+
Use frontmatter `sources:` entries for material that supports the page. Use
|
|
23
|
+
precise file references when source code is relevant to retrieval.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Garden
|
|
3
|
+
topics: [manual]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Garden
|
|
7
|
+
|
|
8
|
+
Garden improves the existing wiki graph.
|
|
9
|
+
|
|
10
|
+
Look for stale claims, duplicate pages, weak leads, missing links, broken file
|
|
11
|
+
references, confusing topics, unsupported claims, disconnected temporal notes,
|
|
12
|
+
and clusters that need hubs.
|
|
13
|
+
|
|
14
|
+
Broken page links should be resolved by linking to the right existing page,
|
|
15
|
+
creating a justified page, or changing the mention back to plain text.
|
|
16
|
+
|
|
17
|
+
Prefer synthesis over activity logs. Fold fragments into durable pages when
|
|
18
|
+
chronology is not part of the meaning.
|
|
19
|
+
|
|
20
|
+
No-op is valid when the wiki is coherent enough for the current pass.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Ingest
|
|
3
|
+
topics: [manual]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ingest
|
|
7
|
+
|
|
8
|
+
Ingest folds selected local material into an existing configured Almanac wiki.
|
|
9
|
+
|
|
10
|
+
Read the new material, then read nearby pages, backlinks, topics, and local
|
|
11
|
+
wiki conventions. Decide what the material changes.
|
|
12
|
+
|
|
13
|
+
Update existing pages when the subject already has a home. Create a page only
|
|
14
|
+
when the material reveals a durable subject that needs one.
|
|
15
|
+
|
|
16
|
+
No-op is valid when the input adds no durable wiki knowledge. If the input
|
|
17
|
+
exposes a graph problem, treat part of the run like Garden.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from codealmanac.core.errors import ValidationFailed
|
|
5
|
+
from codealmanac.manual.models import (
|
|
6
|
+
MANUAL_DOCUMENTS,
|
|
7
|
+
ManualDocument,
|
|
8
|
+
ManualInstallResult,
|
|
9
|
+
ManualInventory,
|
|
10
|
+
ManualWorkspaceStatus,
|
|
11
|
+
)
|
|
12
|
+
from codealmanac.manual.requests import ManualReadRequest
|
|
13
|
+
|
|
14
|
+
MANUAL_PACKAGE = "codealmanac.manual"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ManualLibrary:
|
|
18
|
+
def inventory(self) -> ManualInventory:
|
|
19
|
+
return ManualInventory(
|
|
20
|
+
documents=tuple(
|
|
21
|
+
self.read(ManualReadRequest(document=document))
|
|
22
|
+
for document in MANUAL_DOCUMENTS
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def read(self, request: ManualReadRequest) -> ManualDocument:
|
|
27
|
+
resource = files(MANUAL_PACKAGE).joinpath(request.document.value)
|
|
28
|
+
try:
|
|
29
|
+
body = resource.read_text(encoding="utf-8")
|
|
30
|
+
except (FileNotFoundError, OSError) as error:
|
|
31
|
+
raise ValidationFailed(
|
|
32
|
+
f"cannot read bundled manual document {request.document.value}: {error}"
|
|
33
|
+
) from error
|
|
34
|
+
return ManualDocument(
|
|
35
|
+
name=request.document,
|
|
36
|
+
relative_path=request.document.value,
|
|
37
|
+
body=body,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def install_missing(self, target_path: Path) -> ManualInstallResult:
|
|
41
|
+
try:
|
|
42
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
copied: list[str] = []
|
|
44
|
+
existing: list[str] = []
|
|
45
|
+
for document in MANUAL_DOCUMENTS:
|
|
46
|
+
destination = target_path / document.value
|
|
47
|
+
if destination.exists():
|
|
48
|
+
existing.append(document.value)
|
|
49
|
+
continue
|
|
50
|
+
source = files(MANUAL_PACKAGE).joinpath(document.value)
|
|
51
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
destination.write_bytes(source.read_bytes())
|
|
53
|
+
copied.append(document.value)
|
|
54
|
+
except OSError as error:
|
|
55
|
+
raise ValidationFailed(f"cannot install manual files: {error}") from error
|
|
56
|
+
return ManualInstallResult(
|
|
57
|
+
target_path=target_path,
|
|
58
|
+
copied=tuple(copied),
|
|
59
|
+
existing=tuple(existing),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def workspace_status(self, target_path: Path) -> ManualWorkspaceStatus:
|
|
63
|
+
expected = tuple(document.value for document in MANUAL_DOCUMENTS)
|
|
64
|
+
present: list[str] = []
|
|
65
|
+
changed: list[str] = []
|
|
66
|
+
try:
|
|
67
|
+
for document in MANUAL_DOCUMENTS:
|
|
68
|
+
workspace_file = target_path / document.value
|
|
69
|
+
if not workspace_file.is_file():
|
|
70
|
+
continue
|
|
71
|
+
present.append(document.value)
|
|
72
|
+
bundled = files(MANUAL_PACKAGE).joinpath(document.value).read_bytes()
|
|
73
|
+
if workspace_file.read_bytes() != bundled:
|
|
74
|
+
changed.append(document.value)
|
|
75
|
+
except OSError as error:
|
|
76
|
+
raise ValidationFailed(f"cannot inspect manual files: {error}") from error
|
|
77
|
+
missing = tuple(document for document in expected if document not in present)
|
|
78
|
+
return ManualWorkspaceStatus(
|
|
79
|
+
target_path=target_path,
|
|
80
|
+
expected=expected,
|
|
81
|
+
present=tuple(present),
|
|
82
|
+
missing=missing,
|
|
83
|
+
changed=tuple(changed),
|
|
84
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, field_validator
|
|
5
|
+
|
|
6
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
7
|
+
from codealmanac.core.text import required_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ManualDocumentName(StrEnum):
|
|
11
|
+
README = "README.md"
|
|
12
|
+
PAGES = "pages.md"
|
|
13
|
+
EVIDENCE = "evidence.md"
|
|
14
|
+
STYLE = "style.md"
|
|
15
|
+
SOURCES = "sources.md"
|
|
16
|
+
BUILD = "build.md"
|
|
17
|
+
INGEST = "ingest.md"
|
|
18
|
+
GARDEN = "garden.md"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MANUAL_DOCUMENTS: tuple[ManualDocumentName, ...] = (
|
|
22
|
+
ManualDocumentName.README,
|
|
23
|
+
ManualDocumentName.PAGES,
|
|
24
|
+
ManualDocumentName.EVIDENCE,
|
|
25
|
+
ManualDocumentName.STYLE,
|
|
26
|
+
ManualDocumentName.SOURCES,
|
|
27
|
+
ManualDocumentName.BUILD,
|
|
28
|
+
ManualDocumentName.INGEST,
|
|
29
|
+
ManualDocumentName.GARDEN,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ManualDocument(CodeAlmanacModel):
|
|
34
|
+
name: ManualDocumentName
|
|
35
|
+
relative_path: str
|
|
36
|
+
body: str
|
|
37
|
+
|
|
38
|
+
@field_validator("relative_path")
|
|
39
|
+
@classmethod
|
|
40
|
+
def require_relative_path(cls, value: str) -> str:
|
|
41
|
+
text = required_text(value, "manual path")
|
|
42
|
+
if text.startswith("/") or "/../" in f"/{text}/":
|
|
43
|
+
raise ValueError("manual path must be relative")
|
|
44
|
+
return text
|
|
45
|
+
|
|
46
|
+
@field_validator("body")
|
|
47
|
+
@classmethod
|
|
48
|
+
def require_body(cls, value: str) -> str:
|
|
49
|
+
if not value.strip():
|
|
50
|
+
raise ValueError("manual body must not be empty")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ManualInventory(CodeAlmanacModel):
|
|
55
|
+
documents: tuple[ManualDocument, ...] = Field(min_length=1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ManualInstallResult(CodeAlmanacModel):
|
|
59
|
+
target_path: Path
|
|
60
|
+
copied: tuple[str, ...]
|
|
61
|
+
existing: tuple[str, ...]
|
|
62
|
+
|
|
63
|
+
@field_validator("copied", "existing")
|
|
64
|
+
@classmethod
|
|
65
|
+
def require_paths(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
66
|
+
return tuple(required_text(path, "manual path") for path in value)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ManualWorkspaceStatus(CodeAlmanacModel):
|
|
70
|
+
target_path: Path
|
|
71
|
+
expected: tuple[str, ...] = Field(min_length=1)
|
|
72
|
+
present: tuple[str, ...]
|
|
73
|
+
missing: tuple[str, ...]
|
|
74
|
+
changed: tuple[str, ...] = ()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def complete(self) -> bool:
|
|
78
|
+
return len(self.missing) == 0
|
|
79
|
+
|
|
80
|
+
@field_validator("expected", "present", "missing", "changed")
|
|
81
|
+
@classmethod
|
|
82
|
+
def require_paths(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
83
|
+
return tuple(required_text(path, "manual path") for path in value)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pages
|
|
3
|
+
topics: [manual]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pages
|
|
7
|
+
|
|
8
|
+
A page is the stable home for what the wiki knows about one durable subject.
|
|
9
|
+
|
|
10
|
+
Good subjects include decisions, subsystems, external services, workflows,
|
|
11
|
+
incidents, invariants, failure modes, project strategy, and repeated concepts
|
|
12
|
+
that future agents will need again.
|
|
13
|
+
|
|
14
|
+
Do not create pages that only summarize one file, one transcript, one diff, or
|
|
15
|
+
one task log. Those are material used to justify a change. They are not usually
|
|
16
|
+
the subject of the wiki.
|
|
17
|
+
|
|
18
|
+
Prefer updating an existing page when the subject already has a home. Create a
|
|
19
|
+
new page when the material reveals a durable subject that would otherwise stay
|
|
20
|
+
buried.
|
|
21
|
+
|
|
22
|
+
Use `[[page-slug]]` links for subjects a reader may follow. Use
|
|
23
|
+
`[[src/path.py]]` and `[[src/path/]]` references when a page should be found by
|
|
24
|
+
file-aware search.
|
|
25
|
+
|
|
26
|
+
Page links are for real wiki nodes, not automatic entity markup. Link only to
|
|
27
|
+
an existing page or a page created in the same run. If the subject is useful
|
|
28
|
+
but does not yet deserve a page, mention it as plain text.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sources
|
|
3
|
+
topics: [manual]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sources
|
|
7
|
+
|
|
8
|
+
Sources are raw material CodeAlmanac can learn from. They are not automatically
|
|
9
|
+
source of truth for every claim.
|
|
10
|
+
|
|
11
|
+
Selected material may include files, directories, diffs, commit ranges, PRs,
|
|
12
|
+
issues, web pages, notes, and local agent transcripts. The source runtime
|
|
13
|
+
normalizes that material before a lifecycle run.
|
|
14
|
+
|
|
15
|
+
Adding or discovering material does not imply a wiki update. The lifecycle run
|
|
16
|
+
decides whether the material changes durable wiki knowledge.
|
|
17
|
+
|
|
18
|
+
Keep page shape organized by subject, not by how material arrived.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Style
|
|
3
|
+
topics: [manual]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Style
|
|
7
|
+
|
|
8
|
+
Write for a future coding agent that needs useful context quickly.
|
|
9
|
+
|
|
10
|
+
Use plain factual sentences. Prefer "is" over vague phrasing such as "serves
|
|
11
|
+
as." Avoid promotional language, speculation, and generic architecture prose
|
|
12
|
+
that could describe any repository.
|
|
13
|
+
|
|
14
|
+
Start with the subject and why it matters in this repo. Keep details that would
|
|
15
|
+
otherwise require rediscovery: names, files, commands, dates, constraints,
|
|
16
|
+
failure modes, and rejected alternatives.
|
|
17
|
+
|
|
18
|
+
Use prose first. Use bullets for real lists and tables for structured
|
|
19
|
+
comparison.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Notability
|
|
2
|
+
|
|
3
|
+
Write or edit wiki pages only when the change preserves non-obvious durable
|
|
4
|
+
knowledge that helps future work.
|
|
5
|
+
|
|
6
|
+
Good wiki changes capture decisions, multi-file flows, invariants, incidents,
|
|
7
|
+
gotchas, operational constraints, team conventions, external dependencies as
|
|
8
|
+
this repo uses them, or product context that shapes implementation choices.
|
|
9
|
+
|
|
10
|
+
Do not use the wiki as a scratchpad. Do not preserve unresolved intake work,
|
|
11
|
+
temporary question lists, raw field inventories, or routine activity logs.
|
|
12
|
+
|
|
13
|
+
No-op is valid when the available material does not justify a durable wiki
|
|
14
|
+
change.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# CodeAlmanac Purpose
|
|
2
|
+
|
|
3
|
+
CodeAlmanac maintains a repo-owned wiki for a codebase and the project world
|
|
4
|
+
around that codebase. New installs default to `almanac/`; the repo may choose a
|
|
5
|
+
different configured Almanac root.
|
|
6
|
+
|
|
7
|
+
The wiki is durable project memory for future coding agents. It records why the
|
|
8
|
+
system is shaped this way, what must not be violated, what was tried and failed,
|
|
9
|
+
how workflows move end to end, and what gotchas were discovered through real
|
|
10
|
+
work.
|
|
11
|
+
|
|
12
|
+
The code is authoritative for runtime behavior. The wiki is maintained
|
|
13
|
+
synthesis. Transcripts, PRs, notes, diffs, and docs are raw material that can
|
|
14
|
+
justify wiki changes.
|
|
15
|
+
|
|
16
|
+
The public command and product name is `codealmanac`. Do not introduce public
|
|
17
|
+
`almanac`, `alm`, `absorb`, or hosted CLI language.
|
|
18
|
+
|
|
19
|
+
The public CLI name is codealmanac.
|
|
20
|
+
|
|
21
|
+
Detailed wiki doctrine lives in `manual/` under the configured Almanac root.
|
|
22
|
+
The prompt names the job; the manual defines the page, evidence, style, source,
|
|
23
|
+
and operation rules.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Wiki Syntax
|
|
2
|
+
|
|
3
|
+
Pages are Markdown files under `pages/` inside the configured Almanac root,
|
|
4
|
+
with YAML frontmatter. Use kebab-case slugs and stable page titles.
|
|
5
|
+
|
|
6
|
+
Use `topics:` for topic slugs. Use structured `sources:` entries for evidence
|
|
7
|
+
that supports non-obvious claims. Use `[[...]]` wikilinks for pages, files,
|
|
8
|
+
folders, and cross-wiki references.
|
|
9
|
+
|
|
10
|
+
Page wikilinks must resolve. Link only to existing page slugs or pages you
|
|
11
|
+
create or update in this run. If no page exists and you are not creating it,
|
|
12
|
+
write the name as plain text instead of leaving a broken `[[...]]` link.
|
|
13
|
+
|
|
14
|
+
Every sentence should contain a specific fact. Prefer neutral prose. Do not
|
|
15
|
+
speculate. Do not add promotional language.
|
|
16
|
+
|
|
17
|
+
Update only files inside the configured Almanac root unless the operation
|
|
18
|
+
explicitly says otherwise. Do not edit application code during lifecycle wiki
|
|
19
|
+
operations.
|