@things-factory/figure-ui 10.1.241 → 10.1.244

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 (63) hide show
  1. package/client/graphql/store.ts +3 -2
  2. package/client/modeller/editor.ts +63 -5
  3. package/client/modeller/figure-parts.ts +18 -3
  4. package/client/modeller/figure-preview.ts +1 -1
  5. package/client/modeller/figure-properties.ts +14 -7
  6. package/client/modeller/figure-save-conflict.ts +73 -0
  7. package/client/modeller/figure-section-editor.ts +3 -0
  8. package/client/modeller/keys.ts +8 -2
  9. package/client/modeller/modeller-keys.ts +4 -1
  10. package/client/modeller/product-session.ts +107 -5
  11. package/client/modeller/save-conflict.ts +91 -0
  12. package/client/pages/figure-modeller-page.ts +58 -43
  13. package/dist-client/graphql/store.js +3 -2
  14. package/dist-client/graphql/store.js.map +1 -1
  15. package/dist-client/modeller/editor.d.ts +14 -0
  16. package/dist-client/modeller/editor.js +75 -5
  17. package/dist-client/modeller/editor.js.map +1 -1
  18. package/dist-client/modeller/figure-parts.js +18 -3
  19. package/dist-client/modeller/figure-parts.js.map +1 -1
  20. package/dist-client/modeller/figure-preview.d.ts +1 -1
  21. package/dist-client/modeller/figure-preview.js.map +1 -1
  22. package/dist-client/modeller/figure-properties.d.ts +4 -2
  23. package/dist-client/modeller/figure-properties.js +16 -7
  24. package/dist-client/modeller/figure-properties.js.map +1 -1
  25. package/dist-client/modeller/figure-save-conflict.d.ts +19 -0
  26. package/dist-client/modeller/figure-save-conflict.js +76 -0
  27. package/dist-client/modeller/figure-save-conflict.js.map +1 -0
  28. package/dist-client/modeller/figure-section-editor.d.ts +2 -0
  29. package/dist-client/modeller/figure-section-editor.js +8 -0
  30. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  31. package/dist-client/modeller/keys.d.ts +5 -0
  32. package/dist-client/modeller/keys.js +5 -3
  33. package/dist-client/modeller/keys.js.map +1 -1
  34. package/dist-client/modeller/modeller-keys.d.ts +2 -0
  35. package/dist-client/modeller/modeller-keys.js +2 -1
  36. package/dist-client/modeller/modeller-keys.js.map +1 -1
  37. package/dist-client/modeller/product-session.d.ts +368 -22
  38. package/dist-client/modeller/product-session.js +107 -7
  39. package/dist-client/modeller/product-session.js.map +1 -1
  40. package/dist-client/modeller/save-conflict.d.ts +12 -0
  41. package/dist-client/modeller/save-conflict.js +63 -0
  42. package/dist-client/modeller/save-conflict.js.map +1 -0
  43. package/dist-client/pages/figure-modeller-page.d.ts +11 -5
  44. package/dist-client/pages/figure-modeller-page.js +58 -49
  45. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  46. package/dist-client/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +5 -5
  48. package/test/browser/category-page.js +3 -1
  49. package/test/browser/category.check.mjs +6 -0
  50. package/test/browser/product-page.check.mjs +268 -6
  51. package/test/browser/product-page.js +4 -1
  52. package/test/browser/screen.check.mjs +3 -0
  53. package/test/browser/section.check.mjs +21 -0
  54. package/test/keys.test.ts +8 -0
  55. package/test/product-session.test.ts +137 -1
  56. package/test/save-conflict.test.ts +41 -0
  57. package/test/save-landed.test.ts +75 -0
  58. package/translations/en.json +12 -1
  59. package/translations/es.json +12 -1
  60. package/translations/ja.json +12 -1
  61. package/translations/ko.json +12 -1
  62. package/translations/ms.json +12 -1
  63. package/translations/zh.json +12 -1
@@ -9,6 +9,7 @@ import { createFigureAsset, createFigureEditorWorkspace, serializeFigureAsset, p
9
9
  function refusal(code, message) {
10
10
  return Object.assign(Error(message), { code });
11
11
  }
12
+ const sameValue = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
12
13
  /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
13
14
  export function createFigureProductSession(store) {
14
15
  const workspace = createFigureEditorWorkspace();
@@ -21,6 +22,22 @@ export function createFigureProductSession(store) {
21
22
  undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.
22
23
  */
23
24
  let done = [], undone = [];
25
+ /*
26
+ A save refused because the server holds another source than the one opened (FIGURE_SOURCE_CONFLICT, not one of
27
+ this session's own sends): the edits stay, and the server's latest is kept here to compare with and, if the person
28
+ says so, to write over (2026-11-01 audit item 6). Undefined source: the latest could not be read.
29
+ */
30
+ let conflict;
31
+ /* The catalogue fields changed and not saved yet; none of the history -- undo is the figure's steps. */
32
+ let catalog = {};
33
+ /* The catalogue fields a save is writing now: what the row will hold, not what it held, until it is done. */
34
+ let catalogSending;
35
+ /*
36
+ The catalogue fields the last save wrote, known to be on the row from the moment it answered: a screen compares
37
+ with them before it has read the row again (2026-11-01 third re-check). Empty from the start of each save.
38
+ */
39
+ let catalogWritten = {};
40
+ const catalogHeld = () => Object.keys(catalog).length > 0;
24
41
  /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */
25
42
  let fitted = [];
26
43
  function tracked(kind, run, tag, setsRange = false) {
@@ -46,7 +63,8 @@ export function createFigureProductSession(store) {
46
63
  conversation is anchored on it, and its first save inserts the row under it, so the name holds across that save.
47
64
  */
48
65
  function read() {
49
- return { ...workspace.read(), id, creationId, loading, saving, error };
66
+ const view = workspace.read();
67
+ return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, catalogWritten: { ...catalogWritten }, conflict: conflict ? { latest: conflict.source } : undefined, id, creationId, loading, saving, error };
50
68
  }
51
69
  /*
52
70
  The version on screen, one object for as long as it is the version: the workspace hands out one asset per version
@@ -103,13 +121,16 @@ export function createFigureProductSession(store) {
103
121
  error = undefined;
104
122
  saving = false;
105
123
  creationId = undefined;
124
+ catalog = {};
125
+ catalogWritten = {};
126
+ conflict = undefined;
106
127
  done = [];
107
128
  undone = [];
108
129
  }
109
130
  function canLeave(discard) {
110
131
  if (saving)
111
132
  throw Error('저장이 끝난 뒤 모델을 전환해 주세요.');
112
- if (workspace.read().dirty && !discard)
133
+ if ((workspace.read().dirty || catalogHeld()) && !discard)
113
134
  throw Error('저장하지 않은 변경이 있습니다.');
114
135
  }
115
136
  return {
@@ -121,6 +142,59 @@ export function createFigureProductSession(store) {
121
142
  * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.
122
143
  */
123
144
  confirmedSource: () => (id ? baseline : undefined),
145
+ /**
146
+ * A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no
147
+ * change. Only a saved figure has a row to write: a new figure's type goes with its first save. Taken while a save
148
+ * is on its way too: the field shows it, so it is held for the save after (2026-11-01 re-check). A field that save
149
+ * is writing is held whatever it is changed to -- back to the old value too, which the save is about to replace;
150
+ * the save done lets go only of a value it wrote (2026-11-01 second re-check).
151
+ */
152
+ editCatalog(patch, saved) {
153
+ if (!id)
154
+ throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.');
155
+ const next = { ...catalog };
156
+ for (const key of Object.keys(patch)) {
157
+ if (!(catalogSending && key in catalogSending) && sameValue(patch[key], saved[key]))
158
+ delete next[key];
159
+ else
160
+ next[key] = patch[key];
161
+ }
162
+ catalog = next;
163
+ return read();
164
+ },
165
+ /**
166
+ * After a conflict, the edits written over the server's latest: the next save expects that latest, so it goes
167
+ * through only while the server still holds it -- a third change meanwhile is a conflict again.
168
+ */
169
+ overwriteConflict() {
170
+ if (saving)
171
+ throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.');
172
+ if (!conflict?.source)
173
+ throw refusal('SESSION_NO_CONFLICT', '덮어쓸 최신본을 읽지 못했습니다.');
174
+ baseline = conflict.source;
175
+ conflict = undefined;
176
+ unconfirmed.clear();
177
+ return read();
178
+ },
179
+ /**
180
+ * After a conflict, the edits kept as a figure of their own: from here they are a new figure, under a new creation
181
+ * id, saved by the first save's question; the figure they came from is left as the server holds it.
182
+ */
183
+ detachAsCopy() {
184
+ if (saving)
185
+ throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.');
186
+ if (!conflict)
187
+ throw refusal('SESSION_NO_CONFLICT', '충돌한 저장이 없습니다.');
188
+ id = undefined;
189
+ baseline = undefined;
190
+ creationId = crypto.randomUUID();
191
+ conflict = undefined;
192
+ /* The category and tags changed go with the copy, written once its first save gives it a row; its type is asked anew. */
193
+ const { type: _type, ...kept } = catalog;
194
+ catalog = kept;
195
+ unconfirmed.clear();
196
+ return read();
197
+ },
124
198
  /** A new figure, named in the screen's language by the page: the session has no words of its own. */
125
199
  startNew({ discard = false, name }) {
126
200
  canLeave(discard);
@@ -267,8 +341,36 @@ export function createFigureProductSession(store) {
267
341
  const exported = workspace.export(control);
268
342
  /* Read with the text it goes with: an edit while saving is not what this save holds. */
269
343
  const proposalKeys = inEffect();
344
+ const sentCatalog = { ...catalog };
270
345
  saving = true;
346
+ catalogSending = sentCatalog;
347
+ catalogWritten = {};
271
348
  try {
349
+ /* Only the catalogue changed: the source is not written again -- a released figure would be made a draft for nothing. */
350
+ if (id === undefined || workspace.read().dirty)
351
+ await saveSource();
352
+ if (requestEpoch !== epoch)
353
+ return read();
354
+ if (id !== undefined && Object.keys(sentCatalog).length) {
355
+ await store.updateCatalog(id, sentCatalog);
356
+ if (requestEpoch !== epoch)
357
+ return read();
358
+ catalogWritten = sentCatalog;
359
+ /* What was sent is saved; a field changed again while it was on its way stays held. */
360
+ const next = { ...catalog };
361
+ for (const key of Object.keys(sentCatalog))
362
+ if (sameValue(next[key], sentCatalog[key]))
363
+ delete next[key];
364
+ catalog = next;
365
+ }
366
+ }
367
+ finally {
368
+ if (requestEpoch === epoch)
369
+ saving = false;
370
+ catalogSending = undefined;
371
+ }
372
+ return read();
373
+ async function saveSource() {
272
374
  let record;
273
375
  try {
274
376
  record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) });
@@ -309,6 +411,8 @@ export function createFigureProductSession(store) {
309
411
  else {
310
412
  if (code === 'SAVE_UNREACHABLE')
311
413
  unconfirmed.set(exported.text, { thumbnail });
414
+ if (code === 'FIGURE_SOURCE_CONFLICT' && requestEpoch === epoch)
415
+ conflict = { source: landed?.source };
312
416
  throw error;
313
417
  }
314
418
  }
@@ -322,6 +426,7 @@ export function createFigureProductSession(store) {
322
426
  id = record.id;
323
427
  baseline = record.source;
324
428
  unconfirmed.clear();
429
+ conflict = undefined;
325
430
  /*
326
431
  Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays
327
432
  unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.
@@ -329,11 +434,6 @@ export function createFigureProductSession(store) {
329
434
  if (workspace.read().revision === exported.revision)
330
435
  workspace.acknowledgeSaved(exported);
331
436
  }
332
- finally {
333
- if (requestEpoch === epoch)
334
- saving = false;
335
- }
336
- return read();
337
437
  }
338
438
  };
339
439
  }
@@ -1 +1 @@
1
- {"version":3,"file":"product-session.js","sourceRoot":"","sources":["../../client/modeller/product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import { thumbnailHashOf } from './thumbnail-hash.js'\nimport { revisionFromView, type FigureRevision } from './revision.js'\nimport {\n createFigureAsset,\n createFigureEditorWorkspace,\n serializeFigureAsset,\n parseFigureAsset,\n figureSizeRangeFitsOf,\n type FigureSizeRangeFit,\n type FigureInputChange,\n type FigurePrimitiveCreation,\n type FigurePartEdit,\n type FigureAuthoringAction,\n type FigureProposalStep,\n type FigureClipConsent\n} from '@hatiolab/figure-model'\n\n/** Consent to let clips go, and the figure and version it was given on. */\n\n/**\n * A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the\n * message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).\n */\nfunction refusal(code: string, message: string): Error & { code: string } {\n return Object.assign(Error(message), { code })\n}\n\nexport interface FigureClipConsentFor extends FigureClipConsent {\n context: string\n revision: number\n}\n\nexport interface FigureProductRecord {\n id: string\n source: string\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface FigureProductStore {\n load(id: string): Promise<FigureProductRecord>\n /** Every field a save writes, as the server holds it now: to tell whether a save whose answer was lost landed. */\n readBack?(id: string): Promise<FigureProductRecord & { thumbnailHash?: string; type?: string }>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n /** The namespace a new figure is created under, when the first save chose one. Not sent for an existing one. */\n namespace?: string\n /** Keys of the AI proposals whose steps the source holds: the server records them with the save. */\n proposalKeys?: string[]\n }): Promise<FigureProductRecord>\n}\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createFigureProductSession(store: FigureProductStore) {\n const workspace = createFigureEditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n /*\n The history as steps, each tagged with the AI proposal it came from (or nothing): mirrored here, since the\n workspace's history does not say where a step came from. A proposal is in the model while its step is on `done` --\n undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.\n */\n let done: Array<string | undefined> = [],\n undone: Array<string | undefined> = []\n /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */\n let fitted: FigureSizeRangeFit[] = []\n function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {\n const was = workspace.read()\n const result = run()\n const now = workspace.read()\n if (now.revision === was.revision) return result\n fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []\n if (kind === 'do') {\n done.push(tag)\n undone = []\n } else if (kind === 'undo') undone.push(done.pop())\n else done.push(undone.pop())\n return result\n }\n const inEffect = () => [...new Set(done.filter((tag): tag is string => !!tag))]\n /*\n The creation id is read with the rest: a figure not saved yet is named by it (ADR-0061 decision 3) -- its AI\n conversation is anchored on it, and its first save inserts the row under it, so the name holds across that save.\n */\n function read() {\n return { ...workspace.read(), id, creationId, loading, saving, error }\n }\n /*\n The version on screen, one object for as long as it is the version: the workspace hands out one asset per version\n (and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.\n */\n let shown: FigureRevision | undefined\n function revisionNow(): FigureRevision | undefined {\n const view = workspace.read()\n if (!view.context || view.revision === null) return (shown = undefined)\n const context = view.context\n if (shown?.asset !== view.asset) shown = revisionFromView(view, () => workspace.export({ context }).text)\n return shown\n }\n /* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */\n const unconfirmed = new Map<string, { thumbnail?: string }>()\n /*\n Consent to let clips go: given for one edit, on the figure and the version it was asked on. The first edit made under\n it takes it; an edit on another figure or another version is refused rather than let clips go that were never named\n for it (designer 2026-09-27).\n */\n let consent: FigureClipConsentFor | undefined\n const consented = () => {\n if (!consent) return {}\n const given = consent\n consent = undefined\n const view = workspace.read()\n if (view.context !== given.context || view.revision !== given.revision) {\n const stale = Object.assign(Error('확인한 뒤 모델이 바뀌어 적용하지 않았습니다. 다시 해 주세요.'), { code: 'CONSENT_STALE' })\n throw stale\n }\n return { dropClips: given.dropClips }\n }\n /* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */\n function readable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function writable() {\n const open = readable()\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n return open\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if (workspace.read().dirty && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */\n revisionNow,\n /**\n * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`\n * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.\n */\n confirmedSource: () => (id ? baseline : undefined),\n /** A new figure, named in the screen's language by the page: the session has no words of its own. */\n startNew({ discard = false, name }: { discard?: boolean; name: string }) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset(name)))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: FigureInputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return tracked('do', () =>\n workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n )\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string, key?: string) {\n writable()\n /* Never with consent: only the person may let a clip go, and an AI proposal is not the person (designer 2026-09-27). */\n return tracked('do', () => workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps }), key)\n },\n /**\n * A new figure opened for an AI proposal from another screen is created under the proposal's key (ADR-0097 §5.6\n * item 5). Only before the modeller names the figure to the AI dock: once a conversation is anchored on the creation\n * id it stays (ADR-0061 decision 3), and the save's record of the proposal keeps one proposal one figure. Only on a\n * new figure not yet touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(key)) throw refusal('SESSION_PROPOSAL_KEY', '제안 식별자가 올바르지 않습니다.')\n creationId = key\n },\n /** The AI proposals whose steps are in the model now, each once. */\n proposalsInEffect: inEffect,\n /**\n * A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open\n * now. The same path as a proposal; the person is its origin.\n */\n applyRecipe(steps: readonly FigureProposalStep[]) {\n const at = writable()\n return tracked('do', () => workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps, ...consented() }))\n },\n add(part: FigurePrimitiveCreation) {\n return tracked('do', () => workspace.addPrimitive({ ...writable(), part }))\n },\n edit(edit: FigurePartEdit) {\n return tracked('do', () => workspace.editPart({ ...writable(), edit, ...consented() }))\n },\n /** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */\n previewCommand(action: FigureAuthoringAction) {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return workspace.previewCommand({ context: view.context, action })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: FigureAuthoringAction) {\n return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')\n },\n /** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */\n takeSizeRangeFits(): FigureSizeRangeFit[] {\n const out = fitted\n fitted = []\n return out\n },\n /**\n * The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and\n * the person agreed to -- on the figure and version they were named on (designer 2026-09-27). Any other edit that\n * would stop a clip is refused and asked about.\n */\n withConsent<T>(given: FigureClipConsentFor, run: () => T): T {\n consent = given\n try {\n return run()\n } finally {\n consent = undefined\n }\n },\n undo() {\n return tracked('undo', () => workspace.undo(writable()))\n },\n redo() {\n return tracked('redo', () => workspace.redo(writable()))\n },\n source() {\n const { context } = readable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `thumbnail.ts`).\n */\n async save({ thumbnail, type, namespace }: { thumbnail?: string; type?: string; namespace?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n /* Read with the text it goes with: an edit while saving is not what this save holds. */\n const proposalKeys = inEffect()\n saving = true\n try {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } catch (error) {\n /*\n A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second\n try refused because the first did land -- its expected source is the old one (FIGURE_SOURCE_CONFLICT). Only\n those are read back; a refusal on grounds -- permission, validation -- stays a failure whatever the row\n holds. Read back, every field the save writes must be what was sent: the source, the picture, the type\n (designer 2026-09-27: a network failure alone does not say nothing was saved; nor does a matching source\n alone say it was).\n */\n const code = (error as { code?: string })?.code\n const unknown = code === 'SAVE_UNREACHABLE' || code === 'FIGURE_SOURCE_CONFLICT'\n const landed = unknown && id !== undefined && requestEpoch === epoch && store.readBack ? await store.readBack(id).catch(() => undefined) : undefined\n const held = landed ? serializeFigureAsset(parseFigureAsset(landed.source)) : undefined\n /* The picture is compared by the name the server keeps it under: the hash of its bytes (ADR-0104). */\n const holds = async (sent?: string) => sent === undefined || (!!landed && landed.thumbnailHash !== undefined && landed.thumbnailHash === (await thumbnailHashOf(sent)))\n const same = !!landed && held === exported.text && (await holds(thumbnail)) && (type === undefined || landed.type === type)\n if (same) record = landed!\n else if (\n code === 'FIGURE_SOURCE_CONFLICT' &&\n landed &&\n held !== undefined &&\n unconfirmed.has(held) &&\n /* Every field that earlier save wrote, not the source alone: nothing not compared is written over. */\n (await holds(unconfirmed.get(held)!.thumbnail))\n ) {\n /*\n The row holds a text this session sent and never heard back about -- edited further since, the new text\n is not it. The server is known to hold what was sent: this save is sent again, once, conditional on that\n server text as the expected source (designer 2026-09-27: \"the sent content is confirmed on the server\").\n */\n baseline = landed.source\n unconfirmed.clear()\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\n throw error\n }\n }\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n /* The server has it: the next save expects it, whatever happened on screen meanwhile. */\n id = record.id\n baseline = record.source\n unconfirmed.clear()\n /*\n Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays\n unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.\n */\n if (workspace.read().revision === exported.revision) workspace.acknowledgeSaved(exported)\n } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\n }\n }\n}\nexport type FigureProductSession = ReturnType<typeof createFigureProductSession>\n"]}
1
+ {"version":3,"file":"product-session.js","sourceRoot":"","sources":["../../client/modeller/product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import { thumbnailHashOf } from './thumbnail-hash.js'\nimport { revisionFromView, type FigureRevision } from './revision.js'\nimport {\n createFigureAsset,\n createFigureEditorWorkspace,\n serializeFigureAsset,\n parseFigureAsset,\n figureSizeRangeFitsOf,\n type FigureSizeRangeFit,\n type FigureInputChange,\n type FigurePrimitiveCreation,\n type FigurePartEdit,\n type FigureAuthoringAction,\n type FigureProposalStep,\n type FigureClipConsent\n} from '@hatiolab/figure-model'\n\n/** Consent to let clips go, and the figure and version it was given on. */\n\n/**\n * A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the\n * message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).\n */\nfunction refusal(code: string, message: string): Error & { code: string } {\n return Object.assign(Error(message), { code })\n}\n\nexport interface FigureClipConsentFor extends FigureClipConsent {\n context: string\n revision: number\n}\n\nexport interface FigureProductRecord {\n id: string\n source: string\n}\n\n/**\n * The row's catalogue fields -- the type (before release), the category, the tags. Kept on the row, not in the asset,\n * and saved with it by 「저장」: written at once on change, they stayed after 「저장하지 않고 떠나기」 and the save\n * button never lit for them (2026-11-01 audit item 5).\n */\nexport interface FigureCatalogPatch {\n type?: string\n category?: string\n tags?: string[]\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface FigureProductStore {\n load(id: string): Promise<FigureProductRecord>\n /** Every field a save writes, as the server holds it now: to tell whether a save whose answer was lost landed. */\n readBack?(id: string): Promise<FigureProductRecord & { thumbnailHash?: string; type?: string }>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n /** The namespace a new figure is created under, when the first save chose one. Not sent for an existing one. */\n namespace?: string\n /** Keys of the AI proposals whose steps the source holds: the server records them with the save. */\n proposalKeys?: string[]\n }): Promise<FigureProductRecord>\n /** Writes the row's catalogue fields of a saved figure. */\n updateCatalog(id: string, patch: FigureCatalogPatch): Promise<unknown>\n}\n\nconst sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createFigureProductSession(store: FigureProductStore) {\n const workspace = createFigureEditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n /*\n The history as steps, each tagged with the AI proposal it came from (or nothing): mirrored here, since the\n workspace's history does not say where a step came from. A proposal is in the model while its step is on `done` --\n undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.\n */\n let done: Array<string | undefined> = [],\n undone: Array<string | undefined> = []\n /*\n A save refused because the server holds another source than the one opened (FIGURE_SOURCE_CONFLICT, not one of\n this session's own sends): the edits stay, and the server's latest is kept here to compare with and, if the person\n says so, to write over (2026-11-01 audit item 6). Undefined source: the latest could not be read.\n */\n let conflict: { source?: string } | undefined\n /* The catalogue fields changed and not saved yet; none of the history -- undo is the figure's steps. */\n let catalog: FigureCatalogPatch = {}\n /* The catalogue fields a save is writing now: what the row will hold, not what it held, until it is done. */\n let catalogSending: FigureCatalogPatch | undefined\n /*\n The catalogue fields the last save wrote, known to be on the row from the moment it answered: a screen compares\n with them before it has read the row again (2026-11-01 third re-check). Empty from the start of each save.\n */\n let catalogWritten: FigureCatalogPatch = {}\n const catalogHeld = () => Object.keys(catalog).length > 0\n /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */\n let fitted: FigureSizeRangeFit[] = []\n function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {\n const was = workspace.read()\n const result = run()\n const now = workspace.read()\n if (now.revision === was.revision) return result\n fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []\n if (kind === 'do') {\n done.push(tag)\n undone = []\n } else if (kind === 'undo') undone.push(done.pop())\n else done.push(undone.pop())\n return result\n }\n const inEffect = () => [...new Set(done.filter((tag): tag is string => !!tag))]\n /*\n The creation id is read with the rest: a figure not saved yet is named by it (ADR-0061 decision 3) -- its AI\n conversation is anchored on it, and its first save inserts the row under it, so the name holds across that save.\n */\n function read() {\n const view = workspace.read()\n return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, catalogWritten: { ...catalogWritten }, conflict: conflict ? { latest: conflict.source } : undefined, id, creationId, loading, saving, error }\n }\n /*\n The version on screen, one object for as long as it is the version: the workspace hands out one asset per version\n (and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.\n */\n let shown: FigureRevision | undefined\n function revisionNow(): FigureRevision | undefined {\n const view = workspace.read()\n if (!view.context || view.revision === null) return (shown = undefined)\n const context = view.context\n if (shown?.asset !== view.asset) shown = revisionFromView(view, () => workspace.export({ context }).text)\n return shown\n }\n /* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */\n const unconfirmed = new Map<string, { thumbnail?: string }>()\n /*\n Consent to let clips go: given for one edit, on the figure and the version it was asked on. The first edit made under\n it takes it; an edit on another figure or another version is refused rather than let clips go that were never named\n for it (designer 2026-09-27).\n */\n let consent: FigureClipConsentFor | undefined\n const consented = () => {\n if (!consent) return {}\n const given = consent\n consent = undefined\n const view = workspace.read()\n if (view.context !== given.context || view.revision !== given.revision) {\n const stale = Object.assign(Error('확인한 뒤 모델이 바뀌어 적용하지 않았습니다. 다시 해 주세요.'), { code: 'CONSENT_STALE' })\n throw stale\n }\n return { dropClips: given.dropClips }\n }\n /* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */\n function readable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function writable() {\n const open = readable()\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n return open\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n catalog = {}\n catalogWritten = {}\n conflict = undefined\n done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if ((workspace.read().dirty || catalogHeld()) && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */\n revisionNow,\n /**\n * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`\n * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.\n */\n confirmedSource: () => (id ? baseline : undefined),\n /**\n * A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no\n * change. Only a saved figure has a row to write: a new figure's type goes with its first save. Taken while a save\n * is on its way too: the field shows it, so it is held for the save after (2026-11-01 re-check). A field that save\n * is writing is held whatever it is changed to -- back to the old value too, which the save is about to replace;\n * the save done lets go only of a value it wrote (2026-11-01 second re-check).\n */\n editCatalog(patch: FigureCatalogPatch, saved: FigureCatalogPatch) {\n if (!id) throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.')\n const next = { ...catalog }\n for (const key of Object.keys(patch) as (keyof FigureCatalogPatch)[]) {\n if (!(catalogSending && key in catalogSending) && sameValue(patch[key], saved[key])) delete next[key]\n else (next as Record<string, unknown>)[key] = patch[key]\n }\n catalog = next\n return read()\n },\n /**\n * After a conflict, the edits written over the server's latest: the next save expects that latest, so it goes\n * through only while the server still holds it -- a third change meanwhile is a conflict again.\n */\n overwriteConflict() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n if (!conflict?.source) throw refusal('SESSION_NO_CONFLICT', '덮어쓸 최신본을 읽지 못했습니다.')\n baseline = conflict.source\n conflict = undefined\n unconfirmed.clear()\n return read()\n },\n /**\n * After a conflict, the edits kept as a figure of their own: from here they are a new figure, under a new creation\n * id, saved by the first save's question; the figure they came from is left as the server holds it.\n */\n detachAsCopy() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n if (!conflict) throw refusal('SESSION_NO_CONFLICT', '충돌한 저장이 없습니다.')\n id = undefined\n baseline = undefined\n creationId = crypto.randomUUID()\n conflict = undefined\n /* The category and tags changed go with the copy, written once its first save gives it a row; its type is asked anew. */\n const { type: _type, ...kept } = catalog\n catalog = kept\n unconfirmed.clear()\n return read()\n },\n /** A new figure, named in the screen's language by the page: the session has no words of its own. */\n startNew({ discard = false, name }: { discard?: boolean; name: string }) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset(name)))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: FigureInputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return tracked('do', () =>\n workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n )\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string, key?: string) {\n writable()\n /* Never with consent: only the person may let a clip go, and an AI proposal is not the person (designer 2026-09-27). */\n return tracked('do', () => workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps }), key)\n },\n /**\n * A new figure opened for an AI proposal from another screen is created under the proposal's key (ADR-0097 §5.6\n * item 5). Only before the modeller names the figure to the AI dock: once a conversation is anchored on the creation\n * id it stays (ADR-0061 decision 3), and the save's record of the proposal keeps one proposal one figure. Only on a\n * new figure not yet touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(key)) throw refusal('SESSION_PROPOSAL_KEY', '제안 식별자가 올바르지 않습니다.')\n creationId = key\n },\n /** The AI proposals whose steps are in the model now, each once. */\n proposalsInEffect: inEffect,\n /**\n * A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open\n * now. The same path as a proposal; the person is its origin.\n */\n applyRecipe(steps: readonly FigureProposalStep[]) {\n const at = writable()\n return tracked('do', () => workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps, ...consented() }))\n },\n add(part: FigurePrimitiveCreation) {\n return tracked('do', () => workspace.addPrimitive({ ...writable(), part }))\n },\n edit(edit: FigurePartEdit) {\n return tracked('do', () => workspace.editPart({ ...writable(), edit, ...consented() }))\n },\n /** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */\n previewCommand(action: FigureAuthoringAction) {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return workspace.previewCommand({ context: view.context, action })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: FigureAuthoringAction) {\n return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')\n },\n /** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */\n takeSizeRangeFits(): FigureSizeRangeFit[] {\n const out = fitted\n fitted = []\n return out\n },\n /**\n * The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and\n * the person agreed to -- on the figure and version they were named on (designer 2026-09-27). Any other edit that\n * would stop a clip is refused and asked about.\n */\n withConsent<T>(given: FigureClipConsentFor, run: () => T): T {\n consent = given\n try {\n return run()\n } finally {\n consent = undefined\n }\n },\n undo() {\n return tracked('undo', () => workspace.undo(writable()))\n },\n redo() {\n return tracked('redo', () => workspace.redo(writable()))\n },\n source() {\n const { context } = readable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `thumbnail.ts`).\n */\n async save({ thumbnail, type, namespace }: { thumbnail?: string; type?: string; namespace?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n /* Read with the text it goes with: an edit while saving is not what this save holds. */\n const proposalKeys = inEffect()\n const sentCatalog = { ...catalog }\n saving = true\n catalogSending = sentCatalog\n catalogWritten = {}\n try {\n /* Only the catalogue changed: the source is not written again -- a released figure would be made a draft for nothing. */\n if (id === undefined || workspace.read().dirty) await saveSource()\n if (requestEpoch !== epoch) return read()\n if (id !== undefined && Object.keys(sentCatalog).length) {\n await store.updateCatalog(id, sentCatalog)\n if (requestEpoch !== epoch) return read()\n catalogWritten = sentCatalog\n /* What was sent is saved; a field changed again while it was on its way stays held. */\n const next = { ...catalog }\n for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[]) if (sameValue(next[key], sentCatalog[key])) delete next[key]\n catalog = next\n }\n } finally {\n if (requestEpoch === epoch) saving = false\n catalogSending = undefined\n }\n return read()\n\n async function saveSource() {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } catch (error) {\n /*\n A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second\n try refused because the first did land -- its expected source is the old one (FIGURE_SOURCE_CONFLICT). Only\n those are read back; a refusal on grounds -- permission, validation -- stays a failure whatever the row\n holds. Read back, every field the save writes must be what was sent: the source, the picture, the type\n (designer 2026-09-27: a network failure alone does not say nothing was saved; nor does a matching source\n alone say it was).\n */\n const code = (error as { code?: string })?.code\n const unknown = code === 'SAVE_UNREACHABLE' || code === 'FIGURE_SOURCE_CONFLICT'\n const landed = unknown && id !== undefined && requestEpoch === epoch && store.readBack ? await store.readBack(id).catch(() => undefined) : undefined\n const held = landed ? serializeFigureAsset(parseFigureAsset(landed.source)) : undefined\n /* The picture is compared by the name the server keeps it under: the hash of its bytes (ADR-0104). */\n const holds = async (sent?: string) => sent === undefined || (!!landed && landed.thumbnailHash !== undefined && landed.thumbnailHash === (await thumbnailHashOf(sent)))\n const same = !!landed && held === exported.text && (await holds(thumbnail)) && (type === undefined || landed.type === type)\n if (same) record = landed!\n else if (\n code === 'FIGURE_SOURCE_CONFLICT' &&\n landed &&\n held !== undefined &&\n unconfirmed.has(held) &&\n /* Every field that earlier save wrote, not the source alone: nothing not compared is written over. */\n (await holds(unconfirmed.get(held)!.thumbnail))\n ) {\n /*\n The row holds a text this session sent and never heard back about -- edited further since, the new text\n is not it. The server is known to hold what was sent: this save is sent again, once, conditional on that\n server text as the expected source (designer 2026-09-27: \"the sent content is confirmed on the server\").\n */\n baseline = landed.source\n unconfirmed.clear()\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\n if (code === 'FIGURE_SOURCE_CONFLICT' && requestEpoch === epoch) conflict = { source: landed?.source }\n throw error\n }\n }\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n /* The server has it: the next save expects it, whatever happened on screen meanwhile. */\n id = record.id\n baseline = record.source\n unconfirmed.clear()\n conflict = undefined\n /*\n Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays\n unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.\n */\n if (workspace.read().revision === exported.revision) workspace.acknowledgeSaved(exported)\n }\n }\n }\n}\nexport type FigureProductSession = ReturnType<typeof createFigureProductSession>\n"]}
@@ -0,0 +1,12 @@
1
+ import type { FigureAsset } from '@hatiolab/figure-model';
2
+ export interface SaveConflictChanges {
3
+ /** Parts on both sides, made differently. */
4
+ changed: string[];
5
+ /** Parts only the edits on screen have. */
6
+ mineOnly: string[];
7
+ /** Parts only the server's latest has. */
8
+ theirsOnly: string[];
9
+ /** Something not of one part differs too: the name, the figure's inputs, its capabilities, its clips... */
10
+ otherChanged: boolean;
11
+ }
12
+ export declare function saveConflictChangesOf(mine: FigureAsset, theirs: FigureAsset): SaveConflictChanges;
@@ -0,0 +1,63 @@
1
+ const looksOf = (asset) => (asset.appearance ?? []);
2
+ /** Every node, constant and input by id, each with the values the figure gives it; a part's look under the part. */
3
+ function itemsOf(asset) {
4
+ const model = asset.document.model;
5
+ const design = (asset.designInputs ?? {});
6
+ const state = (asset.stateDefaults ?? {});
7
+ const items = new Map();
8
+ for (const node of model.nodes)
9
+ items.set(node.id, JSON.stringify(['node', node]));
10
+ for (const constant of model.constants ?? [])
11
+ items.set(constant.id, JSON.stringify(['constant', constant]));
12
+ for (const input of model.inputs ?? [])
13
+ items.set(input.id, JSON.stringify(['input', input, design[input.id] ?? null, state[input.id] ?? null]));
14
+ const looks = new Map();
15
+ for (const look of looksOf(asset))
16
+ if (typeof look.target === 'string')
17
+ looks.set(look.target, [...(looks.get(look.target) ?? []), look]);
18
+ for (const [target, of] of looks)
19
+ items.set(`${target}.appearance`, JSON.stringify(['look', of]));
20
+ return items;
21
+ }
22
+ /**
23
+ * A part and all it is made of: figure-model names a part's nodes, constants and inputs under it (`lid.shape`,
24
+ * `lid.size.y`), and its arguments point at their outputs, not at node ids.
25
+ */
26
+ function closureOf(asset, part) {
27
+ return new Set([...itemsOf(asset).keys()].filter(id => id === part || id.startsWith(part + '.')));
28
+ }
29
+ const partsOf = (asset) => asset.document.model.nodes.filter(node => node.op === 'place@1').map(node => node.id);
30
+ const madeOf = (items, ids) => JSON.stringify([...ids].sort().map(id => [id, items.get(id) ?? null]));
31
+ /** Everything of an asset but its items: its name, palette, the look not of one part, capabilities, version. */
32
+ function restOf(asset) {
33
+ const { document, designInputs: _design, stateDefaults: _state, appearance: _looks, ...rest } = asset;
34
+ const { model: _model, ...documentRest } = document;
35
+ return JSON.stringify([rest, documentRest, looksOf(asset).filter(look => typeof look.target !== 'string')]);
36
+ }
37
+ export function saveConflictChangesOf(mine, theirs) {
38
+ const mineItems = itemsOf(mine), theirItems = itemsOf(theirs);
39
+ const mineParts = partsOf(mine), theirParts = partsOf(theirs);
40
+ const covered = new Set();
41
+ const changed = [];
42
+ for (const part of mineParts) {
43
+ const ours = closureOf(mine, part);
44
+ ours.forEach(id => covered.add(id));
45
+ if (!theirParts.includes(part))
46
+ continue;
47
+ const their = closureOf(theirs, part);
48
+ their.forEach(id => covered.add(id));
49
+ if (madeOf(mineItems, ours) !== madeOf(theirItems, their))
50
+ changed.push(part);
51
+ }
52
+ for (const part of theirParts)
53
+ if (!mineParts.includes(part))
54
+ closureOf(theirs, part).forEach(id => covered.add(id));
55
+ const outside = (items) => JSON.stringify([...items].filter(([id]) => !covered.has(id)).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
56
+ return {
57
+ changed,
58
+ mineOnly: mineParts.filter(part => !theirParts.includes(part)),
59
+ theirsOnly: theirParts.filter(part => !mineParts.includes(part)),
60
+ otherChanged: restOf(mine) !== restOf(theirs) || outside(mineItems) !== outside(theirItems)
61
+ };
62
+ }
63
+ //# sourceMappingURL=save-conflict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-conflict.js","sourceRoot":"","sources":["../../client/modeller/save-conflict.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigureAsset } from '@hatiolab/figure-model'\n\n/*\n * A save refused because the server holds another source than the one this editor opened (FIGURE_SOURCE_CONFLICT):\n * what differs between the edits on screen and the server's latest, by part, so the person chooses with it in view --\n * keep the edits as a copy, look at the latest, or write over it (2026-11-01 audit item 6). Read apart from the screen\n * so the rule is tested without a browser.\n */\n\nexport interface SaveConflictChanges {\n /** Parts on both sides, made differently. */\n changed: string[]\n /** Parts only the edits on screen have. */\n mineOnly: string[]\n /** Parts only the server's latest has. */\n theirsOnly: string[]\n /** Something not of one part differs too: the name, the figure's inputs, its capabilities, its clips... */\n otherChanged: boolean\n}\n\ninterface Node {\n id: string\n op: string\n}\ninterface Model {\n nodes: Node[]\n constants?: { id: string }[]\n inputs?: { id: string }[]\n}\n\ntype Look = { target?: unknown }\nconst looksOf = (asset: FigureAsset) => ((asset as unknown as { appearance?: Look[] }).appearance ?? [])\n\n/** Every node, constant and input by id, each with the values the figure gives it; a part's look under the part. */\nfunction itemsOf(asset: FigureAsset): Map<string, string> {\n const model = asset.document.model as unknown as Model\n const design = (asset.designInputs ?? {}) as Record<string, unknown>\n const state = (asset.stateDefaults ?? {}) as Record<string, unknown>\n const items = new Map<string, string>()\n for (const node of model.nodes) items.set(node.id, JSON.stringify(['node', node]))\n for (const constant of model.constants ?? []) items.set(constant.id, JSON.stringify(['constant', constant]))\n for (const input of model.inputs ?? []) items.set(input.id, JSON.stringify(['input', input, design[input.id] ?? null, state[input.id] ?? null]))\n const looks = new Map<string, Look[]>()\n for (const look of looksOf(asset)) if (typeof look.target === 'string') looks.set(look.target, [...(looks.get(look.target) ?? []), look])\n for (const [target, of] of looks) items.set(`${target}.appearance`, JSON.stringify(['look', of]))\n return items\n}\n\n/**\n * A part and all it is made of: figure-model names a part's nodes, constants and inputs under it (`lid.shape`,\n * `lid.size.y`), and its arguments point at their outputs, not at node ids.\n */\nfunction closureOf(asset: FigureAsset, part: string): Set<string> {\n return new Set([...itemsOf(asset).keys()].filter(id => id === part || id.startsWith(part + '.')))\n}\n\nconst partsOf = (asset: FigureAsset) => (asset.document.model as unknown as Model).nodes.filter(node => node.op === 'place@1').map(node => node.id)\n\nconst madeOf = (items: Map<string, string>, ids: Set<string>) => JSON.stringify([...ids].sort().map(id => [id, items.get(id) ?? null]))\n\n/** Everything of an asset but its items: its name, palette, the look not of one part, capabilities, version. */\nfunction restOf(asset: FigureAsset): string {\n const { document, designInputs: _design, stateDefaults: _state, appearance: _looks, ...rest } = asset as FigureAsset & Record<string, unknown>\n const { model: _model, ...documentRest } = document as unknown as Record<string, unknown>\n return JSON.stringify([rest, documentRest, looksOf(asset).filter(look => typeof look.target !== 'string')])\n}\n\nexport function saveConflictChangesOf(mine: FigureAsset, theirs: FigureAsset): SaveConflictChanges {\n const mineItems = itemsOf(mine),\n theirItems = itemsOf(theirs)\n const mineParts = partsOf(mine),\n theirParts = partsOf(theirs)\n const covered = new Set<string>()\n const changed: string[] = []\n for (const part of mineParts) {\n const ours = closureOf(mine, part)\n ours.forEach(id => covered.add(id))\n if (!theirParts.includes(part)) continue\n const their = closureOf(theirs, part)\n their.forEach(id => covered.add(id))\n if (madeOf(mineItems, ours) !== madeOf(theirItems, their)) changed.push(part)\n }\n for (const part of theirParts) if (!mineParts.includes(part)) closureOf(theirs, part).forEach(id => covered.add(id))\n const outside = (items: Map<string, string>) => JSON.stringify([...items].filter(([id]) => !covered.has(id)).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n return {\n changed,\n mineOnly: mineParts.filter(part => !theirParts.includes(part)),\n theirsOnly: theirParts.filter(part => !mineParts.includes(part)),\n otherChanged: restOf(mine) !== restOf(theirs) || outside(mineItems) !== outside(theirItems)\n }\n}\n"]}
@@ -34,13 +34,15 @@ export declare class FigureModellerPage extends PageView {
34
34
  /** The categories the server allows, or null when a category is free text (G5). */
35
35
  private categories;
36
36
  private row;
37
- private typeError;
38
37
  /** Why a command from the side did nothing. Shown beside the tabs — the editor stays, since nothing in it was lost. */
39
38
  private commandError;
40
39
  /** What the verdict says blocks release now, handed to the editor's bar. */
41
40
  private releaseBlockers;
42
41
  private budget?;
43
42
  private wasDirty;
43
+ private wasSaving;
44
+ /** The row reading asked last: an answer to an earlier one, come back after, is not the row now. */
45
+ private rowReading;
44
46
  private titled?;
45
47
  /** The figure the AI dock was last told about - another id is another conversation. */
46
48
  private assistantFor?;
@@ -62,6 +64,13 @@ export declare class FigureModellerPage extends PageView {
62
64
  * 서버의 거절(이미 쓰는 타입 · 발행 뒤)은 그 문장 그대로 칸 아래에 둔다.
63
65
  */
64
66
  private renameType;
67
+ /**
68
+ * Category, tags and type: the catalogue's, kept on the row, not in the asset -- held by the session and written by
69
+ * 「저장」 with the figure (2026-11-01 audit item 5). The server's refusal is said where a save's is.
70
+ */
71
+ private editCatalog;
72
+ /** The row as the screen shows it: what the server holds, with the catalogue fields changed and not saved yet. */
73
+ private shownRow;
65
74
  /** A command from the properties tab — rename, the mounting face. */
66
75
  private onCommand;
67
76
  /**
@@ -69,11 +78,8 @@ export declare class FigureModellerPage extends PageView {
69
78
  * other edit, and it is saved with the figure.
70
79
  */
71
80
  private runCommand;
72
- /**
73
- * Category and tags — the catalogue's, kept on the row, not in the asset. Written at once; the server's
74
- * refusal is shown as it gave it.
75
- */
76
81
  private onCatalog;
82
+ private detached;
77
83
  private released;
78
84
  private saveBeforeRevert;
79
85
  /** 과거 버전으로 되돌렸으면 서버의 원본이 바뀐 것이다 — 다시 연다. 저장 안 된 것이 있으면 패널이 먼저 저장·버리기·취소를 묻는다. */