@absolutejs/artifacts 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -23,6 +23,11 @@ routes, authorization, UI, or hosting.
23
23
  - Standard file-backed kinds for documents, presentations, spreadsheets,
24
24
  datasets, code, images, audio, video, email, archives, and generic files
25
25
  - An optional bridge to `@absolutejs/rag` ingestion
26
+ - Provider-neutral generation registries with atomic multi-file bundles
27
+ - Revision-pinned or explicitly live publications
28
+ - Durable lifecycle events designed for transactional outboxes
29
+ - Per-revision RAG indexing state and an indexing coordinator
30
+ - Artifact/source lineage and history-aware asset garbage collection
26
31
 
27
32
  Your application retains authorization, durable persistence, public tokens,
28
33
  URLs, notifications, analytics, submissions, and product-specific rendering.
@@ -121,6 +126,46 @@ await artifacts.attach("owner-123", report.id, {
121
126
  });
122
127
  ```
123
128
 
129
+ Multiple generated files should use one staged transaction and therefore one
130
+ artifact revision:
131
+
132
+ ```ts
133
+ const report = await artifacts.createBundle("owner-123", {
134
+ assets: [pdfOutput, docxOutput, thumbnailOutput],
135
+ content: { summary: "Quarterly results" },
136
+ createdBy: "agent",
137
+ kind: "document",
138
+ provenance: {
139
+ lineage: [{ relation: "generated_from", sourceId: "rag-document-123" }],
140
+ tool: "quarterly_report_generator",
141
+ },
142
+ title: "Q3 report",
143
+ });
144
+ ```
145
+
146
+ ## Generation
147
+
148
+ Generators are provider-neutral. They return validated structured content and
149
+ zero or more file writes; the registry commits those outputs through the same
150
+ artifact bundle lifecycle:
151
+
152
+ ```ts
153
+ const generators = createArtifactGeneratorRegistry([
154
+ {
155
+ kind: "presentation",
156
+ name: "company-deck",
157
+ generate: async ({ prompt }) => buildPresentation(prompt),
158
+ },
159
+ ]);
160
+
161
+ const deck = await generators.generate(artifacts, {
162
+ createdBy: "agent",
163
+ kind: "presentation",
164
+ ownerId: member.id,
165
+ prompt: "Build the partner launch deck",
166
+ });
167
+ ```
168
+
124
169
  ## RAG ingestion
125
170
 
126
171
  The optional `@absolutejs/artifacts/rag` entry point resolves one current or
@@ -137,6 +182,22 @@ const uploads = await artifactToRAGUploads(revision, assetStore);
137
182
  const upsert = await buildRAGUpsertInputFromUploads({ uploads });
138
183
  ```
139
184
 
185
+ `createArtifactRAGIndexCoordinator` wraps that conversion with durable
186
+ `pending`, `indexed`, and `failed` state. It removes document IDs from the
187
+ previous indexed revision after the replacement succeeds.
188
+
189
+ ## Events and retention
190
+
191
+ Every lifecycle mutation supplies its event to the artifact store in the same
192
+ call that writes the current record and immutable revision. Durable adapters
193
+ should commit those rows in one database transaction, then workers can consume
194
+ unprocessed events for RAG indexing, previews, notifications, scanning, or
195
+ conversion.
196
+
197
+ Asset collection compares storage candidates with references across every
198
+ retained revision. `collectAssetGarbage({ dryRun: true })` previews deletion;
199
+ only unreferenced objects older than the configured minimum age are eligible.
200
+
140
201
  ## Compose publishing and rendering
141
202
 
142
203
  Publishing is an adapter because public access is a host policy:
@@ -154,6 +215,10 @@ const artifacts = createArtifactService({
154
215
  });
155
216
  ```
156
217
 
218
+ Publishing defaults to `pinned`: the public record names the exact immutable
219
+ revision. `mode: "live"` is an explicit alternative whose revision advances
220
+ with later edits.
221
+
157
222
  Renderers are independently registered by artifact kind and output format:
158
223
 
159
224
  ```ts
package/dist/index.js CHANGED
@@ -4,6 +4,18 @@ import { Value } from "@sinclair/typebox/value";
4
4
 
5
5
  // src/types.ts
6
6
  var ARTIFACT_STATUSES = ["draft", "published", "archived"];
7
+ var ARTIFACT_EVENT_TYPES = [
8
+ "artifact.archived",
9
+ "artifact.asset_attached",
10
+ "artifact.asset_detached",
11
+ "artifact.created",
12
+ "artifact.generated",
13
+ "artifact.indexing_changed",
14
+ "artifact.published",
15
+ "artifact.restored",
16
+ "artifact.revised",
17
+ "artifact.unpublished"
18
+ ];
7
19
 
8
20
  class ArtifactError extends Error {
9
21
  code;
@@ -31,6 +43,38 @@ var defineArtifactRegistry = (definitions) => ({
31
43
  return content;
32
44
  }
33
45
  });
46
+ // src/generators.ts
47
+ var createArtifactGeneratorRegistry = (initial = []) => {
48
+ const generators = new Map(initial.map((generator) => [generator.kind, generator]));
49
+ return {
50
+ generate: async (service, input) => {
51
+ const generator = generators.get(input.kind);
52
+ if (!generator) {
53
+ throw new ArtifactError("generator_unavailable", `No generator is registered for ${input.kind} artifacts`);
54
+ }
55
+ const result = await generator.generate(input, {
56
+ ownerId: input.ownerId
57
+ });
58
+ const artifact = await service.createBundle(input.ownerId, {
59
+ assets: result.assets,
60
+ content: result.content,
61
+ createdBy: input.createdBy,
62
+ kind: input.kind,
63
+ metadata: {
64
+ ...result.metadata,
65
+ ...result.warnings?.length ? { generationWarnings: result.warnings } : {}
66
+ },
67
+ provenance: result.provenance,
68
+ title: result.title ?? input.title ?? `Generated ${input.kind}`
69
+ });
70
+ return artifact;
71
+ },
72
+ kinds: () => [...generators.keys()],
73
+ register: (generator) => {
74
+ generators.set(generator.kind, generator);
75
+ }
76
+ };
77
+ };
34
78
  // src/standardKinds.ts
35
79
  import { Type } from "@sinclair/typebox";
36
80
  var FileArtifactContentSchema = Type.Object({
@@ -135,6 +179,16 @@ var mediaTypeMatches = (accepted, actual) => {
135
179
  var createArtifactService = (options) => {
136
180
  const now = () => (options.clock ?? (() => new Date))().toISOString();
137
181
  const idFactory = options.idFactory ?? (() => crypto.randomUUID());
182
+ const eventIdFactory = options.eventIdFactory ?? (() => crypto.randomUUID());
183
+ const event = (artifact, type, payload) => ({
184
+ artifactId: artifact.id,
185
+ createdAt: now(),
186
+ id: eventIdFactory(),
187
+ ownerId: artifact.ownerId,
188
+ payload,
189
+ revision: artifact.revision,
190
+ type
191
+ });
138
192
  const validateAssets = (kind, assets) => {
139
193
  const policy = options.registry.definitions[kind]?.assets;
140
194
  if (!policy) {
@@ -152,47 +206,81 @@ var createArtifactService = (options) => {
152
206
  }
153
207
  return assets;
154
208
  };
155
- const validateNewAsset = (kind, input, currentCount) => {
209
+ const validateAssetInputs = (kind, inputs, currentCount = 0) => {
156
210
  const policy = options.registry.definitions[kind]?.assets;
157
- if (!policy) {
211
+ if (!policy && inputs.length > 0) {
158
212
  throw new ArtifactError("invalid_content", `${kind} artifacts do not accept file assets`);
159
213
  }
160
- if (policy.maxCount !== undefined && currentCount >= policy.maxCount) {
214
+ if (policy?.maxCount !== undefined && currentCount + inputs.length > policy.maxCount) {
161
215
  throw new ArtifactError("invalid_content", `${kind} artifacts accept at most ${policy.maxCount} file assets`);
162
216
  }
163
- if (policy.acceptedMediaTypes?.length && !policy.acceptedMediaTypes.some((accepted) => mediaTypeMatches(accepted, input.mediaType))) {
164
- throw new ArtifactError("invalid_content", `${input.mediaType} is not accepted by ${kind} artifacts`);
217
+ const rejected = inputs.find((input) => policy?.acceptedMediaTypes?.length && !policy.acceptedMediaTypes.some((accepted) => mediaTypeMatches(accepted, input.mediaType)));
218
+ if (rejected) {
219
+ throw new ArtifactError("invalid_content", `${rejected.mediaType} is not accepted by ${kind} artifacts`);
165
220
  }
166
221
  };
222
+ const buildRecord = (ownerId, input, assets) => {
223
+ const definition = options.registry.definitions[input.kind];
224
+ if (!definition) {
225
+ throw new ArtifactError("unknown_kind", `Unknown artifact kind: ${input.kind}`);
226
+ }
227
+ const timestamp = now();
228
+ const artifact = {
229
+ assets: validateAssets(input.kind, assets),
230
+ capabilities: definition.capabilities ?? ["archive", "edit", "preview"],
231
+ content: options.registry.parse(input.kind, input.content),
232
+ createdAt: timestamp,
233
+ createdBy: input.createdBy,
234
+ id: idFactory(),
235
+ kind: input.kind,
236
+ metadata: input.metadata ?? {},
237
+ ownerId,
238
+ provenance: input.provenance,
239
+ revision: 1,
240
+ schemaVersion: definition.schemaVersion ?? 1,
241
+ status: "draft",
242
+ title: input.title.trim(),
243
+ updatedAt: timestamp
244
+ };
245
+ return artifact;
246
+ };
167
247
  const get = async (ownerId, artifactId) => {
168
248
  const artifact = await options.store.get(ownerId, artifactId);
169
- if (!artifact) {
249
+ if (!artifact)
170
250
  throw new ArtifactError("not_found", "Artifact not found");
171
- }
172
251
  return artifact;
173
252
  };
174
- const saveRevision = async (artifact, expectedRevision) => {
175
- const saved = await options.store.save(artifact, expectedRevision);
253
+ const saveRevision = async (artifact, expectedRevision, type, payload) => {
254
+ const saved = await options.store.save(artifact, expectedRevision, [
255
+ event(artifact, type, payload)
256
+ ]);
176
257
  if (!saved) {
177
258
  throw new ArtifactError("conflict", "Artifact changed since it was opened; reload before saving");
178
259
  }
179
260
  return artifact;
180
261
  };
181
- return {
262
+ const requireAssetTransactions = () => {
263
+ if (!options.assetStore?.stage) {
264
+ throw new ArtifactError("asset_transaction_unavailable", "The configured artifact asset store does not support atomic bundles");
265
+ }
266
+ return options.assetStore;
267
+ };
268
+ const service = {
182
269
  archive: async (ownerId, artifactId) => {
183
270
  const current = await get(ownerId, artifactId);
184
271
  requireCapability(current, "archive");
185
- return saveRevision({
272
+ const archived = {
186
273
  ...current,
187
274
  revision: current.revision + 1,
188
275
  status: "archived",
189
276
  updatedAt: now()
190
- }, current.revision);
277
+ };
278
+ return saveRevision(archived, current.revision, "artifact.archived");
191
279
  },
192
280
  attach: async (ownerId, artifactId, input, expectedRevision) => {
193
281
  const current = await get(ownerId, artifactId);
194
282
  requireCapability(current, "attach");
195
- validateNewAsset(current.kind, input, current.assets.length);
283
+ validateAssetInputs(current.kind, [input], current.assets.length);
196
284
  if (!options.assetStore) {
197
285
  throw new ArtifactError("asset_store_unavailable", "No artifact asset store is configured");
198
286
  }
@@ -200,43 +288,96 @@ var createArtifactService = (options) => {
200
288
  artifact: current,
201
289
  idempotencyKey: `artifact:${current.id}:asset:${current.revision + 1}`
202
290
  });
291
+ return saveRevision({
292
+ ...current,
293
+ assets: validateAssets(current.kind, [...current.assets, reference]),
294
+ revision: current.revision + 1,
295
+ updatedAt: now()
296
+ }, expectedRevision ?? current.revision, "artifact.asset_attached", { assetIds: [reference.id] });
297
+ },
298
+ attachBundle: async (ownerId, artifactId, inputs, expectedRevision) => {
299
+ const current = await get(ownerId, artifactId);
300
+ requireCapability(current, "attach");
301
+ validateAssetInputs(current.kind, inputs, current.assets.length);
302
+ const assetStore = requireAssetTransactions();
303
+ const transaction = await assetStore.stage(inputs, {
304
+ artifact: current,
305
+ idempotencyKey: `artifact:${current.id}:bundle:${current.revision + 1}`
306
+ });
203
307
  const assets = validateAssets(current.kind, [
204
- ...current.assets.filter((asset) => asset.id !== reference.id),
205
- reference
308
+ ...current.assets,
309
+ ...transaction.references
206
310
  ]);
207
- return saveRevision({
311
+ const revised = {
208
312
  ...current,
209
313
  assets,
210
314
  revision: current.revision + 1,
211
315
  updatedAt: now()
212
- }, expectedRevision ?? current.revision);
316
+ };
317
+ try {
318
+ await transaction.commit();
319
+ return await saveRevision(revised, expectedRevision ?? current.revision, "artifact.asset_attached", { assetIds: transaction.references.map((asset) => asset.id) });
320
+ } catch (error) {
321
+ await transaction.rollback();
322
+ throw error;
323
+ }
324
+ },
325
+ collectAssetGarbage: async (input) => {
326
+ if (!options.assetStore) {
327
+ throw new ArtifactError("asset_store_unavailable", "No artifact asset store is configured");
328
+ }
329
+ const referenced = new Set(await options.store.listReferencedAssetIds());
330
+ const cutoff = Date.now() - (input.minimumAgeMs ?? 0);
331
+ const candidates = await options.assetStore.listCandidates();
332
+ const deleted = [];
333
+ const retained = [];
334
+ for (const candidate of candidates) {
335
+ if (referenced.has(candidate.reference.id) || new Date(candidate.createdAt).getTime() > cutoff) {
336
+ retained.push(candidate.reference);
337
+ } else {
338
+ deleted.push(candidate.reference);
339
+ if (!input.dryRun)
340
+ await options.assetStore.delete(candidate.reference);
341
+ }
342
+ }
343
+ return { deleted, retained };
213
344
  },
214
345
  create: async (ownerId, input) => {
215
- const definition = options.registry.definitions[input.kind];
216
- if (!definition) {
217
- throw new ArtifactError("unknown_kind", `Unknown artifact kind: ${input.kind}`);
346
+ const artifact = buildRecord(ownerId, input, input.assets ?? []);
347
+ await options.store.create(artifact, [
348
+ event(artifact, "artifact.created")
349
+ ]);
350
+ return artifact;
351
+ },
352
+ createBundle: async (ownerId, input) => {
353
+ const { assets: assetInputs = [], ...createInput } = input;
354
+ validateAssetInputs(input.kind, assetInputs);
355
+ if (assetInputs.length === 0) {
356
+ return service.create(ownerId, createInput);
218
357
  }
219
- const content = options.registry.parse(input.kind, input.content);
220
- const timestamp = now();
358
+ const assetStore = requireAssetTransactions();
359
+ const provisional = buildRecord(ownerId, createInput, []);
360
+ const transaction = await assetStore.stage(assetInputs, {
361
+ artifact: provisional,
362
+ idempotencyKey: `artifact:${provisional.id}:bundle:1`
363
+ });
221
364
  const artifact = {
222
- assets: validateAssets(input.kind, input.assets ?? []),
223
- capabilities: definition.capabilities ?? ["archive", "edit", "preview"],
224
- content,
225
- createdAt: timestamp,
226
- createdBy: input.createdBy,
227
- id: idFactory(),
228
- kind: input.kind,
229
- metadata: input.metadata ?? {},
230
- ownerId,
231
- provenance: input.provenance,
232
- revision: 1,
233
- schemaVersion: definition.schemaVersion ?? 1,
234
- status: "draft",
235
- title: input.title.trim(),
236
- updatedAt: timestamp
365
+ ...provisional,
366
+ assets: validateAssets(input.kind, transaction.references)
237
367
  };
238
- await options.store.create(artifact);
239
- return artifact;
368
+ try {
369
+ await transaction.commit();
370
+ await options.store.create(artifact, [
371
+ event(artifact, "artifact.created"),
372
+ event(artifact, "artifact.generated", {
373
+ assetIds: transaction.references.map((asset) => asset.id)
374
+ })
375
+ ]);
376
+ return artifact;
377
+ } catch (error) {
378
+ await transaction.rollback();
379
+ throw error;
380
+ }
240
381
  },
241
382
  detach: async (ownerId, artifactId, assetId, expectedRevision) => {
242
383
  const current = await get(ownerId, artifactId);
@@ -250,9 +391,10 @@ var createArtifactService = (options) => {
250
391
  assets,
251
392
  revision: current.revision + 1,
252
393
  updatedAt: now()
253
- }, expectedRevision ?? current.revision);
394
+ }, expectedRevision ?? current.revision, "artifact.asset_detached", { assetId });
254
395
  },
255
396
  get,
397
+ getIndexingState: (ownerId, artifactId) => options.store.getIndexingState(ownerId, artifactId),
256
398
  getRevision: async (ownerId, artifactId, revision) => {
257
399
  const snapshot = await options.store.getRevision(ownerId, artifactId, revision);
258
400
  if (!snapshot) {
@@ -261,53 +403,66 @@ var createArtifactService = (options) => {
261
403
  return snapshot;
262
404
  },
263
405
  list: (ownerId, query) => options.store.list(ownerId, query),
406
+ listEvents: (query) => options.store.listEvents(query),
264
407
  listRevisions: (ownerId, artifactId) => options.store.listRevisions(ownerId, artifactId),
265
- publish: async (ownerId, artifactId) => {
408
+ markEventProcessed: (eventId, processedAt = now()) => options.store.markEventProcessed(eventId, processedAt),
409
+ markIndexing: async (ownerId, artifactId, input) => {
410
+ const artifact = await get(ownerId, artifactId);
411
+ const state = {
412
+ artifactId,
413
+ documentIds: input.documentIds ?? [],
414
+ error: input.error,
415
+ indexedAt: input.status === "indexed" ? now() : undefined,
416
+ revision: input.revision,
417
+ status: input.status,
418
+ updatedAt: now()
419
+ };
420
+ await options.store.putIndexingState(ownerId, state, [
421
+ event(artifact, "artifact.indexing_changed", {
422
+ indexingRevision: state.revision,
423
+ indexingStatus: state.status
424
+ })
425
+ ]);
426
+ return state;
427
+ },
428
+ publish: async (ownerId, artifactId, input = {}) => {
266
429
  const current = await get(ownerId, artifactId);
267
430
  requireCapability(current, "publish");
268
431
  if (!options.publisher) {
269
432
  throw new ArtifactError("publisher_unavailable", "No artifact publisher is configured");
270
433
  }
434
+ const mode = input.mode ?? "pinned";
435
+ const publishedRevision = current.revision;
271
436
  const result = await options.publisher.publish(current, {
272
- idempotencyKey: `artifact:${current.id}:publish:${current.revision + 1}`
437
+ idempotencyKey: `artifact:${current.id}:publish:${publishedRevision}:${mode}`,
438
+ mode,
439
+ revision: publishedRevision
273
440
  });
274
441
  const publishedAt = now();
275
442
  const publication = {
276
443
  id: result.id,
444
+ mode,
277
445
  publishedAt,
446
+ revision: publishedRevision,
278
447
  url: result.url
279
448
  };
280
- return saveRevision({
449
+ const published = {
281
450
  ...current,
282
451
  publication,
283
452
  revision: current.revision + 1,
284
453
  status: "published",
285
454
  updatedAt: publishedAt
286
- }, current.revision);
287
- },
288
- unpublish: async (ownerId, artifactId) => {
289
- const current = await get(ownerId, artifactId);
290
- requireCapability(current, "publish");
291
- if (!options.publisher) {
292
- throw new ArtifactError("publisher_unavailable", "No artifact publisher is configured");
293
- }
294
- await options.publisher.unpublish(current, {
295
- idempotencyKey: `artifact:${current.id}:unpublish:${current.revision + 1}`
455
+ };
456
+ return saveRevision(published, current.revision, "artifact.published", {
457
+ mode,
458
+ publishedRevision
296
459
  });
297
- return saveRevision({
298
- ...current,
299
- publication: undefined,
300
- revision: current.revision + 1,
301
- status: "draft",
302
- updatedAt: now()
303
- }, current.revision);
304
460
  },
305
461
  readAsset: async (ownerId, artifactId, assetId) => {
306
462
  const artifact = await get(ownerId, artifactId);
307
463
  const asset = artifact.assets.find((candidate) => candidate.id === assetId);
308
- if (!asset) {
464
+ if (!asset)
309
465
  throw new ArtifactError("not_found", "Artifact asset not found");
310
- }
311
466
  if (!options.assetStore) {
312
467
  throw new ArtifactError("asset_store_unavailable", "No artifact asset store is configured");
313
468
  }
@@ -323,35 +478,54 @@ var createArtifactService = (options) => {
323
478
  if (!snapshot) {
324
479
  throw new ArtifactError("not_found", "Artifact revision not found");
325
480
  }
326
- const timestamp = now();
327
481
  return saveRevision({
328
482
  ...current,
329
483
  assets: validateAssets(current.kind, snapshot.assets),
330
484
  content: options.registry.parse(current.kind, snapshot.content),
331
485
  metadata: snapshot.metadata,
332
486
  publication: undefined,
487
+ provenance: snapshot.provenance,
333
488
  revision: current.revision + 1,
334
489
  status: "draft",
335
490
  title: snapshot.title,
336
- updatedAt: timestamp
337
- }, expectedRevision ?? current.revision);
491
+ updatedAt: now()
492
+ }, expectedRevision ?? current.revision, "artifact.restored", { restoredRevision: revision });
493
+ },
494
+ unpublish: async (ownerId, artifactId) => {
495
+ const current = await get(ownerId, artifactId);
496
+ requireCapability(current, "publish");
497
+ if (!options.publisher) {
498
+ throw new ArtifactError("publisher_unavailable", "No artifact publisher is configured");
499
+ }
500
+ await options.publisher.unpublish(current, {
501
+ idempotencyKey: `artifact:${current.id}:unpublish:${current.revision + 1}`
502
+ });
503
+ return saveRevision({
504
+ ...current,
505
+ publication: undefined,
506
+ revision: current.revision + 1,
507
+ status: "draft",
508
+ updatedAt: now()
509
+ }, current.revision, "artifact.unpublished");
338
510
  },
339
511
  update: async (ownerId, artifactId, input) => {
340
512
  const current = await get(ownerId, artifactId);
341
513
  requireCapability(current, "edit");
342
- const expectedRevision = input.expectedRevision ?? current.revision;
343
- const content = input.content === undefined ? current.content : options.registry.parse(current.kind, input.content);
514
+ const nextRevision = current.revision + 1;
515
+ const publication = current.publication?.mode === "live" ? { ...current.publication, revision: nextRevision } : current.publication;
344
516
  return saveRevision({
345
517
  ...current,
346
518
  assets: input.assets === undefined ? current.assets : validateAssets(current.kind, input.assets),
347
- content,
519
+ content: input.content === undefined ? current.content : options.registry.parse(current.kind, input.content),
348
520
  metadata: input.metadata ?? current.metadata,
349
- revision: current.revision + 1,
521
+ publication,
522
+ revision: nextRevision,
350
523
  title: input.title?.trim() || current.title,
351
524
  updatedAt: now()
352
- }, expectedRevision);
525
+ }, input.expectedRevision ?? current.revision, "artifact.revised");
353
526
  }
354
527
  };
528
+ return service;
355
529
  };
356
530
  // src/store.ts
357
531
  import { createHash } from "crypto";
@@ -361,6 +535,14 @@ var createMemoryArtifactAssetStore = () => {
361
535
  const references = new Map;
362
536
  const idempotency = new Map;
363
537
  return {
538
+ delete: async (reference) => {
539
+ bytes.delete(reference.id);
540
+ references.delete(reference.id);
541
+ },
542
+ listCandidates: async () => [...references.values()].map((reference) => ({
543
+ createdAt: reference.createdAt,
544
+ reference: clone(reference)
545
+ })),
364
546
  read: async (reference) => {
365
547
  const data = bytes.get(reference.id);
366
548
  if (!data)
@@ -377,6 +559,7 @@ var createMemoryArtifactAssetStore = () => {
377
559
  algorithm: "sha256",
378
560
  value: createHash("sha256").update(input.data).digest("hex")
379
561
  },
562
+ createdAt: new Date().toISOString(),
380
563
  id,
381
564
  mediaType: input.mediaType,
382
565
  metadata: input.metadata,
@@ -389,25 +572,81 @@ var createMemoryArtifactAssetStore = () => {
389
572
  references.set(id, clone(reference));
390
573
  idempotency.set(context.idempotencyKey, id);
391
574
  return reference;
575
+ },
576
+ stage: async (inputs, context) => {
577
+ const staged = inputs.map((input, index) => {
578
+ const key = `${context.idempotencyKey}:${index}`;
579
+ const existingId = idempotency.get(key);
580
+ if (existingId) {
581
+ return {
582
+ data: bytes.get(existingId),
583
+ key,
584
+ reference: references.get(existingId)
585
+ };
586
+ }
587
+ const id = crypto.randomUUID();
588
+ const reference = {
589
+ checksum: {
590
+ algorithm: "sha256",
591
+ value: createHash("sha256").update(input.data).digest("hex")
592
+ },
593
+ createdAt: new Date().toISOString(),
594
+ id,
595
+ mediaType: input.mediaType,
596
+ metadata: input.metadata,
597
+ name: input.name,
598
+ role: input.role ?? "attachment",
599
+ size: input.data.byteLength,
600
+ uri: `memory://${id}`
601
+ };
602
+ return { data: clone(input.data), key, reference };
603
+ });
604
+ return {
605
+ commit: async () => {
606
+ for (const item of staged) {
607
+ bytes.set(item.reference.id, clone(item.data));
608
+ references.set(item.reference.id, clone(item.reference));
609
+ idempotency.set(item.key, item.reference.id);
610
+ }
611
+ },
612
+ references: staged.map((item) => clone(item.reference)),
613
+ rollback: async () => {
614
+ for (const item of staged) {
615
+ bytes.delete(item.reference.id);
616
+ references.delete(item.reference.id);
617
+ idempotency.delete(item.key);
618
+ }
619
+ }
620
+ };
392
621
  }
393
622
  };
394
623
  };
395
624
  var createMemoryArtifactStore = (initial = []) => {
396
625
  const records = new Map(initial.map((record) => [record.id, clone(record)]));
397
626
  const revisions = new Map;
627
+ const events = new Map;
628
+ const indexing = new Map;
398
629
  for (const record of initial)
399
630
  revisions.set(record.id, [clone(record)]);
400
631
  return {
401
- create: async (record) => {
632
+ create: async (record, newEvents = []) => {
402
633
  if (records.has(record.id))
403
634
  throw new Error(`Duplicate artifact id: ${record.id}`);
404
635
  records.set(record.id, clone(record));
405
636
  revisions.set(record.id, [clone(record)]);
637
+ for (const event of newEvents)
638
+ events.set(event.id, clone(event));
406
639
  },
407
640
  get: async (ownerId, artifactId) => {
408
641
  const record = records.get(artifactId);
409
642
  return record?.ownerId === ownerId ? clone(record) : null;
410
643
  },
644
+ getIndexingState: async (ownerId, artifactId) => {
645
+ const record = records.get(artifactId);
646
+ if (record?.ownerId !== ownerId)
647
+ return null;
648
+ return clone(indexing.get(artifactId) ?? null);
649
+ },
411
650
  getRevision: async (ownerId, artifactId, revision) => {
412
651
  const current = records.get(artifactId);
413
652
  if (current?.ownerId !== ownerId)
@@ -421,7 +660,27 @@ var createMemoryArtifactStore = (initial = []) => {
421
660
  return [];
422
661
  return (revisions.get(artifactId) ?? []).toSorted((left, right) => right.revision - left.revision).map(clone);
423
662
  },
424
- save: async (record, expectedRevision) => {
663
+ listEvents: async (query = {}) => [...events.values()].filter((event) => (query.processed === undefined || Boolean(event.processedAt) === query.processed) && (!query.type || event.type === query.type)).sort((left, right) => left.createdAt.localeCompare(right.createdAt)).slice(0, query.limit ?? Number.POSITIVE_INFINITY).map(clone),
664
+ listReferencedAssetIds: async () => [
665
+ ...new Set([...revisions.values()].flat().flatMap((revision) => revision.assets.map((asset) => asset.id)))
666
+ ],
667
+ markEventProcessed: async (eventId, processedAt) => {
668
+ const event = events.get(eventId);
669
+ if (!event)
670
+ return false;
671
+ events.set(eventId, { ...event, processedAt });
672
+ return true;
673
+ },
674
+ putIndexingState: async (ownerId, state, newEvents = []) => {
675
+ const record = records.get(state.artifactId);
676
+ if (record?.ownerId !== ownerId) {
677
+ throw new Error("Artifact not found");
678
+ }
679
+ indexing.set(state.artifactId, clone(state));
680
+ for (const event of newEvents)
681
+ events.set(event.id, clone(event));
682
+ },
683
+ save: async (record, expectedRevision, newEvents = []) => {
425
684
  const current = records.get(record.id);
426
685
  if (!current || current.ownerId !== record.ownerId || current.revision !== expectedRevision) {
427
686
  return false;
@@ -431,6 +690,8 @@ var createMemoryArtifactStore = (initial = []) => {
431
690
  ...revisions.get(record.id) ?? [],
432
691
  clone(record)
433
692
  ]);
693
+ for (const event of newEvents)
694
+ events.set(event.id, clone(event));
434
695
  return true;
435
696
  }
436
697
  };
@@ -509,11 +770,14 @@ var createArtifactTools = (options) => ({
509
770
  if (!artifactId || typeof input.published !== "boolean") {
510
771
  return "Provide artifactId and published.";
511
772
  }
512
- const artifact = input.published ? await options.service.publish(options.ownerId, artifactId) : await options.service.unpublish(options.ownerId, artifactId);
773
+ const artifact = input.published ? await options.service.publish(options.ownerId, artifactId, {
774
+ mode: input.mode === "live" ? "live" : "pinned"
775
+ }) : await options.service.unpublish(options.ownerId, artifactId);
513
776
  return JSON.stringify(artifact);
514
777
  },
515
778
  input: Type2.Object({
516
779
  artifactId: Type2.String({ minLength: 1 }),
780
+ mode: Type2.Optional(Type2.Union([Type2.Literal("live"), Type2.Literal("pinned")])),
517
781
  published: Type2.Boolean()
518
782
  })
519
783
  },
@@ -563,7 +827,9 @@ export {
563
827
  createArtifactTools,
564
828
  createArtifactService,
565
829
  createArtifactRendererRegistry,
830
+ createArtifactGeneratorRegistry,
566
831
  STANDARD_ARTIFACT_KIND_NAMES,
567
832
  ArtifactError,
568
- ARTIFACT_STATUSES
833
+ ARTIFACT_STATUSES,
834
+ ARTIFACT_EVENT_TYPES
569
835
  };
package/dist/rag.js CHANGED
@@ -39,6 +39,42 @@ var artifactToRAGUploads = async (artifact, reader, options = {}) => {
39
39
  ...uploads
40
40
  ];
41
41
  };
42
+ var createArtifactRAGIndexCoordinator = (options) => ({
43
+ index: async (artifact) => {
44
+ const previous = await options.service.getIndexingState(artifact.ownerId, artifact.id);
45
+ await options.service.markIndexing(artifact.ownerId, artifact.id, {
46
+ documentIds: previous?.documentIds,
47
+ revision: artifact.revision,
48
+ status: "pending"
49
+ });
50
+ try {
51
+ const uploads = await artifactToRAGUploads(artifact, options.reader);
52
+ const indexed = await options.target.index(uploads, { artifact });
53
+ if (previous?.documentIds.length && options.target.remove) {
54
+ const currentIds = new Set(indexed.documentIds);
55
+ const obsoleteIds = previous.documentIds.filter((documentId) => !currentIds.has(documentId));
56
+ if (obsoleteIds.length) {
57
+ await options.target.remove(obsoleteIds, { artifact });
58
+ }
59
+ }
60
+ await options.service.markIndexing(artifact.ownerId, artifact.id, {
61
+ documentIds: indexed.documentIds,
62
+ revision: artifact.revision,
63
+ status: "indexed"
64
+ });
65
+ return indexed;
66
+ } catch (error) {
67
+ await options.service.markIndexing(artifact.ownerId, artifact.id, {
68
+ documentIds: previous?.documentIds,
69
+ error: error instanceof Error ? error.message : String(error),
70
+ revision: artifact.revision,
71
+ status: "failed"
72
+ });
73
+ throw error;
74
+ }
75
+ }
76
+ });
42
77
  export {
78
+ createArtifactRAGIndexCoordinator,
43
79
  artifactToRAGUploads
44
80
  };
@@ -0,0 +1,34 @@
1
+ import type { ArtifactAssetWriteInput, ArtifactBundleCreateInput, ArtifactProvenance, ArtifactRecord } from "./types";
2
+ export type ArtifactGenerationInput = {
3
+ createdBy: string;
4
+ input?: Record<string, unknown>;
5
+ kind: string;
6
+ ownerId: string;
7
+ prompt?: string;
8
+ title?: string;
9
+ };
10
+ export type ArtifactGenerationContext = {
11
+ ownerId: string;
12
+ };
13
+ export type ArtifactGenerationResult = {
14
+ assets?: ArtifactAssetWriteInput[];
15
+ content: unknown;
16
+ metadata?: Record<string, unknown>;
17
+ provenance?: ArtifactProvenance;
18
+ title?: string;
19
+ warnings?: string[];
20
+ };
21
+ export type ArtifactGenerator = {
22
+ generate(input: ArtifactGenerationInput, context: ArtifactGenerationContext): Promise<ArtifactGenerationResult>;
23
+ kind: string;
24
+ name: string;
25
+ };
26
+ export type ArtifactBundleCreator = {
27
+ createBundle(ownerId: string, input: ArtifactBundleCreateInput): Promise<ArtifactRecord>;
28
+ };
29
+ export declare const createArtifactGeneratorRegistry: (initial?: ArtifactGenerator[]) => {
30
+ generate: (service: ArtifactBundleCreator, input: ArtifactGenerationInput) => Promise<ArtifactRecord>;
31
+ kinds: () => string[];
32
+ register: (generator: ArtifactGenerator) => void;
33
+ };
34
+ export type ArtifactGeneratorRegistry = ReturnType<typeof createArtifactGeneratorRegistry>;
@@ -7,9 +7,10 @@
7
7
  * retains authorization, persistence, URLs, UI, and delivery policy.
8
8
  */
9
9
  export { defineArtifactRegistry, type ArtifactContent, type ArtifactKindDefinition, type ArtifactKindDefinitions, type ArtifactRegistry, } from "./registry";
10
+ export { createArtifactGeneratorRegistry, type ArtifactBundleCreator, type ArtifactGenerationContext, type ArtifactGenerationInput, type ArtifactGenerationResult, type ArtifactGenerator, type ArtifactGeneratorRegistry, } from "./generators";
10
11
  export { STANDARD_ARTIFACT_KIND_NAMES, standardArtifactDefinitions, } from "./standardKinds";
11
12
  export { createArtifactRendererRegistry, type ArtifactRenderer, type ArtifactRendererRegistry, type ArtifactRenderResult, } from "./renderers";
12
13
  export { createArtifactService, type ArtifactPublisher, type ArtifactService, type ArtifactServiceOptions, } from "./service";
13
- export { createMemoryArtifactStore, createMemoryArtifactAssetStore, type ArtifactAssetStore, type ArtifactStore, } from "./store";
14
+ export { createMemoryArtifactStore, createMemoryArtifactAssetStore, type ArtifactAssetStore, type ArtifactAssetTransaction, type ArtifactStore, } from "./store";
14
15
  export { createArtifactTools, type ArtifactToolDefinition, type ArtifactToolMap, type ArtifactToolOptions, } from "./tools";
15
- export { ARTIFACT_STATUSES, ArtifactError, type ArtifactAssetReference, type ArtifactAssetRole, type ArtifactAssetWriteInput, type ArtifactCapability, type ArtifactCreateInput, type ArtifactErrorCode, type ArtifactListQuery, type ArtifactProvenance, type ArtifactPublication, type ArtifactRecord, type ArtifactRevision, type ArtifactStatus, type ArtifactUpdateInput, } from "./types";
16
+ export { ARTIFACT_STATUSES, ARTIFACT_EVENT_TYPES, ArtifactError, type ArtifactAssetReference, type ArtifactAssetRole, type ArtifactAssetWriteInput, type ArtifactBundleCreateInput, type ArtifactCapability, type ArtifactCreateInput, type ArtifactErrorCode, type ArtifactEvent, type ArtifactEventQuery, type ArtifactEventType, type ArtifactGarbageCollectionResult, type ArtifactIndexingState, type ArtifactIndexingStatus, type ArtifactListQuery, type ArtifactLineageReference, type ArtifactLineageRelation, type ArtifactProvenance, type ArtifactPublication, type ArtifactPublishInput, type ArtifactRecord, type ArtifactRevision, type ArtifactRetentionCandidate, type ArtifactStatus, type ArtifactUpdateInput, } from "./types";
@@ -1,18 +1,33 @@
1
1
  export declare const manifest: import("@absolutejs/manifest").PackageManifest<Record<string, never>, {
2
2
  archive: (ownerId: string, artifactId: string) => Promise<import("./types").ArtifactRecord>;
3
3
  attach: (ownerId: string, artifactId: string, input: import("./types").ArtifactAssetWriteInput, expectedRevision?: number) => Promise<import("./types").ArtifactRecord>;
4
+ attachBundle: (ownerId: string, artifactId: string, inputs: import("./types").ArtifactAssetWriteInput[], expectedRevision?: number) => Promise<import("./types").ArtifactRecord>;
5
+ collectAssetGarbage: (input: {
6
+ dryRun?: boolean;
7
+ minimumAgeMs?: number;
8
+ }) => Promise<import("./types").ArtifactGarbageCollectionResult>;
4
9
  create: (ownerId: string, input: import("./types").ArtifactCreateInput) => Promise<import("./types").ArtifactRecord>;
10
+ createBundle: (ownerId: string, input: import("./types").ArtifactBundleCreateInput) => Promise<import("./types").ArtifactRecord>;
5
11
  detach: (ownerId: string, artifactId: string, assetId: string, expectedRevision?: number) => Promise<import("./types").ArtifactRecord>;
6
12
  get: (ownerId: string, artifactId: string) => Promise<import("./types").ArtifactRecord>;
13
+ getIndexingState: (ownerId: string, artifactId: string) => Promise<import("./types").ArtifactIndexingState | null>;
7
14
  getRevision: (ownerId: string, artifactId: string, revision: number) => Promise<Readonly<import("./types").ArtifactRecord<unknown>>>;
8
15
  list: (ownerId: string, query?: import("./types").ArtifactListQuery) => Promise<import("./types").ArtifactRecord[]>;
16
+ listEvents: (query?: import("./types").ArtifactEventQuery) => Promise<import("./types").ArtifactEvent[]>;
9
17
  listRevisions: (ownerId: string, artifactId: string) => Promise<Readonly<import("./types").ArtifactRecord<unknown>>[]>;
10
- publish: (ownerId: string, artifactId: string) => Promise<import("./types").ArtifactRecord>;
11
- unpublish: (ownerId: string, artifactId: string) => Promise<import("./types").ArtifactRecord>;
18
+ markEventProcessed: (eventId: string, processedAt?: string) => Promise<boolean>;
19
+ markIndexing: (ownerId: string, artifactId: string, input: {
20
+ documentIds?: string[];
21
+ error?: string;
22
+ revision: number;
23
+ status: import("./types").ArtifactIndexingStatus;
24
+ }) => Promise<import("./types").ArtifactIndexingState>;
25
+ publish: (ownerId: string, artifactId: string, input?: import("./types").ArtifactPublishInput) => Promise<import("./types").ArtifactRecord>;
12
26
  readAsset: (ownerId: string, artifactId: string, assetId: string) => Promise<{
13
27
  asset: import("./types").ArtifactAssetReference;
14
28
  data: Uint8Array<ArrayBufferLike>;
15
29
  }>;
16
30
  restore: (ownerId: string, artifactId: string, revision: number, expectedRevision?: number) => Promise<import("./types").ArtifactRecord>;
31
+ unpublish: (ownerId: string, artifactId: string) => Promise<import("./types").ArtifactRecord>;
17
32
  update: (ownerId: string, artifactId: string, input: import("./types").ArtifactUpdateInput) => Promise<import("./types").ArtifactRecord>;
18
33
  }>;
package/dist/src/rag.d.ts CHANGED
@@ -8,8 +8,39 @@ export type ArtifactRAGAssetReader = {
8
8
  export type ArtifactRAGUploadOptions = {
9
9
  includeStructuredContent?: boolean;
10
10
  };
11
+ export type ArtifactRAGIndexTarget = {
12
+ index(uploads: RAGDocumentUploadInput[], context: {
13
+ artifact: ArtifactRecord;
14
+ }): Promise<{
15
+ documentIds: string[];
16
+ }>;
17
+ remove?(documentIds: string[], context: {
18
+ artifact: ArtifactRecord;
19
+ }): Promise<void>;
20
+ };
21
+ export type ArtifactRAGIndexStateWriter = {
22
+ getIndexingState(ownerId: string, artifactId: string): Promise<{
23
+ documentIds: string[];
24
+ } | null>;
25
+ markIndexing(ownerId: string, artifactId: string, input: {
26
+ documentIds?: string[];
27
+ error?: string;
28
+ revision: number;
29
+ status: "failed" | "indexed" | "pending" | "stale";
30
+ }): Promise<unknown>;
31
+ };
11
32
  /**
12
33
  * Resolve an artifact revision into upload inputs accepted by @absolutejs/rag.
13
34
  * Storage URIs remain opaque; only the supplied reader is allowed to access bytes.
14
35
  */
15
36
  export declare const artifactToRAGUploads: (artifact: ArtifactRecord, reader: ArtifactRAGAssetReader, options?: ArtifactRAGUploadOptions) => Promise<RAGDocumentUploadInput[]>;
37
+ export declare const createArtifactRAGIndexCoordinator: (options: {
38
+ reader: ArtifactRAGAssetReader;
39
+ service: ArtifactRAGIndexStateWriter;
40
+ target: ArtifactRAGIndexTarget;
41
+ }) => {
42
+ index: (artifact: ArtifactRecord) => Promise<{
43
+ documentIds: string[];
44
+ }>;
45
+ };
46
+ export type ArtifactRAGIndexCoordinator = ReturnType<typeof createArtifactRAGIndexCoordinator>;
@@ -1,9 +1,11 @@
1
1
  import type { ArtifactKindDefinitions, ArtifactRegistry } from "./registry";
2
2
  import type { ArtifactAssetStore, ArtifactStore } from "./store";
3
- import { type ArtifactAssetReference, type ArtifactAssetWriteInput, type ArtifactCreateInput, type ArtifactListQuery, type ArtifactRecord, type ArtifactUpdateInput } from "./types";
3
+ import { type ArtifactAssetReference, type ArtifactAssetWriteInput, type ArtifactBundleCreateInput, type ArtifactCreateInput, type ArtifactEvent, type ArtifactEventQuery, type ArtifactGarbageCollectionResult, type ArtifactIndexingState, type ArtifactIndexingStatus, type ArtifactListQuery, type ArtifactPublishInput, type ArtifactRecord, type ArtifactUpdateInput } from "./types";
4
4
  export type ArtifactPublisher = {
5
5
  publish(artifact: ArtifactRecord, options: {
6
6
  idempotencyKey: string;
7
+ mode: "live" | "pinned";
8
+ revision: number;
7
9
  }): Promise<{
8
10
  id: string;
9
11
  url: string;
@@ -15,6 +17,7 @@ export type ArtifactPublisher = {
15
17
  export type ArtifactServiceOptions<TDefinitions extends ArtifactKindDefinitions = ArtifactKindDefinitions> = {
16
18
  assetStore?: ArtifactAssetStore;
17
19
  clock?: () => Date;
20
+ eventIdFactory?: () => string;
18
21
  idFactory?: () => string;
19
22
  publisher?: ArtifactPublisher;
20
23
  registry: ArtifactRegistry<TDefinitions>;
@@ -24,18 +27,33 @@ export type ArtifactService = ReturnType<typeof createArtifactService>;
24
27
  export declare const createArtifactService: <TDefinitions extends ArtifactKindDefinitions>(options: ArtifactServiceOptions<TDefinitions>) => {
25
28
  archive: (ownerId: string, artifactId: string) => Promise<ArtifactRecord>;
26
29
  attach: (ownerId: string, artifactId: string, input: ArtifactAssetWriteInput, expectedRevision?: number) => Promise<ArtifactRecord>;
30
+ attachBundle: (ownerId: string, artifactId: string, inputs: ArtifactAssetWriteInput[], expectedRevision?: number) => Promise<ArtifactRecord>;
31
+ collectAssetGarbage: (input: {
32
+ dryRun?: boolean;
33
+ minimumAgeMs?: number;
34
+ }) => Promise<ArtifactGarbageCollectionResult>;
27
35
  create: (ownerId: string, input: ArtifactCreateInput) => Promise<ArtifactRecord>;
36
+ createBundle: (ownerId: string, input: ArtifactBundleCreateInput) => Promise<ArtifactRecord>;
28
37
  detach: (ownerId: string, artifactId: string, assetId: string, expectedRevision?: number) => Promise<ArtifactRecord>;
29
38
  get: (ownerId: string, artifactId: string) => Promise<ArtifactRecord>;
39
+ getIndexingState: (ownerId: string, artifactId: string) => Promise<ArtifactIndexingState | null>;
30
40
  getRevision: (ownerId: string, artifactId: string, revision: number) => Promise<Readonly<ArtifactRecord<unknown>>>;
31
41
  list: (ownerId: string, query?: ArtifactListQuery) => Promise<ArtifactRecord[]>;
42
+ listEvents: (query?: ArtifactEventQuery) => Promise<ArtifactEvent[]>;
32
43
  listRevisions: (ownerId: string, artifactId: string) => Promise<Readonly<ArtifactRecord<unknown>>[]>;
33
- publish: (ownerId: string, artifactId: string) => Promise<ArtifactRecord>;
34
- unpublish: (ownerId: string, artifactId: string) => Promise<ArtifactRecord>;
44
+ markEventProcessed: (eventId: string, processedAt?: string) => Promise<boolean>;
45
+ markIndexing: (ownerId: string, artifactId: string, input: {
46
+ documentIds?: string[];
47
+ error?: string;
48
+ revision: number;
49
+ status: ArtifactIndexingStatus;
50
+ }) => Promise<ArtifactIndexingState>;
51
+ publish: (ownerId: string, artifactId: string, input?: ArtifactPublishInput) => Promise<ArtifactRecord>;
35
52
  readAsset: (ownerId: string, artifactId: string, assetId: string) => Promise<{
36
53
  asset: ArtifactAssetReference;
37
54
  data: Uint8Array<ArrayBufferLike>;
38
55
  }>;
39
56
  restore: (ownerId: string, artifactId: string, revision: number, expectedRevision?: number) => Promise<ArtifactRecord>;
57
+ unpublish: (ownerId: string, artifactId: string) => Promise<ArtifactRecord>;
40
58
  update: (ownerId: string, artifactId: string, input: ArtifactUpdateInput) => Promise<ArtifactRecord>;
41
59
  };
@@ -1,5 +1,12 @@
1
- import type { ArtifactAssetReference, ArtifactAssetWriteInput, ArtifactListQuery, ArtifactRecord, ArtifactRevision } from "./types";
1
+ import type { ArtifactAssetReference, ArtifactAssetWriteInput, ArtifactEvent, ArtifactEventQuery, ArtifactIndexingState, ArtifactListQuery, ArtifactRecord, ArtifactRevision, ArtifactRetentionCandidate } from "./types";
2
+ export type ArtifactAssetTransaction = {
3
+ commit(): Promise<void>;
4
+ references: ArtifactAssetReference[];
5
+ rollback(): Promise<void>;
6
+ };
2
7
  export type ArtifactAssetStore = {
8
+ delete(reference: ArtifactAssetReference): Promise<void>;
9
+ listCandidates(): Promise<ArtifactRetentionCandidate[]>;
3
10
  read(reference: ArtifactAssetReference, context: {
4
11
  artifact: ArtifactRecord;
5
12
  }): Promise<Uint8Array>;
@@ -7,16 +14,25 @@ export type ArtifactAssetStore = {
7
14
  artifact: ArtifactRecord;
8
15
  idempotencyKey: string;
9
16
  }): Promise<ArtifactAssetReference>;
17
+ stage?(inputs: ArtifactAssetWriteInput[], context: {
18
+ artifact: ArtifactRecord;
19
+ idempotencyKey: string;
20
+ }): Promise<ArtifactAssetTransaction>;
10
21
  };
11
22
  export type ArtifactStore = {
12
23
  /** Persist the current record and its first immutable revision atomically. */
13
- create(record: ArtifactRecord): Promise<void>;
24
+ create(record: ArtifactRecord, events?: ArtifactEvent[]): Promise<void>;
25
+ getIndexingState(ownerId: string, artifactId: string): Promise<ArtifactIndexingState | null>;
14
26
  get(ownerId: string, artifactId: string): Promise<ArtifactRecord | null>;
15
27
  getRevision(ownerId: string, artifactId: string, revision: number): Promise<ArtifactRevision | null>;
16
28
  list(ownerId: string, query?: ArtifactListQuery): Promise<ArtifactRecord[]>;
17
29
  listRevisions(ownerId: string, artifactId: string): Promise<ArtifactRevision[]>;
30
+ listEvents(query?: ArtifactEventQuery): Promise<ArtifactEvent[]>;
31
+ listReferencedAssetIds(): Promise<string[]>;
32
+ markEventProcessed(eventId: string, processedAt: string): Promise<boolean>;
33
+ putIndexingState(ownerId: string, state: ArtifactIndexingState, events?: ArtifactEvent[]): Promise<void>;
18
34
  /** Compare, replace current state, and append its revision atomically. */
19
- save(record: ArtifactRecord, expectedRevision: number): Promise<boolean>;
35
+ save(record: ArtifactRecord, expectedRevision: number, events?: ArtifactEvent[]): Promise<boolean>;
20
36
  };
21
37
  export declare const createMemoryArtifactAssetStore: () => ArtifactAssetStore;
22
38
  export declare const createMemoryArtifactStore: (initial?: ArtifactRecord[]) => ArtifactStore;
@@ -2,14 +2,24 @@ export declare const ARTIFACT_STATUSES: readonly ["draft", "published", "archive
2
2
  export type ArtifactStatus = (typeof ARTIFACT_STATUSES)[number];
3
3
  export type ArtifactCapability = "attach" | "archive" | "edit" | "export" | "preview" | "publish" | "refine";
4
4
  export type ArtifactProvenance = {
5
+ lineage?: ArtifactLineageReference[];
5
6
  model?: string;
6
7
  sourceIds?: string[];
7
8
  tool?: string;
8
9
  traceId?: string;
9
10
  };
11
+ export type ArtifactLineageRelation = "derived_from" | "generated_from" | "references" | "replaces";
12
+ export type ArtifactLineageReference = {
13
+ artifactId?: string;
14
+ relation: ArtifactLineageRelation;
15
+ revision?: number;
16
+ sourceId?: string;
17
+ };
10
18
  export type ArtifactPublication = {
11
19
  id: string;
20
+ mode: "live" | "pinned";
12
21
  publishedAt: string;
22
+ revision: number;
13
23
  url: string;
14
24
  };
15
25
  export type ArtifactAssetRole = "attachment" | "primary" | "preview" | "source";
@@ -19,6 +29,7 @@ export type ArtifactAssetReference = {
19
29
  value: string;
20
30
  };
21
31
  id: string;
32
+ createdAt: string;
22
33
  mediaType: string;
23
34
  metadata?: Record<string, unknown>;
24
35
  name: string;
@@ -52,6 +63,33 @@ export type ArtifactRecord<TContent = unknown> = {
52
63
  title: string;
53
64
  updatedAt: string;
54
65
  };
66
+ export declare const ARTIFACT_EVENT_TYPES: readonly ["artifact.archived", "artifact.asset_attached", "artifact.asset_detached", "artifact.created", "artifact.generated", "artifact.indexing_changed", "artifact.published", "artifact.restored", "artifact.revised", "artifact.unpublished"];
67
+ export type ArtifactEventType = (typeof ARTIFACT_EVENT_TYPES)[number];
68
+ export type ArtifactEvent = {
69
+ artifactId: string;
70
+ createdAt: string;
71
+ id: string;
72
+ ownerId: string;
73
+ payload?: Record<string, unknown>;
74
+ processedAt?: string;
75
+ revision: number;
76
+ type: ArtifactEventType;
77
+ };
78
+ export type ArtifactEventQuery = {
79
+ limit?: number;
80
+ processed?: boolean;
81
+ type?: ArtifactEventType;
82
+ };
83
+ export type ArtifactIndexingStatus = "failed" | "indexed" | "pending" | "stale";
84
+ export type ArtifactIndexingState = {
85
+ artifactId: string;
86
+ documentIds: string[];
87
+ error?: string;
88
+ indexedAt?: string;
89
+ revision: number;
90
+ status: ArtifactIndexingStatus;
91
+ updatedAt: string;
92
+ };
55
93
  /** An immutable point-in-time copy of an artifact record. */
56
94
  export type ArtifactRevision<TContent = unknown> = Readonly<ArtifactRecord<TContent>>;
57
95
  export type ArtifactListQuery = {
@@ -75,7 +113,21 @@ export type ArtifactUpdateInput = {
75
113
  metadata?: Record<string, unknown>;
76
114
  title?: string;
77
115
  };
78
- export type ArtifactErrorCode = "asset_store_unavailable" | "conflict" | "invalid_content" | "not_found" | "publisher_unavailable" | "renderer_unavailable" | "unsupported_capability" | "unknown_kind";
116
+ export type ArtifactBundleCreateInput = Omit<ArtifactCreateInput, "assets"> & {
117
+ assets?: ArtifactAssetWriteInput[];
118
+ };
119
+ export type ArtifactPublishInput = {
120
+ mode?: "live" | "pinned";
121
+ };
122
+ export type ArtifactRetentionCandidate = {
123
+ createdAt: string;
124
+ reference: ArtifactAssetReference;
125
+ };
126
+ export type ArtifactGarbageCollectionResult = {
127
+ deleted: ArtifactAssetReference[];
128
+ retained: ArtifactAssetReference[];
129
+ };
130
+ export type ArtifactErrorCode = "asset_store_unavailable" | "asset_transaction_unavailable" | "conflict" | "generator_unavailable" | "invalid_content" | "not_found" | "publisher_unavailable" | "renderer_unavailable" | "unsupported_capability" | "unknown_kind";
79
131
  export declare class ArtifactError extends Error {
80
132
  readonly code: ArtifactErrorCode;
81
133
  constructor(code: ArtifactErrorCode, message: string);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/artifacts",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Typed, versioned artifacts for AI products — schemas, lifecycle, storage, rendering, publishing, revisions, and agent tools without prescribing a database or host.",
5
5
  "author": "Alex Kahn",
6
6
  "license": "BUSL-1.1",