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,197 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from codealmanac.core.errors import ExecutionFailed, ValidationFailed
|
|
4
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
5
|
+
from codealmanac.services.harnesses.models import (
|
|
6
|
+
HarnessEvent,
|
|
7
|
+
HarnessEventKind,
|
|
8
|
+
HarnessRunResult,
|
|
9
|
+
HarnessRunStatus,
|
|
10
|
+
terminal_harness_event,
|
|
11
|
+
)
|
|
12
|
+
from codealmanac.services.runs.models import RunEventKind
|
|
13
|
+
from codealmanac.services.workspaces.models import (
|
|
14
|
+
Workspace,
|
|
15
|
+
WorkspaceChangeSnapshot,
|
|
16
|
+
WorkspacePathChange,
|
|
17
|
+
)
|
|
18
|
+
from codealmanac.services.workspaces.ports import WorkspaceChangeProbe
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LifecycleMutationPreflight(CodeAlmanacModel):
|
|
22
|
+
before: WorkspaceChangeSnapshot
|
|
23
|
+
almanac_prefix: Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LifecycleMutationReport(CodeAlmanacModel):
|
|
27
|
+
before: WorkspaceChangeSnapshot
|
|
28
|
+
after: WorkspaceChangeSnapshot
|
|
29
|
+
changed_files: tuple[Path, ...]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LifecycleMutationPolicy:
|
|
33
|
+
def __init__(self, probe: WorkspaceChangeProbe, operation: str):
|
|
34
|
+
self.probe = probe
|
|
35
|
+
self.operation = operation
|
|
36
|
+
|
|
37
|
+
def preflight(self, workspace: Workspace) -> LifecycleMutationPreflight:
|
|
38
|
+
before = self.probe.snapshot(workspace.root_path)
|
|
39
|
+
validate_snapshot_available(before, self.operation)
|
|
40
|
+
almanac_prefix = almanac_relative_path(workspace)
|
|
41
|
+
dirty_almanac = tuple(
|
|
42
|
+
change.path
|
|
43
|
+
for change in before.changes
|
|
44
|
+
if path_is_under(change.path, almanac_prefix)
|
|
45
|
+
)
|
|
46
|
+
if dirty_almanac:
|
|
47
|
+
almanac_label = almanac_prefix.as_posix()
|
|
48
|
+
raise ValidationFailed(
|
|
49
|
+
f"{self.operation} requires a clean {almanac_label} before running: "
|
|
50
|
+
f"{format_paths(dirty_almanac)}"
|
|
51
|
+
)
|
|
52
|
+
return LifecycleMutationPreflight(
|
|
53
|
+
before=before,
|
|
54
|
+
almanac_prefix=almanac_prefix,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def validate(
|
|
58
|
+
self,
|
|
59
|
+
preflight: LifecycleMutationPreflight,
|
|
60
|
+
workspace: Workspace,
|
|
61
|
+
reported_changed_files: tuple[Path, ...],
|
|
62
|
+
) -> LifecycleMutationReport:
|
|
63
|
+
validate_reported_changes(workspace, reported_changed_files)
|
|
64
|
+
after = self.probe.snapshot(workspace.root_path)
|
|
65
|
+
validate_snapshot_available(after, self.operation)
|
|
66
|
+
mutated = changed_paths(preflight.before, after)
|
|
67
|
+
unsafe = tuple(
|
|
68
|
+
path
|
|
69
|
+
for path in mutated
|
|
70
|
+
if not path_is_under(path, preflight.almanac_prefix)
|
|
71
|
+
)
|
|
72
|
+
if unsafe:
|
|
73
|
+
almanac_label = preflight.almanac_prefix.as_posix()
|
|
74
|
+
raise ValidationFailed(
|
|
75
|
+
f"{self.operation} changed file outside {almanac_label}: "
|
|
76
|
+
f"{format_paths(unsafe)}"
|
|
77
|
+
)
|
|
78
|
+
return LifecycleMutationReport(
|
|
79
|
+
before=preflight.before,
|
|
80
|
+
after=after,
|
|
81
|
+
changed_files=tuple(
|
|
82
|
+
workspace.root_path / path
|
|
83
|
+
for path in mutated
|
|
84
|
+
if path_is_under(path, preflight.almanac_prefix)
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_harness_result(result: HarnessRunResult) -> None:
|
|
90
|
+
if result.status != HarnessRunStatus.SUCCEEDED:
|
|
91
|
+
suffix = first_line(result.output_text)
|
|
92
|
+
details = f": {suffix}" if suffix else ""
|
|
93
|
+
raise ExecutionFailed(
|
|
94
|
+
f"harness {result.kind.value} failed with status "
|
|
95
|
+
f"{result.status.value}{details}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def harness_events(result: HarnessRunResult) -> tuple[HarnessEvent, ...]:
|
|
100
|
+
if len(result.events) > 0:
|
|
101
|
+
return result.events
|
|
102
|
+
return (terminal_harness_event(result.kind, result.status, result.output_text),)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def harness_run_event_kind(event: HarnessEvent) -> RunEventKind:
|
|
106
|
+
if event.kind == HarnessEventKind.ERROR:
|
|
107
|
+
return RunEventKind.ERROR
|
|
108
|
+
if event.kind in {
|
|
109
|
+
HarnessEventKind.TOOL_USE,
|
|
110
|
+
HarnessEventKind.TOOL_RESULT,
|
|
111
|
+
HarnessEventKind.TOOL_SUMMARY,
|
|
112
|
+
HarnessEventKind.CONTEXT_USAGE,
|
|
113
|
+
HarnessEventKind.WARNING,
|
|
114
|
+
}:
|
|
115
|
+
return RunEventKind.TOOL
|
|
116
|
+
return RunEventKind.OUTPUT
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def first_line(value: str) -> str:
|
|
120
|
+
return value.splitlines()[0] if value.splitlines() else value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_snapshot_available(
|
|
124
|
+
snapshot: WorkspaceChangeSnapshot,
|
|
125
|
+
operation: str,
|
|
126
|
+
) -> None:
|
|
127
|
+
if snapshot.available:
|
|
128
|
+
return
|
|
129
|
+
reason = snapshot.unavailable_reason or "unknown git status failure"
|
|
130
|
+
raise ValidationFailed(f"{operation} requires Git change tracking: {reason}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_reported_changes(
|
|
134
|
+
workspace: Workspace,
|
|
135
|
+
reported_changed_files: tuple[Path, ...],
|
|
136
|
+
) -> None:
|
|
137
|
+
almanac_root = workspace.almanac_path.resolve()
|
|
138
|
+
for changed_file in reported_changed_files:
|
|
139
|
+
candidate = changed_file
|
|
140
|
+
if not candidate.is_absolute():
|
|
141
|
+
candidate = workspace.root_path / candidate
|
|
142
|
+
try:
|
|
143
|
+
candidate.resolve().relative_to(almanac_root)
|
|
144
|
+
except ValueError as error:
|
|
145
|
+
raise ValidationFailed(
|
|
146
|
+
"harness reported change outside configured Almanac root: "
|
|
147
|
+
f"{changed_file}"
|
|
148
|
+
) from error
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def changed_paths(
|
|
152
|
+
before: WorkspaceChangeSnapshot,
|
|
153
|
+
after: WorkspaceChangeSnapshot,
|
|
154
|
+
) -> tuple[Path, ...]:
|
|
155
|
+
before_by_path = changes_by_path(before.changes)
|
|
156
|
+
after_by_path = changes_by_path(after.changes)
|
|
157
|
+
paths = set(before_by_path) | set(after_by_path)
|
|
158
|
+
changed = [
|
|
159
|
+
path
|
|
160
|
+
for path in paths
|
|
161
|
+
if change_identity(before_by_path.get(path))
|
|
162
|
+
!= change_identity(after_by_path.get(path))
|
|
163
|
+
]
|
|
164
|
+
return tuple(sorted(changed, key=lambda item: item.as_posix()))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def changes_by_path(
|
|
168
|
+
changes: tuple[WorkspacePathChange, ...],
|
|
169
|
+
) -> dict[Path, WorkspacePathChange]:
|
|
170
|
+
return {change.path: change for change in changes}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def change_identity(
|
|
174
|
+
change: WorkspacePathChange | None,
|
|
175
|
+
) -> tuple[str, str, str | None] | None:
|
|
176
|
+
if change is None:
|
|
177
|
+
return None
|
|
178
|
+
return (change.state.value, change.status, change.fingerprint)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def almanac_relative_path(workspace: Workspace) -> Path:
|
|
182
|
+
try:
|
|
183
|
+
return workspace.almanac_path.resolve().relative_to(
|
|
184
|
+
workspace.root_path.resolve()
|
|
185
|
+
)
|
|
186
|
+
except ValueError as error:
|
|
187
|
+
raise ValidationFailed(
|
|
188
|
+
f"Almanac root is outside workspace: {workspace.almanac_path}"
|
|
189
|
+
) from error
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def path_is_under(path: Path, parent: Path) -> bool:
|
|
193
|
+
return path == parent or parent in path.parents
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def format_paths(paths: tuple[Path, ...]) -> str:
|
|
197
|
+
return ", ".join(path.as_posix() for path in paths)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import field_validator
|
|
6
|
+
|
|
7
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
8
|
+
from codealmanac.core.text import required_text
|
|
9
|
+
from codealmanac.services.sources.models import TranscriptApp, TranscriptCandidate
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SyncMode(StrEnum):
|
|
13
|
+
STATUS = "status"
|
|
14
|
+
SYNC = "sync"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SyncLedgerStatus(StrEnum):
|
|
18
|
+
DONE = "done"
|
|
19
|
+
PENDING = "pending"
|
|
20
|
+
FAILED = "failed"
|
|
21
|
+
NEEDS_ATTENTION = "needs_attention"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SyncDecisionKind(StrEnum):
|
|
25
|
+
SKIP = "skip"
|
|
26
|
+
NEEDS_ATTENTION = "needs_attention"
|
|
27
|
+
READY = "ready"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SyncLedgerEntry(CodeAlmanacModel):
|
|
31
|
+
app: TranscriptApp
|
|
32
|
+
session_id: str
|
|
33
|
+
transcript_path: Path
|
|
34
|
+
status: SyncLedgerStatus
|
|
35
|
+
last_absorbed_size: int
|
|
36
|
+
last_absorbed_line: int
|
|
37
|
+
last_absorbed_prefix_hash: str
|
|
38
|
+
last_absorbed_at: datetime | None = None
|
|
39
|
+
last_job_id: str | None = None
|
|
40
|
+
last_error: str | None = None
|
|
41
|
+
failed_attempts: int = 0
|
|
42
|
+
pending_started_at: datetime | None = None
|
|
43
|
+
pending_owner: str | None = None
|
|
44
|
+
pending_run_id: str | None = None
|
|
45
|
+
pending_to_size: int | None = None
|
|
46
|
+
pending_prefix_hash: str | None = None
|
|
47
|
+
pending_from_line: int | None = None
|
|
48
|
+
pending_to_line: int | None = None
|
|
49
|
+
|
|
50
|
+
@field_validator("session_id", "last_absorbed_prefix_hash")
|
|
51
|
+
@classmethod
|
|
52
|
+
def require_text(cls, value: str) -> str:
|
|
53
|
+
return required_text(value, "sync ledger entry")
|
|
54
|
+
|
|
55
|
+
@field_validator("pending_owner", "pending_run_id", "pending_prefix_hash")
|
|
56
|
+
@classmethod
|
|
57
|
+
def require_optional_pending_text(cls, value: str | None) -> str | None:
|
|
58
|
+
if value is None:
|
|
59
|
+
return None
|
|
60
|
+
return required_text(value, "sync pending value")
|
|
61
|
+
|
|
62
|
+
@field_validator("last_absorbed_size", "last_absorbed_line")
|
|
63
|
+
@classmethod
|
|
64
|
+
def non_negative_cursor(cls, value: int) -> int:
|
|
65
|
+
if value < 0:
|
|
66
|
+
raise ValueError("sync cursor must be non-negative")
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
@field_validator("failed_attempts")
|
|
70
|
+
@classmethod
|
|
71
|
+
def non_negative_failed_attempts(cls, value: int) -> int:
|
|
72
|
+
if value < 0:
|
|
73
|
+
raise ValueError("sync failed attempts must be non-negative")
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
@field_validator("pending_to_size", "pending_from_line", "pending_to_line")
|
|
77
|
+
@classmethod
|
|
78
|
+
def non_negative_pending_cursor(cls, value: int | None) -> int | None:
|
|
79
|
+
if value is not None and value < 0:
|
|
80
|
+
raise ValueError("sync pending cursor must be non-negative")
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SyncLedger(CodeAlmanacModel):
|
|
85
|
+
version: int
|
|
86
|
+
updated_at: datetime
|
|
87
|
+
sessions: dict[str, SyncLedgerEntry]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SyncReady(CodeAlmanacModel):
|
|
91
|
+
app: TranscriptApp
|
|
92
|
+
session_id: str
|
|
93
|
+
transcript_path: Path
|
|
94
|
+
repo_root: Path
|
|
95
|
+
from_line: int
|
|
96
|
+
to_line: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SyncStarted(CodeAlmanacModel):
|
|
100
|
+
app: TranscriptApp
|
|
101
|
+
session_id: str
|
|
102
|
+
transcript_path: Path
|
|
103
|
+
repo_root: Path
|
|
104
|
+
run_id: str
|
|
105
|
+
from_line: int
|
|
106
|
+
to_line: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SyncSkipped(CodeAlmanacModel):
|
|
110
|
+
transcript_path: Path
|
|
111
|
+
reason: str
|
|
112
|
+
app: TranscriptApp | None = None
|
|
113
|
+
session_id: str | None = None
|
|
114
|
+
repo_root: Path | None = None
|
|
115
|
+
|
|
116
|
+
@field_validator("reason")
|
|
117
|
+
@classmethod
|
|
118
|
+
def require_reason(cls, value: str) -> str:
|
|
119
|
+
return required_text(value, "sync skip reason")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SyncSummary(CodeAlmanacModel):
|
|
123
|
+
mode: SyncMode
|
|
124
|
+
scanned: int
|
|
125
|
+
eligible: int
|
|
126
|
+
ready: tuple[SyncReady, ...] = ()
|
|
127
|
+
started: tuple[SyncStarted, ...] = ()
|
|
128
|
+
skipped: tuple[SyncSkipped, ...] = ()
|
|
129
|
+
needs_attention: tuple[SyncSkipped, ...] = ()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TranscriptSnapshot(CodeAlmanacModel):
|
|
133
|
+
content: bytes
|
|
134
|
+
current_size: int
|
|
135
|
+
current_line: int
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SyncCursorDecision(CodeAlmanacModel):
|
|
139
|
+
kind: SyncDecisionKind
|
|
140
|
+
reason: str = ""
|
|
141
|
+
from_line: int = 0
|
|
142
|
+
to_line: int = 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SyncWorkItem(CodeAlmanacModel):
|
|
146
|
+
candidate: TranscriptCandidate
|
|
147
|
+
ledger_key: str
|
|
148
|
+
entry: SyncLedgerEntry
|
|
149
|
+
snapshot: TranscriptSnapshot
|
|
150
|
+
from_line: int
|
|
151
|
+
to_line: int
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class SyncEvaluation(CodeAlmanacModel):
|
|
155
|
+
summary: SyncSummary
|
|
156
|
+
work_items: tuple[SyncWorkItem, ...]
|
|
157
|
+
ledgers: dict[Path, SyncLedger]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
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
|
+
from codealmanac.services.harnesses.models import HarnessKind
|
|
9
|
+
from codealmanac.services.sources.models import TranscriptApp
|
|
10
|
+
|
|
11
|
+
DEFAULT_SYNC_PENDING_TIMEOUT = timedelta(hours=24)
|
|
12
|
+
DEFAULT_SYNC_MAX_FAILED_ATTEMPTS = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SyncSelectionRequest(CodeAlmanacModel):
|
|
16
|
+
cwd: Path
|
|
17
|
+
apps: tuple[TranscriptApp, ...]
|
|
18
|
+
quiet: timedelta
|
|
19
|
+
wiki: str | None = None
|
|
20
|
+
home: Path | None = None
|
|
21
|
+
now: datetime | None = None
|
|
22
|
+
pending_timeout: timedelta = DEFAULT_SYNC_PENDING_TIMEOUT
|
|
23
|
+
max_failed_attempts: int = DEFAULT_SYNC_MAX_FAILED_ATTEMPTS
|
|
24
|
+
|
|
25
|
+
@field_validator("apps")
|
|
26
|
+
@classmethod
|
|
27
|
+
def require_apps(
|
|
28
|
+
cls,
|
|
29
|
+
value: tuple[TranscriptApp, ...],
|
|
30
|
+
) -> tuple[TranscriptApp, ...]:
|
|
31
|
+
if len(value) == 0:
|
|
32
|
+
raise ValueError("at least one sync app is required")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
@field_validator("quiet", "pending_timeout")
|
|
36
|
+
@classmethod
|
|
37
|
+
def non_negative_duration(cls, value: timedelta) -> timedelta:
|
|
38
|
+
if value.total_seconds() < 0:
|
|
39
|
+
raise ValueError("sync duration must be non-negative")
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
@field_validator("max_failed_attempts")
|
|
43
|
+
@classmethod
|
|
44
|
+
def non_negative_max_failed_attempts(cls, value: int) -> int:
|
|
45
|
+
if value < 0:
|
|
46
|
+
raise ValueError("sync max failed attempts must be non-negative")
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RunSyncStatusRequest(SyncSelectionRequest):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RunSyncRequest(SyncSelectionRequest):
|
|
55
|
+
harness: HarnessKind
|
|
56
|
+
claim_owner: str | None = None
|
|
57
|
+
|
|
58
|
+
@field_validator("claim_owner")
|
|
59
|
+
@classmethod
|
|
60
|
+
def require_claim_owner(cls, value: str | None) -> str | None:
|
|
61
|
+
if value is None:
|
|
62
|
+
return None
|
|
63
|
+
return required_text(value, "sync claim owner")
|