devmemory-cli 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.
- devmemory/__about__.py +3 -0
- devmemory/__init__.py +14 -0
- devmemory/__main__.py +6 -0
- devmemory/adapters/__init__.py +6 -0
- devmemory/adapters/databricks.py +346 -0
- devmemory/adapters/entire.py +444 -0
- devmemory/adapters/git.py +408 -0
- devmemory/adapters/graph.py +251 -0
- devmemory/adapters/metrics.py +150 -0
- devmemory/adapters/tests.py +227 -0
- devmemory/analysis/__init__.py +19 -0
- devmemory/analysis/base.py +128 -0
- devmemory/analysis/chain.py +53 -0
- devmemory/analysis/llm.py +236 -0
- devmemory/analysis/rules.py +110 -0
- devmemory/api/__init__.py +10 -0
- devmemory/api/app.py +390 -0
- devmemory/api/mappers.py +187 -0
- devmemory/api/schemas.py +201 -0
- devmemory/cli/__init__.py +1 -0
- devmemory/cli/_errors.py +36 -0
- devmemory/cli/_render.py +79 -0
- devmemory/cli/analytics.py +136 -0
- devmemory/cli/analyze.py +58 -0
- devmemory/cli/app.py +163 -0
- devmemory/cli/checkpoint.py +199 -0
- devmemory/cli/compare.py +104 -0
- devmemory/cli/doctor.py +151 -0
- devmemory/cli/history.py +56 -0
- devmemory/cli/impact.py +95 -0
- devmemory/cli/init.py +91 -0
- devmemory/cli/mcp.py +66 -0
- devmemory/cli/memory.py +70 -0
- devmemory/cli/restore.py +91 -0
- devmemory/cli/search.py +48 -0
- devmemory/cli/serve.py +64 -0
- devmemory/cli/show.py +139 -0
- devmemory/cli/status.py +72 -0
- devmemory/cli/task.py +333 -0
- devmemory/config.py +302 -0
- devmemory/domain/__init__.py +5 -0
- devmemory/domain/enums.py +151 -0
- devmemory/domain/errors.py +188 -0
- devmemory/domain/models.py +452 -0
- devmemory/domain/taskloop.py +212 -0
- devmemory/environment.py +67 -0
- devmemory/logging.py +148 -0
- devmemory/mcp/__init__.py +12 -0
- devmemory/mcp/server.py +225 -0
- devmemory/paths.py +112 -0
- devmemory/pipeline/__init__.py +7 -0
- devmemory/pipeline/checkpoint.py +443 -0
- devmemory/pipeline/feature_detect.py +53 -0
- devmemory/pipeline/regression.py +141 -0
- devmemory/pipeline/runlog.py +73 -0
- devmemory/pipeline/status_rules.py +44 -0
- devmemory/py.typed +0 -0
- devmemory/services/__init__.py +9 -0
- devmemory/services/agent_context.py +287 -0
- devmemory/services/analysis.py +116 -0
- devmemory/services/analytics.py +328 -0
- devmemory/services/brief.py +53 -0
- devmemory/services/context.py +88 -0
- devmemory/services/databricks_sync.py +121 -0
- devmemory/services/features.py +85 -0
- devmemory/services/impact.py +47 -0
- devmemory/services/memory.py +212 -0
- devmemory/services/projects.py +226 -0
- devmemory/services/restore.py +194 -0
- devmemory/services/taskloop/__init__.py +39 -0
- devmemory/services/taskloop/collectors.py +263 -0
- devmemory/services/taskloop/engine.py +426 -0
- devmemory/services/taskloop/requirements.py +358 -0
- devmemory/services/trace.py +152 -0
- devmemory/services/versions.py +287 -0
- devmemory/storage/__init__.py +9 -0
- devmemory/storage/artifacts.py +113 -0
- devmemory/storage/db.py +205 -0
- devmemory/storage/graph_impacts.py +63 -0
- devmemory/storage/migrations/0001_init.sql +15 -0
- devmemory/storage/migrations/0002_versions.sql +210 -0
- devmemory/storage/migrations/0003_graph.sql +14 -0
- devmemory/storage/migrations/0004_taskloop.sql +82 -0
- devmemory/storage/migrations/0005_project_brief.sql +12 -0
- devmemory/storage/repositories.py +286 -0
- devmemory/storage/tasks.py +342 -0
- devmemory/storage/versions.py +604 -0
- devmemory/web/static/assets/index-CbV5njRH.js +78 -0
- devmemory/web/static/assets/index-DD-7ceZx.css +1 -0
- devmemory/web/static/index.html +18 -0
- devmemory_cli-0.1.0.dev0.dist-info/METADATA +174 -0
- devmemory_cli-0.1.0.dev0.dist-info/RECORD +95 -0
- devmemory_cli-0.1.0.dev0.dist-info/WHEEL +4 -0
- devmemory_cli-0.1.0.dev0.dist-info/entry_points.txt +3 -0
- devmemory_cli-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Typed domain models.
|
|
2
|
+
|
|
3
|
+
Pure data + light behaviour. No I/O, no framework imports. These are the shapes
|
|
4
|
+
adapters produce, services operate on, and the API/MCP layers serialize.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from devmemory.domain.enums import (
|
|
14
|
+
AssociationMethod,
|
|
15
|
+
ChangeType,
|
|
16
|
+
FeatureStatus,
|
|
17
|
+
MetricDirection,
|
|
18
|
+
VersionStatus,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Model(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="forbid", frozen=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# --- git ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommitInfo(_Model):
|
|
30
|
+
"""A resolved git commit. Facts only - straight from ``git``."""
|
|
31
|
+
|
|
32
|
+
sha: str
|
|
33
|
+
parents: list[str] = Field(default_factory=list)
|
|
34
|
+
author_name: str = ""
|
|
35
|
+
author_email: str = ""
|
|
36
|
+
authored_at: datetime | None = None
|
|
37
|
+
committer_name: str = ""
|
|
38
|
+
committer_email: str = ""
|
|
39
|
+
committed_at: datetime | None = None
|
|
40
|
+
subject: str = ""
|
|
41
|
+
body: str = ""
|
|
42
|
+
trailers: dict[str, list[str]] = Field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def short_sha(self) -> str:
|
|
46
|
+
return self.sha[:12]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def parent(self) -> str | None:
|
|
50
|
+
"""First parent, or ``None`` for a root commit."""
|
|
51
|
+
return self.parents[0] if self.parents else None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_merge(self) -> bool:
|
|
55
|
+
return len(self.parents) > 1
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def message(self) -> str:
|
|
59
|
+
return f"{self.subject}\n\n{self.body}".strip() if self.body else self.subject
|
|
60
|
+
|
|
61
|
+
def trailer(self, key: str) -> str | None:
|
|
62
|
+
"""First value of a git trailer (case-insensitive), or ``None``."""
|
|
63
|
+
lowered = key.lower()
|
|
64
|
+
for name, values in self.trailers.items():
|
|
65
|
+
if name.lower() == lowered and values:
|
|
66
|
+
return values[0]
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ChangedFile(_Model):
|
|
71
|
+
"""One file's change between two commits."""
|
|
72
|
+
|
|
73
|
+
path: str
|
|
74
|
+
change_type: ChangeType
|
|
75
|
+
old_path: str | None = None
|
|
76
|
+
additions: int = 0
|
|
77
|
+
deletions: int = 0
|
|
78
|
+
binary: bool = False
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def net_lines(self) -> int:
|
|
82
|
+
return self.additions - self.deletions
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class DiffStat(_Model):
|
|
86
|
+
"""Aggregate line/file counts for a diff."""
|
|
87
|
+
|
|
88
|
+
files_changed: int = 0
|
|
89
|
+
additions: int = 0
|
|
90
|
+
deletions: int = 0
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_files(cls, files: list[ChangedFile]) -> DiffStat:
|
|
94
|
+
return cls(
|
|
95
|
+
files_changed=len(files),
|
|
96
|
+
additions=sum(f.additions for f in files),
|
|
97
|
+
deletions=sum(f.deletions for f in files),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class WorkingTreeState(_Model):
|
|
102
|
+
"""Snapshot of ``git status`` relevant to safe operations."""
|
|
103
|
+
|
|
104
|
+
branch: str | None = None
|
|
105
|
+
detached: bool = False
|
|
106
|
+
head: str | None = None
|
|
107
|
+
staged: list[str] = Field(default_factory=list)
|
|
108
|
+
unstaged: list[str] = Field(default_factory=list)
|
|
109
|
+
untracked: list[str] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def is_clean(self) -> bool:
|
|
113
|
+
return not (self.staged or self.unstaged or self.untracked)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def has_uncommitted_changes(self) -> bool:
|
|
117
|
+
return bool(self.staged or self.unstaged)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# --- entire ----------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TokenUsage(_Model):
|
|
124
|
+
input_tokens: int = 0
|
|
125
|
+
output_tokens: int = 0
|
|
126
|
+
cache_read_tokens: int = 0
|
|
127
|
+
cache_creation_tokens: int = 0
|
|
128
|
+
api_call_count: int = 0
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def total(self) -> int:
|
|
132
|
+
return (
|
|
133
|
+
self.input_tokens
|
|
134
|
+
+ self.output_tokens
|
|
135
|
+
+ self.cache_read_tokens
|
|
136
|
+
+ self.cache_creation_tokens
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CheckpointSession(_Model):
|
|
141
|
+
"""One AI session inside a checkpoint."""
|
|
142
|
+
|
|
143
|
+
session_id: str | None = None
|
|
144
|
+
agent: str | None = None
|
|
145
|
+
model: str | None = None
|
|
146
|
+
kind: str | None = None
|
|
147
|
+
created_at: datetime | None = None
|
|
148
|
+
tokens: TokenUsage = Field(default_factory=TokenUsage)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class CheckpointReference(_Model):
|
|
152
|
+
"""DevMemory's normalized view of an Entire checkpoint.
|
|
153
|
+
|
|
154
|
+
A reference plus the small amount of normalized context we display and search.
|
|
155
|
+
The full transcript stays in Entire's git refs.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
checkpoint_id: str
|
|
159
|
+
commit_sha: str | None = None
|
|
160
|
+
intent: str | None = None
|
|
161
|
+
"""The developer's prompt (from ``<idx>/prompt.txt``), trimmed for storage."""
|
|
162
|
+
agent: str | None = None
|
|
163
|
+
model: str | None = None
|
|
164
|
+
strategy: str | None = None
|
|
165
|
+
created_at: datetime | None = None
|
|
166
|
+
sessions: list[CheckpointSession] = Field(default_factory=list)
|
|
167
|
+
tokens: TokenUsage = Field(default_factory=TokenUsage)
|
|
168
|
+
association_method: AssociationMethod = AssociationMethod.NONE
|
|
169
|
+
association_confidence: float = 0.0
|
|
170
|
+
ref: str | None = None
|
|
171
|
+
"""The git ref (``refs/entire/checkpoints/<shard>/<id>``) when known."""
|
|
172
|
+
imported: bool = False
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def is_linked(self) -> bool:
|
|
176
|
+
return self.association_method is not AssociationMethod.NONE
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def is_uncertain(self) -> bool:
|
|
180
|
+
return 0.0 < self.association_confidence < 0.8
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class EntireStatus(_Model):
|
|
184
|
+
"""Result of probing the Entire integration for a repository."""
|
|
185
|
+
|
|
186
|
+
installed: bool = False
|
|
187
|
+
enabled: bool = False
|
|
188
|
+
cli_version: str | None = None
|
|
189
|
+
agents: list[str] = Field(default_factory=list)
|
|
190
|
+
binary_path: str | None = None
|
|
191
|
+
detail: str | None = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --- project ----------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Project(_Model):
|
|
198
|
+
"""One repository tracked by DevMemory."""
|
|
199
|
+
|
|
200
|
+
project_id: str
|
|
201
|
+
name: str
|
|
202
|
+
repo_path: str
|
|
203
|
+
created_at: datetime
|
|
204
|
+
updated_at: datetime
|
|
205
|
+
current_version_id: int | None = None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class EnvironmentInfo(_Model):
|
|
209
|
+
"""Best-effort snapshot of the toolchain, attached to versions later."""
|
|
210
|
+
|
|
211
|
+
python_version: str
|
|
212
|
+
platform: str
|
|
213
|
+
machine: str
|
|
214
|
+
git_version: str | None = None
|
|
215
|
+
entire_version: str | None = None
|
|
216
|
+
package_manager: str | None = None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --- results (facts) ------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestOutcome(_Model):
|
|
223
|
+
"""Outcome of running the configured test command. Facts, not judgement."""
|
|
224
|
+
|
|
225
|
+
__test__ = False # this is a data model, not a pytest test case
|
|
226
|
+
|
|
227
|
+
command: str | None = None
|
|
228
|
+
framework: str | None = None
|
|
229
|
+
total: int = 0
|
|
230
|
+
passed: int = 0
|
|
231
|
+
failed: int = 0
|
|
232
|
+
skipped: int = 0
|
|
233
|
+
errors: int = 0
|
|
234
|
+
exit_code: int | None = None
|
|
235
|
+
duration_seconds: float | None = None
|
|
236
|
+
failing: list[str] = Field(default_factory=list)
|
|
237
|
+
output: str | None = None
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def ran(self) -> bool:
|
|
241
|
+
return self.command is not None
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def all_passed(self) -> bool:
|
|
245
|
+
return self.ran and self.failed == 0 and self.errors == 0 and self.exit_code in (0, None)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def pass_rate(self) -> float | None:
|
|
249
|
+
return self.passed / self.total if self.total else None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Metric(_Model):
|
|
253
|
+
"""One measurable project metric, with an explicit better-direction."""
|
|
254
|
+
|
|
255
|
+
name: str
|
|
256
|
+
before: float | None = None
|
|
257
|
+
after: float | None = None
|
|
258
|
+
unit: str | None = None
|
|
259
|
+
direction: MetricDirection = MetricDirection.HIGHER_IS_BETTER
|
|
260
|
+
metadata: dict[str, object] = Field(default_factory=dict)
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def delta(self) -> float | None:
|
|
264
|
+
if self.before is None or self.after is None:
|
|
265
|
+
return None
|
|
266
|
+
return self.after - self.before
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def percent_change(self) -> float | None:
|
|
270
|
+
if self.before is None or self.after is None or self.before == 0:
|
|
271
|
+
return None
|
|
272
|
+
return (self.after - self.before) / abs(self.before) * 100.0
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def is_improvement(self) -> bool:
|
|
276
|
+
delta = self.delta
|
|
277
|
+
if delta is None or delta == 0 or self.direction is MetricDirection.NEUTRAL:
|
|
278
|
+
return False
|
|
279
|
+
return delta > 0 if self.direction is MetricDirection.HIGHER_IS_BETTER else delta < 0
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def is_worse(self) -> bool:
|
|
283
|
+
delta = self.delta
|
|
284
|
+
if delta is None or delta == 0 or self.direction is MetricDirection.NEUTRAL:
|
|
285
|
+
return False
|
|
286
|
+
return delta < 0 if self.direction is MetricDirection.HIGHER_IS_BETTER else delta > 0
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class Regression(_Model):
|
|
290
|
+
version_id: str | None = None
|
|
291
|
+
kind: str # 'metric' | 'test'
|
|
292
|
+
metric: str | None = None
|
|
293
|
+
before: float | None = None
|
|
294
|
+
after: float | None = None
|
|
295
|
+
change_percent: float | None = None
|
|
296
|
+
severity: str = "MEDIUM" # LOW | MEDIUM | HIGH
|
|
297
|
+
detail: str | None = None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class Analysis(_Model):
|
|
301
|
+
"""AI (or rule-based) interpretation. Never overwrites facts."""
|
|
302
|
+
|
|
303
|
+
version_id: str | None = None
|
|
304
|
+
summary: str = ""
|
|
305
|
+
reasoning: str | None = None
|
|
306
|
+
recommendation: str | None = None
|
|
307
|
+
warnings: list[str] = Field(default_factory=list)
|
|
308
|
+
risk: str | None = None
|
|
309
|
+
provider: str = "rules"
|
|
310
|
+
model: str | None = None
|
|
311
|
+
generated_at: datetime | None = None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class Artifact(_Model):
|
|
315
|
+
artifact_id: str
|
|
316
|
+
version_id: str
|
|
317
|
+
path: str
|
|
318
|
+
type: str = "project_snapshot"
|
|
319
|
+
size_bytes: int | None = None
|
|
320
|
+
sha256: str | None = None
|
|
321
|
+
created_at: datetime | None = None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class DocFlag(_Model):
|
|
325
|
+
doc_path: str
|
|
326
|
+
reason: str | None = None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class Feature(_Model):
|
|
330
|
+
feature_id: str
|
|
331
|
+
project_id: str
|
|
332
|
+
name: str
|
|
333
|
+
status: FeatureStatus = FeatureStatus.IN_PROGRESS
|
|
334
|
+
derived_from: str | None = None
|
|
335
|
+
created_at: datetime | None = None
|
|
336
|
+
updated_at: datetime | None = None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# --- the normalized bridge --------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class DevelopmentEvent(_Model):
|
|
343
|
+
"""Everything collected about one development change, normalized.
|
|
344
|
+
|
|
345
|
+
Adapters fill this; the version registry persists it. It is stored verbatim on
|
|
346
|
+
the version (``source_event_json``) for replay and debugging.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
project_id: str
|
|
350
|
+
source: str = "devmemory"
|
|
351
|
+
occurred_at: datetime
|
|
352
|
+
run_id: str | None = None
|
|
353
|
+
|
|
354
|
+
intent: str | None = None
|
|
355
|
+
agent: str | None = None
|
|
356
|
+
model: str | None = None
|
|
357
|
+
feature: str | None = None
|
|
358
|
+
feature_derived_from: str | None = None
|
|
359
|
+
|
|
360
|
+
commit: CommitInfo
|
|
361
|
+
parent_commit: str | None = None
|
|
362
|
+
branch: str | None = None
|
|
363
|
+
changed_files: list[ChangedFile] = Field(default_factory=list)
|
|
364
|
+
|
|
365
|
+
checkpoint: CheckpointReference | None = None
|
|
366
|
+
|
|
367
|
+
status: VersionStatus | None = None
|
|
368
|
+
tests: TestOutcome | None = None
|
|
369
|
+
metrics: list[Metric] = Field(default_factory=list)
|
|
370
|
+
errors: list[str] = Field(default_factory=list)
|
|
371
|
+
environment: EnvironmentInfo | None = None
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def diff_stat(self) -> DiffStat:
|
|
375
|
+
return DiffStat.from_files(self.changed_files)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# --- the record ----------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class DevelopmentVersion(_Model):
|
|
382
|
+
"""The persisted development record - the join of everything above."""
|
|
383
|
+
|
|
384
|
+
version_id: str
|
|
385
|
+
version_number: int
|
|
386
|
+
project_id: str
|
|
387
|
+
|
|
388
|
+
intent: str | None = None
|
|
389
|
+
agent: str | None = None
|
|
390
|
+
model: str | None = None
|
|
391
|
+
|
|
392
|
+
git_commit: str
|
|
393
|
+
parent_commit: str | None = None
|
|
394
|
+
branch: str | None = None
|
|
395
|
+
|
|
396
|
+
feature_id: str | None = None
|
|
397
|
+
status: VersionStatus = VersionStatus.NEEDS_REVIEW
|
|
398
|
+
|
|
399
|
+
files_changed: int = 0
|
|
400
|
+
lines_added: int = 0
|
|
401
|
+
lines_removed: int = 0
|
|
402
|
+
|
|
403
|
+
changed_files: list[ChangedFile] = Field(default_factory=list)
|
|
404
|
+
primary_checkpoint: CheckpointReference | None = None
|
|
405
|
+
checkpoint_ids: list[str] = Field(default_factory=list)
|
|
406
|
+
|
|
407
|
+
entire_association_method: AssociationMethod = AssociationMethod.NONE
|
|
408
|
+
entire_association_confidence: float = 0.0
|
|
409
|
+
|
|
410
|
+
tests: TestOutcome | None = None
|
|
411
|
+
metrics: list[Metric] = Field(default_factory=list)
|
|
412
|
+
regressions: list[Regression] = Field(default_factory=list)
|
|
413
|
+
analysis: Analysis | None = None
|
|
414
|
+
artifacts: list[Artifact] = Field(default_factory=list)
|
|
415
|
+
doc_flags: list[DocFlag] = Field(default_factory=list)
|
|
416
|
+
|
|
417
|
+
environment: EnvironmentInfo | None = None
|
|
418
|
+
run_id: str | None = None
|
|
419
|
+
|
|
420
|
+
created_at: datetime
|
|
421
|
+
committed_at: datetime | None = None
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def net_lines(self) -> int:
|
|
425
|
+
return self.lines_added - self.lines_removed
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def has_uncertain_checkpoint(self) -> bool:
|
|
429
|
+
return bool(self.primary_checkpoint and self.primary_checkpoint.is_uncertain)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
__all__ = [
|
|
433
|
+
"Analysis",
|
|
434
|
+
"Artifact",
|
|
435
|
+
"ChangedFile",
|
|
436
|
+
"CheckpointReference",
|
|
437
|
+
"CheckpointSession",
|
|
438
|
+
"CommitInfo",
|
|
439
|
+
"DevelopmentEvent",
|
|
440
|
+
"DevelopmentVersion",
|
|
441
|
+
"DiffStat",
|
|
442
|
+
"DocFlag",
|
|
443
|
+
"EntireStatus",
|
|
444
|
+
"EnvironmentInfo",
|
|
445
|
+
"Feature",
|
|
446
|
+
"Metric",
|
|
447
|
+
"Project",
|
|
448
|
+
"Regression",
|
|
449
|
+
"TestOutcome",
|
|
450
|
+
"TokenUsage",
|
|
451
|
+
"WorkingTreeState",
|
|
452
|
+
]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Models for the state-aware coding loop.
|
|
2
|
+
|
|
3
|
+
A :class:`Task` is a live, multi-commit unit of work carrying an explicit list of
|
|
4
|
+
:class:`Requirement` s. It is deliberately *not* a ``DevelopmentVersion`` (which
|
|
5
|
+
is a post-commit record of one change): a task spans many commits, many
|
|
6
|
+
checkpoints, and many refreshes.
|
|
7
|
+
|
|
8
|
+
Every ``refresh_state`` call aggregates fresh evidence - Git, Entire, tests,
|
|
9
|
+
optional Graph - into one :class:`NormalizedState` and appends a
|
|
10
|
+
:class:`StateSnapshot`, so the loop is observable as State #1 -> #2 -> #3.
|
|
11
|
+
|
|
12
|
+
Nothing here interprets *how* code should be written. The engine reports
|
|
13
|
+
evidence; Antigravity decides implementation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
21
|
+
|
|
22
|
+
from devmemory.domain.enums import (
|
|
23
|
+
IssueStatus,
|
|
24
|
+
RequirementStatus,
|
|
25
|
+
TaskStatus,
|
|
26
|
+
TestRunStatus,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _Model(BaseModel):
|
|
31
|
+
model_config = ConfigDict(extra="forbid")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --- persisted entities --------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Requirement(_Model):
|
|
38
|
+
"""One normalized, checkable slice of the human task."""
|
|
39
|
+
|
|
40
|
+
id: str # "R1", "R2", ...
|
|
41
|
+
description: str
|
|
42
|
+
status: RequirementStatus = RequirementStatus.INCOMPLETE
|
|
43
|
+
reason: str = ""
|
|
44
|
+
evidence: list[str] = Field(default_factory=list)
|
|
45
|
+
evaluated_at: datetime | None = None
|
|
46
|
+
evaluated_by: str = "" # "rules" | "anthropic" | "openai" | "gemini" | "manual"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Task(_Model):
|
|
50
|
+
"""The original human task, preserved verbatim in ``goal``."""
|
|
51
|
+
|
|
52
|
+
id: str # "TASK-001"
|
|
53
|
+
goal: str
|
|
54
|
+
status: TaskStatus = TaskStatus.IN_PROGRESS
|
|
55
|
+
branch: str = "main"
|
|
56
|
+
base_commit: str | None = None # HEAD when the task was created
|
|
57
|
+
test_command: str | None = None
|
|
58
|
+
push_policy: str = "manual" # "manual" | "on_ready" | "never"
|
|
59
|
+
created_at: datetime | None = None
|
|
60
|
+
updated_at: datetime | None = None
|
|
61
|
+
requirements: list[Requirement] = Field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Issue(_Model):
|
|
65
|
+
"""An unresolved item raised by the agent (``report_issue``) or the engine."""
|
|
66
|
+
|
|
67
|
+
id: int | None = None
|
|
68
|
+
task_id: str
|
|
69
|
+
description: str
|
|
70
|
+
kind: str = "agent" # "agent" | "test" | "requirement" | "engine"
|
|
71
|
+
blocking: bool = False
|
|
72
|
+
status: IssueStatus = IssueStatus.OPEN
|
|
73
|
+
created_at: datetime | None = None
|
|
74
|
+
resolved_at: datetime | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TaskTestRun(_Model):
|
|
78
|
+
"""One execution of the configured test command during a refresh."""
|
|
79
|
+
|
|
80
|
+
id: int | None = None
|
|
81
|
+
task_id: str
|
|
82
|
+
command: str
|
|
83
|
+
status: TestRunStatus
|
|
84
|
+
passed: int = 0
|
|
85
|
+
failed: int = 0
|
|
86
|
+
skipped: int = 0
|
|
87
|
+
exit_code: int | None = None
|
|
88
|
+
duration_seconds: float | None = None
|
|
89
|
+
output_path: str | None = None
|
|
90
|
+
created_at: datetime | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- the normalized state (the MCP payload) -----------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class StateTask(_Model):
|
|
97
|
+
id: str
|
|
98
|
+
goal: str
|
|
99
|
+
status: TaskStatus
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class StateRequirement(_Model):
|
|
103
|
+
id: str
|
|
104
|
+
description: str
|
|
105
|
+
status: RequirementStatus
|
|
106
|
+
reason: str = ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class StateCheckpoint(_Model):
|
|
110
|
+
current_id: str | None = None
|
|
111
|
+
last_committed_id: str | None = None
|
|
112
|
+
session_id: str | None = None
|
|
113
|
+
commit_sha: str | None = None
|
|
114
|
+
association: str | None = None # how it was linked (trailer / entire_cli / ...)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class StateGit(_Model):
|
|
118
|
+
branch: str = "main"
|
|
119
|
+
commit_sha: str | None = None
|
|
120
|
+
commit_subject: str | None = None
|
|
121
|
+
files_changed: int = 0
|
|
122
|
+
lines_added: int = 0
|
|
123
|
+
lines_deleted: int = 0
|
|
124
|
+
working_tree_clean: bool = False
|
|
125
|
+
available: bool = True
|
|
126
|
+
reason: str | None = None # set when available is False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class StateTests(_Model):
|
|
130
|
+
command: str | None = None
|
|
131
|
+
passed: int = 0
|
|
132
|
+
failed: int = 0
|
|
133
|
+
skipped: int = 0
|
|
134
|
+
status: TestRunStatus = TestRunStatus.NOT_RUN
|
|
135
|
+
exit_code: int | None = None
|
|
136
|
+
output_path: str | None = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class StateImpact(_Model):
|
|
140
|
+
affected_files: int = 0
|
|
141
|
+
affected_tests: int = 0
|
|
142
|
+
available: bool = False
|
|
143
|
+
reason: str | None = None
|
|
144
|
+
details: list[str] = Field(default_factory=list)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class StateIssue(_Model):
|
|
148
|
+
id: int | None = None
|
|
149
|
+
description: str
|
|
150
|
+
kind: str = "agent"
|
|
151
|
+
blocking: bool = False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class NormalizedState(_Model):
|
|
155
|
+
"""The single machine-readable project state handed to Antigravity.
|
|
156
|
+
|
|
157
|
+
Compact by design - it orients the agent; the agent still inspects the real
|
|
158
|
+
repository with its own tools.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
task: StateTask
|
|
162
|
+
requirements: list[StateRequirement] = Field(default_factory=list)
|
|
163
|
+
checkpoint: StateCheckpoint = Field(default_factory=StateCheckpoint)
|
|
164
|
+
git: StateGit = Field(default_factory=StateGit)
|
|
165
|
+
tests: StateTests = Field(default_factory=StateTests)
|
|
166
|
+
impact: StateImpact = Field(default_factory=StateImpact)
|
|
167
|
+
unresolved: list[StateIssue] = Field(default_factory=list)
|
|
168
|
+
findings: list[str] = Field(default_factory=list)
|
|
169
|
+
recommended_focus: list[str] = Field(default_factory=list)
|
|
170
|
+
overall_status: TaskStatus = TaskStatus.IN_PROGRESS
|
|
171
|
+
snapshot_id: int | None = None
|
|
172
|
+
refreshed_at: datetime | None = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class StateSnapshot(_Model):
|
|
176
|
+
"""A stored, timestamped :class:`NormalizedState`. One per refresh."""
|
|
177
|
+
|
|
178
|
+
id: int | None = None
|
|
179
|
+
task_id: str
|
|
180
|
+
overall_status: TaskStatus
|
|
181
|
+
state: NormalizedState
|
|
182
|
+
created_at: datetime | None = None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# --- requirement evaluation (structured, auditable) --------------------------
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class RequirementVerdict(_Model):
|
|
189
|
+
"""One requirement's evaluation result - structured and auditable per §5."""
|
|
190
|
+
|
|
191
|
+
requirement_id: str
|
|
192
|
+
status: RequirementStatus
|
|
193
|
+
reason: str
|
|
194
|
+
evidence: list[str] = Field(default_factory=list)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
__all__ = [
|
|
198
|
+
"Issue",
|
|
199
|
+
"NormalizedState",
|
|
200
|
+
"Requirement",
|
|
201
|
+
"RequirementVerdict",
|
|
202
|
+
"StateCheckpoint",
|
|
203
|
+
"StateGit",
|
|
204
|
+
"StateImpact",
|
|
205
|
+
"StateIssue",
|
|
206
|
+
"StateRequirement",
|
|
207
|
+
"StateSnapshot",
|
|
208
|
+
"StateTask",
|
|
209
|
+
"StateTests",
|
|
210
|
+
"Task",
|
|
211
|
+
"TaskTestRun",
|
|
212
|
+
]
|