@artkit-ai/core 0.1.0 → 0.2.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 (87) hide show
  1. package/dist/adapter.d.ts +119 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +45 -0
  4. package/dist/adapter.js.map +1 -0
  5. package/dist/diff.d.ts +1 -1
  6. package/dist/diff.d.ts.map +1 -1
  7. package/dist/engine.d.ts +29 -23
  8. package/dist/engine.d.ts.map +1 -1
  9. package/dist/engine.js +148 -197
  10. package/dist/engine.js.map +1 -1
  11. package/dist/envelope.d.ts +10 -27
  12. package/dist/envelope.d.ts.map +1 -1
  13. package/dist/envelope.js +8 -20
  14. package/dist/envelope.js.map +1 -1
  15. package/dist/errors.d.ts +1 -1
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +4 -4
  18. package/dist/errors.js.map +1 -1
  19. package/dist/events.d.ts +2 -10
  20. package/dist/events.d.ts.map +1 -1
  21. package/dist/events.js.map +1 -1
  22. package/dist/i18n.d.ts +8 -14
  23. package/dist/i18n.d.ts.map +1 -1
  24. package/dist/i18n.js +12 -18
  25. package/dist/i18n.js.map +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/logger.d.ts +1 -2
  31. package/dist/logger.d.ts.map +1 -1
  32. package/dist/logger.js.map +1 -1
  33. package/dist/memory.d.ts +0 -2
  34. package/dist/memory.d.ts.map +1 -1
  35. package/dist/memory.js +15 -109
  36. package/dist/memory.js.map +1 -1
  37. package/dist/migrations.d.ts +6 -5
  38. package/dist/migrations.d.ts.map +1 -1
  39. package/dist/migrations.js +20 -14
  40. package/dist/migrations.js.map +1 -1
  41. package/dist/projection.d.ts +22 -6
  42. package/dist/projection.d.ts.map +1 -1
  43. package/dist/projection.js +5 -5
  44. package/dist/projection.js.map +1 -1
  45. package/dist/registry.d.ts +17 -16
  46. package/dist/registry.d.ts.map +1 -1
  47. package/dist/registry.js +25 -25
  48. package/dist/registry.js.map +1 -1
  49. package/dist/service.d.ts +19 -51
  50. package/dist/service.d.ts.map +1 -1
  51. package/dist/service.js +119 -404
  52. package/dist/service.js.map +1 -1
  53. package/dist/store.d.ts +12 -61
  54. package/dist/store.d.ts.map +1 -1
  55. package/dist/testing/index.d.ts +10 -9
  56. package/dist/testing/index.d.ts.map +1 -1
  57. package/dist/testing/index.js +75 -136
  58. package/dist/testing/index.js.map +1 -1
  59. package/dist/theme.js +1 -1
  60. package/dist/ui.d.ts +18 -1
  61. package/dist/ui.d.ts.map +1 -1
  62. package/dist/ui.js +5 -3
  63. package/dist/ui.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/adapter.ts +180 -0
  66. package/src/diff.ts +1 -1
  67. package/src/engine.ts +183 -230
  68. package/src/envelope.ts +11 -29
  69. package/src/errors.ts +4 -4
  70. package/src/events.ts +2 -11
  71. package/src/i18n.ts +14 -20
  72. package/src/index.ts +2 -4
  73. package/src/logger.ts +1 -2
  74. package/src/memory.ts +16 -122
  75. package/src/migrations.ts +24 -18
  76. package/src/projection.ts +25 -7
  77. package/src/registry.ts +45 -41
  78. package/src/service.ts +144 -540
  79. package/src/store.ts +12 -64
  80. package/src/testing/index.ts +89 -164
  81. package/src/theme.ts +1 -1
  82. package/src/ui.ts +24 -4
  83. package/dist/kind.d.ts +0 -156
  84. package/dist/kind.d.ts.map +0 -1
  85. package/dist/kind.js +0 -52
  86. package/dist/kind.js.map +0 -1
  87. package/src/kind.ts +0 -217
package/src/errors.ts CHANGED
@@ -3,11 +3,11 @@ import { t } from './i18n.js';
3
3
 
4
4
  /** Error catalog (spec §10.3). `INTERNAL_ERROR` is the only extension: unexpected failures never leak details. */
5
5
  export const ERROR_CODES = [
6
- 'UNKNOWN_KIND',
7
- 'DUPLICATE_KIND',
6
+ 'UNKNOWN_FORMAT',
7
+ 'DUPLICATE_FORMAT',
8
8
  'NOT_FOUND',
9
9
  'INVALID_ENVELOPE',
10
- 'UNSUPPORTED_VERSION',
10
+ 'UNSUPPORTED_FORMAT_VERSION',
11
11
  'MIGRATION_FAILED',
12
12
  'INVALID_CONTENT',
13
13
  'INVALID_OPERATION',
@@ -16,9 +16,9 @@ export const ERROR_CODES = [
16
16
  'LOCKED',
17
17
  'LIMIT_EXCEEDED',
18
18
  'PERMISSION_DENIED',
19
- 'APPROVAL_REQUIRED',
20
19
  'ASSET_REJECTED',
21
20
  'EXPORT_FAILED',
21
+ 'ENGINE_UNAVAILABLE',
22
22
  'INTERNAL_ERROR',
23
23
  ] as const;
24
24
 
package/src/events.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Actor, ArtifactStatus, ArtifactSummary } from './envelope.js';
2
- import type { ChangeSummary } from './kind.js';
1
+ import type { ChangeSummary } from './adapter.js';
2
+ import type { Actor, ArtifactSummary } from './envelope.js';
3
3
 
4
4
  /** Typed events (spec §15.4). They carry metadata only, never content. */
5
5
  export type ArtifactEvent =
@@ -10,7 +10,6 @@ export type ArtifactEvent =
10
10
  actor: Actor;
11
11
  operationCount: number;
12
12
  diff: ChangeSummary[];
13
- rebased: boolean;
14
13
  }
15
14
  | {
16
15
  type: 'artifact.conflict';
@@ -19,14 +18,6 @@ export type ArtifactEvent =
19
18
  currentVersion: number;
20
19
  actor: Actor;
21
20
  }
22
- | {
23
- type: 'artifact.status_changed';
24
- artifact: ArtifactSummary;
25
- from: ArtifactStatus;
26
- to: ArtifactStatus;
27
- actor: Actor;
28
- message?: string;
29
- }
30
21
  | { type: 'artifact.exported'; artifact: ArtifactSummary; format: string; actor: Actor; warnings: number }
31
22
  | { type: 'artifact.lint'; artifactId: string; version: number; warnings: number };
32
23
 
package/src/i18n.ts CHANGED
@@ -9,14 +9,14 @@ type Template = (params: Record<string, unknown>) => string;
9
9
 
10
10
  const messages = {
11
11
  'pt-BR': {
12
- UNKNOWN_KIND: (p) => `O tipo "${p.kind}" não está registrado.`,
13
- UNKNOWN_KIND_HINT: (p) => `Use um dos tipos disponíveis: ${p.available}.`,
14
- DUPLICATE_KIND: (p) => `O tipo "${p.kind}" já está registrado.`,
12
+ UNKNOWN_FORMAT: (p) => `O formato "${p.format}" não está registrado.`,
13
+ UNKNOWN_FORMAT_HINT: (p) => `Use um dos formatos disponíveis: ${p.available}.`,
14
+ DUPLICATE_FORMAT: (p) => `O formato "${p.format}" já está registrado.`,
15
15
  NOT_FOUND: (p) => `${p.what} não encontrado: ${p.id}.`,
16
16
  NOT_FOUND_HINT: () => 'Confira o id com artifact_list ou leia o artefato novamente.',
17
17
  INVALID_ENVELOPE: () => 'O envelope do artefato é inválido.',
18
- UNSUPPORTED_VERSION: (p) =>
19
- `O conteúdo usa a versão de esquema ${p.found}, mas o tipo "${p.kind}" suporta até a versão ${p.supported}.`,
18
+ UNSUPPORTED_FORMAT_VERSION: (p) =>
19
+ `O conteúdo usa a versão de formato ${p.found}, mas o formato "${p.format}" lê apenas ${p.supported}.`,
20
20
  MIGRATION_FAILED: (p) => `Falha ao migrar o conteúdo da versão ${p.from} para a ${p.to}.`,
21
21
  INVALID_CONTENT: () => 'O conteúdo é inválido.',
22
22
  INVALID_OPERATION: (p) => `A operação ${p.index} é inválida.`,
@@ -30,15 +30,12 @@ const messages = {
30
30
  LIMIT_EXCEEDED: (p) => `Limite "${p.limit}" excedido: máximo ${p.max}, encontrado ${p.found}.`,
31
31
  LIMIT_EXCEEDED_HINT: () => 'Divida o conteúdo em mais artefatos ou reduza a quantidade de itens.',
32
32
  PERMISSION_DENIED: (p) => `Ação "${p.action}" negada.`,
33
- APPROVAL_REQUIRED: (p) => `A ação "${p.action}" depende de aprovação humana.`,
34
- APPROVAL_REQUIRED_HINT: () => 'Use artifact_submit para enviar o artefato para aprovação.',
35
33
  ASSET_REJECTED: (p) => `Asset rejeitado: ${p.reason}.`,
36
34
  EXPORT_FAILED: (p) => `Falha ao exportar para ${p.format}.`,
37
35
  INTERNAL_ERROR: () => 'Erro interno ao processar a solicitação.',
38
- INVALID_STATUS_TRANSITION: (p) => `Não é possível mudar o status de "${p.from}" para "${p.to}".`,
39
- INPUT_FORMAT_UNSUPPORTED: (p) => `O tipo "${p.kind}" não aceita a entrada "${p.format}".`,
36
+ INPUT_FORMAT_UNSUPPORTED: (p) => `O formato "${p.format}" não aceita a entrada "${p.input}".`,
40
37
  INPUT_FORMAT_HINT: (p) => `Formatos aceitos: ${p.available}.`,
41
- EXPORTER_NOT_FOUND: (p) => `Não há exportador de "${p.kind}" para o formato "${p.format}".`,
38
+ EXPORTER_NOT_FOUND: (p) => `Não há exportador de "${p.format}" para o formato "${p.target}".`,
42
39
  THEME_NOT_FOUND: (p) => `Tema "${p.themeId}" não registrado.`,
43
40
  DUPLICATE_ID: (p) => `O id "${p.id}" está repetido.`,
44
41
  DUPLICATE_ID_HINT: () => 'Cada item precisa de um id único dentro do artefato.',
@@ -62,14 +59,14 @@ const messages = {
62
59
  ZOD_CUSTOM: (p) => String(p.message ?? 'Valor inválido.'),
63
60
  },
64
61
  en: {
65
- UNKNOWN_KIND: (p) => `Kind "${p.kind}" is not registered.`,
66
- UNKNOWN_KIND_HINT: (p) => `Use one of the available kinds: ${p.available}.`,
67
- DUPLICATE_KIND: (p) => `Kind "${p.kind}" is already registered.`,
62
+ UNKNOWN_FORMAT: (p) => `Format "${p.format}" is not registered.`,
63
+ UNKNOWN_FORMAT_HINT: (p) => `Use one of the available formats: ${p.available}.`,
64
+ DUPLICATE_FORMAT: (p) => `Format "${p.format}" is already registered.`,
68
65
  NOT_FOUND: (p) => `${p.what} not found: ${p.id}.`,
69
66
  NOT_FOUND_HINT: () => 'Check the id with artifact_list or read the artifact again.',
70
67
  INVALID_ENVELOPE: () => 'The artifact envelope is invalid.',
71
- UNSUPPORTED_VERSION: (p) =>
72
- `Content uses schema version ${p.found}, but kind "${p.kind}" supports up to version ${p.supported}.`,
68
+ UNSUPPORTED_FORMAT_VERSION: (p) =>
69
+ `Content uses format version ${p.found}, but format "${p.format}" reads only ${p.supported}.`,
73
70
  MIGRATION_FAILED: (p) => `Failed to migrate content from version ${p.from} to ${p.to}.`,
74
71
  INVALID_CONTENT: () => 'The content is invalid.',
75
72
  INVALID_OPERATION: (p) => `Operation ${p.index} is invalid.`,
@@ -83,15 +80,12 @@ const messages = {
83
80
  LIMIT_EXCEEDED: (p) => `Limit "${p.limit}" exceeded: max ${p.max}, found ${p.found}.`,
84
81
  LIMIT_EXCEEDED_HINT: () => 'Split the content into more artifacts or reduce the number of items.',
85
82
  PERMISSION_DENIED: (p) => `Action "${p.action}" denied.`,
86
- APPROVAL_REQUIRED: (p) => `Action "${p.action}" requires human approval.`,
87
- APPROVAL_REQUIRED_HINT: () => 'Use artifact_submit to send the artifact for approval.',
88
83
  ASSET_REJECTED: (p) => `Asset rejected: ${p.reason}.`,
89
84
  EXPORT_FAILED: (p) => `Failed to export to ${p.format}.`,
90
85
  INTERNAL_ERROR: () => 'Internal error while processing the request.',
91
- INVALID_STATUS_TRANSITION: (p) => `Cannot change status from "${p.from}" to "${p.to}".`,
92
- INPUT_FORMAT_UNSUPPORTED: (p) => `Kind "${p.kind}" does not accept "${p.format}" input.`,
86
+ INPUT_FORMAT_UNSUPPORTED: (p) => `Format "${p.format}" does not accept "${p.input}" input.`,
93
87
  INPUT_FORMAT_HINT: (p) => `Accepted formats: ${p.available}.`,
94
- EXPORTER_NOT_FOUND: (p) => `No "${p.kind}" exporter for format "${p.format}".`,
88
+ EXPORTER_NOT_FOUND: (p) => `No exporter from "${p.format}" to "${p.target}".`,
95
89
  THEME_NOT_FOUND: (p) => `Theme "${p.themeId}" is not registered.`,
96
90
  DUPLICATE_ID: (p) => `Id "${p.id}" is duplicated.`,
97
91
  DUPLICATE_ID_HINT: () => 'Every item needs an id that is unique inside the artifact.',
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './adapter.js';
1
2
  export * from './assets.js';
2
3
  export * from './diff.js';
3
4
  export * from './engine.js';
@@ -7,7 +8,6 @@ export * from './events.js';
7
8
  export * from './i18n.js';
8
9
  export * from './ids.js';
9
10
  export * from './jsonschema.js';
10
- export * from './kind.js';
11
11
  export * from './logger.js';
12
12
  export * from './memory.js';
13
13
  export * from './migrations.js';
@@ -16,18 +16,16 @@ export * from './registry.js';
16
16
  export * from './sanitize.js';
17
17
  export {
18
18
  type ApplyRequest,
19
- type ApprovalPolicy,
20
19
  type ArtifactAction,
20
+ type ArtifactReadResult,
21
21
  type ArtifactService,
22
22
  type ArtifactServiceOptions,
23
23
  type AuthorizeFn,
24
24
  type CommitResult,
25
25
  type CreateRequest,
26
26
  createArtifactService,
27
- DEFAULT_APPROVAL_POLICY,
28
27
  type ExportRequest,
29
28
  type ExportResult,
30
- type ReadResult,
31
29
  slugify,
32
30
  } from './service.js';
33
31
  export * from './store.js';
package/src/logger.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  /** Structured, injectable logger (spec §17). Never log artifact content, cell text, prompts or personal data. */
2
2
  export interface LogFields {
3
3
  artifactId?: string;
4
- kind?: string;
4
+ format?: string;
5
5
  version?: number;
6
6
  operationCount?: number;
7
7
  durationMs?: number;
8
8
  code?: string;
9
9
  actorType?: string;
10
- format?: string;
11
10
  [key: string]: string | number | boolean | undefined;
12
11
  }
13
12
 
package/src/memory.ts CHANGED
@@ -7,13 +7,7 @@ import {
7
7
  sha256Hex,
8
8
  toDataUrl,
9
9
  } from './assets.js';
10
- import {
11
- type Actor,
12
- type Artifact,
13
- type ArtifactStatus,
14
- type ArtifactSummary,
15
- summarize,
16
- } from './envelope.js';
10
+ import { type Artifact, type ArtifactSummary, summarize } from './envelope.js';
17
11
  import { ArtifactError } from './errors.js';
18
12
  import { t } from './i18n.js';
19
13
  import { createArtifactId } from './ids.js';
@@ -27,31 +21,24 @@ import type {
27
21
  Page,
28
22
  PageRequest,
29
23
  Scope,
30
- VersionEntry,
31
24
  } from './store.js';
32
25
 
33
26
  export interface MemoryStoreOptions {
34
27
  now?: () => Date;
35
- /** Snapshot every N versions (first version always has one). Memory keeps full versions anyway. */
36
- snapshotEvery?: number;
37
28
  locale?: string;
38
29
  }
39
30
 
40
31
  interface Record_ {
41
32
  workspaceId: string;
42
- versions: Artifact[];
43
- history: VersionEntry[];
33
+ artifact: Artifact;
44
34
  lock?: Lock;
45
35
  }
46
36
 
47
37
  const DEFAULT_PAGE = 50;
48
38
 
49
- const sameActor = (a: Actor, b: Actor) => a.type === b.type && a.id === b.id;
50
-
51
39
  /** Reference in-memory store: tests, playground and prototypes (spec §15.2). Runs in any runtime. */
52
40
  export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactStore {
53
41
  const now = options.now ?? (() => new Date());
54
- const snapshotEvery = Math.max(1, options.snapshotEvery ?? 10);
55
42
  const records = new Map<string, Record_>();
56
43
 
57
44
  const notFound = (id: string): never => {
@@ -65,10 +52,6 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
65
52
  return found;
66
53
  }
67
54
 
68
- function latest(rec: Record_): Artifact {
69
- return rec.versions[rec.versions.length - 1]!;
70
- }
71
-
72
55
  function activeLock(rec: Record_): Lock | null {
73
56
  if (!rec.lock) return null;
74
57
  if (Date.parse(rec.lock.expiresAt) <= now().getTime()) {
@@ -90,12 +73,10 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
90
73
  const clone = <T>(value: T): T => structuredClone(value);
91
74
 
92
75
  return {
93
- async get(scope, id, opts) {
76
+ async get(scope, id) {
94
77
  const rec = records.get(id);
95
78
  if (!rec || rec.workspaceId !== scope.workspaceId) return null;
96
- if (opts?.version === undefined) return clone(latest(rec));
97
- const found = rec.versions.find((v) => v.version === opts.version);
98
- return found ? clone(found) : null;
79
+ return clone(rec.artifact);
99
80
  },
100
81
 
101
82
  async create(scope, input: NewArtifact) {
@@ -103,11 +84,10 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
103
84
  const id = createArtifactId(at.getTime());
104
85
  const artifact: Artifact = {
105
86
  id,
106
- kind: input.kind,
107
- schemaVersion: input.schemaVersion,
87
+ format: input.format,
88
+ formatVersion: input.formatVersion,
108
89
  version: 1,
109
90
  title: input.title,
110
- status: input.status,
111
91
  locale: input.locale,
112
92
  themeId: input.themeId,
113
93
  content: clone(input.content),
@@ -119,30 +99,13 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
119
99
  tags: input.tags ?? [],
120
100
  },
121
101
  };
122
- records.set(id, {
123
- workspaceId: scope.workspaceId,
124
- versions: [artifact],
125
- history: [
126
- {
127
- version: 1,
128
- actor: scope.actor,
129
- createdAt: at.toISOString(),
130
- operations: [],
131
- diff: [],
132
- createdIds: input.createdIds ?? [],
133
- status: input.status,
134
- ...(input.message ? { message: input.message } : {}),
135
- ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
136
- snapshot: { schemaVersion: input.schemaVersion, content: clone(input.content) },
137
- },
138
- ],
139
- });
102
+ records.set(id, { workspaceId: scope.workspaceId, artifact });
140
103
  return clone(artifact);
141
104
  },
142
105
 
143
106
  async commit(scope, id, change: Commit) {
144
107
  const rec = record(scope, id);
145
- const current = latest(rec);
108
+ const current = rec.artifact;
146
109
  if (change.baseVersion !== current.version) {
147
110
  throw new ArtifactError(
148
111
  'VERSION_CONFLICT',
@@ -150,55 +113,29 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
150
113
  baseVersion: change.baseVersion,
151
114
  currentVersion: current.version,
152
115
  }),
153
- {
154
- details: { currentVersion: current.version },
155
- },
116
+ { details: { currentVersion: current.version } },
156
117
  );
157
118
  }
158
- const at = now().toISOString();
159
- const version = current.version + 1;
160
119
  const next: Artifact = {
161
120
  ...current,
162
- schemaVersion: change.schemaVersion,
163
- version,
121
+ formatVersion: change.formatVersion,
122
+ version: current.version + 1,
164
123
  title: change.title,
165
124
  locale: change.locale,
166
125
  themeId: change.themeId,
167
- status: change.status ?? current.status,
168
126
  content: clone(change.content),
169
- meta: { ...current.meta, updatedAt: at, updatedBy: scope.actor },
127
+ meta: { ...current.meta, updatedAt: now().toISOString(), updatedBy: scope.actor },
170
128
  };
171
- rec.versions.push(next);
172
- rec.history.push({
173
- version,
174
- actor: scope.actor,
175
- createdAt: at,
176
- operations: clone(change.operations),
177
- diff: clone(change.diff),
178
- createdIds: change.createdIds ?? [],
179
- status: next.status,
180
- ...(change.message ? { message: change.message } : {}),
181
- ...(change.idempotencyKey ? { idempotencyKey: change.idempotencyKey } : {}),
182
- ...(change.restoredFrom ? { restoredFrom: change.restoredFrom } : {}),
183
- ...(version % snapshotEvery === 0
184
- ? { snapshot: { schemaVersion: change.schemaVersion, content: clone(change.content) } }
185
- : {}),
186
- });
129
+ rec.artifact = next;
187
130
  return clone(next);
188
131
  },
189
132
 
190
- async history(scope, id, page) {
191
- const rec = record(scope, id);
192
- return paginate(clone([...rec.history].reverse()), page);
193
- },
194
-
195
133
  async list(scope, filter: ArtifactFilter, page) {
196
134
  const items: ArtifactSummary[] = [];
197
135
  for (const rec of records.values()) {
198
136
  if (rec.workspaceId !== scope.workspaceId) continue;
199
- const artifact = latest(rec);
200
- if (filter.kind && artifact.kind !== filter.kind) continue;
201
- if (filter.status && artifact.status !== filter.status) continue;
137
+ const artifact = rec.artifact;
138
+ if (filter.format && artifact.format !== filter.format) continue;
202
139
  if (filter.title && !artifact.title.toLowerCase().includes(filter.title.toLowerCase())) continue;
203
140
  if (filter.tag && !artifact.meta.tags.includes(filter.tag)) continue;
204
141
  items.push(summarize(artifact));
@@ -209,32 +146,6 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
209
146
  return paginate(items, page);
210
147
  },
211
148
 
212
- async setStatus(scope, id, status: ArtifactStatus, actor: Actor, message?: string) {
213
- const rec = record(scope, id);
214
- const current = latest(rec);
215
- const at = now().toISOString();
216
- const updated: Artifact = {
217
- ...current,
218
- status,
219
- meta: {
220
- ...current.meta,
221
- updatedAt: at,
222
- updatedBy: actor,
223
- ...(status === 'published' ? { publishedVersion: current.version } : {}),
224
- },
225
- };
226
- rec.versions[rec.versions.length - 1] = updated;
227
- const entry = rec.history[rec.history.length - 1];
228
- if (entry) {
229
- entry.status = status;
230
- entry.statusLog = [
231
- ...(entry.statusLog ?? []),
232
- { status, actor, at, ...(message ? { message } : {}) },
233
- ];
234
- }
235
- return clone(updated);
236
- },
237
-
238
149
  async lock(scope, id, actor, ttlMs) {
239
150
  const rec = record(scope, id);
240
151
  const existing = activeLock(rec);
@@ -245,9 +156,7 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
245
156
  holder: `${existing.holder.type}:${existing.holder.id}`,
246
157
  expiresAt: existing.expiresAt,
247
158
  }),
248
- {
249
- details: { expiresAt: existing.expiresAt },
250
- },
159
+ { details: { expiresAt: existing.expiresAt } },
251
160
  );
252
161
  }
253
162
  const token = createArtifactId(now().getTime()).slice(4);
@@ -270,21 +179,6 @@ export function createMemoryStore(options: MemoryStoreOptions = {}): ArtifactSto
270
179
  const lock = activeLock(rec);
271
180
  return lock ? clone(lock) : null;
272
181
  },
273
-
274
- async findByIdempotencyKey(scope, id, key) {
275
- const rec = record(scope, id);
276
- const entry = rec.history.find((h) => h.idempotencyKey === key);
277
- return entry ? clone(entry) : null;
278
- },
279
-
280
- async findCreatedByIdempotencyKey(scope, key) {
281
- for (const rec of records.values()) {
282
- if (rec.workspaceId !== scope.workspaceId) continue;
283
- const first = rec.history[0];
284
- if (first?.idempotencyKey === key && sameActor(first.actor, scope.actor)) return clone(latest(rec));
285
- }
286
- return null;
287
- },
288
182
  };
289
183
  }
290
184
 
package/src/migrations.ts CHANGED
@@ -1,40 +1,44 @@
1
+ import type { AnyFormatAdapter } from './adapter.js';
1
2
  import { ArtifactError } from './errors.js';
2
3
  import { t } from './i18n.js';
3
- import type { AnyArtifactKind } from './kind.js';
4
4
 
5
5
  export interface MigrationResult {
6
6
  content: unknown;
7
- schemaVersion: number;
7
+ formatVersion: string;
8
8
  migrated: boolean;
9
9
  }
10
10
 
11
11
  /**
12
- * Migrates content in memory along the kind's migration chain (spec §6.3).
13
- * Persisting the migrated version only happens on the next commit.
12
+ * Migrates content in memory along the adapter's migration chain (spec §7.2). Reading a version the adapter does not
13
+ * declare is refused with `UNSUPPORTED_FORMAT_VERSION`. Persisting the migrated content only happens on the next
14
+ * commit.
14
15
  */
15
16
  export function migrateContent(
16
- kind: AnyArtifactKind,
17
+ adapter: AnyFormatAdapter,
17
18
  content: unknown,
18
- schemaVersion: number,
19
+ formatVersion: string,
19
20
  locale?: string,
20
21
  ): MigrationResult {
21
- if (schemaVersion > kind.version) {
22
+ if (formatVersion === adapter.formatVersion) return { content, formatVersion, migrated: false };
23
+ if (!adapter.readableVersions.includes(formatVersion)) {
22
24
  throw new ArtifactError(
23
- 'UNSUPPORTED_VERSION',
24
- t(locale, 'UNSUPPORTED_VERSION', { found: schemaVersion, supported: kind.version, kind: kind.kind }),
25
- { details: { found: schemaVersion, supported: kind.version } },
25
+ 'UNSUPPORTED_FORMAT_VERSION',
26
+ t(locale, 'UNSUPPORTED_FORMAT_VERSION', {
27
+ found: formatVersion,
28
+ supported: adapter.readableVersions.join(', '),
29
+ format: adapter.format,
30
+ }),
31
+ { details: { found: formatVersion, supported: adapter.readableVersions } },
26
32
  );
27
33
  }
28
- if (schemaVersion === kind.version) return { content, schemaVersion, migrated: false };
29
34
  let current = content;
30
- let version = schemaVersion;
31
- const chain = [...kind.migrations].sort((a, b) => a.from - b.from);
32
- while (version < kind.version) {
33
- const step = chain.find((migration) => migration.from === version);
35
+ let version = formatVersion;
36
+ while (version !== adapter.formatVersion) {
37
+ const step = adapter.migrations.find((migration) => migration.from === version);
34
38
  if (!step) {
35
39
  throw new ArtifactError(
36
40
  'MIGRATION_FAILED',
37
- t(locale, 'MIGRATION_FAILED', { from: version, to: version + 1 }),
41
+ t(locale, 'MIGRATION_FAILED', { from: version, to: adapter.formatVersion }),
38
42
  );
39
43
  }
40
44
  try {
@@ -43,10 +47,12 @@ export function migrateContent(
43
47
  throw new ArtifactError(
44
48
  'MIGRATION_FAILED',
45
49
  t(locale, 'MIGRATION_FAILED', { from: step.from, to: step.to }),
46
- { cause },
50
+ {
51
+ cause,
52
+ },
47
53
  );
48
54
  }
49
55
  version = step.to;
50
56
  }
51
- return { content: current, schemaVersion: version, migrated: true };
57
+ return { content: current, formatVersion: version, migrated: true };
52
58
  }
package/src/projection.ts CHANGED
@@ -1,7 +1,25 @@
1
- import type { Projection, ProjectionOptions } from './kind.js';
1
+ import type { ReadMode } from './adapter.js';
2
2
 
3
3
  export const DEFAULT_CHARS_PER_TOKEN = 4;
4
4
 
5
+ export interface Projection {
6
+ text: string;
7
+ truncated: boolean;
8
+ /** Present when truncated: pass it back to continue reading. */
9
+ cursor?: string;
10
+ /** Outline of what was left out, so the model can jump with `range`. */
11
+ remainingOutline?: string;
12
+ }
13
+
14
+ export interface ProjectionOptions {
15
+ mode: ReadMode;
16
+ /** Format-specific range: anchors, A1 ranges, declaration names or 1-based positions ("3-5"). */
17
+ range?: string;
18
+ maxTokens?: number;
19
+ cursor?: string;
20
+ charsPerToken?: number;
21
+ }
22
+
5
23
  export function estimateTokens(text: string, charsPerToken = DEFAULT_CHARS_PER_TOKEN): number {
6
24
  return Math.ceil(text.length / Math.max(1, charsPerToken));
7
25
  }
@@ -9,7 +27,7 @@ export function estimateTokens(text: string, charsPerToken = DEFAULT_CHARS_PER_T
9
27
  /** One unit of projected text. Truncation only happens between items. */
10
28
  export interface ProjectionItem {
11
29
  id: string;
12
- /** Full text for `full` and `range` modes. */
30
+ /** Full text for `full`, `section` and `range` modes. */
13
31
  text: string;
14
32
  /** One-line summary used for `outline` and for the remaining outline after truncation. */
15
33
  outline: string;
@@ -32,8 +50,8 @@ function decodeCursor(cursor: string | undefined): number {
32
50
  }
33
51
 
34
52
  /**
35
- * Joins items under a token budget (spec §9.2). When the budget runs out, the text is cut at an item
36
- * boundary and a cursor plus the outline of the remaining items is returned.
53
+ * Joins items under a token budget (spec §10.2 of the reading rules). When the budget runs out, the text is cut at an
54
+ * item boundary and a cursor plus the outline of the remaining items is returned.
37
55
  */
38
56
  export function paginate(items: ProjectionItem[], options: PaginateOptions = {}): Projection {
39
57
  const start = Math.min(decodeCursor(options.cursor), items.length);
@@ -65,7 +83,7 @@ export function paginate(items: ProjectionItem[], options: PaginateOptions = {})
65
83
  return { text: parts.join('\n'), truncated: false };
66
84
  }
67
85
 
68
- /** Wraps a projection between explicit data markers so the model treats it as data, not instructions (spec §16). */
86
+ /** Wraps a projection between explicit data markers so the model treats it as data, not instructions (spec §18). */
69
87
  export function delimitProjection(artifactId: string, text: string): string {
70
88
  return `<artifact-content id="${artifactId}">\n${text}\n</artifact-content>`;
71
89
  }
@@ -110,14 +128,14 @@ function resolvePosition(ids: readonly string[], token: string): number | undefi
110
128
  return undefined;
111
129
  }
112
130
 
113
- /** Applies mode/range/budget to a list of items. Kinds with simple ordered items can use it directly. */
131
+ /** Applies mode/range/budget to a list of items. Adapters with ordered items can use it directly. */
114
132
  export function projectItems(
115
133
  items: ProjectionItem[],
116
134
  options: ProjectionOptions,
117
135
  extras: { header?: string; footer?: string } = {},
118
136
  ): Projection {
119
137
  let selected = items;
120
- if (options.mode === 'range') {
138
+ if (options.mode === 'range' || options.mode === 'statements' || options.mode === 'section') {
121
139
  const ids = selectRange(
122
140
  items.map((item) => item.id),
123
141
  options.range,