@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.
- package/dist/adapter.d.ts +119 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +45 -0
- package/dist/adapter.js.map +1 -0
- package/dist/diff.d.ts +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/engine.d.ts +29 -23
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +148 -197
- package/dist/engine.js.map +1 -1
- package/dist/envelope.d.ts +10 -27
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +8 -20
- package/dist/envelope.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -4
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +2 -10
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/i18n.d.ts +8 -14
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +12 -18
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory.d.ts +0 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +15 -109
- package/dist/memory.js.map +1 -1
- package/dist/migrations.d.ts +6 -5
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +20 -14
- package/dist/migrations.js.map +1 -1
- package/dist/projection.d.ts +22 -6
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +5 -5
- package/dist/projection.js.map +1 -1
- package/dist/registry.d.ts +17 -16
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +25 -25
- package/dist/registry.js.map +1 -1
- package/dist/service.d.ts +19 -51
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +119 -404
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +12 -61
- package/dist/store.d.ts.map +1 -1
- package/dist/testing/index.d.ts +10 -9
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +75 -136
- package/dist/testing/index.js.map +1 -1
- package/dist/theme.js +1 -1
- package/dist/ui.d.ts +18 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +5 -3
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter.ts +180 -0
- package/src/diff.ts +1 -1
- package/src/engine.ts +183 -230
- package/src/envelope.ts +11 -29
- package/src/errors.ts +4 -4
- package/src/events.ts +2 -11
- package/src/i18n.ts +14 -20
- package/src/index.ts +2 -4
- package/src/logger.ts +1 -2
- package/src/memory.ts +16 -122
- package/src/migrations.ts +24 -18
- package/src/projection.ts +25 -7
- package/src/registry.ts +45 -41
- package/src/service.ts +144 -540
- package/src/store.ts +12 -64
- package/src/testing/index.ts +89 -164
- package/src/theme.ts +1 -1
- package/src/ui.ts +24 -4
- package/dist/kind.d.ts +0 -156
- package/dist/kind.d.ts.map +0 -1
- package/dist/kind.js +0 -52
- package/dist/kind.js.map +0 -1
- package/src/kind.ts +0 -217
package/dist/service.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { canTransition, summarize, titleSchema, } from './envelope.js';
|
|
1
|
+
import { applyOperations, prepareContent } from './engine.js';
|
|
2
|
+
import { summarize, titleSchema } from './envelope.js';
|
|
4
3
|
import { ArtifactError, zodIssuesToErrorIssues } from './errors.js';
|
|
5
4
|
import { createEventBus } from './events.js';
|
|
6
5
|
import { t } from './i18n.js';
|
|
@@ -8,14 +7,9 @@ import { silentLogger } from './logger.js';
|
|
|
8
7
|
import { migrateContent } from './migrations.js';
|
|
9
8
|
import { delimitProjection } from './projection.js';
|
|
10
9
|
import { sanitizeDeep } from './sanitize.js';
|
|
11
|
-
export const DEFAULT_APPROVAL_POLICY = {
|
|
12
|
-
requireApprovalToPublish: true,
|
|
13
|
-
requireApprovalToExport: false,
|
|
14
|
-
requireApprovalToEditPublished: false,
|
|
15
|
-
};
|
|
16
10
|
/**
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
11
|
+
* The core pipeline (spec §9.1): authorization, migration, baseVersion check, atomic apply, optimistic commit, events
|
|
12
|
+
* and metadata-only logging. There is no history, approval or archive here: the harness decides what a save means.
|
|
19
13
|
*/
|
|
20
14
|
export function createArtifactService(options) {
|
|
21
15
|
const { registry, store } = options;
|
|
@@ -24,8 +18,6 @@ export function createArtifactService(options) {
|
|
|
24
18
|
const metrics = options.metrics ?? {};
|
|
25
19
|
const events = options.events ??
|
|
26
20
|
createEventBus((error) => logger.error('event handler failed', { code: String(error) }));
|
|
27
|
-
const policy = { ...DEFAULT_APPROVAL_POLICY, ...options.policy };
|
|
28
|
-
const retries = options.commitRetries ?? 3;
|
|
29
21
|
async function authorize(action, artifact, scope) {
|
|
30
22
|
if (!options.authorize)
|
|
31
23
|
return;
|
|
@@ -35,10 +27,10 @@ export function createArtifactService(options) {
|
|
|
35
27
|
throw new ArtifactError('PERMISSION_DENIED', t(locale, 'PERMISSION_DENIED', { action }));
|
|
36
28
|
}
|
|
37
29
|
}
|
|
38
|
-
async function load(scope, id
|
|
39
|
-
const found = await store.get(scope, id
|
|
30
|
+
async function load(scope, id) {
|
|
31
|
+
const found = await store.get(scope, id);
|
|
40
32
|
if (!found) {
|
|
41
|
-
throw new ArtifactError('NOT_FOUND', t(locale, 'NOT_FOUND', { what:
|
|
33
|
+
throw new ArtifactError('NOT_FOUND', t(locale, 'NOT_FOUND', { what: 'Artifact', id }), {
|
|
42
34
|
issues: [
|
|
43
35
|
{
|
|
44
36
|
path: 'id',
|
|
@@ -51,16 +43,16 @@ export function createArtifactService(options) {
|
|
|
51
43
|
return migrated(found);
|
|
52
44
|
}
|
|
53
45
|
function migrated(artifact) {
|
|
54
|
-
const
|
|
55
|
-
const result = migrateContent(
|
|
46
|
+
const adapter = registry.get(artifact.format, locale);
|
|
47
|
+
const result = migrateContent(adapter, artifact.content, artifact.formatVersion, locale);
|
|
56
48
|
return result.migrated
|
|
57
|
-
? { ...artifact, content: result.content,
|
|
49
|
+
? { ...artifact, content: result.content, formatVersion: result.formatVersion }
|
|
58
50
|
: artifact;
|
|
59
51
|
}
|
|
60
52
|
function fields(artifact, scope, extra = {}) {
|
|
61
53
|
return {
|
|
62
54
|
artifactId: artifact.id,
|
|
63
|
-
|
|
55
|
+
format: artifact.format,
|
|
64
56
|
version: artifact.version,
|
|
65
57
|
actorType: scope.actor.type,
|
|
66
58
|
...extra,
|
|
@@ -83,154 +75,36 @@ export function createArtifactService(options) {
|
|
|
83
75
|
details: { expiresAt: lock.expiresAt },
|
|
84
76
|
});
|
|
85
77
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return undefined;
|
|
92
|
-
const artifact = await load(scope, id, entry.version);
|
|
93
|
-
const kind = registry.get(artifact.kind, locale);
|
|
94
|
-
return {
|
|
95
|
-
artifact,
|
|
96
|
-
version: entry.version,
|
|
97
|
-
createdIds: entry.createdIds,
|
|
98
|
-
diff: entry.diff,
|
|
99
|
-
lint: kind.lint(artifact.content, {
|
|
100
|
-
locale: artifact.locale,
|
|
101
|
-
theme: registry.theme(artifact.themeId, locale),
|
|
102
|
-
}),
|
|
103
|
-
rebased: false,
|
|
104
|
-
replayed: true,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
/** Applies on the current version, rebasing from an older base when the touched items did not change. */
|
|
108
|
-
async function computeBatch(scope, current, request) {
|
|
109
|
-
if (request.baseVersion === current.version) {
|
|
110
|
-
return { result: applyOperations(registry, current, request.operations, { locale }), rebased: false };
|
|
111
|
-
}
|
|
112
|
-
const conflict = (changes) => {
|
|
113
|
-
metrics.conflict?.(fields(current, scope));
|
|
114
|
-
events.emit({
|
|
115
|
-
type: 'artifact.conflict',
|
|
116
|
-
artifactId: current.id,
|
|
117
|
-
baseVersion: request.baseVersion,
|
|
118
|
-
currentVersion: current.version,
|
|
119
|
-
actor: scope.actor,
|
|
120
|
-
});
|
|
121
|
-
return new ArtifactError('VERSION_CONFLICT', t(locale, 'VERSION_CONFLICT', { baseVersion: request.baseVersion, currentVersion: current.version }), {
|
|
122
|
-
issues: [
|
|
123
|
-
{
|
|
124
|
-
path: 'baseVersion',
|
|
125
|
-
message: t(locale, 'VERSION_CONFLICT', {
|
|
126
|
-
baseVersion: request.baseVersion,
|
|
127
|
-
currentVersion: current.version,
|
|
128
|
-
}),
|
|
129
|
-
hint: t(locale, 'VERSION_CONFLICT_HINT'),
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
details: { currentVersion: current.version, changesSinceBase: changes },
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
if (request.baseVersion > current.version || request.baseVersion < 1)
|
|
136
|
-
throw conflict([]);
|
|
137
|
-
const base = await load(scope, current.id, request.baseVersion);
|
|
138
|
-
const kind = registry.get(current.kind, locale);
|
|
139
|
-
// Validate against the base first: an invalid batch is the model's error, not a conflict.
|
|
140
|
-
const onBase = applyOperations(registry, base, request.operations, { locale });
|
|
141
|
-
const changedSince = [
|
|
142
|
-
...(base.title !== current.title
|
|
143
|
-
? [
|
|
144
|
-
{
|
|
145
|
-
type: 'changed',
|
|
146
|
-
target: 'envelope:title',
|
|
147
|
-
summary: `'${base.title}' → '${current.title}'`,
|
|
148
|
-
},
|
|
149
|
-
]
|
|
150
|
-
: []),
|
|
151
|
-
...(base.themeId !== current.themeId
|
|
152
|
-
? [
|
|
153
|
-
{
|
|
154
|
-
type: 'changed',
|
|
155
|
-
target: 'envelope:themeId',
|
|
156
|
-
summary: `${base.themeId} → ${current.themeId}`,
|
|
157
|
-
},
|
|
158
|
-
]
|
|
159
|
-
: []),
|
|
160
|
-
...(base.locale !== current.locale
|
|
161
|
-
? [
|
|
162
|
-
{
|
|
163
|
-
type: 'changed',
|
|
164
|
-
target: 'envelope:locale',
|
|
165
|
-
summary: `${base.locale} → ${current.locale}`,
|
|
166
|
-
},
|
|
167
|
-
]
|
|
168
|
-
: []),
|
|
169
|
-
...kind.diff(base.content, current.content, locale),
|
|
170
|
-
];
|
|
171
|
-
const parsed = parseOperations(registry, current.kind, request.operations, locale);
|
|
172
|
-
const touched = batchTargets(kind, parsed, onBase.diff);
|
|
173
|
-
const changed = new Set(changedSince.map((change) => change.target));
|
|
174
|
-
if (targetsOverlap(touched, changed))
|
|
175
|
-
throw conflict(changedSince);
|
|
176
|
-
try {
|
|
177
|
-
return { result: applyOperations(registry, current, request.operations, { locale }), rebased: true };
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
if (error instanceof ArtifactError &&
|
|
181
|
-
(error.code === 'OPERATION_FAILED' || error.code === 'INVALID_CONTENT')) {
|
|
182
|
-
throw conflict(changedSince);
|
|
183
|
-
}
|
|
184
|
-
throw error;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
async function changeStatus(scope, id, to, action, message) {
|
|
188
|
-
const current = await load(scope, id);
|
|
189
|
-
await authorize(action, summarize(current), scope);
|
|
190
|
-
if (!canTransition(current.status, to)) {
|
|
191
|
-
throw new ArtifactError('PERMISSION_DENIED', t(locale, 'INVALID_STATUS_TRANSITION', { from: current.status, to }), {
|
|
192
|
-
details: { from: current.status, to },
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
const updated = await store.setStatus(scope, id, to, scope.actor, message);
|
|
78
|
+
/** The batch must be computed against the current version; a stale base is a conflict, never a silent rebase. */
|
|
79
|
+
function assertBase(scope, current, baseVersion) {
|
|
80
|
+
if (baseVersion === current.version)
|
|
81
|
+
return;
|
|
82
|
+
metrics.conflict?.(fields(current, scope));
|
|
196
83
|
events.emit({
|
|
197
|
-
type: 'artifact.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
84
|
+
type: 'artifact.conflict',
|
|
85
|
+
artifactId: current.id,
|
|
86
|
+
baseVersion,
|
|
87
|
+
currentVersion: current.version,
|
|
201
88
|
actor: scope.actor,
|
|
202
|
-
...(message ? { message } : {}),
|
|
203
89
|
});
|
|
204
|
-
|
|
205
|
-
|
|
90
|
+
const message = t(locale, 'VERSION_CONFLICT', { baseVersion, currentVersion: current.version });
|
|
91
|
+
throw new ArtifactError('VERSION_CONFLICT', message, {
|
|
92
|
+
issues: [
|
|
93
|
+
{
|
|
94
|
+
path: 'baseVersion',
|
|
95
|
+
message,
|
|
96
|
+
hint: t(locale, 'VERSION_CONFLICT_HINT'),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
details: { currentVersion: current.version },
|
|
100
|
+
});
|
|
206
101
|
}
|
|
207
102
|
const service = {
|
|
208
103
|
registry,
|
|
209
104
|
events,
|
|
210
105
|
async create(scope, request) {
|
|
211
106
|
await authorize('create', null, scope);
|
|
212
|
-
const
|
|
213
|
-
if (request.idempotencyKey && store.findCreatedByIdempotencyKey) {
|
|
214
|
-
const existing = await store.findCreatedByIdempotencyKey(scope, request.idempotencyKey);
|
|
215
|
-
if (existing) {
|
|
216
|
-
const first = await load(scope, existing.id, 1);
|
|
217
|
-
const prepared = [...registry.get(first.kind, locale).ids(first.content)];
|
|
218
|
-
return {
|
|
219
|
-
artifact: first,
|
|
220
|
-
version: 1,
|
|
221
|
-
createdIds: prepared,
|
|
222
|
-
diff: [
|
|
223
|
-
{ type: 'added', target: `artifact:${first.id}`, summary: `${kind.label} "${first.title}"` },
|
|
224
|
-
],
|
|
225
|
-
lint: kind.lint(first.content, {
|
|
226
|
-
locale: first.locale,
|
|
227
|
-
theme: registry.theme(first.themeId, locale),
|
|
228
|
-
}),
|
|
229
|
-
rebased: false,
|
|
230
|
-
replayed: true,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
}
|
|
107
|
+
const adapter = registry.get(request.format, locale);
|
|
234
108
|
const title = titleSchema.safeParse(sanitizeDeep(request.title));
|
|
235
109
|
if (!title.success) {
|
|
236
110
|
throw new ArtifactError('INVALID_CONTENT', t(locale, 'INVALID_CONTENT'), {
|
|
@@ -240,295 +114,148 @@ export function createArtifactService(options) {
|
|
|
240
114
|
const artifactLocale = request.locale ?? options.defaultArtifactLocale ?? 'en';
|
|
241
115
|
const themeId = request.themeId ?? options.defaultThemeId ?? 'default';
|
|
242
116
|
registry.theme(themeId, locale);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
const parsedInput = input.schema.safeParse(sanitizeDeep(request.input.source));
|
|
263
|
-
if (!parsedInput.success) {
|
|
264
|
-
throw new ArtifactError('INVALID_CONTENT', t(locale, 'INVALID_CONTENT'), {
|
|
265
|
-
issues: zodIssuesToErrorIssues(parsedInput.error.issues, locale, 'input.source'),
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
raw = input.toContent(parsedInput.data, {
|
|
269
|
-
locale: artifactLocale,
|
|
270
|
-
note: (message) => inputNotes.push(message),
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
raw = request.content ?? kind.empty();
|
|
275
|
-
}
|
|
276
|
-
const prepared = prepareContent(registry, kind.kind, raw, { locale, theme: themeId });
|
|
277
|
-
prepared.lint.push(...inputNotes.map((message) => ({
|
|
278
|
-
code: 'input-note',
|
|
279
|
-
severity: 'info',
|
|
280
|
-
target: `input:${request.input?.format ?? 'content'}`,
|
|
281
|
-
message,
|
|
282
|
-
})));
|
|
117
|
+
const base = {
|
|
118
|
+
id: '',
|
|
119
|
+
format: adapter.format,
|
|
120
|
+
formatVersion: adapter.formatVersion,
|
|
121
|
+
version: 1,
|
|
122
|
+
title: title.data,
|
|
123
|
+
locale: artifactLocale,
|
|
124
|
+
themeId,
|
|
125
|
+
content: request.content ?? adapter.empty(),
|
|
126
|
+
};
|
|
127
|
+
// Creation with an initial batch goes through the same pipeline as any edit.
|
|
128
|
+
const applied = request.operations?.length
|
|
129
|
+
? await applyOperations(registry, base, request.operations, { locale })
|
|
130
|
+
: undefined;
|
|
131
|
+
const prepared = applied
|
|
132
|
+
? { content: applied.content, lint: applied.lint }
|
|
133
|
+
: prepareContent(registry, request.format, base.content, { locale: artifactLocale, themeId });
|
|
283
134
|
const artifact = await store.create(scope, {
|
|
284
|
-
|
|
285
|
-
|
|
135
|
+
format: adapter.format,
|
|
136
|
+
formatVersion: adapter.formatVersion,
|
|
286
137
|
title: title.data,
|
|
287
|
-
status: 'draft',
|
|
288
138
|
locale: artifactLocale,
|
|
289
139
|
themeId,
|
|
290
140
|
content: prepared.content,
|
|
291
|
-
createdIds: prepared.ids,
|
|
292
141
|
...(request.tags ? { tags: request.tags } : {}),
|
|
293
|
-
...(request.message ? { message: request.message } : {}),
|
|
294
|
-
...(request.idempotencyKey ? { idempotencyKey: request.idempotencyKey } : {}),
|
|
295
142
|
});
|
|
296
143
|
events.emit({ type: 'artifact.created', artifact: summarize(artifact), actor: scope.actor });
|
|
297
|
-
if (prepared.lint.length > 0)
|
|
144
|
+
if (prepared.lint.length > 0) {
|
|
298
145
|
events.emit({
|
|
299
146
|
type: 'artifact.lint',
|
|
300
147
|
artifactId: artifact.id,
|
|
301
148
|
version: artifact.version,
|
|
302
149
|
warnings: prepared.lint.length,
|
|
303
150
|
});
|
|
151
|
+
}
|
|
304
152
|
logger.info('artifact.created', fields(artifact, scope));
|
|
305
153
|
return {
|
|
306
154
|
artifact,
|
|
307
155
|
version: artifact.version,
|
|
308
|
-
|
|
156
|
+
createdRefs: applied?.createdRefs ?? [],
|
|
309
157
|
diff: [
|
|
310
|
-
{
|
|
158
|
+
{
|
|
159
|
+
type: 'added',
|
|
160
|
+
target: `artifact:${artifact.id}`,
|
|
161
|
+
summary: `${adapter.label} "${artifact.title}"`,
|
|
162
|
+
},
|
|
311
163
|
],
|
|
312
164
|
lint: prepared.lint,
|
|
313
|
-
rebased: false,
|
|
314
|
-
replayed: false,
|
|
315
165
|
};
|
|
316
166
|
},
|
|
317
|
-
async get(scope, id
|
|
318
|
-
const artifact = await load(scope, id
|
|
167
|
+
async get(scope, id) {
|
|
168
|
+
const artifact = await load(scope, id);
|
|
319
169
|
await authorize('read', summarize(artifact), scope);
|
|
320
170
|
return artifact;
|
|
321
171
|
},
|
|
322
172
|
async read(scope, id, opts = {}) {
|
|
323
|
-
const artifact = await load(scope, id
|
|
173
|
+
const artifact = await load(scope, id);
|
|
324
174
|
await authorize('read', summarize(artifact), scope);
|
|
325
|
-
const
|
|
326
|
-
const
|
|
175
|
+
const adapter = registry.get(artifact.format, locale);
|
|
176
|
+
const result = adapter.read(artifact.content, {
|
|
327
177
|
mode: opts.mode ?? 'outline',
|
|
328
178
|
locale: artifact.locale,
|
|
329
|
-
theme: registry.theme(artifact.themeId, locale),
|
|
330
179
|
...(opts.range === undefined ? {} : { range: opts.range }),
|
|
331
180
|
...(opts.maxTokens === undefined ? {} : { maxTokens: opts.maxTokens }),
|
|
332
181
|
...(opts.cursor === undefined ? {} : { cursor: opts.cursor }),
|
|
333
182
|
...(opts.charsPerToken === undefined ? {} : { charsPerToken: opts.charsPerToken }),
|
|
334
183
|
});
|
|
335
|
-
const
|
|
184
|
+
const projection = {
|
|
185
|
+
text: result.text,
|
|
186
|
+
truncated: result.truncated,
|
|
187
|
+
...(result.cursor === undefined ? {} : { cursor: result.cursor }),
|
|
188
|
+
};
|
|
189
|
+
const header = `${adapter.label} "${artifact.title}" (id: ${artifact.id}, format: ${artifact.format}, version: ${artifact.version}, locale: ${artifact.locale})`;
|
|
336
190
|
let text = `${header}\n${delimitProjection(artifact.id, projection.text)}`;
|
|
337
191
|
if (projection.truncated) {
|
|
338
|
-
text += `\n[truncated — continue with cursor "${projection.cursor}"]
|
|
192
|
+
text += `\n[truncated — continue with cursor "${projection.cursor}"]`;
|
|
339
193
|
}
|
|
340
194
|
return { artifact: summarize(artifact), version: artifact.version, projection, text };
|
|
341
195
|
},
|
|
342
196
|
async apply(scope, request) {
|
|
343
197
|
const started = Date.now();
|
|
344
|
-
|
|
198
|
+
const current = await load(scope, request.artifactId);
|
|
345
199
|
await authorize('apply', summarize(current), scope);
|
|
346
|
-
const replayed = await replay(scope, current.id, request.idempotencyKey);
|
|
347
|
-
if (replayed)
|
|
348
|
-
return replayed;
|
|
349
200
|
await assertNotLocked(scope, current.id);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
201
|
+
assertBase(scope, current, request.baseVersion);
|
|
202
|
+
const result = await applyOperations(registry, current, request.operations, { locale });
|
|
203
|
+
let committed;
|
|
204
|
+
try {
|
|
205
|
+
committed = await store.commit(scope, current.id, {
|
|
206
|
+
baseVersion: current.version,
|
|
207
|
+
formatVersion: result.formatVersion,
|
|
208
|
+
content: result.content,
|
|
209
|
+
title: result.title,
|
|
210
|
+
locale: result.locale,
|
|
211
|
+
themeId: result.themeId,
|
|
361
212
|
});
|
|
362
213
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
baseVersion: current.version,
|
|
368
|
-
schemaVersion: result.schemaVersion,
|
|
369
|
-
content: result.content,
|
|
370
|
-
title: result.title,
|
|
371
|
-
locale: result.locale,
|
|
372
|
-
themeId: result.themeId,
|
|
373
|
-
// Editing a published (or pending) artifact starts a new draft (spec §15.3).
|
|
374
|
-
...(current.status === 'published' || current.status === 'pending_approval'
|
|
375
|
-
? { status: 'draft' }
|
|
376
|
-
: {}),
|
|
377
|
-
operations: request.operations,
|
|
378
|
-
diff: result.diff,
|
|
379
|
-
createdIds: result.createdIds,
|
|
380
|
-
...(request.message ? { message: request.message } : {}),
|
|
381
|
-
...(request.idempotencyKey ? { idempotencyKey: request.idempotencyKey } : {}),
|
|
382
|
-
});
|
|
383
|
-
const duration = Date.now() - started;
|
|
384
|
-
const logFields = fields(committed, scope, {
|
|
385
|
-
operationCount: request.operations.length,
|
|
386
|
-
durationMs: duration,
|
|
387
|
-
});
|
|
388
|
-
metrics.applyDuration?.(duration, logFields);
|
|
389
|
-
logger.info('artifact.committed', logFields);
|
|
390
|
-
events.emit({
|
|
391
|
-
type: 'artifact.committed',
|
|
392
|
-
artifact: summarize(committed),
|
|
393
|
-
actor: scope.actor,
|
|
394
|
-
operationCount: request.operations.length,
|
|
395
|
-
diff: result.diff,
|
|
396
|
-
rebased,
|
|
397
|
-
});
|
|
398
|
-
if (result.lint.length > 0)
|
|
399
|
-
events.emit({
|
|
400
|
-
type: 'artifact.lint',
|
|
401
|
-
artifactId: committed.id,
|
|
402
|
-
version: committed.version,
|
|
403
|
-
warnings: result.lint.length,
|
|
404
|
-
});
|
|
405
|
-
return {
|
|
406
|
-
artifact: committed,
|
|
407
|
-
version: committed.version,
|
|
408
|
-
createdIds: result.createdIds,
|
|
409
|
-
diff: result.diff,
|
|
410
|
-
lint: result.lint,
|
|
411
|
-
rebased,
|
|
412
|
-
replayed: false,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
catch (error) {
|
|
416
|
-
// Another writer won the race: reload and rebase from the original base.
|
|
417
|
-
if (error instanceof ArtifactError && error.code === 'VERSION_CONFLICT' && attempt < retries) {
|
|
418
|
-
current = await load(scope, request.artifactId);
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (error instanceof ArtifactError)
|
|
422
|
-
metrics.rejection?.(error.code, fields(current, scope));
|
|
423
|
-
throw error;
|
|
424
|
-
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof ArtifactError)
|
|
216
|
+
metrics.rejection?.(error.code, fields(current, scope));
|
|
217
|
+
throw error;
|
|
425
218
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return (await computeBatch(scope, current, request)).result;
|
|
431
|
-
},
|
|
432
|
-
async list(scope, filter = {}, page = {}) {
|
|
433
|
-
await authorize('list', null, scope);
|
|
434
|
-
return store.list(scope, filter, page);
|
|
435
|
-
},
|
|
436
|
-
async history(scope, id, page = {}) {
|
|
437
|
-
const current = await load(scope, id);
|
|
438
|
-
await authorize('history', summarize(current), scope);
|
|
439
|
-
return store.history(scope, id, page);
|
|
440
|
-
},
|
|
441
|
-
async restore(scope, id, version, message) {
|
|
442
|
-
const current = await load(scope, id);
|
|
443
|
-
await authorize('restore', summarize(current), scope);
|
|
444
|
-
await assertNotLocked(scope, id);
|
|
445
|
-
const old = await load(scope, id, version);
|
|
446
|
-
const kind = registry.get(current.kind, locale);
|
|
447
|
-
const diff = [
|
|
448
|
-
...(old.title !== current.title
|
|
449
|
-
? [
|
|
450
|
-
{
|
|
451
|
-
type: 'changed',
|
|
452
|
-
target: 'envelope:title',
|
|
453
|
-
summary: `'${current.title}' → '${old.title}'`,
|
|
454
|
-
},
|
|
455
|
-
]
|
|
456
|
-
: []),
|
|
457
|
-
...kind.diff(current.content, old.content, locale),
|
|
458
|
-
];
|
|
459
|
-
const beforeIds = new Set(kind.ids(current.content));
|
|
460
|
-
const createdIds = [...kind.ids(old.content)].filter((itemId) => !beforeIds.has(itemId));
|
|
461
|
-
const committed = await store.commit(scope, id, {
|
|
462
|
-
baseVersion: current.version,
|
|
463
|
-
schemaVersion: old.schemaVersion,
|
|
464
|
-
content: old.content,
|
|
465
|
-
title: old.title,
|
|
466
|
-
locale: old.locale,
|
|
467
|
-
themeId: old.themeId,
|
|
468
|
-
...(current.status !== 'draft' && current.status !== 'archived' ? { status: 'draft' } : {}),
|
|
469
|
-
operations: [{ op: 'replace_content', content: '[restored]' }],
|
|
470
|
-
diff,
|
|
471
|
-
createdIds,
|
|
472
|
-
restoredFrom: version,
|
|
473
|
-
message: message ?? `Restored version ${version}`,
|
|
219
|
+
const duration = Date.now() - started;
|
|
220
|
+
const logFields = fields(committed, scope, {
|
|
221
|
+
operationCount: request.operations.length,
|
|
222
|
+
durationMs: duration,
|
|
474
223
|
});
|
|
224
|
+
metrics.applyDuration?.(duration, logFields);
|
|
225
|
+
logger.info('artifact.committed', logFields);
|
|
475
226
|
events.emit({
|
|
476
227
|
type: 'artifact.committed',
|
|
477
228
|
artifact: summarize(committed),
|
|
478
229
|
actor: scope.actor,
|
|
479
|
-
operationCount:
|
|
480
|
-
diff,
|
|
481
|
-
rebased: false,
|
|
230
|
+
operationCount: request.operations.length,
|
|
231
|
+
diff: result.diff,
|
|
482
232
|
});
|
|
233
|
+
if (result.lint.length > 0) {
|
|
234
|
+
events.emit({
|
|
235
|
+
type: 'artifact.lint',
|
|
236
|
+
artifactId: committed.id,
|
|
237
|
+
version: committed.version,
|
|
238
|
+
warnings: result.lint.length,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
483
241
|
return {
|
|
484
242
|
artifact: committed,
|
|
485
243
|
version: committed.version,
|
|
486
|
-
|
|
487
|
-
diff,
|
|
488
|
-
lint:
|
|
489
|
-
locale: committed.locale,
|
|
490
|
-
theme: registry.theme(committed.themeId, locale),
|
|
491
|
-
}),
|
|
492
|
-
rebased: false,
|
|
493
|
-
replayed: false,
|
|
244
|
+
createdRefs: result.createdRefs,
|
|
245
|
+
diff: result.diff,
|
|
246
|
+
lint: result.lint,
|
|
494
247
|
};
|
|
495
248
|
},
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
const current = await load(scope, id);
|
|
502
|
-
if (current.status !== 'pending_approval') {
|
|
503
|
-
throw new ArtifactError('PERMISSION_DENIED', t(locale, 'INVALID_STATUS_TRANSITION', { from: current.status, to: 'published' }));
|
|
504
|
-
}
|
|
505
|
-
return changeStatus(scope, id, 'published', 'approve', message);
|
|
506
|
-
},
|
|
507
|
-
async reject(scope, id, reason) {
|
|
508
|
-
if (scope.actor.type === 'agent') {
|
|
509
|
-
throw new ArtifactError('PERMISSION_DENIED', t(locale, 'PERMISSION_DENIED', { action: 'reject' }));
|
|
510
|
-
}
|
|
511
|
-
const current = await load(scope, id);
|
|
512
|
-
if (current.status !== 'pending_approval') {
|
|
513
|
-
throw new ArtifactError('PERMISSION_DENIED', t(locale, 'INVALID_STATUS_TRANSITION', { from: current.status, to: 'draft' }));
|
|
514
|
-
}
|
|
515
|
-
return changeStatus(scope, id, 'draft', 'reject', reason);
|
|
249
|
+
async preview(scope, request) {
|
|
250
|
+
const current = await load(scope, request.artifactId);
|
|
251
|
+
await authorize('read', summarize(current), scope);
|
|
252
|
+
assertBase(scope, current, request.baseVersion);
|
|
253
|
+
return applyOperations(registry, current, request.operations, { locale });
|
|
516
254
|
},
|
|
517
|
-
async
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
issues: [
|
|
521
|
-
{
|
|
522
|
-
path: 'id',
|
|
523
|
-
message: t(locale, 'APPROVAL_REQUIRED', { action: 'publish' }),
|
|
524
|
-
hint: t(locale, 'APPROVAL_REQUIRED_HINT'),
|
|
525
|
-
},
|
|
526
|
-
],
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
return changeStatus(scope, id, 'published', 'publish');
|
|
255
|
+
async list(scope, filter = {}, page = {}) {
|
|
256
|
+
await authorize('list', null, scope);
|
|
257
|
+
return store.list(scope, filter, page);
|
|
530
258
|
},
|
|
531
|
-
archive: (scope, id) => changeStatus(scope, id, 'archived', 'archive'),
|
|
532
259
|
async lock(scope, id, ttlMs = 5 * 60_000) {
|
|
533
260
|
const current = await load(scope, id);
|
|
534
261
|
await authorize('lock', summarize(current), scope);
|
|
@@ -542,20 +269,9 @@ export function createArtifactService(options) {
|
|
|
542
269
|
},
|
|
543
270
|
async export(scope, request) {
|
|
544
271
|
const started = Date.now();
|
|
545
|
-
const artifact = await load(scope, request.artifactId
|
|
272
|
+
const artifact = await load(scope, request.artifactId);
|
|
546
273
|
await authorize('export', summarize(artifact), scope);
|
|
547
|
-
|
|
548
|
-
throw new ArtifactError('APPROVAL_REQUIRED', t(locale, 'APPROVAL_REQUIRED', { action: 'export' }), {
|
|
549
|
-
issues: [
|
|
550
|
-
{
|
|
551
|
-
path: 'artifactId',
|
|
552
|
-
message: t(locale, 'APPROVAL_REQUIRED', { action: 'export' }),
|
|
553
|
-
hint: t(locale, 'APPROVAL_REQUIRED_HINT'),
|
|
554
|
-
},
|
|
555
|
-
],
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
const exporter = registry.exporter(artifact.kind, request.format, locale);
|
|
274
|
+
const exporter = registry.exporter(artifact.format, request.format, locale);
|
|
559
275
|
const warnings = [];
|
|
560
276
|
const timeout = AbortSignal.timeout(request.timeoutMs ?? options.exportTimeoutMs ?? 60_000);
|
|
561
277
|
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
@@ -573,8 +289,8 @@ export function createArtifactService(options) {
|
|
|
573
289
|
}), signal);
|
|
574
290
|
}
|
|
575
291
|
catch (cause) {
|
|
576
|
-
metrics.rejection?.('EXPORT_FAILED', fields(artifact, scope, {
|
|
577
|
-
logger.error('artifact.export_failed', fields(artifact, scope, {
|
|
292
|
+
metrics.rejection?.('EXPORT_FAILED', fields(artifact, scope, { target: request.format }));
|
|
293
|
+
logger.error('artifact.export_failed', fields(artifact, scope, { target: request.format }));
|
|
578
294
|
throw new ArtifactError('EXPORT_FAILED', t(locale, 'EXPORT_FAILED', { format: request.format }), {
|
|
579
295
|
cause,
|
|
580
296
|
});
|
|
@@ -601,7 +317,7 @@ export function createArtifactService(options) {
|
|
|
601
317
|
result.asset = { ref: stored.ref, size: stored.size, hash: stored.hash };
|
|
602
318
|
}
|
|
603
319
|
const duration = Date.now() - started;
|
|
604
|
-
metrics.exportDuration?.(duration, fields(artifact, scope, {
|
|
320
|
+
metrics.exportDuration?.(duration, fields(artifact, scope, { target: request.format, durationMs: duration }));
|
|
605
321
|
events.emit({
|
|
606
322
|
type: 'artifact.exported',
|
|
607
323
|
artifact: summarize(artifact),
|
|
@@ -648,5 +364,4 @@ export function slugify(title) {
|
|
|
648
364
|
.slice(0, 80);
|
|
649
365
|
return slug || 'artifact';
|
|
650
366
|
}
|
|
651
|
-
export { deepEqual, envelopeOperationSchema };
|
|
652
367
|
//# sourceMappingURL=service.js.map
|