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,210 @@
|
|
|
1
|
+
-- 0002_versions: the development-version record and everything hanging off it.
|
|
2
|
+
--
|
|
3
|
+
-- Facts (git, entire, tests, metrics) and AI analysis are kept in separate
|
|
4
|
+
-- tables. A version can reference more than one Entire checkpoint.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE versions (
|
|
7
|
+
version_number INTEGER NOT NULL,
|
|
8
|
+
version_id TEXT PRIMARY KEY, -- "v7"
|
|
9
|
+
project_id TEXT NOT NULL,
|
|
10
|
+
|
|
11
|
+
intent TEXT,
|
|
12
|
+
agent TEXT,
|
|
13
|
+
model TEXT,
|
|
14
|
+
|
|
15
|
+
git_commit TEXT NOT NULL,
|
|
16
|
+
parent_commit TEXT,
|
|
17
|
+
branch TEXT,
|
|
18
|
+
|
|
19
|
+
feature_id TEXT,
|
|
20
|
+
status TEXT NOT NULL,
|
|
21
|
+
|
|
22
|
+
files_changed INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
lines_added INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
lines_removed INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
|
|
26
|
+
entire_association_method TEXT,
|
|
27
|
+
entire_association_confidence REAL,
|
|
28
|
+
|
|
29
|
+
environment_json TEXT,
|
|
30
|
+
source_event_json TEXT,
|
|
31
|
+
run_id TEXT,
|
|
32
|
+
|
|
33
|
+
created_at TEXT NOT NULL, -- when DevMemory recorded it
|
|
34
|
+
committed_at TEXT, -- the git commit's own time
|
|
35
|
+
|
|
36
|
+
UNIQUE (project_id, version_number),
|
|
37
|
+
UNIQUE (project_id, git_commit),
|
|
38
|
+
FOREIGN KEY (project_id) REFERENCES projects(project_id),
|
|
39
|
+
FOREIGN KEY (feature_id) REFERENCES features(feature_id)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE entire_checkpoints (
|
|
43
|
+
checkpoint_id TEXT PRIMARY KEY,
|
|
44
|
+
project_id TEXT NOT NULL,
|
|
45
|
+
ref TEXT,
|
|
46
|
+
agent TEXT,
|
|
47
|
+
model TEXT,
|
|
48
|
+
intent TEXT,
|
|
49
|
+
strategy TEXT,
|
|
50
|
+
created_at TEXT,
|
|
51
|
+
git_commit TEXT,
|
|
52
|
+
association_method TEXT,
|
|
53
|
+
association_confidence REAL,
|
|
54
|
+
tokens_json TEXT,
|
|
55
|
+
imported INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
sessions_json TEXT,
|
|
57
|
+
FOREIGN KEY (project_id) REFERENCES projects(project_id)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE version_checkpoints (
|
|
61
|
+
version_id TEXT NOT NULL,
|
|
62
|
+
checkpoint_id TEXT NOT NULL,
|
|
63
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
64
|
+
PRIMARY KEY (version_id, checkpoint_id),
|
|
65
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE,
|
|
66
|
+
FOREIGN KEY (checkpoint_id) REFERENCES entire_checkpoints(checkpoint_id)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE changed_files (
|
|
70
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
71
|
+
version_id TEXT NOT NULL,
|
|
72
|
+
path TEXT NOT NULL,
|
|
73
|
+
old_path TEXT,
|
|
74
|
+
change_type TEXT NOT NULL,
|
|
75
|
+
additions INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
deletions INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
binary INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE features (
|
|
82
|
+
feature_id TEXT PRIMARY KEY,
|
|
83
|
+
project_id TEXT NOT NULL,
|
|
84
|
+
name TEXT NOT NULL,
|
|
85
|
+
status TEXT NOT NULL,
|
|
86
|
+
derived_from TEXT,
|
|
87
|
+
created_at TEXT NOT NULL,
|
|
88
|
+
updated_at TEXT NOT NULL,
|
|
89
|
+
UNIQUE (project_id, name),
|
|
90
|
+
FOREIGN KEY (project_id) REFERENCES projects(project_id)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
CREATE TABLE tests (
|
|
94
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
95
|
+
version_id TEXT NOT NULL,
|
|
96
|
+
command TEXT,
|
|
97
|
+
framework TEXT,
|
|
98
|
+
total INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
passed INTEGER NOT NULL DEFAULT 0,
|
|
100
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
101
|
+
skipped INTEGER NOT NULL DEFAULT 0,
|
|
102
|
+
errors INTEGER NOT NULL DEFAULT 0,
|
|
103
|
+
exit_code INTEGER,
|
|
104
|
+
duration_seconds REAL,
|
|
105
|
+
failing_json TEXT,
|
|
106
|
+
output TEXT,
|
|
107
|
+
collected_at TEXT,
|
|
108
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE metrics (
|
|
112
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
113
|
+
version_id TEXT NOT NULL,
|
|
114
|
+
name TEXT NOT NULL,
|
|
115
|
+
before_value REAL,
|
|
116
|
+
after_value REAL,
|
|
117
|
+
unit TEXT,
|
|
118
|
+
direction TEXT,
|
|
119
|
+
metadata_json TEXT,
|
|
120
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE regressions (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
version_id TEXT NOT NULL,
|
|
126
|
+
kind TEXT NOT NULL, -- 'metric' | 'test'
|
|
127
|
+
metric TEXT,
|
|
128
|
+
before_value REAL,
|
|
129
|
+
after_value REAL,
|
|
130
|
+
change_percent REAL,
|
|
131
|
+
severity TEXT NOT NULL, -- 'LOW' | 'MEDIUM' | 'HIGH'
|
|
132
|
+
detail TEXT,
|
|
133
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE analysis (
|
|
137
|
+
version_id TEXT PRIMARY KEY,
|
|
138
|
+
summary TEXT,
|
|
139
|
+
reasoning TEXT,
|
|
140
|
+
recommendation TEXT,
|
|
141
|
+
warnings_json TEXT,
|
|
142
|
+
risk TEXT,
|
|
143
|
+
provider TEXT NOT NULL, -- 'rules' | 'anthropic' | ...
|
|
144
|
+
model TEXT,
|
|
145
|
+
generated_at TEXT NOT NULL,
|
|
146
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE artifacts (
|
|
150
|
+
artifact_id TEXT PRIMARY KEY,
|
|
151
|
+
version_id TEXT NOT NULL,
|
|
152
|
+
path TEXT NOT NULL,
|
|
153
|
+
type TEXT NOT NULL,
|
|
154
|
+
size_bytes INTEGER,
|
|
155
|
+
sha256 TEXT,
|
|
156
|
+
created_at TEXT NOT NULL,
|
|
157
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE TABLE doc_flags (
|
|
161
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
162
|
+
version_id TEXT NOT NULL,
|
|
163
|
+
doc_path TEXT NOT NULL,
|
|
164
|
+
reason TEXT,
|
|
165
|
+
FOREIGN KEY (version_id) REFERENCES versions(version_id) ON DELETE CASCADE
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
CREATE TABLE events (
|
|
169
|
+
event_id TEXT PRIMARY KEY,
|
|
170
|
+
project_id TEXT NOT NULL,
|
|
171
|
+
version_id TEXT,
|
|
172
|
+
type TEXT NOT NULL,
|
|
173
|
+
source TEXT NOT NULL DEFAULT 'devmemory',
|
|
174
|
+
payload_json TEXT,
|
|
175
|
+
created_at TEXT NOT NULL,
|
|
176
|
+
FOREIGN KEY (project_id) REFERENCES projects(project_id)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX idx_versions_project ON versions(project_id, version_number);
|
|
180
|
+
CREATE INDEX idx_versions_git_commit ON versions(git_commit);
|
|
181
|
+
CREATE INDEX idx_versions_feature ON versions(feature_id);
|
|
182
|
+
CREATE INDEX idx_versions_status ON versions(status);
|
|
183
|
+
CREATE INDEX idx_changed_files_version ON changed_files(version_id);
|
|
184
|
+
CREATE INDEX idx_changed_files_path ON changed_files(path);
|
|
185
|
+
CREATE INDEX idx_metrics_version ON metrics(version_id);
|
|
186
|
+
CREATE INDEX idx_metrics_name ON metrics(name);
|
|
187
|
+
CREATE INDEX idx_tests_version ON tests(version_id);
|
|
188
|
+
CREATE INDEX idx_regressions_version ON regressions(version_id);
|
|
189
|
+
CREATE INDEX idx_checkpoints_project ON entire_checkpoints(project_id);
|
|
190
|
+
CREATE INDEX idx_checkpoints_commit ON entire_checkpoints(git_commit);
|
|
191
|
+
CREATE INDEX idx_version_checkpoints_cp ON version_checkpoints(checkpoint_id);
|
|
192
|
+
CREATE INDEX idx_events_version ON events(version_id);
|
|
193
|
+
|
|
194
|
+
-- Full-text search over normalized version metadata. Plain (not external-content)
|
|
195
|
+
-- FTS5 - the version repository keeps it in sync explicitly on write/delete.
|
|
196
|
+
CREATE VIRTUAL TABLE version_search USING fts5 (
|
|
197
|
+
version_id UNINDEXED,
|
|
198
|
+
intent,
|
|
199
|
+
feature,
|
|
200
|
+
agent,
|
|
201
|
+
model,
|
|
202
|
+
status,
|
|
203
|
+
files,
|
|
204
|
+
commit_sha,
|
|
205
|
+
checkpoint_id,
|
|
206
|
+
analysis,
|
|
207
|
+
recommendation,
|
|
208
|
+
errors,
|
|
209
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
210
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Phase 12: change-impact analysis from the Entire `graph` plugin.
|
|
2
|
+
-- One row per version. `payload` is the full GraphImpact JSON; the summary
|
|
3
|
+
-- columns are denormalized for cheap listing. Local-only telemetry - never
|
|
4
|
+
-- published to Databricks.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE graph_impacts (
|
|
7
|
+
version_id TEXT PRIMARY KEY REFERENCES versions(version_id) ON DELETE CASCADE,
|
|
8
|
+
base_commit TEXT NOT NULL DEFAULT '',
|
|
9
|
+
head_commit TEXT NOT NULL DEFAULT '',
|
|
10
|
+
entity_count INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
max_dependents INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
generated_at TEXT NOT NULL,
|
|
13
|
+
payload TEXT NOT NULL
|
|
14
|
+
);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
-- 0004_taskloop: the state-aware coding loop.
|
|
2
|
+
--
|
|
3
|
+
-- A task is a live, multi-commit unit of work with an explicit requirement list.
|
|
4
|
+
-- Every refresh_state appends one immutable row to state_snapshots, so the loop
|
|
5
|
+
-- is observable as snapshot #1 -> #2 -> #3. Nothing here stores application code
|
|
6
|
+
-- or full transcripts.
|
|
7
|
+
|
|
8
|
+
CREATE TABLE tasks (
|
|
9
|
+
id TEXT PRIMARY KEY, -- "TASK-001"
|
|
10
|
+
goal TEXT NOT NULL, -- the human task, verbatim
|
|
11
|
+
status TEXT NOT NULL DEFAULT 'IN_PROGRESS',
|
|
12
|
+
branch TEXT NOT NULL DEFAULT 'main',
|
|
13
|
+
base_commit TEXT, -- HEAD when the task was created
|
|
14
|
+
test_command TEXT,
|
|
15
|
+
push_policy TEXT NOT NULL DEFAULT 'manual',
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE requirements (
|
|
21
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
22
|
+
req_id TEXT NOT NULL, -- "R1"
|
|
23
|
+
description TEXT NOT NULL,
|
|
24
|
+
status TEXT NOT NULL DEFAULT 'INCOMPLETE',
|
|
25
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
26
|
+
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
27
|
+
evaluated_at TEXT,
|
|
28
|
+
evaluated_by TEXT NOT NULL DEFAULT '',
|
|
29
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
PRIMARY KEY (task_id, req_id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE issues (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
36
|
+
description TEXT NOT NULL,
|
|
37
|
+
kind TEXT NOT NULL DEFAULT 'agent', -- agent | test | requirement | engine
|
|
38
|
+
blocking INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
status TEXT NOT NULL DEFAULT 'OPEN', -- OPEN | RESOLVED
|
|
40
|
+
created_at TEXT NOT NULL,
|
|
41
|
+
resolved_at TEXT
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE INDEX idx_issues_task ON issues(task_id, status);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE task_test_runs (
|
|
47
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
49
|
+
command TEXT NOT NULL,
|
|
50
|
+
status TEXT NOT NULL, -- NOT_RUN | PASSED | FAILED | FAILED_TO_PARSE | ERROR
|
|
51
|
+
passed INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
skipped INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
exit_code INTEGER,
|
|
55
|
+
duration_seconds REAL,
|
|
56
|
+
output_path TEXT,
|
|
57
|
+
created_at TEXT NOT NULL
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX idx_test_runs_task ON task_test_runs(task_id, created_at);
|
|
61
|
+
|
|
62
|
+
-- The most important table: one immutable snapshot per refresh_state.
|
|
63
|
+
CREATE TABLE state_snapshots (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
66
|
+
overall_status TEXT NOT NULL,
|
|
67
|
+
commit_sha TEXT,
|
|
68
|
+
checkpoint_id TEXT,
|
|
69
|
+
state_json TEXT NOT NULL, -- the full NormalizedState
|
|
70
|
+
created_at TEXT NOT NULL
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE INDEX idx_snapshots_task ON state_snapshots(task_id, created_at);
|
|
74
|
+
|
|
75
|
+
-- Optional: link versions produced under a task (the existing versions table is
|
|
76
|
+
-- untouched; this is a side index).
|
|
77
|
+
CREATE TABLE task_commits (
|
|
78
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
79
|
+
commit_sha TEXT NOT NULL,
|
|
80
|
+
created_at TEXT NOT NULL,
|
|
81
|
+
PRIMARY KEY (task_id, commit_sha)
|
|
82
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- 0005_project_brief: the project's single source of truth.
|
|
2
|
+
--
|
|
3
|
+
-- One editable document (specs, constraints, decisions, conventions) the human
|
|
4
|
+
-- uploads or pastes. It is fed into requirement normalization and the prompt
|
|
5
|
+
-- suggestions so the loop stays anchored to what the project is actually for.
|
|
6
|
+
-- One row, id = 1.
|
|
7
|
+
|
|
8
|
+
CREATE TABLE project_brief (
|
|
9
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
10
|
+
content TEXT NOT NULL DEFAULT '',
|
|
11
|
+
updated_at TEXT NOT NULL
|
|
12
|
+
);
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Data-access objects. Every SQL statement in DevMemory lives in this module.
|
|
2
|
+
|
|
3
|
+
Repositories take a :class:`~devmemory.storage.db.Database` and expose typed
|
|
4
|
+
methods returning :mod:`devmemory.domain.models`. Services depend on repositories;
|
|
5
|
+
nothing else touches the connection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
|
|
14
|
+
from devmemory.domain.enums import AssociationMethod, FeatureStatus
|
|
15
|
+
from devmemory.domain.models import (
|
|
16
|
+
CheckpointReference,
|
|
17
|
+
CheckpointSession,
|
|
18
|
+
Feature,
|
|
19
|
+
Project,
|
|
20
|
+
TokenUsage,
|
|
21
|
+
)
|
|
22
|
+
from devmemory.storage.db import Database
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _utcnow() -> str:
|
|
26
|
+
return datetime.now(UTC).isoformat()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _dt(value: str | None) -> datetime | None:
|
|
30
|
+
if not value:
|
|
31
|
+
return None
|
|
32
|
+
try:
|
|
33
|
+
return datetime.fromisoformat(value)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _json_or_none(value: object) -> str | None:
|
|
39
|
+
return json.dumps(value) if value is not None else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_json(value: str | None) -> object:
|
|
43
|
+
if not value:
|
|
44
|
+
return None
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(value)
|
|
47
|
+
except (json.JSONDecodeError, TypeError):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --- projects -----------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProjectRepository:
|
|
55
|
+
def __init__(self, db: Database) -> None:
|
|
56
|
+
self._db = db
|
|
57
|
+
|
|
58
|
+
def get(self) -> Project | None:
|
|
59
|
+
row = self._db.query_one("SELECT * FROM projects LIMIT 1")
|
|
60
|
+
return _project_from_row(row) if row else None
|
|
61
|
+
|
|
62
|
+
def get_by_id(self, project_id: str) -> Project | None:
|
|
63
|
+
row = self._db.query_one("SELECT * FROM projects WHERE project_id = ?", (project_id,))
|
|
64
|
+
return _project_from_row(row) if row else None
|
|
65
|
+
|
|
66
|
+
def create(self, *, project_id: str, name: str, repo_path: str) -> Project:
|
|
67
|
+
now = _utcnow()
|
|
68
|
+
try:
|
|
69
|
+
with self._db.transaction() as conn:
|
|
70
|
+
conn.execute(
|
|
71
|
+
"INSERT INTO projects (project_id, name, repo_path, created_at, updated_at) "
|
|
72
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
73
|
+
(project_id, name, repo_path, now, now),
|
|
74
|
+
)
|
|
75
|
+
except sqlite3.IntegrityError as exc:
|
|
76
|
+
raise ValueError(f"project {project_id!r} already exists") from exc
|
|
77
|
+
created = self.get_by_id(project_id)
|
|
78
|
+
assert created is not None # noqa: S101 - just inserted
|
|
79
|
+
return created
|
|
80
|
+
|
|
81
|
+
def set_current_version(self, project_id: str, version_id: int | None) -> None:
|
|
82
|
+
with self._db.transaction() as conn:
|
|
83
|
+
conn.execute(
|
|
84
|
+
"UPDATE projects SET current_version_id = ?, updated_at = ? WHERE project_id = ?",
|
|
85
|
+
(version_id, _utcnow(), project_id),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def touch(self, project_id: str) -> None:
|
|
89
|
+
with self._db.transaction() as conn:
|
|
90
|
+
conn.execute(
|
|
91
|
+
"UPDATE projects SET updated_at = ? WHERE project_id = ?",
|
|
92
|
+
(_utcnow(), project_id),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _project_from_row(row: sqlite3.Row) -> Project:
|
|
97
|
+
return Project(
|
|
98
|
+
project_id=row["project_id"],
|
|
99
|
+
name=row["name"],
|
|
100
|
+
repo_path=row["repo_path"],
|
|
101
|
+
created_at=datetime.fromisoformat(row["created_at"]),
|
|
102
|
+
updated_at=datetime.fromisoformat(row["updated_at"]),
|
|
103
|
+
current_version_id=row["current_version_id"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- entire checkpoints ------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CheckpointRepository:
|
|
111
|
+
def __init__(self, db: Database) -> None:
|
|
112
|
+
self._db = db
|
|
113
|
+
|
|
114
|
+
def upsert(self, project_id: str, ref: CheckpointReference) -> None:
|
|
115
|
+
with self._db.transaction() as conn:
|
|
116
|
+
conn.execute(
|
|
117
|
+
"""
|
|
118
|
+
INSERT INTO entire_checkpoints (
|
|
119
|
+
checkpoint_id, project_id, ref, agent, model, intent, strategy,
|
|
120
|
+
created_at, git_commit, association_method, association_confidence,
|
|
121
|
+
tokens_json, imported, sessions_json
|
|
122
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
123
|
+
ON CONFLICT(checkpoint_id) DO UPDATE SET
|
|
124
|
+
ref = COALESCE(excluded.ref, entire_checkpoints.ref),
|
|
125
|
+
agent = COALESCE(excluded.agent, entire_checkpoints.agent),
|
|
126
|
+
model = COALESCE(excluded.model, entire_checkpoints.model),
|
|
127
|
+
intent = COALESCE(excluded.intent, entire_checkpoints.intent),
|
|
128
|
+
strategy = COALESCE(excluded.strategy, entire_checkpoints.strategy),
|
|
129
|
+
created_at = COALESCE(excluded.created_at, entire_checkpoints.created_at),
|
|
130
|
+
git_commit = COALESCE(excluded.git_commit, entire_checkpoints.git_commit),
|
|
131
|
+
association_method = excluded.association_method,
|
|
132
|
+
association_confidence = excluded.association_confidence,
|
|
133
|
+
tokens_json = excluded.tokens_json,
|
|
134
|
+
imported = excluded.imported,
|
|
135
|
+
sessions_json = excluded.sessions_json
|
|
136
|
+
""",
|
|
137
|
+
(
|
|
138
|
+
ref.checkpoint_id,
|
|
139
|
+
project_id,
|
|
140
|
+
ref.ref,
|
|
141
|
+
ref.agent,
|
|
142
|
+
ref.model,
|
|
143
|
+
ref.intent,
|
|
144
|
+
ref.strategy,
|
|
145
|
+
ref.created_at.isoformat() if ref.created_at else None,
|
|
146
|
+
ref.commit_sha,
|
|
147
|
+
ref.association_method.value,
|
|
148
|
+
ref.association_confidence,
|
|
149
|
+
json.dumps(ref.tokens.model_dump()),
|
|
150
|
+
1 if ref.imported else 0,
|
|
151
|
+
json.dumps([s.model_dump(mode="json") for s in ref.sessions]),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def get(self, checkpoint_id: str) -> CheckpointReference | None:
|
|
156
|
+
row = self._db.query_one(
|
|
157
|
+
"SELECT * FROM entire_checkpoints WHERE checkpoint_id = ?", (checkpoint_id,)
|
|
158
|
+
)
|
|
159
|
+
return _checkpoint_from_row(row) if row else None
|
|
160
|
+
|
|
161
|
+
def link(self, version_id: str, checkpoint_id: str, *, is_primary: bool) -> None:
|
|
162
|
+
with self._db.transaction() as conn:
|
|
163
|
+
conn.execute(
|
|
164
|
+
"INSERT OR REPLACE INTO version_checkpoints (version_id, checkpoint_id, is_primary) "
|
|
165
|
+
"VALUES (?, ?, ?)",
|
|
166
|
+
(version_id, checkpoint_id, 1 if is_primary else 0),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def for_version(self, version_id: str) -> list[tuple[CheckpointReference, bool]]:
|
|
170
|
+
rows = self._db.query(
|
|
171
|
+
"""
|
|
172
|
+
SELECT c.*, vc.is_primary FROM entire_checkpoints c
|
|
173
|
+
JOIN version_checkpoints vc ON vc.checkpoint_id = c.checkpoint_id
|
|
174
|
+
WHERE vc.version_id = ?
|
|
175
|
+
ORDER BY vc.is_primary DESC
|
|
176
|
+
""",
|
|
177
|
+
(version_id,),
|
|
178
|
+
)
|
|
179
|
+
return [(_checkpoint_from_row(r), bool(r["is_primary"])) for r in rows]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _checkpoint_from_row(row: sqlite3.Row) -> CheckpointReference:
|
|
183
|
+
tokens_raw = _load_json(row["tokens_json"])
|
|
184
|
+
sessions_raw = _load_json(row["sessions_json"])
|
|
185
|
+
method_value = row["association_method"] or AssociationMethod.NONE.value
|
|
186
|
+
return CheckpointReference(
|
|
187
|
+
checkpoint_id=row["checkpoint_id"],
|
|
188
|
+
commit_sha=row["git_commit"],
|
|
189
|
+
intent=row["intent"],
|
|
190
|
+
agent=row["agent"],
|
|
191
|
+
model=row["model"],
|
|
192
|
+
strategy=row["strategy"],
|
|
193
|
+
created_at=_dt(row["created_at"]),
|
|
194
|
+
sessions=[CheckpointSession.model_validate(s) for s in sessions_raw]
|
|
195
|
+
if isinstance(sessions_raw, list)
|
|
196
|
+
else [],
|
|
197
|
+
tokens=TokenUsage.model_validate(tokens_raw)
|
|
198
|
+
if isinstance(tokens_raw, dict)
|
|
199
|
+
else TokenUsage(),
|
|
200
|
+
association_method=AssociationMethod(method_value),
|
|
201
|
+
association_confidence=row["association_confidence"] or 0.0,
|
|
202
|
+
ref=row["ref"],
|
|
203
|
+
imported=bool(row["imported"]),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# --- features ---------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class FeatureRepository:
|
|
211
|
+
def __init__(self, db: Database) -> None:
|
|
212
|
+
self._db = db
|
|
213
|
+
|
|
214
|
+
def upsert(
|
|
215
|
+
self,
|
|
216
|
+
project_id: str,
|
|
217
|
+
name: str,
|
|
218
|
+
*,
|
|
219
|
+
status: FeatureStatus,
|
|
220
|
+
derived_from: str | None = None,
|
|
221
|
+
) -> Feature:
|
|
222
|
+
now = _utcnow()
|
|
223
|
+
with self._db.transaction() as conn:
|
|
224
|
+
conn.execute(
|
|
225
|
+
"""
|
|
226
|
+
INSERT INTO features (feature_id, project_id, name, status, derived_from,
|
|
227
|
+
created_at, updated_at)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
229
|
+
ON CONFLICT(project_id, name) DO UPDATE SET
|
|
230
|
+
status = excluded.status,
|
|
231
|
+
derived_from = COALESCE(excluded.derived_from, features.derived_from),
|
|
232
|
+
updated_at = excluded.updated_at
|
|
233
|
+
""",
|
|
234
|
+
(
|
|
235
|
+
_feature_id(project_id, name),
|
|
236
|
+
project_id,
|
|
237
|
+
name,
|
|
238
|
+
status.value,
|
|
239
|
+
derived_from,
|
|
240
|
+
now,
|
|
241
|
+
now,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
feature = self.get_by_name(project_id, name)
|
|
245
|
+
assert feature is not None # noqa: S101
|
|
246
|
+
return feature
|
|
247
|
+
|
|
248
|
+
def get_by_name(self, project_id: str, name: str) -> Feature | None:
|
|
249
|
+
row = self._db.query_one(
|
|
250
|
+
"SELECT * FROM features WHERE project_id = ? AND name = ?", (project_id, name)
|
|
251
|
+
)
|
|
252
|
+
return _feature_from_row(row) if row else None
|
|
253
|
+
|
|
254
|
+
def get(self, feature_id: str) -> Feature | None:
|
|
255
|
+
row = self._db.query_one("SELECT * FROM features WHERE feature_id = ?", (feature_id,))
|
|
256
|
+
return _feature_from_row(row) if row else None
|
|
257
|
+
|
|
258
|
+
def list_all(self, project_id: str) -> list[Feature]:
|
|
259
|
+
rows = self._db.query(
|
|
260
|
+
"SELECT * FROM features WHERE project_id = ? ORDER BY name", (project_id,)
|
|
261
|
+
)
|
|
262
|
+
return [_feature_from_row(r) for r in rows]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _feature_id(project_id: str, name: str) -> str:
|
|
266
|
+
slug = "".join(c if c.isalnum() else "-" for c in name.lower()).strip("-") or "feature"
|
|
267
|
+
return f"{project_id}:{slug}"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _feature_from_row(row: sqlite3.Row) -> Feature:
|
|
271
|
+
return Feature(
|
|
272
|
+
feature_id=row["feature_id"],
|
|
273
|
+
project_id=row["project_id"],
|
|
274
|
+
name=row["name"],
|
|
275
|
+
status=FeatureStatus(row["status"]),
|
|
276
|
+
derived_from=row["derived_from"],
|
|
277
|
+
created_at=_dt(row["created_at"]),
|
|
278
|
+
updated_at=_dt(row["updated_at"]),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
__all__ = [
|
|
283
|
+
"CheckpointRepository",
|
|
284
|
+
"FeatureRepository",
|
|
285
|
+
"ProjectRepository",
|
|
286
|
+
]
|