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.
Files changed (95) hide show
  1. devmemory/__about__.py +3 -0
  2. devmemory/__init__.py +14 -0
  3. devmemory/__main__.py +6 -0
  4. devmemory/adapters/__init__.py +6 -0
  5. devmemory/adapters/databricks.py +346 -0
  6. devmemory/adapters/entire.py +444 -0
  7. devmemory/adapters/git.py +408 -0
  8. devmemory/adapters/graph.py +251 -0
  9. devmemory/adapters/metrics.py +150 -0
  10. devmemory/adapters/tests.py +227 -0
  11. devmemory/analysis/__init__.py +19 -0
  12. devmemory/analysis/base.py +128 -0
  13. devmemory/analysis/chain.py +53 -0
  14. devmemory/analysis/llm.py +236 -0
  15. devmemory/analysis/rules.py +110 -0
  16. devmemory/api/__init__.py +10 -0
  17. devmemory/api/app.py +390 -0
  18. devmemory/api/mappers.py +187 -0
  19. devmemory/api/schemas.py +201 -0
  20. devmemory/cli/__init__.py +1 -0
  21. devmemory/cli/_errors.py +36 -0
  22. devmemory/cli/_render.py +79 -0
  23. devmemory/cli/analytics.py +136 -0
  24. devmemory/cli/analyze.py +58 -0
  25. devmemory/cli/app.py +163 -0
  26. devmemory/cli/checkpoint.py +199 -0
  27. devmemory/cli/compare.py +104 -0
  28. devmemory/cli/doctor.py +151 -0
  29. devmemory/cli/history.py +56 -0
  30. devmemory/cli/impact.py +95 -0
  31. devmemory/cli/init.py +91 -0
  32. devmemory/cli/mcp.py +66 -0
  33. devmemory/cli/memory.py +70 -0
  34. devmemory/cli/restore.py +91 -0
  35. devmemory/cli/search.py +48 -0
  36. devmemory/cli/serve.py +64 -0
  37. devmemory/cli/show.py +139 -0
  38. devmemory/cli/status.py +72 -0
  39. devmemory/cli/task.py +333 -0
  40. devmemory/config.py +302 -0
  41. devmemory/domain/__init__.py +5 -0
  42. devmemory/domain/enums.py +151 -0
  43. devmemory/domain/errors.py +188 -0
  44. devmemory/domain/models.py +452 -0
  45. devmemory/domain/taskloop.py +212 -0
  46. devmemory/environment.py +67 -0
  47. devmemory/logging.py +148 -0
  48. devmemory/mcp/__init__.py +12 -0
  49. devmemory/mcp/server.py +225 -0
  50. devmemory/paths.py +112 -0
  51. devmemory/pipeline/__init__.py +7 -0
  52. devmemory/pipeline/checkpoint.py +443 -0
  53. devmemory/pipeline/feature_detect.py +53 -0
  54. devmemory/pipeline/regression.py +141 -0
  55. devmemory/pipeline/runlog.py +73 -0
  56. devmemory/pipeline/status_rules.py +44 -0
  57. devmemory/py.typed +0 -0
  58. devmemory/services/__init__.py +9 -0
  59. devmemory/services/agent_context.py +287 -0
  60. devmemory/services/analysis.py +116 -0
  61. devmemory/services/analytics.py +328 -0
  62. devmemory/services/brief.py +53 -0
  63. devmemory/services/context.py +88 -0
  64. devmemory/services/databricks_sync.py +121 -0
  65. devmemory/services/features.py +85 -0
  66. devmemory/services/impact.py +47 -0
  67. devmemory/services/memory.py +212 -0
  68. devmemory/services/projects.py +226 -0
  69. devmemory/services/restore.py +194 -0
  70. devmemory/services/taskloop/__init__.py +39 -0
  71. devmemory/services/taskloop/collectors.py +263 -0
  72. devmemory/services/taskloop/engine.py +426 -0
  73. devmemory/services/taskloop/requirements.py +358 -0
  74. devmemory/services/trace.py +152 -0
  75. devmemory/services/versions.py +287 -0
  76. devmemory/storage/__init__.py +9 -0
  77. devmemory/storage/artifacts.py +113 -0
  78. devmemory/storage/db.py +205 -0
  79. devmemory/storage/graph_impacts.py +63 -0
  80. devmemory/storage/migrations/0001_init.sql +15 -0
  81. devmemory/storage/migrations/0002_versions.sql +210 -0
  82. devmemory/storage/migrations/0003_graph.sql +14 -0
  83. devmemory/storage/migrations/0004_taskloop.sql +82 -0
  84. devmemory/storage/migrations/0005_project_brief.sql +12 -0
  85. devmemory/storage/repositories.py +286 -0
  86. devmemory/storage/tasks.py +342 -0
  87. devmemory/storage/versions.py +604 -0
  88. devmemory/web/static/assets/index-CbV5njRH.js +78 -0
  89. devmemory/web/static/assets/index-DD-7ceZx.css +1 -0
  90. devmemory/web/static/index.html +18 -0
  91. devmemory_cli-0.1.0.dev0.dist-info/METADATA +174 -0
  92. devmemory_cli-0.1.0.dev0.dist-info/RECORD +95 -0
  93. devmemory_cli-0.1.0.dev0.dist-info/WHEEL +4 -0
  94. devmemory_cli-0.1.0.dev0.dist-info/entry_points.txt +3 -0
  95. 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
+ ]