patchrail 0.1.0__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 (47) hide show
  1. patchrail/__init__.py +7 -0
  2. patchrail/__main__.py +7 -0
  3. patchrail/ci/__init__.py +7 -0
  4. patchrail/ci/classify.py +888 -0
  5. patchrail/cli.py +8566 -0
  6. patchrail/funded_issues/__init__.py +138 -0
  7. patchrail/funded_issues/algora_board.py +240 -0
  8. patchrail/funded_issues/blocklist.py +112 -0
  9. patchrail/funded_issues/discovery.py +4091 -0
  10. patchrail/funded_issues/importers.py +316 -0
  11. patchrail/funded_issues/source_noise.py +349 -0
  12. patchrail/funded_issues/store.py +459 -0
  13. patchrail/queue/__init__.py +75 -0
  14. patchrail/queue/server.py +273 -0
  15. patchrail/queue/status.py +756 -0
  16. patchrail/queue/store.py +600 -0
  17. patchrail/reviewer_quick_check.py +650 -0
  18. patchrail/schemas/__init__.py +1 -0
  19. patchrail/schemas/application-dossier.v1.schema.json +305 -0
  20. patchrail/schemas/ci-benchmark.v1.schema.json +174 -0
  21. patchrail/schemas/ci-fixture-check.v1.schema.json +122 -0
  22. patchrail/schemas/ci-pilot-metrics.v1.schema.json +164 -0
  23. patchrail/schemas/ci-pilot-summary.v1.schema.json +146 -0
  24. patchrail/schemas/ci-result.v1.schema.json +133 -0
  25. patchrail/schemas/funded-issues-client-report.v1.schema.json +524 -0
  26. patchrail/schemas/funded-issues-recheck-queue.v1.schema.json +333 -0
  27. patchrail/schemas/funded-issues-recheck-summary.v1.schema.json +136 -0
  28. patchrail/schemas/funded-issues-report.v1.schema.json +836 -0
  29. patchrail/schemas/funded-issues-shortlist.v1.schema.json +953 -0
  30. patchrail/schemas/funded-issues-store-status.v1.schema.json +96 -0
  31. patchrail/schemas/funded-issues-store.v1.schema.json +117 -0
  32. patchrail/schemas/queue-audit-event.v1.schema.json +44 -0
  33. patchrail/schemas/queue-audit-summary.v1.schema.json +169 -0
  34. patchrail/schemas/queue-gate-report.v1.schema.json +158 -0
  35. patchrail/schemas/queue-policy-resolution.v1.schema.json +188 -0
  36. patchrail/schemas/queue-policy-scan.v1.schema.json +175 -0
  37. patchrail/schemas/queue-proposal.v1.schema.json +61 -0
  38. patchrail/schemas/queue-review.v1.schema.json +218 -0
  39. patchrail/schemas/queue-status.v1.schema.json +179 -0
  40. patchrail/schemas/queue-work-item.v1.schema.json +64 -0
  41. patchrail/schemas/reviewer-quick-check-artifacts.v1.schema.json +104 -0
  42. patchrail/web_metrics.py +649 -0
  43. patchrail-0.1.0.dist-info/METADATA +279 -0
  44. patchrail-0.1.0.dist-info/RECORD +47 -0
  45. patchrail-0.1.0.dist-info/WHEEL +4 -0
  46. patchrail-0.1.0.dist-info/entry_points.txt +2 -0
  47. patchrail-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,600 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ from dataclasses import dataclass
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from uuid import uuid4
10
+
11
+ SCHEMA_VERSION = "patchrail.queue.v1"
12
+ DEFAULT_QUEUE_PATH = Path(".patchrail") / "queue.sqlite"
13
+
14
+ VALID_APPROVAL_STATES = {"pending", "approved", "rejected"}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class QueueItem:
19
+ id: str
20
+ kind: str
21
+ title: str
22
+ source: str
23
+ status: str
24
+ approval_state: str
25
+ write_actions_allowed: bool
26
+ created_at: str
27
+ updated_at: str
28
+ payload: dict[str, Any]
29
+ decision_note: str | None = None
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return {
33
+ "id": self.id,
34
+ "kind": self.kind,
35
+ "title": self.title,
36
+ "source": self.source,
37
+ "status": self.status,
38
+ "approval_state": self.approval_state,
39
+ "write_actions_allowed": self.write_actions_allowed,
40
+ "created_at": self.created_at,
41
+ "updated_at": self.updated_at,
42
+ "payload": self.payload,
43
+ "decision_note": self.decision_note,
44
+ }
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class AuditEvent:
49
+ id: int
50
+ ts: str
51
+ event_type: str
52
+ work_item_id: str | None
53
+ payload: dict[str, Any]
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ return {
57
+ "id": self.id,
58
+ "ts": self.ts,
59
+ "event_type": self.event_type,
60
+ "work_item_id": self.work_item_id,
61
+ "payload": self.payload,
62
+ }
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class ProposalRecord:
67
+ id: str
68
+ work_item_id: str
69
+ title: str
70
+ summary: str
71
+ patch_plan: str
72
+ risk_level: str
73
+ approval_state: str
74
+ created_at: str
75
+ updated_at: str
76
+ decision_note: str | None = None
77
+
78
+ def to_dict(self) -> dict[str, Any]:
79
+ return {
80
+ "id": self.id,
81
+ "work_item_id": self.work_item_id,
82
+ "title": self.title,
83
+ "summary": self.summary,
84
+ "patch_plan": self.patch_plan,
85
+ "risk_level": self.risk_level,
86
+ "approval_state": self.approval_state,
87
+ "created_at": self.created_at,
88
+ "updated_at": self.updated_at,
89
+ "decision_note": self.decision_note,
90
+ }
91
+
92
+
93
+ def _now() -> str:
94
+ return datetime.now(tz=UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
95
+
96
+
97
+ def _connect(db_path: Path) -> sqlite3.Connection:
98
+ db_path.parent.mkdir(parents=True, exist_ok=True)
99
+ conn = sqlite3.connect(db_path)
100
+ conn.row_factory = sqlite3.Row
101
+ return conn
102
+
103
+
104
+ def init_queue(db_path: Path = DEFAULT_QUEUE_PATH) -> dict[str, Any]:
105
+ with _connect(db_path) as conn:
106
+ conn.execute("PRAGMA journal_mode=WAL")
107
+ conn.execute(
108
+ """
109
+ CREATE TABLE IF NOT EXISTS metadata (
110
+ key TEXT PRIMARY KEY,
111
+ value TEXT NOT NULL
112
+ )
113
+ """
114
+ )
115
+ conn.execute(
116
+ """
117
+ CREATE TABLE IF NOT EXISTS work_items (
118
+ id TEXT PRIMARY KEY,
119
+ kind TEXT NOT NULL,
120
+ title TEXT NOT NULL,
121
+ source TEXT NOT NULL,
122
+ status TEXT NOT NULL,
123
+ approval_state TEXT NOT NULL,
124
+ write_actions_allowed INTEGER NOT NULL DEFAULT 0,
125
+ created_at TEXT NOT NULL,
126
+ updated_at TEXT NOT NULL,
127
+ payload_json TEXT NOT NULL,
128
+ decision_note TEXT
129
+ )
130
+ """
131
+ )
132
+ conn.execute(
133
+ """
134
+ CREATE TABLE IF NOT EXISTS audit_events (
135
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
136
+ ts TEXT NOT NULL,
137
+ event_type TEXT NOT NULL,
138
+ work_item_id TEXT,
139
+ payload_json TEXT NOT NULL
140
+ )
141
+ """
142
+ )
143
+ conn.execute(
144
+ """
145
+ CREATE TABLE IF NOT EXISTS proposals (
146
+ id TEXT PRIMARY KEY,
147
+ work_item_id TEXT NOT NULL,
148
+ title TEXT NOT NULL,
149
+ summary TEXT NOT NULL,
150
+ patch_plan TEXT NOT NULL,
151
+ risk_level TEXT NOT NULL,
152
+ approval_state TEXT NOT NULL,
153
+ created_at TEXT NOT NULL,
154
+ updated_at TEXT NOT NULL,
155
+ decision_note TEXT,
156
+ FOREIGN KEY(work_item_id) REFERENCES work_items(id)
157
+ )
158
+ """
159
+ )
160
+ conn.execute(
161
+ """
162
+ INSERT INTO metadata(key, value)
163
+ VALUES('schema_version', ?)
164
+ ON CONFLICT(key) DO UPDATE SET value=excluded.value
165
+ """,
166
+ (SCHEMA_VERSION,),
167
+ )
168
+ return {
169
+ "schema_version": SCHEMA_VERSION,
170
+ "db_path": str(db_path),
171
+ "local_first": True,
172
+ "write_actions_allowed_by_default": False,
173
+ }
174
+
175
+
176
+ def _row_to_item(row: sqlite3.Row) -> QueueItem:
177
+ return QueueItem(
178
+ id=str(row["id"]),
179
+ kind=str(row["kind"]),
180
+ title=str(row["title"]),
181
+ source=str(row["source"]),
182
+ status=str(row["status"]),
183
+ approval_state=str(row["approval_state"]),
184
+ write_actions_allowed=bool(row["write_actions_allowed"]),
185
+ created_at=str(row["created_at"]),
186
+ updated_at=str(row["updated_at"]),
187
+ payload=json.loads(str(row["payload_json"])),
188
+ decision_note=row["decision_note"],
189
+ )
190
+
191
+
192
+ def _row_to_audit_event(row: sqlite3.Row) -> AuditEvent:
193
+ return AuditEvent(
194
+ id=int(row["id"]),
195
+ ts=str(row["ts"]),
196
+ event_type=str(row["event_type"]),
197
+ work_item_id=row["work_item_id"],
198
+ payload=json.loads(str(row["payload_json"])),
199
+ )
200
+
201
+
202
+ def _row_to_proposal(row: sqlite3.Row) -> ProposalRecord:
203
+ return ProposalRecord(
204
+ id=str(row["id"]),
205
+ work_item_id=str(row["work_item_id"]),
206
+ title=str(row["title"]),
207
+ summary=str(row["summary"]),
208
+ patch_plan=str(row["patch_plan"]),
209
+ risk_level=str(row["risk_level"]),
210
+ approval_state=str(row["approval_state"]),
211
+ created_at=str(row["created_at"]),
212
+ updated_at=str(row["updated_at"]),
213
+ decision_note=row["decision_note"],
214
+ )
215
+
216
+
217
+ def _write_audit_event(
218
+ conn: sqlite3.Connection,
219
+ *,
220
+ event_type: str,
221
+ work_item_id: str | None,
222
+ payload: dict[str, Any],
223
+ ) -> None:
224
+ conn.execute(
225
+ """
226
+ INSERT INTO audit_events(ts, event_type, work_item_id, payload_json)
227
+ VALUES(?, ?, ?, ?)
228
+ """,
229
+ (_now(), event_type, work_item_id, json.dumps(payload, sort_keys=True)),
230
+ )
231
+
232
+
233
+ def add_work_item(
234
+ *,
235
+ db_path: Path = DEFAULT_QUEUE_PATH,
236
+ kind: str,
237
+ title: str,
238
+ source: str = "manual",
239
+ payload: dict[str, Any] | None = None,
240
+ ) -> QueueItem:
241
+ init_queue(db_path)
242
+ item_id = f"prq_{uuid4().hex[:12]}"
243
+ ts = _now()
244
+ safe_payload = payload or {}
245
+ with _connect(db_path) as conn:
246
+ conn.execute(
247
+ """
248
+ INSERT INTO work_items(
249
+ id, kind, title, source, status, approval_state,
250
+ write_actions_allowed, created_at, updated_at, payload_json
251
+ )
252
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
+ """,
254
+ (
255
+ item_id,
256
+ kind,
257
+ title,
258
+ source,
259
+ "open",
260
+ "pending",
261
+ 0,
262
+ ts,
263
+ ts,
264
+ json.dumps(safe_payload, sort_keys=True),
265
+ ),
266
+ )
267
+ _write_audit_event(
268
+ conn,
269
+ event_type="work_item_added",
270
+ work_item_id=item_id,
271
+ payload={"kind": kind, "source": source, "approval_state": "pending"},
272
+ )
273
+ return show_work_item(db_path=db_path, item_id=item_id)
274
+
275
+
276
+ def list_work_items(
277
+ *,
278
+ db_path: Path = DEFAULT_QUEUE_PATH,
279
+ status: str | None = None,
280
+ approval_state: str | None = None,
281
+ ) -> list[QueueItem]:
282
+ init_queue(db_path)
283
+ clauses: list[str] = []
284
+ values: list[str] = []
285
+ if status:
286
+ clauses.append("status = ?")
287
+ values.append(status)
288
+ if approval_state:
289
+ if approval_state not in VALID_APPROVAL_STATES:
290
+ raise ValueError(f"unknown approval state: {approval_state}")
291
+ clauses.append("approval_state = ?")
292
+ values.append(approval_state)
293
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
294
+ with _connect(db_path) as conn:
295
+ rows = conn.execute(
296
+ f"SELECT * FROM work_items {where} ORDER BY created_at DESC, id DESC",
297
+ values,
298
+ ).fetchall()
299
+ return [_row_to_item(row) for row in rows]
300
+
301
+
302
+ def show_work_item(*, db_path: Path = DEFAULT_QUEUE_PATH, item_id: str) -> QueueItem:
303
+ init_queue(db_path)
304
+ with _connect(db_path) as conn:
305
+ row = conn.execute("SELECT * FROM work_items WHERE id = ?", (item_id,)).fetchone()
306
+ if row is None:
307
+ raise KeyError(item_id)
308
+ return _row_to_item(row)
309
+
310
+
311
+ def _set_approval_state(
312
+ *,
313
+ db_path: Path,
314
+ item_id: str,
315
+ approval_state: str,
316
+ decision_note: str | None,
317
+ ) -> QueueItem:
318
+ if approval_state not in VALID_APPROVAL_STATES:
319
+ raise ValueError(f"unknown approval state: {approval_state}")
320
+ init_queue(db_path)
321
+ ts = _now()
322
+ status = "open" if approval_state == "approved" else approval_state
323
+ with _connect(db_path) as conn:
324
+ current = conn.execute("SELECT id FROM work_items WHERE id = ?", (item_id,)).fetchone()
325
+ if current is None:
326
+ raise KeyError(item_id)
327
+ conn.execute(
328
+ """
329
+ UPDATE work_items
330
+ SET approval_state = ?, status = ?, decision_note = ?, updated_at = ?
331
+ WHERE id = ?
332
+ """,
333
+ (approval_state, status, decision_note, ts, item_id),
334
+ )
335
+ _write_audit_event(
336
+ conn,
337
+ event_type=f"work_item_{approval_state}",
338
+ work_item_id=item_id,
339
+ payload={"decision_note": decision_note},
340
+ )
341
+ return show_work_item(db_path=db_path, item_id=item_id)
342
+
343
+
344
+ def approve_work_item(
345
+ *,
346
+ db_path: Path = DEFAULT_QUEUE_PATH,
347
+ item_id: str,
348
+ decision_note: str | None = None,
349
+ ) -> QueueItem:
350
+ return _set_approval_state(
351
+ db_path=db_path,
352
+ item_id=item_id,
353
+ approval_state="approved",
354
+ decision_note=decision_note,
355
+ )
356
+
357
+
358
+ def reject_work_item(
359
+ *,
360
+ db_path: Path = DEFAULT_QUEUE_PATH,
361
+ item_id: str,
362
+ decision_note: str | None = None,
363
+ ) -> QueueItem:
364
+ return _set_approval_state(
365
+ db_path=db_path,
366
+ item_id=item_id,
367
+ approval_state="rejected",
368
+ decision_note=decision_note,
369
+ )
370
+
371
+
372
+ def skip_work_item(
373
+ *,
374
+ db_path: Path = DEFAULT_QUEUE_PATH,
375
+ item_id: str,
376
+ decision_note: str,
377
+ ) -> QueueItem:
378
+ init_queue(db_path)
379
+ ts = _now()
380
+ with _connect(db_path) as conn:
381
+ current = conn.execute("SELECT id FROM work_items WHERE id = ?", (item_id,)).fetchone()
382
+ if current is None:
383
+ raise KeyError(item_id)
384
+ conn.execute(
385
+ """
386
+ UPDATE work_items
387
+ SET approval_state = ?, status = ?, decision_note = ?, updated_at = ?
388
+ WHERE id = ?
389
+ """,
390
+ ("rejected", "skipped", decision_note, ts, item_id),
391
+ )
392
+ _write_audit_event(
393
+ conn,
394
+ event_type="work_item_skipped",
395
+ work_item_id=item_id,
396
+ payload={
397
+ "approval_state": "rejected",
398
+ "status": "skipped",
399
+ "decision_note": decision_note,
400
+ },
401
+ )
402
+ return show_work_item(db_path=db_path, item_id=item_id)
403
+
404
+
405
+ def add_proposal(
406
+ *,
407
+ db_path: Path = DEFAULT_QUEUE_PATH,
408
+ work_item_id: str,
409
+ title: str,
410
+ summary: str,
411
+ patch_plan: str,
412
+ risk_level: str = "medium",
413
+ ) -> ProposalRecord:
414
+ init_queue(db_path)
415
+ show_work_item(db_path=db_path, item_id=work_item_id)
416
+ proposal_id = f"prp_{uuid4().hex[:12]}"
417
+ ts = _now()
418
+ with _connect(db_path) as conn:
419
+ conn.execute(
420
+ """
421
+ INSERT INTO proposals(
422
+ id, work_item_id, title, summary, patch_plan, risk_level,
423
+ approval_state, created_at, updated_at
424
+ )
425
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
426
+ """,
427
+ (
428
+ proposal_id,
429
+ work_item_id,
430
+ title,
431
+ summary,
432
+ patch_plan,
433
+ risk_level,
434
+ "pending",
435
+ ts,
436
+ ts,
437
+ ),
438
+ )
439
+ _write_audit_event(
440
+ conn,
441
+ event_type="proposal_added",
442
+ work_item_id=work_item_id,
443
+ payload={
444
+ "proposal_id": proposal_id,
445
+ "risk_level": risk_level,
446
+ "approval_state": "pending",
447
+ },
448
+ )
449
+ return show_proposal(db_path=db_path, proposal_id=proposal_id)
450
+
451
+
452
+ def list_proposals(
453
+ *,
454
+ db_path: Path = DEFAULT_QUEUE_PATH,
455
+ work_item_id: str | None = None,
456
+ approval_state: str | None = None,
457
+ ) -> list[ProposalRecord]:
458
+ init_queue(db_path)
459
+ clauses: list[str] = []
460
+ values: list[str] = []
461
+ if work_item_id:
462
+ clauses.append("work_item_id = ?")
463
+ values.append(work_item_id)
464
+ if approval_state:
465
+ if approval_state not in VALID_APPROVAL_STATES:
466
+ raise ValueError(f"unknown approval state: {approval_state}")
467
+ clauses.append("approval_state = ?")
468
+ values.append(approval_state)
469
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
470
+ with _connect(db_path) as conn:
471
+ rows = conn.execute(
472
+ f"SELECT * FROM proposals {where} ORDER BY created_at DESC, id DESC",
473
+ values,
474
+ ).fetchall()
475
+ return [_row_to_proposal(row) for row in rows]
476
+
477
+
478
+ def show_proposal(*, db_path: Path = DEFAULT_QUEUE_PATH, proposal_id: str) -> ProposalRecord:
479
+ init_queue(db_path)
480
+ with _connect(db_path) as conn:
481
+ row = conn.execute("SELECT * FROM proposals WHERE id = ?", (proposal_id,)).fetchone()
482
+ if row is None:
483
+ raise KeyError(proposal_id)
484
+ return _row_to_proposal(row)
485
+
486
+
487
+ def _set_proposal_approval_state(
488
+ *,
489
+ db_path: Path,
490
+ proposal_id: str,
491
+ approval_state: str,
492
+ decision_note: str | None,
493
+ ) -> ProposalRecord:
494
+ if approval_state not in VALID_APPROVAL_STATES:
495
+ raise ValueError(f"unknown approval state: {approval_state}")
496
+ init_queue(db_path)
497
+ ts = _now()
498
+ with _connect(db_path) as conn:
499
+ current = conn.execute(
500
+ "SELECT id, work_item_id FROM proposals WHERE id = ?",
501
+ (proposal_id,),
502
+ ).fetchone()
503
+ if current is None:
504
+ raise KeyError(proposal_id)
505
+ conn.execute(
506
+ """
507
+ UPDATE proposals
508
+ SET approval_state = ?, decision_note = ?, updated_at = ?
509
+ WHERE id = ?
510
+ """,
511
+ (approval_state, decision_note, ts, proposal_id),
512
+ )
513
+ _write_audit_event(
514
+ conn,
515
+ event_type=f"proposal_{approval_state}",
516
+ work_item_id=str(current["work_item_id"]),
517
+ payload={"proposal_id": proposal_id, "decision_note": decision_note},
518
+ )
519
+ return show_proposal(db_path=db_path, proposal_id=proposal_id)
520
+
521
+
522
+ def approve_proposal(
523
+ *,
524
+ db_path: Path = DEFAULT_QUEUE_PATH,
525
+ proposal_id: str,
526
+ decision_note: str | None = None,
527
+ ) -> ProposalRecord:
528
+ return _set_proposal_approval_state(
529
+ db_path=db_path,
530
+ proposal_id=proposal_id,
531
+ approval_state="approved",
532
+ decision_note=decision_note,
533
+ )
534
+
535
+
536
+ def reject_proposal(
537
+ *,
538
+ db_path: Path = DEFAULT_QUEUE_PATH,
539
+ proposal_id: str,
540
+ decision_note: str | None = None,
541
+ ) -> ProposalRecord:
542
+ return _set_proposal_approval_state(
543
+ db_path=db_path,
544
+ proposal_id=proposal_id,
545
+ approval_state="rejected",
546
+ decision_note=decision_note,
547
+ )
548
+
549
+
550
+ def export_work_items(*, db_path: Path = DEFAULT_QUEUE_PATH) -> dict[str, Any]:
551
+ work_items = [item.to_dict() for item in list_work_items(db_path=db_path)]
552
+ with _connect(db_path) as conn:
553
+ _write_audit_event(
554
+ conn,
555
+ event_type="work_items_exported",
556
+ work_item_id=None,
557
+ payload={"count": len(work_items), "export": "work_items"},
558
+ )
559
+ return {
560
+ "schema_version": SCHEMA_VERSION,
561
+ "db_path": str(db_path),
562
+ "local_first": True,
563
+ "work_items": work_items,
564
+ }
565
+
566
+
567
+ def list_audit_events(
568
+ *,
569
+ db_path: Path = DEFAULT_QUEUE_PATH,
570
+ work_item_id: str | None = None,
571
+ ) -> list[AuditEvent]:
572
+ init_queue(db_path)
573
+ clauses: list[str] = []
574
+ values: list[str] = []
575
+ if work_item_id:
576
+ clauses.append("work_item_id = ?")
577
+ values.append(work_item_id)
578
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
579
+ with _connect(db_path) as conn:
580
+ rows = conn.execute(
581
+ f"SELECT * FROM audit_events {where} ORDER BY id ASC",
582
+ values,
583
+ ).fetchall()
584
+ return [_row_to_audit_event(row) for row in rows]
585
+
586
+
587
+ def export_audit_events(
588
+ *,
589
+ db_path: Path = DEFAULT_QUEUE_PATH,
590
+ work_item_id: str | None = None,
591
+ ) -> dict[str, Any]:
592
+ return {
593
+ "schema_version": SCHEMA_VERSION,
594
+ "db_path": str(db_path),
595
+ "local_first": True,
596
+ "audit_events": [
597
+ event.to_dict()
598
+ for event in list_audit_events(db_path=db_path, work_item_id=work_item_id)
599
+ ],
600
+ }