delimit-cli 4.5.1 → 4.5.3
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.
- package/CHANGELOG.md +87 -0
- package/README.md +15 -5
- package/bin/delimit-cli.js +109 -24
- package/gateway/ai/content_engine.py +3 -4
- package/gateway/ai/inbox_classifier.py +215 -0
- package/gateway/ai/integrations/opensage_wrapper.py +4 -1
- package/gateway/ai/ledger_manager.py +218 -38
- package/gateway/ai/license.py +26 -0
- package/gateway/ai/notify.py +68 -3
- package/gateway/ai/reddit_proxy.py +93 -15
- package/gateway/ai/reddit_scanner.py +36 -18
- package/gateway/ai/remote_resolve.py +422 -0
- package/gateway/ai/server.py +301 -117
- package/gateway/ai/social_capability/__init__.py +6 -0
- package/gateway/ai/social_capability/capability_validator.py +367 -0
- package/gateway/ai/social_capability/current_capabilities.yaml +95 -0
- package/gateway/ai/social_capability/fit_floor.py +360 -0
- package/gateway/ai/social_queue.py +307 -0
- package/gateway/ai/supabase_sync.py +14 -2
- package/gateway/ai/swarm.py +29 -11
- package/gateway/ai/tui.py +6 -2
- package/gateway/ai/vendor_news/__init__.py +14 -0
- package/gateway/ai/vendor_news/drafter.py +562 -0
- package/gateway/ai/vendor_news/sensor.py +509 -0
- package/gateway/ai/vendor_news/watchlist.yaml +71 -0
- package/gateway/ai/x_ranker.py +417 -0
- package/lib/attest-mcp.js +487 -0
- package/lib/attest-telemetry.js +48 -0
- package/lib/delimit-home.js +35 -0
- package/lib/delimit-template.js +14 -0
- package/package.json +25 -3
- package/scripts/postinstall.js +89 -40
- package/adapters/codex-security.js +0 -64
- package/adapters/codex-skill.js +0 -78
- package/gateway/ai/content_grounding/__init__.py +0 -98
- package/gateway/ai/content_grounding/build.py +0 -350
- package/gateway/ai/content_grounding/consume.py +0 -280
- package/gateway/ai/content_grounding/features.py +0 -218
- package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +0 -9
- package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +0 -9
- package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +0 -17
- package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +0 -18
- package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +0 -18
- package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +0 -23
- package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +0 -16
- package/gateway/ai/content_grounding/schemas/claim.schema.json +0 -40
- package/gateway/ai/content_grounding/schemas/event.schema.json +0 -23
- package/gateway/ai/content_grounding/schemas.py +0 -276
- package/gateway/ai/content_grounding/telemetry.py +0 -221
- package/gateway/ai/inbox_drafts/__init__.py +0 -61
- package/gateway/ai/inbox_drafts/registry.py +0 -412
- package/gateway/ai/inbox_drafts/schema.py +0 -374
- package/gateway/ai/inbox_executor.py +0 -565
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
"""SQLite draft registry — LED-1129 Phase 1 PR-2.
|
|
2
|
-
|
|
3
|
-
Two-table shape (per the deliberation):
|
|
4
|
-
|
|
5
|
-
drafts — durable state machine. One row per draft_id.
|
|
6
|
-
Columns mirror SignedDraft + status + lifecycle timestamps.
|
|
7
|
-
attempts — execution history. One row per execute attempt against a
|
|
8
|
-
draft_id. Forensics for failures + replay-detection.
|
|
9
|
-
|
|
10
|
-
State transitions are enforced atomically via SQLite transactions. The
|
|
11
|
-
schema layer (schema.py) owns the cryptography; this layer owns the
|
|
12
|
-
durable state. The executor (Phase 2) consumes from this layer.
|
|
13
|
-
|
|
14
|
-
Crash semantics: a row at status='executing' after a process restart
|
|
15
|
-
surfaces for human reconciliation — we do NOT auto-retry. That's the
|
|
16
|
-
at-most-once contract.
|
|
17
|
-
|
|
18
|
-
Concurrency: SQLite WAL mode + UPDATE ... WHERE status=? gives us atomic
|
|
19
|
-
state transitions without explicit file locking. Multiple readers OK;
|
|
20
|
-
the executor takes a row by transitioning approved→executing in a single
|
|
21
|
-
UPDATE that returns rowcount.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import json
|
|
27
|
-
import sqlite3
|
|
28
|
-
import time
|
|
29
|
-
from contextlib import contextmanager
|
|
30
|
-
from dataclasses import dataclass
|
|
31
|
-
from pathlib import Path
|
|
32
|
-
from typing import Any, Dict, Iterator, List, Optional
|
|
33
|
-
|
|
34
|
-
from ai.inbox_drafts.schema import (
|
|
35
|
-
DEFAULT_TTL_SECONDS,
|
|
36
|
-
DraftStatus,
|
|
37
|
-
SignedDraft,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
DEFAULT_DB_PATH = Path.home() / ".delimit" / "drafts.db"
|
|
41
|
-
|
|
42
|
-
# Schema version for the SQLite tables themselves. Distinct from the
|
|
43
|
-
# draft schema_version (the JSON contract) — this one tracks DB migrations.
|
|
44
|
-
DB_SCHEMA_VERSION = 1
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _resolve_db_path(db_path: Optional[Path]) -> Path:
|
|
48
|
-
"""Resolve db_path arg, reading module default at call time.
|
|
49
|
-
|
|
50
|
-
Reading at call time (rather than as a default-arg) lets tests
|
|
51
|
-
monkeypatch `ai.inbox_drafts.registry.DEFAULT_DB_PATH` and have the
|
|
52
|
-
change propagate. With default-arg capture, the value is bound at
|
|
53
|
-
function-definition time and monkeypatching is invisible.
|
|
54
|
-
"""
|
|
55
|
-
if db_path is not None:
|
|
56
|
-
return db_path
|
|
57
|
-
import ai.inbox_drafts.registry as _self
|
|
58
|
-
return _self.DEFAULT_DB_PATH
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# ── Migrations ────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
_MIGRATIONS = [
|
|
65
|
-
# v1: initial schema
|
|
66
|
-
"""
|
|
67
|
-
CREATE TABLE IF NOT EXISTS drafts (
|
|
68
|
-
draft_id TEXT PRIMARY KEY,
|
|
69
|
-
draft_kind TEXT NOT NULL,
|
|
70
|
-
target_json TEXT NOT NULL,
|
|
71
|
-
payload_json TEXT NOT NULL,
|
|
72
|
-
issued_at INTEGER NOT NULL,
|
|
73
|
-
key_version INTEGER NOT NULL,
|
|
74
|
-
schema_version TEXT NOT NULL,
|
|
75
|
-
content_hash TEXT NOT NULL,
|
|
76
|
-
signature TEXT NOT NULL,
|
|
77
|
-
status TEXT NOT NULL,
|
|
78
|
-
led_ref TEXT,
|
|
79
|
-
approval_subject TEXT,
|
|
80
|
-
executed_url TEXT,
|
|
81
|
-
last_error TEXT,
|
|
82
|
-
created_at INTEGER NOT NULL,
|
|
83
|
-
updated_at INTEGER NOT NULL,
|
|
84
|
-
completed_at INTEGER
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
CREATE INDEX IF NOT EXISTS idx_drafts_status ON drafts(status);
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_drafts_issued_at ON drafts(issued_at);
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_drafts_led_ref ON drafts(led_ref);
|
|
90
|
-
|
|
91
|
-
CREATE TABLE IF NOT EXISTS attempts (
|
|
92
|
-
attempt_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
-
draft_id TEXT NOT NULL,
|
|
94
|
-
kind TEXT NOT NULL, -- "verify" | "execute"
|
|
95
|
-
outcome TEXT NOT NULL, -- "ok" | "failed" | "skipped"
|
|
96
|
-
reason TEXT,
|
|
97
|
-
executed_url TEXT,
|
|
98
|
-
attempted_at INTEGER NOT NULL,
|
|
99
|
-
FOREIGN KEY (draft_id) REFERENCES drafts(draft_id)
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
CREATE INDEX IF NOT EXISTS idx_attempts_draft_id ON attempts(draft_id);
|
|
103
|
-
|
|
104
|
-
CREATE TABLE IF NOT EXISTS db_meta (
|
|
105
|
-
key TEXT PRIMARY KEY,
|
|
106
|
-
value TEXT NOT NULL
|
|
107
|
-
);
|
|
108
|
-
""",
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _open(db_path: Path) -> sqlite3.Connection:
|
|
113
|
-
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
-
conn = sqlite3.connect(str(db_path), isolation_level=None, timeout=10.0)
|
|
115
|
-
conn.row_factory = sqlite3.Row
|
|
116
|
-
# WAL gives us cleaner concurrent-reader semantics than the default
|
|
117
|
-
# rollback journal. busy_timeout makes blocked writers wait briefly
|
|
118
|
-
# instead of immediately raising — keeps the executor's poll loop
|
|
119
|
-
# robust against transient daemon writes.
|
|
120
|
-
conn.execute("PRAGMA journal_mode=WAL")
|
|
121
|
-
conn.execute("PRAGMA busy_timeout=5000")
|
|
122
|
-
conn.execute("PRAGMA foreign_keys=ON")
|
|
123
|
-
return conn
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def migrate(db_path: Optional[Path] = None) -> int:
|
|
127
|
-
"""Apply pending migrations. Returns the resulting DB schema version.
|
|
128
|
-
|
|
129
|
-
Idempotent — running again on an up-to-date DB is a no-op.
|
|
130
|
-
"""
|
|
131
|
-
db_path = _resolve_db_path(db_path)
|
|
132
|
-
conn = _open(db_path)
|
|
133
|
-
try:
|
|
134
|
-
# Bootstrap meta table so we can read the version.
|
|
135
|
-
conn.executescript(
|
|
136
|
-
"CREATE TABLE IF NOT EXISTS db_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);"
|
|
137
|
-
)
|
|
138
|
-
cur = conn.execute("SELECT value FROM db_meta WHERE key = 'db_schema_version'")
|
|
139
|
-
row = cur.fetchone()
|
|
140
|
-
current = int(row["value"]) if row else 0
|
|
141
|
-
for i, sql in enumerate(_MIGRATIONS, start=1):
|
|
142
|
-
if i > current:
|
|
143
|
-
conn.executescript(sql)
|
|
144
|
-
conn.execute(
|
|
145
|
-
"INSERT OR REPLACE INTO db_meta (key, value) VALUES (?, ?)",
|
|
146
|
-
("db_schema_version", str(i)),
|
|
147
|
-
)
|
|
148
|
-
return DB_SCHEMA_VERSION
|
|
149
|
-
finally:
|
|
150
|
-
conn.close()
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# ── DAO ───────────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
@dataclass
|
|
157
|
-
class DraftRow:
|
|
158
|
-
draft_id: str
|
|
159
|
-
draft_kind: str
|
|
160
|
-
target: Dict[str, Any]
|
|
161
|
-
payload: Any
|
|
162
|
-
issued_at: int
|
|
163
|
-
key_version: int
|
|
164
|
-
schema_version: str
|
|
165
|
-
content_hash: str
|
|
166
|
-
signature: str
|
|
167
|
-
status: str
|
|
168
|
-
led_ref: Optional[str]
|
|
169
|
-
approval_subject: Optional[str]
|
|
170
|
-
executed_url: Optional[str]
|
|
171
|
-
last_error: Optional[str]
|
|
172
|
-
created_at: int
|
|
173
|
-
updated_at: int
|
|
174
|
-
completed_at: Optional[int]
|
|
175
|
-
|
|
176
|
-
@classmethod
|
|
177
|
-
def from_sqlite_row(cls, row: sqlite3.Row) -> "DraftRow":
|
|
178
|
-
return cls(
|
|
179
|
-
draft_id=row["draft_id"],
|
|
180
|
-
draft_kind=row["draft_kind"],
|
|
181
|
-
target=json.loads(row["target_json"]),
|
|
182
|
-
payload=json.loads(row["payload_json"]),
|
|
183
|
-
issued_at=row["issued_at"],
|
|
184
|
-
key_version=row["key_version"],
|
|
185
|
-
schema_version=row["schema_version"],
|
|
186
|
-
content_hash=row["content_hash"],
|
|
187
|
-
signature=row["signature"],
|
|
188
|
-
status=row["status"],
|
|
189
|
-
led_ref=row["led_ref"],
|
|
190
|
-
approval_subject=row["approval_subject"],
|
|
191
|
-
executed_url=row["executed_url"],
|
|
192
|
-
last_error=row["last_error"],
|
|
193
|
-
created_at=row["created_at"],
|
|
194
|
-
updated_at=row["updated_at"],
|
|
195
|
-
completed_at=row["completed_at"],
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
def to_signed_dict(self) -> Dict[str, Any]:
|
|
199
|
-
"""Return only the fields that are part of the HMAC scope.
|
|
200
|
-
|
|
201
|
-
Used by the executor to re-verify the signature before acting.
|
|
202
|
-
"""
|
|
203
|
-
return {
|
|
204
|
-
"draft_id": self.draft_id,
|
|
205
|
-
"draft_kind": self.draft_kind,
|
|
206
|
-
"target": self.target,
|
|
207
|
-
"payload": self.payload,
|
|
208
|
-
"issued_at": self.issued_at,
|
|
209
|
-
"key_version": self.key_version,
|
|
210
|
-
"schema_version": self.schema_version,
|
|
211
|
-
"content_hash": self.content_hash,
|
|
212
|
-
"signature": self.signature,
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
@contextmanager
|
|
217
|
-
def connection(db_path: Optional[Path] = None) -> Iterator[sqlite3.Connection]:
|
|
218
|
-
"""Context-managed connection. Ensures migrations are applied first."""
|
|
219
|
-
db_path = _resolve_db_path(db_path)
|
|
220
|
-
migrate(db_path)
|
|
221
|
-
conn = _open(db_path)
|
|
222
|
-
try:
|
|
223
|
-
yield conn
|
|
224
|
-
finally:
|
|
225
|
-
conn.close()
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def insert_draft(
|
|
229
|
-
signed: SignedDraft,
|
|
230
|
-
*,
|
|
231
|
-
led_ref: Optional[str] = None,
|
|
232
|
-
db_path: Optional[Path] = None,
|
|
233
|
-
) -> None:
|
|
234
|
-
"""Insert a freshly-signed draft in PENDING state.
|
|
235
|
-
|
|
236
|
-
Raises sqlite3.IntegrityError if the draft_id already exists — by
|
|
237
|
-
construction (ULID) this only happens on real ID collision (~impossible)
|
|
238
|
-
or replay attempt with the same id, both of which we want to refuse.
|
|
239
|
-
"""
|
|
240
|
-
now = int(time.time())
|
|
241
|
-
with connection(db_path) as conn:
|
|
242
|
-
conn.execute(
|
|
243
|
-
"""
|
|
244
|
-
INSERT INTO drafts (
|
|
245
|
-
draft_id, draft_kind, target_json, payload_json, issued_at,
|
|
246
|
-
key_version, schema_version, content_hash, signature, status,
|
|
247
|
-
led_ref, created_at, updated_at
|
|
248
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
249
|
-
""",
|
|
250
|
-
(
|
|
251
|
-
signed.draft_id,
|
|
252
|
-
signed.draft_kind,
|
|
253
|
-
json.dumps(signed.target, sort_keys=True),
|
|
254
|
-
json.dumps(signed.payload, sort_keys=True),
|
|
255
|
-
signed.issued_at,
|
|
256
|
-
signed.key_version,
|
|
257
|
-
signed.schema_version,
|
|
258
|
-
signed.content_hash,
|
|
259
|
-
signed.signature,
|
|
260
|
-
DraftStatus.PENDING.value,
|
|
261
|
-
led_ref,
|
|
262
|
-
now,
|
|
263
|
-
now,
|
|
264
|
-
),
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def get_draft(draft_id: str, db_path: Optional[Path] = None) -> Optional[DraftRow]:
|
|
269
|
-
with connection(db_path) as conn:
|
|
270
|
-
row = conn.execute(
|
|
271
|
-
"SELECT * FROM drafts WHERE draft_id = ?",
|
|
272
|
-
(draft_id,),
|
|
273
|
-
).fetchone()
|
|
274
|
-
return DraftRow.from_sqlite_row(row) if row else None
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def find_draft_by_led_ref(led_ref: str, db_path: Optional[Path] = None) -> List[DraftRow]:
|
|
278
|
-
"""Return drafts associated with a given LED reference.
|
|
279
|
-
|
|
280
|
-
Used by the executor when matching founder Ship-it replies whose
|
|
281
|
-
subject line carries an [LED-XXXX] tag.
|
|
282
|
-
"""
|
|
283
|
-
with connection(db_path) as conn:
|
|
284
|
-
rows = conn.execute(
|
|
285
|
-
"SELECT * FROM drafts WHERE led_ref = ? ORDER BY created_at DESC",
|
|
286
|
-
(led_ref,),
|
|
287
|
-
).fetchall()
|
|
288
|
-
return [DraftRow.from_sqlite_row(r) for r in rows]
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def transition(
|
|
292
|
-
draft_id: str,
|
|
293
|
-
*,
|
|
294
|
-
expected: str,
|
|
295
|
-
new: str,
|
|
296
|
-
db_path: Optional[Path] = None,
|
|
297
|
-
approval_subject: Optional[str] = None,
|
|
298
|
-
executed_url: Optional[str] = None,
|
|
299
|
-
last_error: Optional[str] = None,
|
|
300
|
-
completed: bool = False,
|
|
301
|
-
) -> bool:
|
|
302
|
-
"""Atomically move a draft from `expected` → `new`.
|
|
303
|
-
|
|
304
|
-
Returns True iff the transition occurred (the row was in `expected`
|
|
305
|
-
state at the moment of the UPDATE). Returns False otherwise — the
|
|
306
|
-
caller did not win the race or the row is in a different state.
|
|
307
|
-
|
|
308
|
-
This is the at-most-once primitive: the executor calls
|
|
309
|
-
transition(approved → executing) before any side effect; the
|
|
310
|
-
rowcount tells it whether it owns the action.
|
|
311
|
-
"""
|
|
312
|
-
now = int(time.time())
|
|
313
|
-
with connection(db_path) as conn:
|
|
314
|
-
cur = conn.execute(
|
|
315
|
-
"""
|
|
316
|
-
UPDATE drafts SET
|
|
317
|
-
status = ?,
|
|
318
|
-
approval_subject = COALESCE(?, approval_subject),
|
|
319
|
-
executed_url = COALESCE(?, executed_url),
|
|
320
|
-
last_error = COALESCE(?, last_error),
|
|
321
|
-
completed_at = CASE WHEN ? = 1 THEN ? ELSE completed_at END,
|
|
322
|
-
updated_at = ?
|
|
323
|
-
WHERE draft_id = ? AND status = ?
|
|
324
|
-
""",
|
|
325
|
-
(
|
|
326
|
-
new,
|
|
327
|
-
approval_subject,
|
|
328
|
-
executed_url,
|
|
329
|
-
last_error,
|
|
330
|
-
1 if completed else 0,
|
|
331
|
-
now,
|
|
332
|
-
now,
|
|
333
|
-
draft_id,
|
|
334
|
-
expected,
|
|
335
|
-
),
|
|
336
|
-
)
|
|
337
|
-
return cur.rowcount == 1
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def expire_pending(
|
|
341
|
-
db_path: Optional[Path] = None,
|
|
342
|
-
ttl_seconds: int = DEFAULT_TTL_SECONDS,
|
|
343
|
-
) -> int:
|
|
344
|
-
"""Mark pending drafts older than TTL as EXPIRED.
|
|
345
|
-
|
|
346
|
-
Returns the count expired. Idempotent.
|
|
347
|
-
"""
|
|
348
|
-
now = int(time.time())
|
|
349
|
-
cutoff = now - ttl_seconds
|
|
350
|
-
with connection(db_path) as conn:
|
|
351
|
-
cur = conn.execute(
|
|
352
|
-
"""
|
|
353
|
-
UPDATE drafts SET status = ?, updated_at = ?
|
|
354
|
-
WHERE status = ? AND issued_at < ?
|
|
355
|
-
""",
|
|
356
|
-
(DraftStatus.EXPIRED.value, now, DraftStatus.PENDING.value, cutoff),
|
|
357
|
-
)
|
|
358
|
-
return cur.rowcount
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def record_attempt(
|
|
362
|
-
draft_id: str,
|
|
363
|
-
*,
|
|
364
|
-
kind: str,
|
|
365
|
-
outcome: str,
|
|
366
|
-
reason: Optional[str] = None,
|
|
367
|
-
executed_url: Optional[str] = None,
|
|
368
|
-
db_path: Optional[Path] = None,
|
|
369
|
-
) -> int:
|
|
370
|
-
"""Append an attempt row. Returns the new attempt_id.
|
|
371
|
-
|
|
372
|
-
`kind`: "verify" | "execute"
|
|
373
|
-
`outcome`: "ok" | "failed" | "skipped"
|
|
374
|
-
"""
|
|
375
|
-
now = int(time.time())
|
|
376
|
-
with connection(db_path) as conn:
|
|
377
|
-
cur = conn.execute(
|
|
378
|
-
"""
|
|
379
|
-
INSERT INTO attempts (draft_id, kind, outcome, reason, executed_url, attempted_at)
|
|
380
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
381
|
-
""",
|
|
382
|
-
(draft_id, kind, outcome, reason, executed_url, now),
|
|
383
|
-
)
|
|
384
|
-
return cur.lastrowid
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def list_attempts(draft_id: str, db_path: Optional[Path] = None) -> List[Dict[str, Any]]:
|
|
388
|
-
with connection(db_path) as conn:
|
|
389
|
-
rows = conn.execute(
|
|
390
|
-
"SELECT * FROM attempts WHERE draft_id = ? ORDER BY attempt_id ASC",
|
|
391
|
-
(draft_id,),
|
|
392
|
-
).fetchall()
|
|
393
|
-
return [dict(r) for r in rows]
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def list_drafts(
|
|
397
|
-
status: Optional[str] = None,
|
|
398
|
-
limit: int = 50,
|
|
399
|
-
db_path: Optional[Path] = None,
|
|
400
|
-
) -> List[DraftRow]:
|
|
401
|
-
with connection(db_path) as conn:
|
|
402
|
-
if status:
|
|
403
|
-
rows = conn.execute(
|
|
404
|
-
"SELECT * FROM drafts WHERE status = ? ORDER BY created_at DESC LIMIT ?",
|
|
405
|
-
(status, limit),
|
|
406
|
-
).fetchall()
|
|
407
|
-
else:
|
|
408
|
-
rows = conn.execute(
|
|
409
|
-
"SELECT * FROM drafts ORDER BY created_at DESC LIMIT ?",
|
|
410
|
-
(limit,),
|
|
411
|
-
).fetchall()
|
|
412
|
-
return [DraftRow.from_sqlite_row(r) for r in rows]
|