@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/src/service.ts
CHANGED
|
@@ -1,79 +1,26 @@
|
|
|
1
|
+
import type { AnyFormatAdapter, ChangeSummary, LintIssue } from './adapter.js';
|
|
1
2
|
import type { AssetRef, AssetResolver } from './assets.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
applyOperations,
|
|
5
|
-
type BatchResult,
|
|
6
|
-
batchTargets,
|
|
7
|
-
envelopeOperationSchema,
|
|
8
|
-
parseOperations,
|
|
9
|
-
prepareContent,
|
|
10
|
-
targetsOverlap,
|
|
11
|
-
} from './engine.js';
|
|
12
|
-
import {
|
|
13
|
-
type Artifact,
|
|
14
|
-
type ArtifactStatus,
|
|
15
|
-
type ArtifactSummary,
|
|
16
|
-
canTransition,
|
|
17
|
-
summarize,
|
|
18
|
-
titleSchema,
|
|
19
|
-
} from './envelope.js';
|
|
3
|
+
import { applyOperations, type BatchResult, prepareContent } from './engine.js';
|
|
4
|
+
import { type Artifact, type ArtifactSummary, summarize, titleSchema } from './envelope.js';
|
|
20
5
|
import { ArtifactError, zodIssuesToErrorIssues } from './errors.js';
|
|
21
6
|
import { createEventBus, type EventBus } from './events.js';
|
|
22
7
|
import { t } from './i18n.js';
|
|
23
|
-
import type { ChangeSummary, LintIssue, Projection, ProjectionOptions } from './kind.js';
|
|
24
8
|
import { type LogFields, type Logger, type MetricsHooks, silentLogger } from './logger.js';
|
|
25
9
|
import { migrateContent } from './migrations.js';
|
|
26
|
-
import { delimitProjection } from './projection.js';
|
|
10
|
+
import { delimitProjection, type Projection, type ProjectionOptions } from './projection.js';
|
|
27
11
|
import type { ArtifactRegistry } from './registry.js';
|
|
28
12
|
import { sanitizeDeep } from './sanitize.js';
|
|
29
|
-
import type {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Page,
|
|
35
|
-
PageRequest,
|
|
36
|
-
Scope,
|
|
37
|
-
VersionEntry,
|
|
38
|
-
} from './store.js';
|
|
39
|
-
|
|
40
|
-
export type ArtifactAction =
|
|
41
|
-
| 'create'
|
|
42
|
-
| 'read'
|
|
43
|
-
| 'apply'
|
|
44
|
-
| 'export'
|
|
45
|
-
| 'list'
|
|
46
|
-
| 'history'
|
|
47
|
-
| 'submit'
|
|
48
|
-
| 'approve'
|
|
49
|
-
| 'reject'
|
|
50
|
-
| 'publish'
|
|
51
|
-
| 'archive'
|
|
52
|
-
| 'restore'
|
|
53
|
-
| 'lock';
|
|
54
|
-
|
|
55
|
-
/** Mandatory authorization hook in toolkits (spec §16). Return false to deny. */
|
|
13
|
+
import type { ArtifactFilter, ArtifactStore, AssetStore, Lock, Page, PageRequest, Scope } from './store.js';
|
|
14
|
+
|
|
15
|
+
export type ArtifactAction = 'create' | 'read' | 'apply' | 'export' | 'list' | 'lock';
|
|
16
|
+
|
|
17
|
+
/** Mandatory authorization hook in toolkits (spec §16 and §15.1). Return false to deny. */
|
|
56
18
|
export type AuthorizeFn = (
|
|
57
19
|
action: ArtifactAction,
|
|
58
20
|
artifact: ArtifactSummary | null,
|
|
59
21
|
scope: Scope,
|
|
60
22
|
) => boolean | Promise<boolean>;
|
|
61
23
|
|
|
62
|
-
export interface ApprovalPolicy {
|
|
63
|
-
/** Publishing goes through submit → approve (default true). */
|
|
64
|
-
requireApprovalToPublish: boolean;
|
|
65
|
-
/** Only published artifacts can be exported (default false). */
|
|
66
|
-
requireApprovalToExport: boolean;
|
|
67
|
-
/** Agents cannot edit published artifacts without approval (default false). Edits create a new draft. */
|
|
68
|
-
requireApprovalToEditPublished: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export const DEFAULT_APPROVAL_POLICY: ApprovalPolicy = {
|
|
72
|
-
requireApprovalToPublish: true,
|
|
73
|
-
requireApprovalToExport: false,
|
|
74
|
-
requireApprovalToEditPublished: false,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
24
|
export interface ArtifactServiceOptions {
|
|
78
25
|
registry: ArtifactRegistry;
|
|
79
26
|
store: ArtifactStore;
|
|
@@ -87,47 +34,38 @@ export interface ArtifactServiceOptions {
|
|
|
87
34
|
/** Default locale of new artifacts. */
|
|
88
35
|
defaultArtifactLocale?: string;
|
|
89
36
|
defaultThemeId?: string;
|
|
90
|
-
policy?: Partial<ApprovalPolicy>;
|
|
91
|
-
/** Times a commit is retried when another writer wins the race (default 3). */
|
|
92
|
-
commitRetries?: number;
|
|
93
37
|
/** Default export timeout (default 60s). */
|
|
94
38
|
exportTimeoutMs?: number;
|
|
95
39
|
}
|
|
96
40
|
|
|
97
41
|
export interface CreateRequest {
|
|
98
|
-
|
|
42
|
+
format: string;
|
|
99
43
|
title: string;
|
|
44
|
+
/** Native content: Markdown text, OpenUI Lang text or an `IWorkbookData` object (spec §15.1). */
|
|
100
45
|
content?: unknown;
|
|
101
|
-
/**
|
|
102
|
-
|
|
46
|
+
/** Initial batch applied on the adapter's empty content, e.g. the first operations of a sheet. */
|
|
47
|
+
operations?: unknown[];
|
|
103
48
|
locale?: string;
|
|
104
49
|
themeId?: string;
|
|
105
50
|
tags?: string[];
|
|
106
|
-
message?: string;
|
|
107
|
-
idempotencyKey?: string;
|
|
108
51
|
}
|
|
109
52
|
|
|
110
53
|
export interface CommitResult {
|
|
111
54
|
artifact: Artifact;
|
|
112
55
|
version: number;
|
|
113
|
-
|
|
56
|
+
createdRefs: string[];
|
|
114
57
|
diff: ChangeSummary[];
|
|
115
58
|
lint: LintIssue[];
|
|
116
|
-
/** True when the batch was applied on a newer version than its baseVersion. */
|
|
117
|
-
rebased: boolean;
|
|
118
|
-
/** True when an earlier call with the same idempotency key was replayed. */
|
|
119
|
-
replayed: boolean;
|
|
120
59
|
}
|
|
121
60
|
|
|
122
61
|
export interface ApplyRequest {
|
|
123
62
|
artifactId: string;
|
|
63
|
+
/** Version the operations were computed against. Anything else than the current version is a VERSION_CONFLICT. */
|
|
124
64
|
baseVersion: number;
|
|
125
65
|
operations: unknown[];
|
|
126
|
-
idempotencyKey?: string;
|
|
127
|
-
message?: string;
|
|
128
66
|
}
|
|
129
67
|
|
|
130
|
-
export interface
|
|
68
|
+
export interface ArtifactReadResult {
|
|
131
69
|
artifact: ArtifactSummary;
|
|
132
70
|
version: number;
|
|
133
71
|
projection: Projection;
|
|
@@ -137,8 +75,8 @@ export interface ReadResult {
|
|
|
137
75
|
|
|
138
76
|
export interface ExportRequest {
|
|
139
77
|
artifactId: string;
|
|
78
|
+
/** Target file format: `docx`, `xlsx`, `pptx`, `pdf`, `png`, `html`, `md`, `csv`… */
|
|
140
79
|
format: string;
|
|
141
|
-
version?: number;
|
|
142
80
|
signal?: AbortSignal;
|
|
143
81
|
timeoutMs?: number;
|
|
144
82
|
/** Store the result in the AssetStore and return its reference. */
|
|
@@ -159,23 +97,12 @@ export interface ArtifactService {
|
|
|
159
97
|
readonly registry: ArtifactRegistry;
|
|
160
98
|
readonly events: EventBus;
|
|
161
99
|
create(scope: Scope, request: CreateRequest): Promise<CommitResult>;
|
|
162
|
-
get(scope: Scope, id: string
|
|
163
|
-
read(
|
|
164
|
-
scope: Scope,
|
|
165
|
-
id: string,
|
|
166
|
-
options?: Partial<ProjectionOptions> & { version?: number },
|
|
167
|
-
): Promise<ReadResult>;
|
|
100
|
+
get(scope: Scope, id: string): Promise<Artifact>;
|
|
101
|
+
read(scope: Scope, id: string, options?: Partial<ProjectionOptions>): Promise<ArtifactReadResult>;
|
|
168
102
|
apply(scope: Scope, request: ApplyRequest): Promise<CommitResult>;
|
|
169
103
|
/** Pure dry run on the current version: validates and returns diff/lint without committing. */
|
|
170
104
|
preview(scope: Scope, request: ApplyRequest): Promise<BatchResult>;
|
|
171
105
|
list(scope: Scope, filter?: ArtifactFilter, page?: PageRequest): Promise<Page<ArtifactSummary>>;
|
|
172
|
-
history(scope: Scope, id: string, page?: PageRequest): Promise<Page<VersionEntry>>;
|
|
173
|
-
restore(scope: Scope, id: string, version: number, message?: string): Promise<CommitResult>;
|
|
174
|
-
submit(scope: Scope, id: string, message?: string): Promise<Artifact>;
|
|
175
|
-
approve(scope: Scope, id: string, message?: string): Promise<Artifact>;
|
|
176
|
-
reject(scope: Scope, id: string, reason?: string): Promise<Artifact>;
|
|
177
|
-
publish(scope: Scope, id: string): Promise<Artifact>;
|
|
178
|
-
archive(scope: Scope, id: string): Promise<Artifact>;
|
|
179
106
|
lock(scope: Scope, id: string, ttlMs?: number): Promise<Lock>;
|
|
180
107
|
unlock(scope: Scope, lock: Lock): Promise<void>;
|
|
181
108
|
/** Current lock holder, or null; lets UIs show who is editing before trying to write. */
|
|
@@ -186,8 +113,8 @@ export interface ArtifactService {
|
|
|
186
113
|
}
|
|
187
114
|
|
|
188
115
|
/**
|
|
189
|
-
* The
|
|
190
|
-
*
|
|
116
|
+
* The core pipeline (spec §9.1): authorization, migration, baseVersion check, atomic apply, optimistic commit, events
|
|
117
|
+
* and metadata-only logging. There is no history, approval or archive here: the harness decides what a save means.
|
|
191
118
|
*/
|
|
192
119
|
export function createArtifactService(options: ArtifactServiceOptions): ArtifactService {
|
|
193
120
|
const { registry, store } = options;
|
|
@@ -197,8 +124,6 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
197
124
|
const events =
|
|
198
125
|
options.events ??
|
|
199
126
|
createEventBus((error) => logger.error('event handler failed', { code: String(error) }));
|
|
200
|
-
const policy: ApprovalPolicy = { ...DEFAULT_APPROVAL_POLICY, ...options.policy };
|
|
201
|
-
const retries = options.commitRetries ?? 3;
|
|
202
127
|
|
|
203
128
|
async function authorize(
|
|
204
129
|
action: ArtifactAction,
|
|
@@ -213,42 +138,38 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
213
138
|
}
|
|
214
139
|
}
|
|
215
140
|
|
|
216
|
-
async function load(scope: Scope, id: string
|
|
217
|
-
const found = await store.get(scope, id
|
|
141
|
+
async function load(scope: Scope, id: string): Promise<Artifact> {
|
|
142
|
+
const found = await store.get(scope, id);
|
|
218
143
|
if (!found) {
|
|
219
|
-
throw new ArtifactError(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
},
|
|
231
|
-
);
|
|
144
|
+
throw new ArtifactError('NOT_FOUND', t(locale, 'NOT_FOUND', { what: 'Artifact', id }), {
|
|
145
|
+
issues: [
|
|
146
|
+
{
|
|
147
|
+
path: 'id',
|
|
148
|
+
message: t(locale, 'NOT_FOUND', { what: 'Artifact', id }),
|
|
149
|
+
hint: t(locale, 'NOT_FOUND_HINT'),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
});
|
|
232
153
|
}
|
|
233
154
|
return migrated(found);
|
|
234
155
|
}
|
|
235
156
|
|
|
236
157
|
function migrated(artifact: Artifact): Artifact {
|
|
237
|
-
const
|
|
238
|
-
const result = migrateContent(
|
|
158
|
+
const adapter = registry.get(artifact.format, locale);
|
|
159
|
+
const result = migrateContent(adapter, artifact.content, artifact.formatVersion, locale);
|
|
239
160
|
return result.migrated
|
|
240
|
-
? { ...artifact, content: result.content,
|
|
161
|
+
? { ...artifact, content: result.content, formatVersion: result.formatVersion }
|
|
241
162
|
: artifact;
|
|
242
163
|
}
|
|
243
164
|
|
|
244
165
|
function fields(
|
|
245
|
-
artifact: Pick<Artifact, 'id' | '
|
|
166
|
+
artifact: Pick<Artifact, 'id' | 'format' | 'version'>,
|
|
246
167
|
scope: Scope,
|
|
247
168
|
extra: LogFields = {},
|
|
248
169
|
): LogFields {
|
|
249
170
|
return {
|
|
250
171
|
artifactId: artifact.id,
|
|
251
|
-
|
|
172
|
+
format: artifact.format,
|
|
252
173
|
version: artifact.version,
|
|
253
174
|
actorType: scope.actor.type,
|
|
254
175
|
...extra,
|
|
@@ -275,147 +196,28 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
275
196
|
);
|
|
276
197
|
}
|
|
277
198
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
): Promise<CommitResult | undefined> {
|
|
283
|
-
if (!key) return undefined;
|
|
284
|
-
const entry = await store.findByIdempotencyKey(scope, id, key);
|
|
285
|
-
if (!entry) return undefined;
|
|
286
|
-
const artifact = await load(scope, id, entry.version);
|
|
287
|
-
const kind = registry.get(artifact.kind, locale);
|
|
288
|
-
return {
|
|
289
|
-
artifact,
|
|
290
|
-
version: entry.version,
|
|
291
|
-
createdIds: entry.createdIds,
|
|
292
|
-
diff: entry.diff,
|
|
293
|
-
lint: kind.lint(artifact.content, {
|
|
294
|
-
locale: artifact.locale,
|
|
295
|
-
theme: registry.theme(artifact.themeId, locale),
|
|
296
|
-
}),
|
|
297
|
-
rebased: false,
|
|
298
|
-
replayed: true,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Applies on the current version, rebasing from an older base when the touched items did not change. */
|
|
303
|
-
async function computeBatch(
|
|
304
|
-
scope: Scope,
|
|
305
|
-
current: Artifact,
|
|
306
|
-
request: ApplyRequest,
|
|
307
|
-
): Promise<{ result: BatchResult; rebased: boolean }> {
|
|
308
|
-
if (request.baseVersion === current.version) {
|
|
309
|
-
return { result: applyOperations(registry, current, request.operations, { locale }), rebased: false };
|
|
310
|
-
}
|
|
311
|
-
const conflict = (changes: ChangeSummary[]) => {
|
|
312
|
-
metrics.conflict?.(fields(current, scope));
|
|
313
|
-
events.emit({
|
|
314
|
-
type: 'artifact.conflict',
|
|
315
|
-
artifactId: current.id,
|
|
316
|
-
baseVersion: request.baseVersion,
|
|
317
|
-
currentVersion: current.version,
|
|
318
|
-
actor: scope.actor,
|
|
319
|
-
});
|
|
320
|
-
return new ArtifactError(
|
|
321
|
-
'VERSION_CONFLICT',
|
|
322
|
-
t(locale, 'VERSION_CONFLICT', { baseVersion: request.baseVersion, currentVersion: current.version }),
|
|
323
|
-
{
|
|
324
|
-
issues: [
|
|
325
|
-
{
|
|
326
|
-
path: 'baseVersion',
|
|
327
|
-
message: t(locale, 'VERSION_CONFLICT', {
|
|
328
|
-
baseVersion: request.baseVersion,
|
|
329
|
-
currentVersion: current.version,
|
|
330
|
-
}),
|
|
331
|
-
hint: t(locale, 'VERSION_CONFLICT_HINT'),
|
|
332
|
-
},
|
|
333
|
-
],
|
|
334
|
-
details: { currentVersion: current.version, changesSinceBase: changes },
|
|
335
|
-
},
|
|
336
|
-
);
|
|
337
|
-
};
|
|
338
|
-
if (request.baseVersion > current.version || request.baseVersion < 1) throw conflict([]);
|
|
339
|
-
const base = await load(scope, current.id, request.baseVersion);
|
|
340
|
-
const kind = registry.get(current.kind, locale);
|
|
341
|
-
// Validate against the base first: an invalid batch is the model's error, not a conflict.
|
|
342
|
-
const onBase = applyOperations(registry, base, request.operations, { locale });
|
|
343
|
-
const changedSince = [
|
|
344
|
-
...(base.title !== current.title
|
|
345
|
-
? [
|
|
346
|
-
{
|
|
347
|
-
type: 'changed' as const,
|
|
348
|
-
target: 'envelope:title',
|
|
349
|
-
summary: `'${base.title}' → '${current.title}'`,
|
|
350
|
-
},
|
|
351
|
-
]
|
|
352
|
-
: []),
|
|
353
|
-
...(base.themeId !== current.themeId
|
|
354
|
-
? [
|
|
355
|
-
{
|
|
356
|
-
type: 'changed' as const,
|
|
357
|
-
target: 'envelope:themeId',
|
|
358
|
-
summary: `${base.themeId} → ${current.themeId}`,
|
|
359
|
-
},
|
|
360
|
-
]
|
|
361
|
-
: []),
|
|
362
|
-
...(base.locale !== current.locale
|
|
363
|
-
? [
|
|
364
|
-
{
|
|
365
|
-
type: 'changed' as const,
|
|
366
|
-
target: 'envelope:locale',
|
|
367
|
-
summary: `${base.locale} → ${current.locale}`,
|
|
368
|
-
},
|
|
369
|
-
]
|
|
370
|
-
: []),
|
|
371
|
-
...kind.diff(base.content, current.content, locale),
|
|
372
|
-
];
|
|
373
|
-
const parsed = parseOperations(registry, current.kind, request.operations, locale);
|
|
374
|
-
const touched = batchTargets(kind, parsed, onBase.diff);
|
|
375
|
-
const changed = new Set(changedSince.map((change) => change.target));
|
|
376
|
-
if (targetsOverlap(touched, changed)) throw conflict(changedSince);
|
|
377
|
-
try {
|
|
378
|
-
return { result: applyOperations(registry, current, request.operations, { locale }), rebased: true };
|
|
379
|
-
} catch (error) {
|
|
380
|
-
if (
|
|
381
|
-
error instanceof ArtifactError &&
|
|
382
|
-
(error.code === 'OPERATION_FAILED' || error.code === 'INVALID_CONTENT')
|
|
383
|
-
) {
|
|
384
|
-
throw conflict(changedSince);
|
|
385
|
-
}
|
|
386
|
-
throw error;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function changeStatus(
|
|
391
|
-
scope: Scope,
|
|
392
|
-
id: string,
|
|
393
|
-
to: ArtifactStatus,
|
|
394
|
-
action: ArtifactAction,
|
|
395
|
-
message?: string,
|
|
396
|
-
): Promise<Artifact> {
|
|
397
|
-
const current = await load(scope, id);
|
|
398
|
-
await authorize(action, summarize(current), scope);
|
|
399
|
-
if (!canTransition(current.status, to)) {
|
|
400
|
-
throw new ArtifactError(
|
|
401
|
-
'PERMISSION_DENIED',
|
|
402
|
-
t(locale, 'INVALID_STATUS_TRANSITION', { from: current.status, to }),
|
|
403
|
-
{
|
|
404
|
-
details: { from: current.status, to },
|
|
405
|
-
},
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
const updated = await store.setStatus(scope, id, to, scope.actor, message);
|
|
199
|
+
/** The batch must be computed against the current version; a stale base is a conflict, never a silent rebase. */
|
|
200
|
+
function assertBase(scope: Scope, current: Artifact, baseVersion: number): void {
|
|
201
|
+
if (baseVersion === current.version) return;
|
|
202
|
+
metrics.conflict?.(fields(current, scope));
|
|
409
203
|
events.emit({
|
|
410
|
-
type: 'artifact.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
204
|
+
type: 'artifact.conflict',
|
|
205
|
+
artifactId: current.id,
|
|
206
|
+
baseVersion,
|
|
207
|
+
currentVersion: current.version,
|
|
414
208
|
actor: scope.actor,
|
|
415
|
-
...(message ? { message } : {}),
|
|
416
209
|
});
|
|
417
|
-
|
|
418
|
-
|
|
210
|
+
const message = t(locale, 'VERSION_CONFLICT', { baseVersion, currentVersion: current.version });
|
|
211
|
+
throw new ArtifactError('VERSION_CONFLICT', message, {
|
|
212
|
+
issues: [
|
|
213
|
+
{
|
|
214
|
+
path: 'baseVersion',
|
|
215
|
+
message,
|
|
216
|
+
hint: t(locale, 'VERSION_CONFLICT_HINT'),
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
details: { currentVersion: current.version },
|
|
220
|
+
});
|
|
419
221
|
}
|
|
420
222
|
|
|
421
223
|
const service: ArtifactService = {
|
|
@@ -424,28 +226,7 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
424
226
|
|
|
425
227
|
async create(scope, request) {
|
|
426
228
|
await authorize('create', null, scope);
|
|
427
|
-
const
|
|
428
|
-
if (request.idempotencyKey && store.findCreatedByIdempotencyKey) {
|
|
429
|
-
const existing = await store.findCreatedByIdempotencyKey(scope, request.idempotencyKey);
|
|
430
|
-
if (existing) {
|
|
431
|
-
const first = await load(scope, existing.id, 1);
|
|
432
|
-
const prepared = [...registry.get(first.kind, locale).ids(first.content)];
|
|
433
|
-
return {
|
|
434
|
-
artifact: first,
|
|
435
|
-
version: 1,
|
|
436
|
-
createdIds: prepared,
|
|
437
|
-
diff: [
|
|
438
|
-
{ type: 'added', target: `artifact:${first.id}`, summary: `${kind.label} "${first.title}"` },
|
|
439
|
-
],
|
|
440
|
-
lint: kind.lint(first.content, {
|
|
441
|
-
locale: first.locale,
|
|
442
|
-
theme: registry.theme(first.themeId, locale),
|
|
443
|
-
}),
|
|
444
|
-
rebased: false,
|
|
445
|
-
replayed: true,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
}
|
|
229
|
+
const adapter = registry.get(request.format, locale);
|
|
449
230
|
const title = titleSchema.safeParse(sanitizeDeep(request.title));
|
|
450
231
|
if (!title.success) {
|
|
451
232
|
throw new ArtifactError('INVALID_CONTENT', t(locale, 'INVALID_CONTENT'), {
|
|
@@ -455,318 +236,152 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
455
236
|
const artifactLocale = request.locale ?? options.defaultArtifactLocale ?? 'en';
|
|
456
237
|
const themeId = request.themeId ?? options.defaultThemeId ?? 'default';
|
|
457
238
|
registry.theme(themeId, locale);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
hint: t(locale, 'INPUT_FORMAT_HINT', { available: available.join(', ') || '—' }),
|
|
476
|
-
},
|
|
477
|
-
],
|
|
478
|
-
},
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
const parsedInput = input.schema.safeParse(sanitizeDeep(request.input.source));
|
|
482
|
-
if (!parsedInput.success) {
|
|
483
|
-
throw new ArtifactError('INVALID_CONTENT', t(locale, 'INVALID_CONTENT'), {
|
|
484
|
-
issues: zodIssuesToErrorIssues(parsedInput.error.issues, locale, 'input.source'),
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
raw = input.toContent(parsedInput.data, {
|
|
488
|
-
locale: artifactLocale,
|
|
489
|
-
note: (message) => inputNotes.push(message),
|
|
490
|
-
});
|
|
491
|
-
} else {
|
|
492
|
-
raw = request.content ?? kind.empty();
|
|
493
|
-
}
|
|
494
|
-
const prepared = prepareContent(registry, kind.kind, raw, { locale, theme: themeId });
|
|
495
|
-
prepared.lint.push(
|
|
496
|
-
...inputNotes.map((message) => ({
|
|
497
|
-
code: 'input-note',
|
|
498
|
-
severity: 'info' as const,
|
|
499
|
-
target: `input:${request.input?.format ?? 'content'}`,
|
|
500
|
-
message,
|
|
501
|
-
})),
|
|
502
|
-
);
|
|
239
|
+
const base = {
|
|
240
|
+
id: '',
|
|
241
|
+
format: adapter.format,
|
|
242
|
+
formatVersion: adapter.formatVersion,
|
|
243
|
+
version: 1,
|
|
244
|
+
title: title.data,
|
|
245
|
+
locale: artifactLocale,
|
|
246
|
+
themeId,
|
|
247
|
+
content: request.content ?? adapter.empty(),
|
|
248
|
+
};
|
|
249
|
+
// Creation with an initial batch goes through the same pipeline as any edit.
|
|
250
|
+
const applied = request.operations?.length
|
|
251
|
+
? await applyOperations(registry, base as Artifact, request.operations, { locale })
|
|
252
|
+
: undefined;
|
|
253
|
+
const prepared = applied
|
|
254
|
+
? { content: applied.content, lint: applied.lint }
|
|
255
|
+
: prepareContent(registry, request.format, base.content, { locale: artifactLocale, themeId });
|
|
503
256
|
const artifact = await store.create(scope, {
|
|
504
|
-
|
|
505
|
-
|
|
257
|
+
format: adapter.format,
|
|
258
|
+
formatVersion: adapter.formatVersion,
|
|
506
259
|
title: title.data,
|
|
507
|
-
status: 'draft',
|
|
508
260
|
locale: artifactLocale,
|
|
509
261
|
themeId,
|
|
510
262
|
content: prepared.content,
|
|
511
|
-
createdIds: prepared.ids,
|
|
512
263
|
...(request.tags ? { tags: request.tags } : {}),
|
|
513
|
-
...(request.message ? { message: request.message } : {}),
|
|
514
|
-
...(request.idempotencyKey ? { idempotencyKey: request.idempotencyKey } : {}),
|
|
515
264
|
});
|
|
516
265
|
events.emit({ type: 'artifact.created', artifact: summarize(artifact), actor: scope.actor });
|
|
517
|
-
if (prepared.lint.length > 0)
|
|
266
|
+
if (prepared.lint.length > 0) {
|
|
518
267
|
events.emit({
|
|
519
268
|
type: 'artifact.lint',
|
|
520
269
|
artifactId: artifact.id,
|
|
521
270
|
version: artifact.version,
|
|
522
271
|
warnings: prepared.lint.length,
|
|
523
272
|
});
|
|
273
|
+
}
|
|
524
274
|
logger.info('artifact.created', fields(artifact, scope));
|
|
525
275
|
return {
|
|
526
276
|
artifact,
|
|
527
277
|
version: artifact.version,
|
|
528
|
-
|
|
278
|
+
createdRefs: applied?.createdRefs ?? [],
|
|
529
279
|
diff: [
|
|
530
|
-
{
|
|
280
|
+
{
|
|
281
|
+
type: 'added',
|
|
282
|
+
target: `artifact:${artifact.id}`,
|
|
283
|
+
summary: `${adapter.label} "${artifact.title}"`,
|
|
284
|
+
},
|
|
531
285
|
],
|
|
532
286
|
lint: prepared.lint,
|
|
533
|
-
rebased: false,
|
|
534
|
-
replayed: false,
|
|
535
287
|
};
|
|
536
288
|
},
|
|
537
289
|
|
|
538
|
-
async get(scope, id
|
|
539
|
-
const artifact = await load(scope, id
|
|
290
|
+
async get(scope, id) {
|
|
291
|
+
const artifact = await load(scope, id);
|
|
540
292
|
await authorize('read', summarize(artifact), scope);
|
|
541
293
|
return artifact;
|
|
542
294
|
},
|
|
543
295
|
|
|
544
296
|
async read(scope, id, opts = {}) {
|
|
545
|
-
const artifact = await load(scope, id
|
|
297
|
+
const artifact = await load(scope, id);
|
|
546
298
|
await authorize('read', summarize(artifact), scope);
|
|
547
|
-
const
|
|
548
|
-
const
|
|
299
|
+
const adapter = registry.get(artifact.format, locale);
|
|
300
|
+
const result = adapter.read(artifact.content as never, {
|
|
549
301
|
mode: opts.mode ?? 'outline',
|
|
550
302
|
locale: artifact.locale,
|
|
551
|
-
theme: registry.theme(artifact.themeId, locale),
|
|
552
303
|
...(opts.range === undefined ? {} : { range: opts.range }),
|
|
553
304
|
...(opts.maxTokens === undefined ? {} : { maxTokens: opts.maxTokens }),
|
|
554
305
|
...(opts.cursor === undefined ? {} : { cursor: opts.cursor }),
|
|
555
306
|
...(opts.charsPerToken === undefined ? {} : { charsPerToken: opts.charsPerToken }),
|
|
556
307
|
});
|
|
557
|
-
const
|
|
308
|
+
const projection: Projection = {
|
|
309
|
+
text: result.text,
|
|
310
|
+
truncated: result.truncated,
|
|
311
|
+
...(result.cursor === undefined ? {} : { cursor: result.cursor }),
|
|
312
|
+
};
|
|
313
|
+
const header = `${adapter.label} "${artifact.title}" (id: ${artifact.id}, format: ${artifact.format}, version: ${artifact.version}, locale: ${artifact.locale})`;
|
|
558
314
|
let text = `${header}\n${delimitProjection(artifact.id, projection.text)}`;
|
|
559
315
|
if (projection.truncated) {
|
|
560
|
-
text += `\n[truncated — continue with cursor "${projection.cursor}"]
|
|
316
|
+
text += `\n[truncated — continue with cursor "${projection.cursor}"]`;
|
|
561
317
|
}
|
|
562
318
|
return { artifact: summarize(artifact), version: artifact.version, projection, text };
|
|
563
319
|
},
|
|
564
320
|
|
|
565
321
|
async apply(scope, request) {
|
|
566
322
|
const started = Date.now();
|
|
567
|
-
|
|
323
|
+
const current = await load(scope, request.artifactId);
|
|
568
324
|
await authorize('apply', summarize(current), scope);
|
|
569
|
-
const replayed = await replay(scope, current.id, request.idempotencyKey);
|
|
570
|
-
if (replayed) return replayed;
|
|
571
325
|
await assertNotLocked(scope, current.id);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
},
|
|
584
|
-
],
|
|
326
|
+
assertBase(scope, current, request.baseVersion);
|
|
327
|
+
const result = await applyOperations(registry, current, request.operations, { locale });
|
|
328
|
+
let committed: Artifact;
|
|
329
|
+
try {
|
|
330
|
+
committed = await store.commit(scope, current.id, {
|
|
331
|
+
baseVersion: current.version,
|
|
332
|
+
formatVersion: result.formatVersion,
|
|
333
|
+
content: result.content,
|
|
334
|
+
title: result.title,
|
|
335
|
+
locale: result.locale,
|
|
336
|
+
themeId: result.themeId,
|
|
585
337
|
});
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error instanceof ArtifactError) metrics.rejection?.(error.code, fields(current, scope));
|
|
340
|
+
throw error;
|
|
586
341
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
baseVersion: current.version,
|
|
592
|
-
schemaVersion: result.schemaVersion,
|
|
593
|
-
content: result.content,
|
|
594
|
-
title: result.title,
|
|
595
|
-
locale: result.locale,
|
|
596
|
-
themeId: result.themeId,
|
|
597
|
-
// Editing a published (or pending) artifact starts a new draft (spec §15.3).
|
|
598
|
-
...(current.status === 'published' || current.status === 'pending_approval'
|
|
599
|
-
? { status: 'draft' as const }
|
|
600
|
-
: {}),
|
|
601
|
-
operations: request.operations,
|
|
602
|
-
diff: result.diff,
|
|
603
|
-
createdIds: result.createdIds,
|
|
604
|
-
...(request.message ? { message: request.message } : {}),
|
|
605
|
-
...(request.idempotencyKey ? { idempotencyKey: request.idempotencyKey } : {}),
|
|
606
|
-
});
|
|
607
|
-
const duration = Date.now() - started;
|
|
608
|
-
const logFields = fields(committed, scope, {
|
|
609
|
-
operationCount: request.operations.length,
|
|
610
|
-
durationMs: duration,
|
|
611
|
-
});
|
|
612
|
-
metrics.applyDuration?.(duration, logFields);
|
|
613
|
-
logger.info('artifact.committed', logFields);
|
|
614
|
-
events.emit({
|
|
615
|
-
type: 'artifact.committed',
|
|
616
|
-
artifact: summarize(committed),
|
|
617
|
-
actor: scope.actor,
|
|
618
|
-
operationCount: request.operations.length,
|
|
619
|
-
diff: result.diff,
|
|
620
|
-
rebased,
|
|
621
|
-
});
|
|
622
|
-
if (result.lint.length > 0)
|
|
623
|
-
events.emit({
|
|
624
|
-
type: 'artifact.lint',
|
|
625
|
-
artifactId: committed.id,
|
|
626
|
-
version: committed.version,
|
|
627
|
-
warnings: result.lint.length,
|
|
628
|
-
});
|
|
629
|
-
return {
|
|
630
|
-
artifact: committed,
|
|
631
|
-
version: committed.version,
|
|
632
|
-
createdIds: result.createdIds,
|
|
633
|
-
diff: result.diff,
|
|
634
|
-
lint: result.lint,
|
|
635
|
-
rebased,
|
|
636
|
-
replayed: false,
|
|
637
|
-
};
|
|
638
|
-
} catch (error) {
|
|
639
|
-
// Another writer won the race: reload and rebase from the original base.
|
|
640
|
-
if (error instanceof ArtifactError && error.code === 'VERSION_CONFLICT' && attempt < retries) {
|
|
641
|
-
current = await load(scope, request.artifactId);
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
if (error instanceof ArtifactError) metrics.rejection?.(error.code, fields(current, scope));
|
|
645
|
-
throw error;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
},
|
|
649
|
-
|
|
650
|
-
async preview(scope, request) {
|
|
651
|
-
const current = await load(scope, request.artifactId);
|
|
652
|
-
await authorize('read', summarize(current), scope);
|
|
653
|
-
return (await computeBatch(scope, current, request)).result;
|
|
654
|
-
},
|
|
655
|
-
|
|
656
|
-
async list(scope, filter = {}, page = {}) {
|
|
657
|
-
await authorize('list', null, scope);
|
|
658
|
-
return store.list(scope, filter, page);
|
|
659
|
-
},
|
|
660
|
-
|
|
661
|
-
async history(scope, id, page = {}) {
|
|
662
|
-
const current = await load(scope, id);
|
|
663
|
-
await authorize('history', summarize(current), scope);
|
|
664
|
-
return store.history(scope, id, page);
|
|
665
|
-
},
|
|
666
|
-
|
|
667
|
-
async restore(scope, id, version, message) {
|
|
668
|
-
const current = await load(scope, id);
|
|
669
|
-
await authorize('restore', summarize(current), scope);
|
|
670
|
-
await assertNotLocked(scope, id);
|
|
671
|
-
const old = await load(scope, id, version);
|
|
672
|
-
const kind = registry.get(current.kind, locale);
|
|
673
|
-
const diff = [
|
|
674
|
-
...(old.title !== current.title
|
|
675
|
-
? [
|
|
676
|
-
{
|
|
677
|
-
type: 'changed' as const,
|
|
678
|
-
target: 'envelope:title',
|
|
679
|
-
summary: `'${current.title}' → '${old.title}'`,
|
|
680
|
-
},
|
|
681
|
-
]
|
|
682
|
-
: []),
|
|
683
|
-
...kind.diff(current.content, old.content, locale),
|
|
684
|
-
];
|
|
685
|
-
const beforeIds = new Set<string>(kind.ids(current.content));
|
|
686
|
-
const createdIds = [...kind.ids(old.content)].filter((itemId) => !beforeIds.has(itemId));
|
|
687
|
-
const committed = await store.commit(scope, id, {
|
|
688
|
-
baseVersion: current.version,
|
|
689
|
-
schemaVersion: old.schemaVersion,
|
|
690
|
-
content: old.content,
|
|
691
|
-
title: old.title,
|
|
692
|
-
locale: old.locale,
|
|
693
|
-
themeId: old.themeId,
|
|
694
|
-
...(current.status !== 'draft' && current.status !== 'archived' ? { status: 'draft' as const } : {}),
|
|
695
|
-
operations: [{ op: 'replace_content', content: '[restored]' }],
|
|
696
|
-
diff,
|
|
697
|
-
createdIds,
|
|
698
|
-
restoredFrom: version,
|
|
699
|
-
message: message ?? `Restored version ${version}`,
|
|
342
|
+
const duration = Date.now() - started;
|
|
343
|
+
const logFields = fields(committed, scope, {
|
|
344
|
+
operationCount: request.operations.length,
|
|
345
|
+
durationMs: duration,
|
|
700
346
|
});
|
|
347
|
+
metrics.applyDuration?.(duration, logFields);
|
|
348
|
+
logger.info('artifact.committed', logFields);
|
|
701
349
|
events.emit({
|
|
702
350
|
type: 'artifact.committed',
|
|
703
351
|
artifact: summarize(committed),
|
|
704
352
|
actor: scope.actor,
|
|
705
|
-
operationCount:
|
|
706
|
-
diff,
|
|
707
|
-
rebased: false,
|
|
353
|
+
operationCount: request.operations.length,
|
|
354
|
+
diff: result.diff,
|
|
708
355
|
});
|
|
356
|
+
if (result.lint.length > 0) {
|
|
357
|
+
events.emit({
|
|
358
|
+
type: 'artifact.lint',
|
|
359
|
+
artifactId: committed.id,
|
|
360
|
+
version: committed.version,
|
|
361
|
+
warnings: result.lint.length,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
709
364
|
return {
|
|
710
365
|
artifact: committed,
|
|
711
366
|
version: committed.version,
|
|
712
|
-
|
|
713
|
-
diff,
|
|
714
|
-
lint:
|
|
715
|
-
locale: committed.locale,
|
|
716
|
-
theme: registry.theme(committed.themeId, locale),
|
|
717
|
-
}),
|
|
718
|
-
rebased: false,
|
|
719
|
-
replayed: false,
|
|
367
|
+
createdRefs: result.createdRefs,
|
|
368
|
+
diff: result.diff,
|
|
369
|
+
lint: result.lint,
|
|
720
370
|
};
|
|
721
371
|
},
|
|
722
372
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
const current = await load(scope, id);
|
|
730
|
-
if (current.status !== 'pending_approval') {
|
|
731
|
-
throw new ArtifactError(
|
|
732
|
-
'PERMISSION_DENIED',
|
|
733
|
-
t(locale, 'INVALID_STATUS_TRANSITION', { from: current.status, to: 'published' }),
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
return changeStatus(scope, id, 'published', 'approve', message);
|
|
737
|
-
},
|
|
738
|
-
|
|
739
|
-
async reject(scope, id, reason) {
|
|
740
|
-
if (scope.actor.type === 'agent') {
|
|
741
|
-
throw new ArtifactError('PERMISSION_DENIED', t(locale, 'PERMISSION_DENIED', { action: 'reject' }));
|
|
742
|
-
}
|
|
743
|
-
const current = await load(scope, id);
|
|
744
|
-
if (current.status !== 'pending_approval') {
|
|
745
|
-
throw new ArtifactError(
|
|
746
|
-
'PERMISSION_DENIED',
|
|
747
|
-
t(locale, 'INVALID_STATUS_TRANSITION', { from: current.status, to: 'draft' }),
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
return changeStatus(scope, id, 'draft', 'reject', reason);
|
|
373
|
+
async preview(scope, request) {
|
|
374
|
+
const current = await load(scope, request.artifactId);
|
|
375
|
+
await authorize('read', summarize(current), scope);
|
|
376
|
+
assertBase(scope, current, request.baseVersion);
|
|
377
|
+
return applyOperations(registry, current, request.operations, { locale });
|
|
751
378
|
},
|
|
752
379
|
|
|
753
|
-
async
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
issues: [
|
|
757
|
-
{
|
|
758
|
-
path: 'id',
|
|
759
|
-
message: t(locale, 'APPROVAL_REQUIRED', { action: 'publish' }),
|
|
760
|
-
hint: t(locale, 'APPROVAL_REQUIRED_HINT'),
|
|
761
|
-
},
|
|
762
|
-
],
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
return changeStatus(scope, id, 'published', 'publish');
|
|
380
|
+
async list(scope, filter = {}, page = {}) {
|
|
381
|
+
await authorize('list', null, scope);
|
|
382
|
+
return store.list(scope, filter, page);
|
|
766
383
|
},
|
|
767
384
|
|
|
768
|
-
archive: (scope, id) => changeStatus(scope, id, 'archived', 'archive'),
|
|
769
|
-
|
|
770
385
|
async lock(scope, id, ttlMs = 5 * 60_000) {
|
|
771
386
|
const current = await load(scope, id);
|
|
772
387
|
await authorize('lock', summarize(current), scope);
|
|
@@ -783,20 +398,9 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
783
398
|
|
|
784
399
|
async export(scope, request) {
|
|
785
400
|
const started = Date.now();
|
|
786
|
-
const artifact = await load(scope, request.artifactId
|
|
401
|
+
const artifact = await load(scope, request.artifactId);
|
|
787
402
|
await authorize('export', summarize(artifact), scope);
|
|
788
|
-
|
|
789
|
-
throw new ArtifactError('APPROVAL_REQUIRED', t(locale, 'APPROVAL_REQUIRED', { action: 'export' }), {
|
|
790
|
-
issues: [
|
|
791
|
-
{
|
|
792
|
-
path: 'artifactId',
|
|
793
|
-
message: t(locale, 'APPROVAL_REQUIRED', { action: 'export' }),
|
|
794
|
-
hint: t(locale, 'APPROVAL_REQUIRED_HINT'),
|
|
795
|
-
},
|
|
796
|
-
],
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
const exporter = registry.exporter(artifact.kind, request.format, locale);
|
|
403
|
+
const exporter = registry.exporter(artifact.format, request.format, locale);
|
|
800
404
|
const warnings: string[] = [];
|
|
801
405
|
const timeout = AbortSignal.timeout(request.timeoutMs ?? options.exportTimeoutMs ?? 60_000);
|
|
802
406
|
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
@@ -816,8 +420,8 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
816
420
|
signal,
|
|
817
421
|
);
|
|
818
422
|
} catch (cause) {
|
|
819
|
-
metrics.rejection?.('EXPORT_FAILED', fields(artifact, scope, {
|
|
820
|
-
logger.error('artifact.export_failed', fields(artifact, scope, {
|
|
423
|
+
metrics.rejection?.('EXPORT_FAILED', fields(artifact, scope, { target: request.format }));
|
|
424
|
+
logger.error('artifact.export_failed', fields(artifact, scope, { target: request.format }));
|
|
821
425
|
throw new ArtifactError('EXPORT_FAILED', t(locale, 'EXPORT_FAILED', { format: request.format }), {
|
|
822
426
|
cause,
|
|
823
427
|
});
|
|
@@ -846,7 +450,7 @@ export function createArtifactService(options: ArtifactServiceOptions): Artifact
|
|
|
846
450
|
const duration = Date.now() - started;
|
|
847
451
|
metrics.exportDuration?.(
|
|
848
452
|
duration,
|
|
849
|
-
fields(artifact, scope, {
|
|
453
|
+
fields(artifact, scope, { target: request.format, durationMs: duration }),
|
|
850
454
|
);
|
|
851
455
|
events.emit({
|
|
852
456
|
type: 'artifact.exported',
|
|
@@ -900,4 +504,4 @@ export function slugify(title: string): string {
|
|
|
900
504
|
return slug || 'artifact';
|
|
901
505
|
}
|
|
902
506
|
|
|
903
|
-
export {
|
|
507
|
+
export type { AnyFormatAdapter };
|