@danypops/papyrus 0.11.4 → 0.13.0

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 (57) hide show
  1. package/README.md +16 -2
  2. package/extension/src/active-task-continuation.ts +6 -0
  3. package/extension/src/artifact-browser.ts +13 -7
  4. package/extension/src/artifact-status-presentation.ts +53 -0
  5. package/extension/src/context-budget.ts +173 -0
  6. package/extension/src/context-view.ts +172 -0
  7. package/extension/src/docs.ts +6 -5
  8. package/extension/src/domain-tools.ts +108 -52
  9. package/extension/src/index.ts +124 -38
  10. package/extension/src/notes.ts +16 -4
  11. package/extension/src/rules.ts +7 -7
  12. package/extension/src/skill-catalog-footprint.ts +183 -0
  13. package/extension/src/skills.ts +2 -3
  14. package/extension/src/task-focus-events.ts +57 -0
  15. package/extension/src/task-widget.ts +13 -1
  16. package/extension/src/tasks.ts +51 -15
  17. package/extension/src/tool-rendering/artifact-card.ts +117 -0
  18. package/extension/src/tool-rendering/artifact-list.ts +179 -0
  19. package/extension/src/tool-rendering/index.ts +107 -0
  20. package/extension/src/tool-rendering/render-model.ts +406 -0
  21. package/package.json +4 -2
  22. package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
  23. package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
  24. package/src/adapters/sqlite-artifact-store.ts +20 -11
  25. package/src/adapters/sqlite-discourse-store.ts +325 -0
  26. package/src/adapters/sqlite-graph-projection-store.ts +41 -0
  27. package/src/adapters/sqlite-task-focus-store.ts +34 -15
  28. package/src/authority-registry.ts +115 -0
  29. package/src/cli.ts +904 -124
  30. package/src/constants.ts +77 -5
  31. package/src/conversation-journal-service.ts +87 -0
  32. package/src/db.ts +285 -33
  33. package/src/domain/artifact-event.ts +99 -0
  34. package/src/domain/conversation-journal.ts +168 -0
  35. package/src/domain/discourse-store.ts +142 -0
  36. package/src/domain/graph-projection.ts +74 -0
  37. package/src/domain/skill-definition.ts +57 -8
  38. package/src/domain/task-event.ts +4 -0
  39. package/src/domain-services.ts +201 -40
  40. package/src/graph-projection-service.ts +103 -0
  41. package/src/id-migration.ts +200 -0
  42. package/src/module-registry.ts +53 -0
  43. package/src/modules/docs.ts +77 -0
  44. package/src/modules/graph-projection.ts +82 -0
  45. package/src/modules/notes.ts +76 -0
  46. package/src/modules/rules.ts +81 -0
  47. package/src/modules/skills.ts +113 -0
  48. package/src/modules/tasks.ts +164 -0
  49. package/src/ops.ts +142 -15
  50. package/src/ports/artifact-scope-store.ts +20 -0
  51. package/src/ports/artifact-store.ts +10 -5
  52. package/src/ports/conversation-journal-store.ts +17 -0
  53. package/src/ports/graph-projection-store.ts +15 -0
  54. package/src/ports/task-focus-store.ts +62 -20
  55. package/src/service.ts +218 -223
  56. package/src/skill-execution.ts +169 -75
  57. package/src/task-service.ts +70 -38
@@ -0,0 +1,406 @@
1
+ import {
2
+ TOOL_DETAILS_BODY_MAX_CHARACTERS,
3
+ TOOL_DETAILS_FIELD_MAX_CHARACTERS,
4
+ TOOL_DETAILS_MAX_EDGES,
5
+ TOOL_DETAILS_MAX_ITEMS,
6
+ TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS,
7
+ TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS,
8
+ TOOL_MODEL_CONTENT_MAX_CHARACTERS,
9
+ } from "../../../src/constants.ts";
10
+ import type { Artifact } from "../../../src/domain/artifact.ts";
11
+
12
+ export const PAPYRUS_TOOL_DETAILS_SCHEMA = "papyrus.tool-details/v1" as const;
13
+
14
+ export interface ResultCompleteness {
15
+ truncated: boolean;
16
+ omitted: number;
17
+ }
18
+
19
+ export interface ToolArtifactSummary {
20
+ id: string;
21
+ kind: string;
22
+ title: string;
23
+ status: string;
24
+ subtype: string;
25
+ labels: string[];
26
+ }
27
+
28
+ export interface ToolArtifact extends ToolArtifactSummary {
29
+ body: string;
30
+ createdAt: string;
31
+ updatedAt: string;
32
+ }
33
+
34
+ interface ToolDetailsBase {
35
+ schemaVersion: typeof PAPYRUS_TOOL_DETAILS_SCHEMA;
36
+ operation: string;
37
+ kind: string;
38
+ }
39
+
40
+ export interface ArtifactToolDetails extends ToolDetailsBase {
41
+ kind: "artifact";
42
+ artifact: ToolArtifact;
43
+ completeness: ResultCompleteness;
44
+ }
45
+
46
+ export interface ArtifactListToolDetails extends ToolDetailsBase {
47
+ kind: "artifact-list";
48
+ rows: ToolArtifactSummary[];
49
+ total: number;
50
+ completeness: ResultCompleteness;
51
+ }
52
+
53
+ export interface TransitionToolDetails extends ToolDetailsBase {
54
+ kind: "transition";
55
+ artifact: ToolArtifactSummary;
56
+ fromStatus: string;
57
+ toStatus: string;
58
+ }
59
+
60
+ export interface ToolGraphEdge {
61
+ from: string;
62
+ relation: string;
63
+ to: string;
64
+ }
65
+
66
+ export interface GraphToolDetails extends ToolDetailsBase {
67
+ kind: "graph";
68
+ nodes: ToolArtifactSummary[];
69
+ edges: ToolGraphEdge[];
70
+ nodeCompleteness: ResultCompleteness;
71
+ edgeCompleteness: ResultCompleteness;
72
+ }
73
+
74
+ export interface ToolGateRow {
75
+ passed: boolean;
76
+ type: string;
77
+ target: string;
78
+ output: string;
79
+ }
80
+
81
+ export interface GateRunToolDetails extends ToolDetailsBase {
82
+ kind: "gate-run";
83
+ artifactId: string;
84
+ gates: ToolGateRow[];
85
+ completeness: ResultCompleteness;
86
+ }
87
+
88
+ export interface ToolInvocationCreated {
89
+ tasks: string[];
90
+ docs: string[];
91
+ rules: string[];
92
+ roots: string[];
93
+ }
94
+
95
+ export interface InvocationToolDetails extends ToolDetailsBase {
96
+ kind: "invocation";
97
+ runId: string;
98
+ created: ToolInvocationCreated;
99
+ completeness: ResultCompleteness;
100
+ }
101
+
102
+ export interface PreviewToolDetails extends ToolDetailsBase {
103
+ kind: "preview";
104
+ title: string;
105
+ content: string;
106
+ completeness: ResultCompleteness;
107
+ }
108
+
109
+ export interface ErrorToolDetails extends ToolDetailsBase {
110
+ kind: "error";
111
+ code: string;
112
+ message: string;
113
+ }
114
+
115
+ export type PapyrusToolDetails =
116
+ | ArtifactToolDetails
117
+ | ArtifactListToolDetails
118
+ | TransitionToolDetails
119
+ | GraphToolDetails
120
+ | GateRunToolDetails
121
+ | InvocationToolDetails
122
+ | PreviewToolDetails
123
+ | ErrorToolDetails;
124
+
125
+ export interface ModelContent {
126
+ text: string;
127
+ truncated: boolean;
128
+ omitted: number;
129
+ }
130
+
131
+ function completeness(total: number, returned: number): ResultCompleteness {
132
+ const omitted = Math.max(0, total - returned);
133
+ return { truncated: omitted > 0, omitted };
134
+ }
135
+
136
+ function boundedText(value: string, maximum: number): { value: string; completeness: ResultCompleteness } {
137
+ const clipped = value.slice(0, maximum);
138
+ return { value: clipped, completeness: completeness(value.length, clipped.length) };
139
+ }
140
+
141
+ function artifactSummary(artifact: Artifact): ToolArtifactSummary {
142
+ return {
143
+ id: artifact.id,
144
+ kind: artifact.kind,
145
+ title: artifact.title,
146
+ status: artifact.status,
147
+ subtype: artifact.subtype,
148
+ labels: artifact.labels.slice(0, TOOL_DETAILS_MAX_ITEMS),
149
+ };
150
+ }
151
+
152
+ export function createArtifactDetails(operation: string, artifact: Artifact): ArtifactToolDetails {
153
+ const body = boundedText(artifact.body, TOOL_DETAILS_BODY_MAX_CHARACTERS);
154
+ return {
155
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
156
+ kind: "artifact",
157
+ operation,
158
+ artifact: {
159
+ ...artifactSummary(artifact),
160
+ body: body.value,
161
+ createdAt: artifact.created_at,
162
+ updatedAt: artifact.updated_at,
163
+ },
164
+ completeness: body.completeness,
165
+ };
166
+ }
167
+
168
+ export function createArtifactListDetails(
169
+ operation: string,
170
+ artifacts: readonly Artifact[],
171
+ total = artifacts.length,
172
+ ): ArtifactListToolDetails {
173
+ const rows = artifacts.slice(0, TOOL_DETAILS_MAX_ITEMS).map(artifactSummary);
174
+ return {
175
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
176
+ kind: "artifact-list",
177
+ operation,
178
+ rows,
179
+ total,
180
+ completeness: completeness(Math.max(total, artifacts.length), rows.length),
181
+ };
182
+ }
183
+
184
+ export function createTransitionDetails(
185
+ operation: string,
186
+ artifact: Artifact,
187
+ fromStatus: string,
188
+ toStatus: string,
189
+ ): TransitionToolDetails {
190
+ return {
191
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
192
+ kind: "transition",
193
+ operation,
194
+ artifact: artifactSummary(artifact),
195
+ fromStatus,
196
+ toStatus,
197
+ };
198
+ }
199
+
200
+ export function createGraphDetails(
201
+ operation: string,
202
+ artifacts: readonly Artifact[],
203
+ edges: readonly ToolGraphEdge[],
204
+ ): GraphToolDetails {
205
+ const nodes = artifacts.slice(0, TOOL_DETAILS_MAX_ITEMS).map(artifactSummary);
206
+ const boundedEdges = edges.slice(0, TOOL_DETAILS_MAX_EDGES).map((edge) => ({ ...edge }));
207
+ return {
208
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
209
+ kind: "graph",
210
+ operation,
211
+ nodes,
212
+ edges: boundedEdges,
213
+ nodeCompleteness: completeness(artifacts.length, nodes.length),
214
+ edgeCompleteness: completeness(edges.length, boundedEdges.length),
215
+ };
216
+ }
217
+
218
+ export function createGateRunDetails(
219
+ operation: string,
220
+ artifactId: string,
221
+ gates: readonly ToolGateRow[],
222
+ ): GateRunToolDetails {
223
+ const boundedGates = gates.slice(0, TOOL_DETAILS_MAX_ITEMS).map((gate) => ({
224
+ ...gate,
225
+ output: gate.output.slice(0, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS),
226
+ }));
227
+ return {
228
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
229
+ kind: "gate-run",
230
+ operation,
231
+ artifactId,
232
+ gates: boundedGates,
233
+ completeness: completeness(gates.length, boundedGates.length),
234
+ };
235
+ }
236
+
237
+ export function createInvocationDetails(
238
+ operation: string,
239
+ runId: string,
240
+ created: ToolInvocationCreated,
241
+ ): InvocationToolDetails {
242
+ const bounded: ToolInvocationCreated = {
243
+ tasks: created.tasks.slice(0, TOOL_DETAILS_MAX_ITEMS),
244
+ docs: created.docs.slice(0, TOOL_DETAILS_MAX_ITEMS),
245
+ rules: created.rules.slice(0, TOOL_DETAILS_MAX_ITEMS),
246
+ roots: created.roots.slice(0, TOOL_DETAILS_MAX_ITEMS),
247
+ };
248
+ const total = created.tasks.length + created.docs.length + created.rules.length + created.roots.length;
249
+ const returned = bounded.tasks.length + bounded.docs.length + bounded.rules.length + bounded.roots.length;
250
+ return {
251
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
252
+ kind: "invocation",
253
+ operation,
254
+ runId,
255
+ created: bounded,
256
+ completeness: completeness(total, returned),
257
+ };
258
+ }
259
+
260
+ export function createPreviewDetails(operation: string, title: string, content: string): PreviewToolDetails {
261
+ const bounded = boundedText(content, TOOL_DETAILS_BODY_MAX_CHARACTERS);
262
+ return {
263
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
264
+ kind: "preview",
265
+ operation,
266
+ title,
267
+ content: bounded.value,
268
+ completeness: bounded.completeness,
269
+ };
270
+ }
271
+
272
+ export function createErrorDetails(operation: string, code: string, message: string): ErrorToolDetails {
273
+ return {
274
+ schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
275
+ kind: "error",
276
+ operation,
277
+ code: code.slice(0, TOOL_DETAILS_FIELD_MAX_CHARACTERS),
278
+ message: message.slice(0, TOOL_DETAILS_BODY_MAX_CHARACTERS),
279
+ };
280
+ }
281
+
282
+ export function createModelContent(value: string): ModelContent {
283
+ if (value.length <= TOOL_MODEL_CONTENT_MAX_CHARACTERS) {
284
+ return { text: value, truncated: false, omitted: 0 };
285
+ }
286
+ let omitted = value.length - TOOL_MODEL_CONTENT_MAX_CHARACTERS;
287
+ let marker = "";
288
+ let kept = 0;
289
+ for (let iteration = 0; iteration < 5; iteration += 1) {
290
+ const nextMarker = `\n[truncated ${omitted} characters]`;
291
+ const nextKept = Math.max(0, TOOL_MODEL_CONTENT_MAX_CHARACTERS - nextMarker.length);
292
+ const nextOmitted = value.length - nextKept;
293
+ marker = nextMarker;
294
+ kept = nextKept;
295
+ if (nextOmitted === omitted) break;
296
+ omitted = nextOmitted;
297
+ }
298
+ return { text: `${value.slice(0, kept)}${marker}`, truncated: true, omitted: value.length - kept };
299
+ }
300
+
301
+ function isRecord(value: unknown): value is Record<string, unknown> {
302
+ return typeof value === "object" && value !== null && !Array.isArray(value);
303
+ }
304
+
305
+ function isBoundedString(value: unknown, maximum = TOOL_DETAILS_FIELD_MAX_CHARACTERS): value is string {
306
+ return typeof value === "string" && value.length <= maximum;
307
+ }
308
+
309
+ function isStringArray(value: unknown): value is string[] {
310
+ return Array.isArray(value) && value.length <= TOOL_DETAILS_MAX_ITEMS && value.every((item) => isBoundedString(item));
311
+ }
312
+
313
+ function isCompleteness(value: unknown): value is ResultCompleteness {
314
+ return isRecord(value) && typeof value.truncated === "boolean" && Number.isSafeInteger(value.omitted) && Number(value.omitted) >= 0;
315
+ }
316
+
317
+ function isArtifactSummary(value: unknown): value is ToolArtifactSummary {
318
+ return isRecord(value)
319
+ && isBoundedString(value.id)
320
+ && isBoundedString(value.kind)
321
+ && isBoundedString(value.title)
322
+ && isBoundedString(value.status)
323
+ && isBoundedString(value.subtype)
324
+ && isStringArray(value.labels);
325
+ }
326
+
327
+ function isToolArtifact(value: unknown): value is ToolArtifact {
328
+ if (!isRecord(value)) return false;
329
+ const body = value.body;
330
+ const createdAt = value.createdAt;
331
+ const updatedAt = value.updatedAt;
332
+ return isArtifactSummary(value)
333
+ && isBoundedString(body, TOOL_DETAILS_BODY_MAX_CHARACTERS)
334
+ && isBoundedString(createdAt)
335
+ && isBoundedString(updatedAt);
336
+ }
337
+
338
+ function isGraphEdge(value: unknown): value is ToolGraphEdge {
339
+ return isRecord(value) && isBoundedString(value.from) && isBoundedString(value.relation) && isBoundedString(value.to);
340
+ }
341
+
342
+ function isGateRow(value: unknown): value is ToolGateRow {
343
+ return isRecord(value)
344
+ && typeof value.passed === "boolean"
345
+ && isBoundedString(value.type)
346
+ && isBoundedString(value.target)
347
+ && isBoundedString(value.output, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS);
348
+ }
349
+
350
+ function isBoundedArray<T>(value: unknown, maximum: number, predicate: (entry: unknown) => entry is T): value is T[] {
351
+ return Array.isArray(value) && value.length <= maximum && value.every(predicate);
352
+ }
353
+
354
+ /** Validate renderer details restored from session history before using them as typed presentation state. */
355
+ export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | undefined {
356
+ let serializedLength: number;
357
+ try {
358
+ serializedLength = JSON.stringify(value).length;
359
+ } catch {
360
+ return undefined;
361
+ }
362
+ if (serializedLength > TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS || !isRecord(value)
363
+ || value.schemaVersion !== PAPYRUS_TOOL_DETAILS_SCHEMA
364
+ || !isBoundedString(value.operation)
365
+ || !isBoundedString(value.kind)) return undefined;
366
+
367
+ switch (value.kind) {
368
+ case "artifact":
369
+ return isToolArtifact(value.artifact) && isCompleteness(value.completeness)
370
+ ? value as unknown as ArtifactToolDetails : undefined;
371
+ case "artifact-list":
372
+ return isBoundedArray(value.rows, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary)
373
+ && Number.isSafeInteger(value.total) && Number(value.total) >= value.rows.length
374
+ && isCompleteness(value.completeness)
375
+ ? value as unknown as ArtifactListToolDetails : undefined;
376
+ case "transition":
377
+ return isArtifactSummary(value.artifact) && isBoundedString(value.fromStatus) && isBoundedString(value.toStatus)
378
+ ? value as unknown as TransitionToolDetails : undefined;
379
+ case "graph":
380
+ return isBoundedArray(value.nodes, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary)
381
+ && isBoundedArray(value.edges, TOOL_DETAILS_MAX_EDGES, isGraphEdge)
382
+ && isCompleteness(value.nodeCompleteness) && isCompleteness(value.edgeCompleteness)
383
+ ? value as unknown as GraphToolDetails : undefined;
384
+ case "gate-run":
385
+ return isBoundedString(value.artifactId)
386
+ && isBoundedArray(value.gates, TOOL_DETAILS_MAX_ITEMS, isGateRow)
387
+ && isCompleteness(value.completeness)
388
+ ? value as unknown as GateRunToolDetails : undefined;
389
+ case "invocation": {
390
+ if (!isRecord(value.created)) return undefined;
391
+ return isBoundedString(value.runId)
392
+ && isStringArray(value.created.tasks) && isStringArray(value.created.docs)
393
+ && isStringArray(value.created.rules) && isStringArray(value.created.roots)
394
+ && isCompleteness(value.completeness)
395
+ ? value as unknown as InvocationToolDetails : undefined;
396
+ }
397
+ case "preview":
398
+ return isBoundedString(value.title) && isBoundedString(value.content, TOOL_DETAILS_BODY_MAX_CHARACTERS) && isCompleteness(value.completeness)
399
+ ? value as unknown as PreviewToolDetails : undefined;
400
+ case "error":
401
+ return isBoundedString(value.code) && isBoundedString(value.message, TOOL_DETAILS_BODY_MAX_CHARACTERS)
402
+ ? value as unknown as ErrorToolDetails : undefined;
403
+ default:
404
+ return undefined;
405
+ }
406
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.11.4",
3
+ "version": "0.13.0",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "bun test",
12
+ "typecheck": "tsc --noEmit -p tsconfig.json",
12
13
  "cli": "bun src/cli.ts",
13
14
  "serve": "bun src/cli.ts serve",
14
15
  "service:install": "bun src/cli.ts service install",
@@ -23,7 +24,8 @@
23
24
  "typebox": "*"
24
25
  },
25
26
  "devDependencies": {
26
- "bun-types": "latest"
27
+ "bun-types": "latest",
28
+ "typescript": "^5.7.3"
27
29
  },
28
30
  "repository": {
29
31
  "type": "git",
@@ -0,0 +1,48 @@
1
+ import type { JournalPost, JournalThread } from "../domain/conversation-journal.ts";
2
+ import type { ConversationJournalStore } from "../ports/conversation-journal-store.ts";
3
+
4
+ /**
5
+ * Bounded in-memory conformance fixture -- the reference implementation the
6
+ * conversationJournalConformanceSuite is proven against first, before any real
7
+ * persistence backend needs to satisfy the same contract.
8
+ */
9
+ export class InMemoryConversationJournalStore implements ConversationJournalStore {
10
+ private readonly threads = new Map<string, JournalThread>();
11
+ private readonly posts = new Map<string, JournalPost>();
12
+ private readonly postIdsByOperationId = new Map<string, string>();
13
+ private readonly postIdsByThread = new Map<string, string[]>();
14
+
15
+ ensureThread(threadId: string): JournalThread {
16
+ const existing = this.threads.get(threadId);
17
+ if (existing) return existing;
18
+ const thread: JournalThread = { id: threadId, createdAt: new Date().toISOString() };
19
+ this.threads.set(threadId, thread);
20
+ return thread;
21
+ }
22
+
23
+ getThread(threadId: string): JournalThread | undefined {
24
+ return this.threads.get(threadId);
25
+ }
26
+
27
+ findPostByOperationId(operationId: string): JournalPost | undefined {
28
+ const postId = this.postIdsByOperationId.get(operationId);
29
+ return postId ? this.posts.get(postId) : undefined;
30
+ }
31
+
32
+ insertPost(post: JournalPost): void {
33
+ this.posts.set(post.id, post);
34
+ this.postIdsByOperationId.set(post.operationId, post.id);
35
+ const ids = this.postIdsByThread.get(post.threadId) ?? [];
36
+ ids.push(post.id);
37
+ this.postIdsByThread.set(post.threadId, ids);
38
+ }
39
+
40
+ getPost(id: string): JournalPost | undefined {
41
+ return this.posts.get(id);
42
+ }
43
+
44
+ postsForThread(threadId: string): readonly JournalPost[] {
45
+ const ids = this.postIdsByThread.get(threadId) ?? [];
46
+ return ids.map((id) => this.posts.get(id)!);
47
+ }
48
+ }
@@ -0,0 +1,36 @@
1
+ import type { Db } from "../db.ts";
2
+ import { inTransaction } from "../db.ts";
3
+ import type { TaskScopeSource } from "../domain/task-scope.ts";
4
+ import type { ArtifactScope, ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
5
+
6
+ export class SQLiteArtifactScopeStore implements ArtifactScopeStore {
7
+ constructor(private readonly db: Db) {}
8
+
9
+ assign(artifactId: string, projectRoot: string | undefined, source: TaskScopeSource): ArtifactScope {
10
+ inTransaction(this.db, () => {
11
+ this.db.prepare(`
12
+ INSERT INTO artifact_scopes (artifact_id, project_root, source, assigned_at)
13
+ VALUES (?, ?, ?, ?)
14
+ ON CONFLICT(artifact_id) DO UPDATE SET
15
+ project_root = excluded.project_root,
16
+ source = excluded.source,
17
+ assigned_at = excluded.assigned_at
18
+ `).run(artifactId, projectRoot ?? null, source, new Date().toISOString());
19
+ });
20
+ return { artifactId, ...(projectRoot === undefined ? {} : { projectRoot }), source };
21
+ }
22
+
23
+ get(artifactId: string): ArtifactScope | undefined {
24
+ const row = this.db.prepare("SELECT artifact_id, project_root, source FROM artifact_scopes WHERE artifact_id = ?").get(artifactId) as
25
+ | { artifact_id: string; project_root: string | null; source: TaskScopeSource }
26
+ | null;
27
+ return row ? { artifactId: row.artifact_id, ...(row.project_root === null ? {} : { projectRoot: row.project_root }), source: row.source } : undefined;
28
+ }
29
+
30
+ ids(projectRoot: string | undefined, limit: number): string[] {
31
+ const rows = projectRoot === undefined
32
+ ? this.db.prepare("SELECT artifact_id FROM artifact_scopes WHERE project_root IS NULL ORDER BY artifact_id LIMIT ?").all(limit)
33
+ : this.db.prepare("SELECT artifact_id FROM artifact_scopes WHERE project_root = ? ORDER BY artifact_id LIMIT ?").all(projectRoot, limit);
34
+ return (rows as Array<{ artifact_id: string }>).map((row) => row.artifact_id);
35
+ }
36
+ }
@@ -11,7 +11,8 @@ import type {
11
11
  RelationshipQuery,
12
12
  UpdateArtifactInput,
13
13
  } from "../domain/artifact.ts";
14
- import { createArtifact, getArtifact, linkArtifacts, queryArtifacts, updateArtifactContent, updateExtra, updateStatus } from "../ops.ts";
14
+ import type { ArtifactEventContext, ArtifactEventPage, ArtifactEventQuery } from "../domain/artifact-event.ts";
15
+ import { createArtifact, getArtifact, linkArtifacts, queryArtifactEvents, queryArtifacts, unlinkArtifacts, updateArtifactContent, updateExtra, updateStatus } from "../ops.ts";
15
16
 
16
17
  export class SQLiteArtifactStore implements AtomicArtifactStore {
17
18
  constructor(private readonly db: Db) {}
@@ -20,8 +21,8 @@ export class SQLiteArtifactStore implements AtomicArtifactStore {
20
21
  return inTransaction(this.db, operation);
21
22
  }
22
23
 
23
- create(input: CreateArtifactInput): Artifact {
24
- return createArtifact(this.db, input);
24
+ create(input: CreateArtifactInput, context?: ArtifactEventContext): Artifact {
25
+ return createArtifact(this.db, input, context);
25
26
  }
26
27
 
27
28
  get(id: string, options?: ArtifactGraphOptions): Artifact | null {
@@ -32,20 +33,28 @@ export class SQLiteArtifactStore implements AtomicArtifactStore {
32
33
  return queryArtifacts(this.db, filter);
33
34
  }
34
35
 
35
- link(link: ArtifactLink): void {
36
- linkArtifacts(this.db, link.from, link.relation, link.to);
36
+ link(link: ArtifactLink, context?: ArtifactEventContext): void {
37
+ linkArtifacts(this.db, link.from, link.relation, link.to, context);
37
38
  }
38
39
 
39
- setStatus(id: string, status: string): Artifact | null {
40
- return updateStatus(this.db, id, status);
40
+ unlink(link: ArtifactLink, context?: ArtifactEventContext): boolean {
41
+ return unlinkArtifacts(this.db, link.from, link.relation, link.to, context);
41
42
  }
42
43
 
43
- setExtra(id: string, extra: Record<string, unknown>): Artifact | null {
44
- return updateExtra(this.db, id, extra);
44
+ setStatus(id: string, status: string, context?: ArtifactEventContext): Artifact | null {
45
+ return updateStatus(this.db, id, status, context);
45
46
  }
46
47
 
47
- updateContent(id: string, input: UpdateArtifactInput): Artifact | null {
48
- return updateArtifactContent(this.db, id, input);
48
+ setExtra(id: string, extra: Record<string, unknown>, context?: ArtifactEventContext): Artifact | null {
49
+ return updateExtra(this.db, id, extra, context);
50
+ }
51
+
52
+ updateContent(id: string, input: UpdateArtifactInput, context?: ArtifactEventContext): Artifact | null {
53
+ return updateArtifactContent(this.db, id, input, context);
54
+ }
55
+
56
+ events(query: ArtifactEventQuery): ArtifactEventPage {
57
+ return queryArtifactEvents(this.db, query);
49
58
  }
50
59
 
51
60
  relationships(filter: RelationshipQuery = {}): ArtifactEdge[] {