@lumenkey/cli 0.3.0 → 0.4.0-rc.1

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 (184) hide show
  1. package/README.md +51 -2
  2. package/dist/cli/copied.js +86 -85
  3. package/dist/cli/index.js +56 -44
  4. package/dist/cli/render.js +5 -1
  5. package/dist/core/asset-stream.d.ts +16 -0
  6. package/dist/core/asset-stream.js +64 -0
  7. package/dist/core/atomic-config-file.js +8 -7
  8. package/dist/core/chinese-messages.d.ts +4 -0
  9. package/dist/core/chinese-messages.js +907 -0
  10. package/dist/core/chinese-script.d.ts +7 -0
  11. package/dist/core/chinese-script.js +28 -0
  12. package/dist/core/client.js +2 -1
  13. package/dist/core/config-dir.js +2 -1
  14. package/dist/core/config.js +7 -6
  15. package/dist/core/download-root.d.ts +5 -0
  16. package/dist/core/download-root.js +64 -0
  17. package/dist/core/download.js +15 -12
  18. package/dist/core/generation-input.js +3 -2
  19. package/dist/core/generation.d.ts +8 -1
  20. package/dist/core/generation.js +13 -10
  21. package/dist/core/job-display.js +7 -6
  22. package/dist/core/locale.d.ts +2 -0
  23. package/dist/core/locale.js +172 -142
  24. package/dist/core/outcome.d.ts +6 -0
  25. package/dist/core/outcome.js +17 -4
  26. package/dist/core/output-path.d.ts +3 -1
  27. package/dist/core/output-path.js +5 -2
  28. package/dist/core/params.js +26 -25
  29. package/dist/core/profiles.js +5 -4
  30. package/dist/core/set-values.d.ts +0 -1
  31. package/dist/core/set-values.js +7 -7
  32. package/dist/core/submission-journal.js +43 -17
  33. package/dist/core/version.js +2 -3
  34. package/dist/mcp/server.js +30 -29
  35. package/dist/studio/asset-integrity-hash.d.ts +28 -0
  36. package/dist/studio/asset-integrity-hash.js +93 -0
  37. package/dist/studio/asset-integrity-queue.d.ts +24 -0
  38. package/dist/studio/asset-integrity-queue.js +94 -0
  39. package/dist/studio/asset-store.d.ts +52 -0
  40. package/dist/studio/asset-store.js +352 -0
  41. package/dist/studio/canvas/LICENSES.txt +1590 -0
  42. package/dist/studio/canvas/dependencies.json +862 -0
  43. package/dist/studio/canvas/director-source-manifest.json +47 -0
  44. package/dist/studio/canvas/editor.css +239 -0
  45. package/dist/studio/canvas/editor.js +4613 -0
  46. package/dist/studio/canvas/integration-source-manifest.json +384 -0
  47. package/dist/studio/canvas/source-manifest.json +240 -0
  48. package/dist/studio/canvas-store.d.ts +1254 -0
  49. package/dist/studio/canvas-store.js +205 -0
  50. package/dist/studio/capture-placement.d.ts +11 -0
  51. package/dist/studio/capture-placement.js +85 -0
  52. package/dist/studio/cloud-contract.d.ts +128 -0
  53. package/dist/studio/cloud-contract.js +229 -0
  54. package/dist/studio/cloud-service.d.ts +178 -0
  55. package/dist/studio/cloud-service.js +202 -0
  56. package/dist/studio/director-captures.d.ts +59 -0
  57. package/dist/studio/director-captures.js +141 -0
  58. package/dist/studio/director-contract.d.ts +1489 -0
  59. package/dist/studio/director-contract.js +288 -0
  60. package/dist/studio/director-png.d.ts +6 -0
  61. package/dist/studio/director-png.js +41 -0
  62. package/dist/studio/director-relations.d.ts +40 -0
  63. package/dist/studio/director-relations.js +103 -0
  64. package/dist/studio/document-authority.d.ts +7 -0
  65. package/dist/studio/document-authority.js +50 -0
  66. package/dist/studio/draft-store.d.ts +95 -0
  67. package/dist/studio/draft-store.js +100 -0
  68. package/dist/studio/errors.d.ts +85 -0
  69. package/dist/studio/errors.js +94 -0
  70. package/dist/studio/launch.d.ts +5 -0
  71. package/dist/studio/launch.js +31 -0
  72. package/dist/studio/media.d.ts +9 -0
  73. package/dist/studio/media.js +50 -0
  74. package/dist/studio/output-save-contract.d.ts +1321 -0
  75. package/dist/studio/output-save-contract.js +26 -0
  76. package/dist/studio/output-save-journal.d.ts +971 -0
  77. package/dist/studio/output-save-journal.js +287 -0
  78. package/dist/studio/output-save-recovery.d.ts +18 -0
  79. package/dist/studio/output-save-recovery.js +161 -0
  80. package/dist/studio/output-save-service.d.ts +436 -0
  81. package/dist/studio/output-save-service.js +251 -0
  82. package/dist/studio/output-save-worker.d.ts +40 -0
  83. package/dist/studio/output-save-worker.js +360 -0
  84. package/dist/studio/paths.d.ts +6 -0
  85. package/dist/studio/paths.js +68 -0
  86. package/dist/studio/run-journal.d.ts +993 -0
  87. package/dist/studio/run-journal.js +125 -0
  88. package/dist/studio/server.d.ts +17 -0
  89. package/dist/studio/server.js +440 -0
  90. package/dist/studio/session.d.ts +6 -0
  91. package/dist/studio/session.js +26 -0
  92. package/dist/studio/shell.d.ts +3 -0
  93. package/dist/studio/shell.js +23 -0
  94. package/dist/studio/style.d.ts +2 -0
  95. package/dist/studio/style.js +29 -0
  96. package/dist/studio/types.d.ts +26 -0
  97. package/dist/studio/types.js +2 -0
  98. package/dist/studio/web/app.d.ts +2 -0
  99. package/dist/studio/web/app.js +662 -0
  100. package/dist/studio/web/asset-controller.d.ts +37 -0
  101. package/dist/studio/web/asset-controller.js +191 -0
  102. package/dist/studio/web/asset-model.d.ts +29 -0
  103. package/dist/studio/web/asset-model.js +51 -0
  104. package/dist/studio/web/controller.d.ts +113 -0
  105. package/dist/studio/web/controller.js +401 -0
  106. package/dist/studio/web/messages.d.ts +2 -0
  107. package/dist/studio/web/messages.js +74 -0
  108. package/dist/studio/web/model.d.ts +75 -0
  109. package/dist/studio/web/model.js +182 -0
  110. package/dist/studio/web/save-controller.d.ts +85 -0
  111. package/dist/studio/web/save-controller.js +495 -0
  112. package/dist/studio/web/save-model.d.ts +63 -0
  113. package/dist/studio/web/save-model.js +64 -0
  114. package/dist/studio/web/save-queue.d.ts +7 -0
  115. package/dist/studio/web/save-queue.js +62 -0
  116. package/dist/studio/web/save-view.d.ts +9 -0
  117. package/dist/studio/web/save-view.js +19 -0
  118. package/dist/studio/writer-lock.d.ts +5 -0
  119. package/dist/studio/writer-lock.js +88 -0
  120. package/package.json +11 -5
  121. package/dist/cli/copied.d.ts.map +0 -1
  122. package/dist/cli/copied.js.map +0 -1
  123. package/dist/cli/index.d.ts.map +0 -1
  124. package/dist/cli/index.js.map +0 -1
  125. package/dist/cli/render.d.ts.map +0 -1
  126. package/dist/cli/render.js.map +0 -1
  127. package/dist/core/atomic-config-file.d.ts.map +0 -1
  128. package/dist/core/atomic-config-file.js.map +0 -1
  129. package/dist/core/chat.d.ts.map +0 -1
  130. package/dist/core/chat.js.map +0 -1
  131. package/dist/core/client.d.ts.map +0 -1
  132. package/dist/core/client.js.map +0 -1
  133. package/dist/core/config-dir.d.ts.map +0 -1
  134. package/dist/core/config-dir.js.map +0 -1
  135. package/dist/core/config-error.d.ts.map +0 -1
  136. package/dist/core/config-error.js.map +0 -1
  137. package/dist/core/config.d.ts.map +0 -1
  138. package/dist/core/config.js.map +0 -1
  139. package/dist/core/credit-cap.d.ts.map +0 -1
  140. package/dist/core/credit-cap.js.map +0 -1
  141. package/dist/core/download.d.ts.map +0 -1
  142. package/dist/core/download.js.map +0 -1
  143. package/dist/core/error-result.d.ts.map +0 -1
  144. package/dist/core/error-result.js.map +0 -1
  145. package/dist/core/explain.d.ts.map +0 -1
  146. package/dist/core/explain.js.map +0 -1
  147. package/dist/core/generation-input.d.ts.map +0 -1
  148. package/dist/core/generation-input.js.map +0 -1
  149. package/dist/core/generation-result.d.ts.map +0 -1
  150. package/dist/core/generation-result.js.map +0 -1
  151. package/dist/core/generation.d.ts.map +0 -1
  152. package/dist/core/generation.js.map +0 -1
  153. package/dist/core/job-display.d.ts.map +0 -1
  154. package/dist/core/job-display.js.map +0 -1
  155. package/dist/core/locale.d.ts.map +0 -1
  156. package/dist/core/locale.js.map +0 -1
  157. package/dist/core/origin.d.ts.map +0 -1
  158. package/dist/core/origin.js.map +0 -1
  159. package/dist/core/outcome.d.ts.map +0 -1
  160. package/dist/core/outcome.js.map +0 -1
  161. package/dist/core/output-path.d.ts.map +0 -1
  162. package/dist/core/output-path.js.map +0 -1
  163. package/dist/core/params.d.ts.map +0 -1
  164. package/dist/core/params.js.map +0 -1
  165. package/dist/core/poller.d.ts.map +0 -1
  166. package/dist/core/poller.js.map +0 -1
  167. package/dist/core/profiles.d.ts.map +0 -1
  168. package/dist/core/profiles.js.map +0 -1
  169. package/dist/core/set-values.d.ts.map +0 -1
  170. package/dist/core/set-values.js.map +0 -1
  171. package/dist/core/submission-journal.d.ts.map +0 -1
  172. package/dist/core/submission-journal.js.map +0 -1
  173. package/dist/core/types.d.ts.map +0 -1
  174. package/dist/core/types.js.map +0 -1
  175. package/dist/core/version.d.ts.map +0 -1
  176. package/dist/core/version.js.map +0 -1
  177. package/dist/core/wait.d.ts.map +0 -1
  178. package/dist/core/wait.js.map +0 -1
  179. package/dist/index.d.ts.map +0 -1
  180. package/dist/index.js.map +0 -1
  181. package/dist/mcp/render.d.ts.map +0 -1
  182. package/dist/mcp/render.js.map +0 -1
  183. package/dist/mcp/server.d.ts.map +0 -1
  184. package/dist/mcp/server.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { link, open, readdir, rename, unlink } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
5
+ import { assertRegularFile, assertStudioRoot } from './paths.js';
6
+ import { prepareDocumentAuthority } from './document-authority.js';
7
+ import { hasFsCode, StudioError } from './errors.js';
8
+ import { isSafeJson, isoDate } from './run-journal.js';
9
+ import { referenceIds } from './director-relations.js';
10
+ const MAX_FILE_BYTES = 8 * 1024 * 1024;
11
+ const MAX_SNAPSHOT_BYTES = 512 * 1024;
12
+ const revision = z.number().int().nonnegative().safe();
13
+ const uuid = z.string().uuid();
14
+ const name = z.string().trim().min(1).max(120).refine(v => !/[\u0000-\u001f]/.test(v));
15
+ const identifier = z.string().min(1).max(160).regex(/^[a-zA-Z0-9_-]+$/);
16
+ const coordinate = z.number().finite().min(-1e7).max(1e7);
17
+ const status = z.enum(['idle', 'editing', 'queued', 'running', 'success', 'error', 'disabled', 'locked']);
18
+ const record = z.record(z.unknown());
19
+ const config = z.object({
20
+ provider: z.string().max(200), model: z.string().max(200), input: z.string().max(200), output: z.string().max(200), status,
21
+ options: z.array(z.string().max(1000)).max(100), params: record.optional(),
22
+ uploads: z.array(z.object({ assetId: uuid, name: z.string().max(200), mimeType: z.string().max(100), size: revision,
23
+ role: z.enum(['reference', 'mask', 'firstFrame', 'lastFrame', 'audio', 'document', 'source']) }).strict()).max(100).optional(),
24
+ }).strict();
25
+ const node = z.object({
26
+ id: identifier, type: z.enum(['text', 'image', 'video', 'asset']), title: z.string().max(200), subtitle: z.string().max(2000).optional(),
27
+ position: z.object({ x: coordinate, y: coordinate }).strict(), width: z.number().finite().positive().max(10000), height: z.number().finite().positive().max(10000),
28
+ status, metadata: record.refine(v => v.studioAssetId === undefined || uuid.safeParse(v.studioAssetId).success),
29
+ config: config.optional(), allowedModels: z.array(z.string().max(200)).max(500).optional(),
30
+ }).strict();
31
+ const connection = z.object({ id: identifier, fromNodeId: identifier, toNodeId: identifier,
32
+ fromHandle: z.string().max(100).optional(), toHandle: z.string().max(100).optional(),
33
+ status: z.enum(['idle', 'queued', 'running', 'success', 'error', 'skipped']).optional(),
34
+ }).strict();
35
+ const customStyle = z.object({
36
+ id: z.string().min(1).max(40).regex(/^[a-zA-Z0-9_-]+$/),
37
+ name: z.string().trim().min(1).max(40).refine(v => !/[\u0000-\u001f]/.test(v) && !/^(https?:|file:)/i.test(v)),
38
+ prompt: z.string().trim().min(1).max(500).refine(v => !/[\u0000-\u001f]/.test(v) && !/^(https?:|file:)/i.test(v)),
39
+ }).strict();
40
+ const safeLine = (max) => z.string().trim().min(1).max(max).refine(v => !/[\u0000-\u001f]/.test(v) && !/^(https?:|file:)/i.test(v));
41
+ const characterCard = z.object({
42
+ id: z.string().min(1).max(40).regex(/^[a-zA-Z0-9_-]+$/),
43
+ name: safeLine(40),
44
+ hair: safeLine(120).optional(),
45
+ face: safeLine(120).optional(),
46
+ clothing: safeLine(120).optional(),
47
+ marks: safeLine(120).optional(),
48
+ change: safeLine(200).optional(),
49
+ sourceAssetId: uuid.optional(),
50
+ skillId: z.enum(['character-view', 'character-sheet']).optional(),
51
+ }).strict();
52
+ const snapshotSchema = z.object({
53
+ version: z.literal('lumenkey-canvas-v1'), schemaVersion: z.literal(1), nodes: z.array(node).max(200), connections: z.array(connection).max(400),
54
+ viewport: z.object({ x: coordinate, y: coordinate, k: z.number().finite().min(0.05).max(10) }).strict(),
55
+ appearance: z.object({
56
+ grid: z.enum(['dot', 'line', 'blank']).optional(),
57
+ edge: z.enum(['curve', 'straight', 'ortho']).optional(),
58
+ hiddenEdges: z.boolean().optional(),
59
+ styles: z.array(customStyle).max(24).optional(),
60
+ characters: z.array(characterCard).max(24).optional(),
61
+ }).strict().optional(),
62
+ }).strict().refine(v => {
63
+ const ids = new Set(v.nodes.map(n => n.id));
64
+ return ids.size === v.nodes.length && new Set(v.connections.map(c => c.id)).size === v.connections.length &&
65
+ v.connections.every(c => ids.has(c.fromNodeId) && ids.has(c.toNodeId)) && Buffer.byteLength(JSON.stringify(v)) <= MAX_SNAPSHOT_BYTES;
66
+ });
67
+ const document = z.object({ id: uuid, name, revision, createdAt: isoDate, updatedAt: isoDate, snapshot: snapshotSchema }).strict();
68
+ const collection = z.object({ schemaVersion: z.literal(1), revision, documents: z.array(document).max(100) }).strict()
69
+ .refine(v => new Set(v.documents.map(d => d.id)).size === v.documents.length);
70
+ const createInput = z.object({ name }).strict();
71
+ const saveInput = z.object({ revision, name, snapshot: snapshotSchema }).strict();
72
+ function parseInput(schema, value) {
73
+ if (!isSafeJson(value))
74
+ throw new StudioError('STUDIO_INPUT_INVALID');
75
+ const parsed = schema.safeParse(value);
76
+ if (!parsed.success)
77
+ throw new StudioError('STUDIO_INPUT_INVALID');
78
+ return parsed.data;
79
+ }
80
+ export function createCanvasAuthority(root, assertAssets, validateDirectors) {
81
+ const file = path.join(root.managedDir, 'canvases.json');
82
+ let current;
83
+ async function disk() {
84
+ await assertStudioRoot(root);
85
+ try {
86
+ await assertRegularFile(file);
87
+ const handle = await open(file, 'r');
88
+ try {
89
+ if ((await handle.stat()).size > MAX_FILE_BYTES)
90
+ throw new Error();
91
+ const value = JSON.parse(await handle.readFile('utf8'));
92
+ if (!isSafeJson(value))
93
+ throw new Error();
94
+ return collection.parse(value);
95
+ }
96
+ finally {
97
+ await handle.close();
98
+ }
99
+ }
100
+ catch (error) {
101
+ if (hasFsCode(error, 'ENOENT'))
102
+ return undefined;
103
+ throw new StudioError('STUDIO_CANVAS_INVALID', 409);
104
+ }
105
+ }
106
+ function serializedCollection(next) {
107
+ const valid = collection.safeParse(next);
108
+ if (!valid.success)
109
+ throw new StudioError('STUDIO_CANVAS_INVALID', 409);
110
+ const text = JSON.stringify(valid.data);
111
+ if (Buffer.byteLength(text) > MAX_FILE_BYTES)
112
+ throw new StudioError('STUDIO_CANVAS_CAPACITY_EXCEEDED', 409);
113
+ return text;
114
+ }
115
+ async function commit(next, initial = false) {
116
+ await assertStudioRoot(root);
117
+ const text = serializedCollection(next);
118
+ const temp = path.join(root.managedDir, `canvases.${randomUUID()}.tmp`);
119
+ const handle = await open(temp, 'wx', 0o600);
120
+ try {
121
+ await handle.writeFile(text);
122
+ await handle.sync();
123
+ }
124
+ finally {
125
+ await handle.close();
126
+ }
127
+ await assertStudioRoot(root);
128
+ if (initial) {
129
+ await link(temp, file);
130
+ await unlink(temp);
131
+ }
132
+ else {
133
+ await assertRegularFile(file);
134
+ await rename(temp, file);
135
+ }
136
+ }
137
+ async function authority() {
138
+ const loaded = await disk();
139
+ if (loaded)
140
+ await prepareDocumentAuthority(root, 'canvases', true, 'STUDIO_CANVAS_INVALID');
141
+ if (current) {
142
+ if (!loaded || JSON.stringify(loaded) !== JSON.stringify(current))
143
+ throw new StudioError('STUDIO_CANVAS_INVALID', 409);
144
+ }
145
+ else if (loaded)
146
+ current = loaded;
147
+ else {
148
+ if ((await readdir(root.managedDir)).some(n => /^canvases\..+\.tmp$/.test(n)))
149
+ throw new StudioError('STUDIO_CANVAS_INVALID', 409);
150
+ await prepareDocumentAuthority(root, 'canvases', false, 'STUDIO_CANVAS_INVALID');
151
+ const empty = { schemaVersion: 1, revision: 0, documents: [] };
152
+ await commit(empty, true);
153
+ current = empty;
154
+ }
155
+ return current;
156
+ }
157
+ function find(before, id) {
158
+ if (!uuid.safeParse(id).success)
159
+ throw new StudioError('STUDIO_INPUT_INVALID');
160
+ const found = before.documents.find(d => d.id === id);
161
+ if (!found)
162
+ throw new StudioError('STUDIO_CANVAS_NOT_FOUND', 404);
163
+ return found;
164
+ }
165
+ async function prepareSave(id, value, pendingAssets = []) {
166
+ const input = parseInput(saveInput, value), before = await authority(), previous = find(before, id);
167
+ if (input.revision !== previous.revision)
168
+ throw new StudioError('STUDIO_CANVAS_CONFLICT', 409);
169
+ const assetIds = input.snapshot.nodes.flatMap(n => [
170
+ ...(typeof n.metadata.studioAssetId === 'string' ? [n.metadata.studioAssetId] : []), ...(n.config?.uploads?.map(u => u.assetId) ?? []), ...referenceIds(n.metadata),
171
+ ]);
172
+ await assertAssets([...new Set(assetIds)], pendingAssets);
173
+ const saved = { ...previous, ...input, revision: previous.revision + 1, updatedAt: new Date().toISOString() };
174
+ await validateDirectors(saved, previous, before.documents, pendingAssets);
175
+ const next = { ...before, revision: before.revision + 1, documents: before.documents.map(d => d.id === id ? saved : d) };
176
+ // Both capture preview and the actual write validate the complete file before any asset is registered.
177
+ serializedCollection(next);
178
+ return { before, saved, next };
179
+ }
180
+ return {
181
+ list: async () => ({ items: (await authority()).documents.map(({ snapshot: _snapshot, ...summary }) => structuredClone(summary)) }),
182
+ create: async (value) => {
183
+ const input = parseInput(createInput, value), before = await authority();
184
+ const now = new Date().toISOString();
185
+ const created = { id: randomUUID(), name: input.name, revision: 0, createdAt: now, updatedAt: now,
186
+ snapshot: { version: 'lumenkey-canvas-v1', schemaVersion: 1, nodes: [], connections: [], viewport: { x: 0, y: 0, k: 1 } } };
187
+ const next = { ...before, revision: before.revision + 1, documents: [...before.documents, created] };
188
+ await commit(next);
189
+ current = next;
190
+ return structuredClone(created);
191
+ },
192
+ read: async (id) => structuredClone(find(await authority(), id)),
193
+ previewSave: async (id, value, pendingAssets) => structuredClone((await prepareSave(id, value, pendingAssets)).saved),
194
+ save: async (id, value) => {
195
+ const { saved, next } = await prepareSave(id, value);
196
+ await commit(next);
197
+ current = next;
198
+ return structuredClone(saved);
199
+ },
200
+ };
201
+ }
202
+ export function serializeCanvasAuthority(authority, serial) {
203
+ return { list: () => serial(() => authority.list()), create: value => serial(() => authority.create(value)), read: id => serial(() => authority.read(id)), save: (id, value) => serial(() => authority.save(id, value)) };
204
+ }
205
+ //# sourceMappingURL=canvas-store.js.map
@@ -0,0 +1,11 @@
1
+ import type { StudioCanvasSnapshot } from './canvas-store.js';
2
+ export declare const CAPTURE_NODE_SIZE: {
3
+ readonly width: 300;
4
+ readonly height: 240;
5
+ };
6
+ type Node = StudioCanvasSnapshot['nodes'][number];
7
+ type Position = Node['position'];
8
+ /** Only chooses the new node's position; persisted/replayed nodes never enter here. */
9
+ export declare function capturePosition(nodes: readonly Node[], source: Node): Position;
10
+ export {};
11
+ //# sourceMappingURL=capture-placement.d.ts.map
@@ -0,0 +1,85 @@
1
+ import { StudioError } from './errors.js';
2
+ export const CAPTURE_NODE_SIZE = { width: 300, height: 240 };
3
+ const GAP = 60;
4
+ const MIN = -1e7;
5
+ const MAX = 1e7;
6
+ // NodeCard uses center coordinates and border-box sizing. Legacy positive sizes
7
+ // can be smaller than its padding: full cards use 26px (video24), selected
8
+ // borders1.5px per side; compact cards are smaller. Reserve that render maximum
9
+ // across selection/LOD without rewriting stored dimensions or moving old nodes.
10
+ const rectangle = (n) => {
11
+ const minimum = 2 * (n.type === 'video' ? 24 : 26) + 2 * 1.5;
12
+ const width = Math.max(n.width, minimum), height = Math.max(n.height, minimum);
13
+ return { left: n.position.x - width / 2, right: n.position.x + width / 2,
14
+ top: n.position.y - height / 2, bottom: n.position.y + height / 2 };
15
+ };
16
+ // A fallback candidate, not a rounded obstacle: preserve exact touching slots.
17
+ const roundingMargin = (value) => Number.EPSILON * Math.max(1, Math.abs(value), 1000) * 8;
18
+ /** Only chooses the new node's position; persisted/replayed nodes never enter here. */
19
+ export function capturePosition(nodes, source) {
20
+ const { width, height } = CAPTURE_NODE_SIZE;
21
+ const halfWidth = width / 2, halfHeight = height / 2, obstacles = nodes.map(rectangle);
22
+ const preferred = { x: rectangle(source).left - GAP - halfWidth, y: source.position.y };
23
+ const minX = MIN + halfWidth, minY = MIN + halfHeight, maxX = MAX - halfWidth, maxY = MAX - halfHeight;
24
+ const preferredX = Math.max(minX, Math.min(maxX, preferred.x));
25
+ const rows = new Set([Math.max(minY, Math.min(maxY, preferred.y)), minY, maxY]);
26
+ for (const b of obstacles) {
27
+ const top = b.top - GAP - halfHeight, bottom = b.bottom + GAP + halfHeight;
28
+ const boundaries = [top, bottom];
29
+ if (top + halfHeight + GAP > b.top)
30
+ boundaries.push(top - roundingMargin(top));
31
+ if (bottom - halfHeight < b.bottom + GAP)
32
+ boundaries.push(bottom + roundingMargin(bottom));
33
+ for (const y of boundaries) {
34
+ if (y >= minY && y <= maxY)
35
+ rows.add(y);
36
+ }
37
+ }
38
+ let best, bestDistance = Infinity;
39
+ for (const y of rows) {
40
+ // A canvas has at most 200 nodes: <=803 rows, each O(n log n), no grid search.
41
+ const blocked = obstacles
42
+ .filter(b => y - halfHeight < b.bottom + GAP && y + halfHeight + GAP > b.top)
43
+ .map(b => ({ left: b.left - GAP - halfWidth, right: b.right + GAP + halfWidth }))
44
+ .sort((a, b) => a.left - b.left || a.right - b.right);
45
+ const merged = [];
46
+ for (const range of blocked) {
47
+ const last = merged.at(-1);
48
+ // Open intervals: touching endpoints remain a usable exact-clearance slot.
49
+ if (last && range.left < last.right)
50
+ last.right = Math.max(last.right, range.right);
51
+ else
52
+ merged.push({ ...range });
53
+ }
54
+ const obstructed = merged.find(range => preferredX > range.left && preferredX < range.right);
55
+ const clear = (x) => obstacles.every(b => x + halfWidth + GAP <= b.left || b.right + GAP <= x - halfWidth ||
56
+ y + halfHeight + GAP <= b.top || b.bottom + GAP <= y - halfHeight);
57
+ const columns = obstructed ? [obstructed.left, obstructed.right] : [preferredX];
58
+ // Only add fallback positions when exact boundary arithmetic is unsafe.
59
+ // Otherwise a sub-ULP distance tie could prefer an unnecessary tiny shift.
60
+ for (const x of [...columns])
61
+ if (!clear(x)) {
62
+ if (!obstructed || x === obstructed.left)
63
+ columns.push(x - roundingMargin(x));
64
+ if (!obstructed || x === obstructed.right)
65
+ columns.push(x + roundingMargin(x));
66
+ }
67
+ for (const x of columns) {
68
+ if (x < minX || x > maxX)
69
+ continue;
70
+ // Verify the actual arithmetic used by rectangles, including cancellation
71
+ // at fractional boundaries. A near-boundary candidate must never collide.
72
+ if (!clear(x))
73
+ continue;
74
+ const distance = (x - preferred.x) ** 2 + (y - preferred.y) ** 2;
75
+ if (!best || distance < bestDistance || (distance === bestDistance && (y < best.y || (y === best.y && x < best.x)))) {
76
+ best = { x, y };
77
+ bestDistance = distance;
78
+ }
79
+ }
80
+ }
81
+ if (!best)
82
+ throw new StudioError('STUDIO_CAPTURE_CAPACITY_EXCEEDED', 409);
83
+ return best;
84
+ }
85
+ //# sourceMappingURL=capture-placement.js.map
@@ -0,0 +1,128 @@
1
+ import { z } from 'zod';
2
+ import { LumenKeyError } from '../core/client.js';
3
+ import { type FailureContext } from '../core/outcome.js';
4
+ import type { ModelItem } from '../core/types.js';
5
+ import { StudioError } from './errors.js';
6
+ import { type StudioJob, type StudioRun } from './run-journal.js';
7
+ export declare const quoteRequest: z.ZodObject<{
8
+ requestId: z.ZodString;
9
+ modelId: z.ZodString;
10
+ input: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, unknown, unknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
11
+ }, "strict", z.ZodTypeAny, {
12
+ modelId: string;
13
+ input: Record<string, unknown>;
14
+ requestId: string;
15
+ }, {
16
+ modelId: string;
17
+ requestId: string;
18
+ input?: unknown;
19
+ }>;
20
+ export declare function studioKeepsModel(model: {
21
+ type?: string;
22
+ capabilities?: string[];
23
+ inputSchema?: {
24
+ properties?: unknown;
25
+ };
26
+ }): boolean;
27
+ export declare function projectModels(models: ModelItem[], secret: string): ModelItem[];
28
+ export declare function assertNoCredential(value: unknown, secret: string): void;
29
+ export declare function validateInput(model: ModelItem, input: Record<string, unknown>): void;
30
+ export declare function safeCloudCode(error: unknown, secret?: string): string;
31
+ export declare function cloudDiagnostic(error: unknown, secret?: string, ctx?: FailureContext): {
32
+ resumeDeadline?: string | undefined;
33
+ recovery?: import("../core/locale.js").UnknownRecovery | undefined;
34
+ submission?: import("../core/outcome.js").Submission | undefined;
35
+ httpStatus?: number | undefined;
36
+ code: string;
37
+ message: string;
38
+ type: string;
39
+ };
40
+ export declare function safeCloudFailure(error: unknown, secret?: string, ctx?: FailureContext): StudioError | LumenKeyError;
41
+ export declare function cloudHttpError(error: unknown): {
42
+ status: number;
43
+ body: {
44
+ error: {
45
+ resumeDeadline?: string | undefined;
46
+ recovery?: import("../core/locale.js").UnknownRecovery | undefined;
47
+ submission?: import("../core/outcome.js").Submission | undefined;
48
+ httpStatus?: number | undefined;
49
+ code: string;
50
+ message: string;
51
+ type: string;
52
+ };
53
+ };
54
+ };
55
+ export declare function outputIdentity(job: StudioJob): {
56
+ status: "UNVERIFIED";
57
+ reason: "LEGACY_AWAITING_FRESH_GET" | "JOB_UNAVAILABLE" | "NO_VERIFIABLE_OUTPUTS" | "IDENTITY_CONFLICT";
58
+ } | {
59
+ status: "VERIFIED";
60
+ reason: null;
61
+ };
62
+ /** Phase-1 read projection; the caller must obtain one authorized authority snapshot. */
63
+ export declare function projectOutputViews(run: Pick<StudioRun, 'runId' | 'job'>, options: {
64
+ rootReady: boolean;
65
+ authorityHealthy: boolean;
66
+ rootNotAllowed?: boolean;
67
+ }): {
68
+ localOutputId: string | null;
69
+ runId: string;
70
+ jobId: string;
71
+ gatewayAssetId: string | null;
72
+ outputIndex: number | null;
73
+ displayOrdinal: number;
74
+ outputType: string | null;
75
+ mediaType: null;
76
+ identity: {
77
+ status: "UNVERIFIED";
78
+ reason: "LEGACY_AWAITING_FRESH_GET" | "JOB_UNAVAILABLE" | "NO_VERIFIABLE_OUTPUTS" | "IDENTITY_CONFLICT";
79
+ } | {
80
+ status: "VERIFIED";
81
+ reason: null;
82
+ };
83
+ acceptance: "UNKNOWN" | "NEVER_ACCEPTED";
84
+ saveAllowed: boolean;
85
+ saveBlockedCode: string | null;
86
+ saveOperationId: null;
87
+ save: null;
88
+ }[];
89
+ export declare function projectJob(value: unknown, secret: string, previous?: StudioJob, freshGet?: boolean): StudioJob;
90
+ export declare function publicRun(run: StudioRun): {
91
+ createdAt: string;
92
+ updatedAt: string;
93
+ failure?: {
94
+ code: string;
95
+ message: string;
96
+ type: string;
97
+ httpStatus?: number | undefined;
98
+ submission?: "created" | "none" | "unknown" | undefined;
99
+ recovery?: "expired" | "rerun" | "resume-command" | "ledger" | undefined;
100
+ resumeDeadline?: string | undefined;
101
+ } | undefined;
102
+ lastErrorCode?: string | undefined;
103
+ job?: {
104
+ status: "accepted" | "policy_checking" | "queued" | "submitting" | "provider_running" | "output_checking" | "succeeded" | "rejected" | "failed" | "cancelled" | "expired";
105
+ jobId: string;
106
+ outputs: {
107
+ url: string;
108
+ type?: string | undefined;
109
+ assetId?: string | undefined;
110
+ outputIndex?: number | undefined;
111
+ }[];
112
+ error?: {
113
+ code: string;
114
+ message: string;
115
+ } | undefined;
116
+ identityReason?: "LEGACY_AWAITING_FRESH_GET" | "JOB_UNAVAILABLE" | "NO_VERIFIABLE_OUTPUTS" | "IDENTITY_CONFLICT" | undefined;
117
+ } | undefined;
118
+ runId: string;
119
+ modelId: string;
120
+ state: "QUOTED" | "SUBMISSION_UNKNOWN" | "SUBMISSION_REJECTED" | "ACCEPTED" | "TERMINAL";
121
+ quote: {
122
+ credits: number;
123
+ quoteId: string;
124
+ modelId: string;
125
+ expiresAt: string;
126
+ };
127
+ };
128
+ //# sourceMappingURL=cloud-contract.d.ts.map
@@ -0,0 +1,229 @@
1
+ import { z } from 'zod';
2
+ import { createHash } from 'node:crypto';
3
+ import { LumenKeyError, redactPublicDetail } from '../core/client.js';
4
+ import { failure } from '../core/outcome.js';
5
+ import { StudioError } from './errors.js';
6
+ import { errorCode, inputSchema, isSafeJson, jobSchema, uuid } from './run-journal.js';
7
+ export const quoteRequest = z.object({ requestId: uuid, modelId: z.string().min(1).max(200), input: inputSchema }).strict();
8
+ const modelSchema = z.object({
9
+ id: z.string().min(1).max(200), displayName: z.string().max(300).optional(), name: z.string().max(300).optional(),
10
+ type: z.string().max(40).optional(), capabilities: z.array(z.string().max(80)).max(50).optional(),
11
+ inputSchema: z.record(z.unknown()).refine(isSafeJson),
12
+ pricing: z.array(z.object({ unit: z.string().max(100), credits: z.number().nonnegative().finite() })).max(200).optional(),
13
+ });
14
+ const IMAGE_INPUT = /^(image|image_url|imageUrl|reference_image|referenceImage|first_frame|firstFrame|images|reference_images)$/;
15
+ export function studioKeepsModel(model) {
16
+ const caps = Array.isArray(model?.capabilities) ? model.capabilities : [];
17
+ if (['image', 'video'].some(kind => model?.type === kind || caps.includes(kind)))
18
+ return true;
19
+ const readsText = model?.type === 'chat' || model?.type === 'text' || caps.includes('chat') || caps.includes('text');
20
+ const properties = model?.inputSchema?.properties;
21
+ if (!readsText || !properties || typeof properties !== 'object' || Array.isArray(properties))
22
+ return false;
23
+ return Object.keys(properties).some(key => IMAGE_INPUT.test(key));
24
+ }
25
+ export function projectModels(models, secret) {
26
+ if (models.length > 10000 || new Set(models.map(m => m.id)).size !== models.length)
27
+ throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
28
+ const kept = [];
29
+ for (const model of models) {
30
+ if (!studioKeepsModel(model))
31
+ continue;
32
+ const parsed = modelSchema.safeParse(model);
33
+ const properties = parsed.success ? parsed.data.inputSchema.properties : undefined;
34
+ const usable = parsed.success && properties && typeof properties === 'object' && !Array.isArray(properties) && isSafeJson(properties);
35
+ const generates = ['image', 'video'].some(kind => model.type === kind || model.capabilities?.includes(kind));
36
+ if (!usable) {
37
+ if (generates)
38
+ throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
39
+ continue;
40
+ }
41
+ assertNoCredential(parsed.data, secret);
42
+ kept.push(parsed.data);
43
+ }
44
+ return kept;
45
+ }
46
+ export function assertNoCredential(value, secret) {
47
+ if (secret && JSON.stringify(value).includes(secret))
48
+ throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
49
+ }
50
+ export function validateInput(model, input) {
51
+ const properties = model.inputSchema?.properties;
52
+ function check(value, spec, depth) {
53
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec) || depth > 20)
54
+ throw new StudioError('STUDIO_INPUT_INVALID');
55
+ const s = spec;
56
+ if (Array.isArray(s.enum) && !s.enum.some(v => JSON.stringify(v) === JSON.stringify(value)))
57
+ throw new StudioError('STUDIO_INPUT_INVALID');
58
+ const type = s.type;
59
+ if (typeof type === 'string') {
60
+ const valid = type === 'integer' ? Number.isSafeInteger(value) : type === 'number' ? typeof value === 'number' && Number.isFinite(value) : type === 'array' ? Array.isArray(value) : type === 'object' ? !!value && typeof value === 'object' && !Array.isArray(value) : type === 'null' ? value === null : ['string', 'boolean'].includes(type) ? typeof value === type : false;
61
+ if (!valid)
62
+ throw new StudioError('STUDIO_INPUT_INVALID');
63
+ }
64
+ if (Array.isArray(value) && s.items)
65
+ for (const item of value)
66
+ check(item, s.items, depth + 1);
67
+ if (value && typeof value === 'object' && !Array.isArray(value) && s.properties && typeof s.properties === 'object') {
68
+ const p = s.properties;
69
+ for (const [k, v] of Object.entries(value)) {
70
+ if (Object.hasOwn(p, k))
71
+ check(v, p[k], depth + 1);
72
+ else if (s.additionalProperties === false)
73
+ throw new StudioError('STUDIO_INPUT_INVALID');
74
+ }
75
+ }
76
+ }
77
+ for (const [key, value] of Object.entries(input)) {
78
+ if (!Object.hasOwn(properties, key))
79
+ throw new StudioError('STUDIO_INPUT_INVALID');
80
+ check(value, properties[key], 0);
81
+ }
82
+ // Required fields/default application and full JSON Schema remain gateway authority.
83
+ }
84
+ export function safeCloudCode(error, secret) {
85
+ const code = error instanceof LumenKeyError && errorCode.safeParse(error.code).success ? error.code : error instanceof StudioError ? error.code : 'STUDIO_CLOUD_FAILED';
86
+ return secret && code.includes(secret) ? 'STUDIO_CLOUD_FAILED' : code;
87
+ }
88
+ const diagnostics = new WeakMap();
89
+ export function cloudDiagnostic(error, secret, ctx = {}) {
90
+ const status = error instanceof LumenKeyError || error instanceof StudioError ? error.status : undefined;
91
+ const safe = error instanceof StudioError ? error : new LumenKeyError(safeCloudCode(error, secret), status);
92
+ // Never pass keys, paths, raw job bodies or upstream detail to the HTTP projection.
93
+ const result = failure(safe, { submission: ctx.submission, recovery: ctx.recovery, recordedAt: ctx.recordedAt });
94
+ return { code: result.code, message: result.message, type: result.type,
95
+ ...(result.httpStatus !== undefined ? { httpStatus: result.httpStatus } : {}),
96
+ ...(result.submission ? { submission: result.submission } : {}),
97
+ ...(result.recovery ? { recovery: result.recovery } : {}),
98
+ ...(result.resumeDeadline ? { resumeDeadline: result.resumeDeadline } : {}) };
99
+ }
100
+ export function safeCloudFailure(error, secret, ctx) {
101
+ if (error instanceof Error && diagnostics.has(error) && !ctx)
102
+ return error;
103
+ const status = error instanceof LumenKeyError && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : undefined;
104
+ const safe = error instanceof StudioError ? error : new LumenKeyError(safeCloudCode(error, secret), status);
105
+ diagnostics.set(safe, cloudDiagnostic(safe, secret, ctx));
106
+ return safe;
107
+ }
108
+ export function cloudHttpError(error) {
109
+ const status = error instanceof StudioError ? error.status : error instanceof LumenKeyError && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : 502;
110
+ const diagnostic = error instanceof Error ? diagnostics.get(error) : undefined;
111
+ return { status, body: { error: diagnostic ?? cloudDiagnostic(error) } };
112
+ }
113
+ export function outputIdentity(job) {
114
+ const withIds = job.outputs.filter(o => o.assetId !== undefined || o.outputIndex !== undefined);
115
+ if (withIds.some(o => !uuid.safeParse(o.assetId).success || !Number.isSafeInteger(o.outputIndex) || o.outputIndex < 0 || !['image', 'video', 'audio'].includes(o.type ?? '')) ||
116
+ new Set(withIds.map(o => o.assetId?.toLowerCase())).size !== withIds.length || new Set(withIds.map(o => o.outputIndex)).size !== withIds.length) {
117
+ return { status: 'UNVERIFIED', reason: 'IDENTITY_CONFLICT' };
118
+ }
119
+ const reason = job.identityReason ?? (job.outputs.length && job.outputs.every(o => o.assetId !== undefined && o.outputIndex !== undefined) ? null : 'LEGACY_AWAITING_FRESH_GET');
120
+ return reason ? { status: 'UNVERIFIED', reason } : { status: 'VERIFIED', reason: null };
121
+ }
122
+ // Fixed Studio output namespace. Names are runId:gatewayAssetId, not URL/ordinal.
123
+ const OUTPUT_NAMESPACE = Buffer.from('a5fd4b08c7a54fe99ba09c1383a484c5', 'hex');
124
+ function localOutputId(runId, assetId) {
125
+ const bytes = createHash('sha1').update(OUTPUT_NAMESPACE).update(`${runId.toLowerCase()}:${assetId.toLowerCase()}`, 'utf8').digest().subarray(0, 16);
126
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
127
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
128
+ const hex = bytes.toString('hex');
129
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
130
+ }
131
+ /** Phase-1 read projection; the caller must obtain one authorized authority snapshot. */
132
+ export function projectOutputViews(run, options) {
133
+ if (options.rootReady && !options.rootNotAllowed && !options.authorityHealthy)
134
+ throw new StudioError('SAVE_AUTHORITY_UNAVAILABLE', 503);
135
+ if (!run.job)
136
+ return [];
137
+ const job = run.job;
138
+ const identity = outputIdentity(job);
139
+ const terminal = ['succeeded', 'failed', 'rejected', 'cancelled', 'expired'].includes(job.status);
140
+ return job.outputs.map((output, index) => {
141
+ const verified = identity.status === 'VERIFIED';
142
+ let saveBlockedCode = options.rootNotAllowed ? 'SAVE_ROOT_NOT_ALLOWED' : !options.rootReady ? 'SAVE_OUTPUT_ROOT_REQUIRED' : null;
143
+ saveBlockedCode ??= !verified ? identity.reason === 'IDENTITY_CONFLICT' ? 'SAVE_OUTPUT_IDENTITY_CONFLICT' : 'SAVE_OUTPUT_IDENTITY_UNVERIFIED' : null;
144
+ saveBlockedCode ??= !terminal ? 'SAVE_JOB_NOT_TERMINAL' : null;
145
+ saveBlockedCode ??= terminal && (job.status !== 'succeeded' || job.error) ? 'SAVE_SOURCE_UNAVAILABLE' : null;
146
+ saveBlockedCode ??= !['image', 'video'].includes(output.type ?? '') || !output.url.startsWith('https://') ? 'SAVE_MEDIA_UNSUPPORTED' : null;
147
+ return { localOutputId: verified ? localOutputId(run.runId, output.assetId) : null,
148
+ runId: run.runId, jobId: job.jobId, gatewayAssetId: verified ? output.assetId : null,
149
+ outputIndex: verified ? output.outputIndex : null, displayOrdinal: index + 1,
150
+ outputType: ['image', 'video', 'audio'].includes(output.type ?? '') ? output.type : null,
151
+ mediaType: null, identity, acceptance: verified && options.rootReady && options.authorityHealthy ? 'NEVER_ACCEPTED' : 'UNKNOWN',
152
+ saveAllowed: saveBlockedCode === null, saveBlockedCode, saveOperationId: null, save: null };
153
+ });
154
+ }
155
+ export function projectJob(value, secret, previous, freshGet = false) {
156
+ const v = value;
157
+ const outputs = [];
158
+ // A known contradiction must not silently heal merely because the same bad
159
+ // response repeats (or the source temporarily omits all outputs).
160
+ let conflict = previous?.identityReason === 'IDENTITY_CONFLICT';
161
+ // Private immutable type evidence outlives an absent/partial locator snapshot.
162
+ // Legacy jobs seed it from their known valid outputs; never infer it by order.
163
+ const evidence = new Map();
164
+ for (const item of [...previous?.identityEvidence ?? [], ...previous?.outputs ?? []]) {
165
+ if (uuid.safeParse(item.assetId).success && ['image', 'video', 'audio'].includes(item.type ?? '')) {
166
+ const id = item.assetId.toLowerCase();
167
+ const old = evidence.get(id);
168
+ if (old && old !== item.type)
169
+ conflict = true;
170
+ else
171
+ evidence.set(id, item.type);
172
+ }
173
+ }
174
+ const ids = new Set();
175
+ const indexes = new Set();
176
+ if (Array.isArray(v.outputs))
177
+ for (const item of v.outputs) {
178
+ if (!item || typeof item.url !== 'string')
179
+ throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
180
+ let url;
181
+ try {
182
+ url = new URL(item.url);
183
+ }
184
+ catch {
185
+ throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
186
+ }
187
+ const imageData = url.protocol === 'data:' && /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]*={0,2}$/.test(item.url);
188
+ if ((!['https:', 'http:'].includes(url.protocol) && !imageData) || url.username || url.password)
189
+ throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
190
+ const projected = { url: item.url, ...(typeof item.type === 'string' ? { type: item.type } : {}) };
191
+ if (item.assetId !== undefined || item.outputIndex !== undefined) {
192
+ if (!uuid.safeParse(item.assetId).success || !Number.isSafeInteger(item.outputIndex) || item.outputIndex < 0 || !['image', 'video', 'audio'].includes(item.type))
193
+ conflict = true;
194
+ else {
195
+ projected.assetId = item.assetId;
196
+ projected.outputIndex = item.outputIndex;
197
+ const id = item.assetId.toLowerCase();
198
+ const old = evidence.get(id);
199
+ if (ids.has(id) || indexes.has(item.outputIndex) || (old && old !== item.type))
200
+ conflict = true;
201
+ if (freshGet && !old)
202
+ evidence.set(id, item.type);
203
+ ids.add(id);
204
+ indexes.add(item.outputIndex);
205
+ }
206
+ }
207
+ outputs.push(projected);
208
+ }
209
+ let error;
210
+ if (v.error) {
211
+ const raw = typeof v.error === 'object' ? v.error : {};
212
+ error = { code: errorCode.safeParse(raw.code).success ? raw.code : 'JOB_TERMINAL_FAILURE', message: redactPublicDetail(raw.message, secret) || '任务返回错误;扣费状态以平台账本为准。' };
213
+ }
214
+ else if (v.status === 'succeeded' && outputs.length === 0)
215
+ error = { code: 'STUDIO_JOB_OUTPUT_MISSING', message: '任务没有可用输出,不能确认生成成功。' };
216
+ const identityReason = conflict ? 'IDENTITY_CONFLICT' : freshGet && (!outputs.length || outputs.some(o => !o.assetId)) ? 'NO_VERIFIABLE_OUTPUTS' : !freshGet && outputs.some(o => o.assetId) ? 'LEGACY_AWAITING_FRESH_GET' : undefined;
217
+ const identityEvidence = [...evidence].map(([assetId, type]) => ({ assetId, type }));
218
+ const parsed = jobSchema.safeParse({ jobId: v.jobId ?? v.id, status: v.status, outputs, ...(identityEvidence.length ? { identityEvidence } : {}), ...(identityReason ? { identityReason } : {}), ...(error ? { error } : {}) });
219
+ if (!parsed.success)
220
+ throw new StudioError('STUDIO_CLOUD_RESPONSE_INVALID', 502);
221
+ assertNoCredential(parsed.data, secret);
222
+ return parsed.data;
223
+ }
224
+ export function publicRun(run) {
225
+ const publicJob = run.job ? (({ identityEvidence: _private, ...job }) => job)(run.job) : undefined;
226
+ const diagnostic = run.lastFailure ?? (['SUBMITTING', 'SUBMISSION_UNKNOWN'].includes(run.state) ? cloudDiagnostic(new LumenKeyError(run.lastErrorCode ?? 'STUDIO_SUBMISSION_UNRESOLVED'), undefined, { submission: run.job ? 'created' : 'unknown', recovery: run.job ? undefined : 'ledger' }) : undefined);
227
+ return structuredClone({ runId: run.runId, modelId: run.modelId, state: run.state === 'SUBMITTING' ? 'SUBMISSION_UNKNOWN' : run.state, quote: run.quote, ...(publicJob ? { job: publicJob } : {}), ...(run.lastErrorCode ? { lastErrorCode: run.lastErrorCode } : {}), ...(diagnostic ? { failure: diagnostic } : {}), createdAt: run.createdAt, updatedAt: run.updatedAt });
228
+ }
229
+ //# sourceMappingURL=cloud-contract.js.map