@things-factory/figure-service 10.1.44 → 10.1.46

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.
@@ -148,10 +148,30 @@ function v3Wording(violation, say, hasParts) {
148
148
  why: say(key('why'), {}, 'A tilted face cannot lie on the plane, so how the figure stands is not settled.'),
149
149
  how: say(key('how'), { part }, `Choose a mounting face parallel to the plane, or turn '${part}' upright.`)
150
150
  };
151
+ case 'PARTS_SPLIT_ON_RESIZE': {
152
+ /*
153
+ What comes off is every piece but the largest — the figure as the author sees it is the big piece, and the
154
+ small ones are what they will see hanging in the air (V2's sizing-splits, carried to V3 on 2026-09-24).
155
+ */
156
+ const resize = violation.resize;
157
+ const pieces = [...resize.pieces].sort((a, b) => b.length - a.length);
158
+ const parts = pieces.slice(1).flat().map(one => `'${one}'`).join(', ');
159
+ const axes = resize.axes.map(one => one.toUpperCase()).join('·');
160
+ const factor = resize.factor;
161
+ return {
162
+ message: say(key('message'), { parts, axes, factor }, `At ${factor}x on ${axes}, ${parts} come away from the rest of the figure.`),
163
+ why: say(key('why'), {}, 'A figure placed at another size shows those parts hanging in the air — to a person, it looks broken.'),
164
+ how: say(key('how'), { parts }, `Fasten ${parts} to a face of the part they rest on, or press "fix here" under placement, so they stay put when the figure is resized.`)
165
+ };
166
+ }
151
167
  }
152
168
  }
169
+ /*
170
+ The sentence is shown as text, never as HTML, so the values go in as they are. i18next escapes interpolated
171
+ values by default, and the screen then showed `'lamp-in'` as `'lamp-in'`.
172
+ */
153
173
  function sayerOf(t) {
154
- return (key, params, english) => (t ? t(key, { ...params, defaultValue: english }) : english);
174
+ return (key, params, english) => t ? t(key, { ...params, defaultValue: english, interpolation: { escapeValue: false } }) : english;
155
175
  }
156
176
  function inspectAnySource(sourceJson, releasedJson, t,
157
177
  /** The row's `type` — for V3 the only place the name lives, since the asset carries none. */
@@ -169,9 +189,10 @@ rowType) {
169
189
  if (format === 'v2')
170
190
  return (0, figure_gate_js_1.inspectSource)(sourceJson, releasedJson);
171
191
  let inspection;
192
+ let asset;
172
193
  let hasParts = true;
173
194
  try {
174
- const asset = (0, figure_model_1.parseV3Asset)(sourceJson);
195
+ asset = (0, figure_model_1.parseV3Asset)(sourceJson);
175
196
  inspection = (0, figure_model_1.inspectV3Asset)(asset);
176
197
  const cost = (0, figure_model_1.v3CostOf)(asset);
177
198
  hasParts = cost.groups + cost.hidden > 0;
@@ -217,22 +238,159 @@ rowType) {
217
238
  blocking: false
218
239
  });
219
240
  }
220
- /*
221
- V2 compares a release with the one before it and names the changes that break placed instances (ADR-0063
222
- decision 3). Nothing does that for a V3 asset yet, and a V2→V3 conversion has no V3 release to compare with.
223
- The gate says so with the finding V2 uses when it cannot compare, rather than passing in silence — a
224
- release that was not checked against the last one is a different fact from one that was and was found safe.
225
- */
226
- if (releasedJson) {
227
- findings.push({
228
- code: 'released-unreadable',
229
- message: say('figure.gate.v3-released-not-compared.message', {}, 'Not compared with the last release — V3 figures have no comparison between releases yet.'),
230
- why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),
231
- blocking: false
232
- });
233
- }
241
+ if (releasedJson)
242
+ findings.push(...v3ContractFindings(asset, releasedJson, say));
234
243
  return { blocked: findings.some(finding => finding.blocking), findings };
235
244
  }
245
+ /**
246
+ * The last release against this one — what the drawings that already hold this type lean on (ADR-0063 decision 3).
247
+ *
248
+ * figure-model names the changes and whether each blocks (`v3ContractChanges`); the sentence is made here. When
249
+ * the last release cannot be compared — it was a V2 source, which is what every converted figure's last release
250
+ * is, or it no longer parses — the gate says so rather than passing in silence: a release that was not checked
251
+ * against the last one is a different fact from one that was checked and found safe.
252
+ */
253
+ function v3ContractFindings(asset, releasedJson, say) {
254
+ let before;
255
+ try {
256
+ if (formatOf(releasedJson) === 'v2') {
257
+ return [
258
+ {
259
+ code: 'released-unreadable',
260
+ message: say('figure.gate.v3-released-was-v2.message', {}, 'The last release is a V2 figure, so this release was not compared with it.'),
261
+ why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),
262
+ blocking: false
263
+ }
264
+ ];
265
+ }
266
+ before = (0, figure_model_1.parseV3Asset)(releasedJson);
267
+ }
268
+ catch (e) {
269
+ return [
270
+ {
271
+ code: 'released-unreadable',
272
+ message: say('figure.gate.v3-released-not-compared.message', { reason: e.message }, `The last release could not be read, so this release was not compared with it: ${e.message}`),
273
+ why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),
274
+ blocking: false
275
+ }
276
+ ];
277
+ }
278
+ return (0, figure_model_1.v3ContractChanges)(before, asset).map(change => ({
279
+ code: change.code,
280
+ ...v3ContractWording(change, say),
281
+ at: change.subject,
282
+ blocking: change.blocking
283
+ }));
284
+ }
285
+ /** One contract change in the words of the release panel. The English line is the fallback for a missing key. */
286
+ function v3ContractWording(change, say) {
287
+ const v = change.values;
288
+ const key = (field) => `figure.gate.contract.${change.code}.${field}`;
289
+ const gone = {
290
+ how: say('figure.gate.contract.gone.how', {}, 'Put it back as it was, or publish this as a new figure type so the placed ones keep the old one.')
291
+ };
292
+ switch (change.code) {
293
+ case 'placement-changed':
294
+ return {
295
+ message: say(key('message'), v, `Placement changed from ${v.before} to ${v.after}.`),
296
+ why: say(key('why'), {}, 'Placement sets the height of every placed instance. They all move with it.'),
297
+ ...gone
298
+ };
299
+ case 'size-inputs-gone':
300
+ return {
301
+ message: say(key('message'), v, 'The figure no longer takes its size from the place it is given.'),
302
+ why: say(key('why'), {}, 'Placed instances were sized on the board. After this release they all draw at the authored size.'),
303
+ ...gone
304
+ };
305
+ case 'size-inputs-added':
306
+ return {
307
+ message: say(key('message'), v, 'The figure now takes its size from the place it is given.'),
308
+ why: say(key('why'), {}, 'Placed instances now follow their own size on the board.')
309
+ };
310
+ case 'size-range-narrowed':
311
+ return {
312
+ message: say(key('message'), { ...v, input: change.subject ?? '' }, `The size range of ${change.subject} narrowed from ${v.before} to ${v.after}.`),
313
+ why: say(key('why'), {}, 'An instance placed at a size outside the new range will not draw. Which sizes are placed is not recorded.')
314
+ };
315
+ case 'authored-size-changed':
316
+ return {
317
+ message: say(key('message'), v, `The authored size changed from ${v.before} to ${v.after}.`),
318
+ why: say(key('why'), {}, 'Placed instances keep their own size. An instance with no depth of its own takes its depth from this.')
319
+ };
320
+ case 'state-input-gone':
321
+ return {
322
+ message: say(key('message'), v, `The control '${v.id}' is gone.`),
323
+ why: say(key('why'), {}, 'Placed instances and connectors give values under this name. After this release those values do nothing, and nothing on screen says why.'),
324
+ ...gone
325
+ };
326
+ case 'state-unit-changed':
327
+ return {
328
+ message: say(key('message'), v, `The unit of the control '${v.id}' changed from ${v.before} to ${v.after}.`),
329
+ why: say(key('why'), {}, 'Stored values keep their number. With another unit the same number means something else.'),
330
+ ...gone
331
+ };
332
+ case 'state-range-narrowed':
333
+ return {
334
+ message: say(key('message'), v, `The range of the control '${v.id}' narrowed from ${v.before} to ${v.after}.`),
335
+ why: say(key('why'), {}, 'A value outside the new range may already be stored on a placed instance.')
336
+ };
337
+ case 'state-input-added':
338
+ return {
339
+ message: say(key('message'), v, `The control '${v.id}' is new.`),
340
+ why: say(key('why'), {}, 'Placed instances that give no value stand at its default.')
341
+ };
342
+ case 'clip-gone':
343
+ return {
344
+ message: say(key('message'), v, `The clip '${v.clip}' is gone.`),
345
+ why: say(key('why'), {}, 'Placed instances play clips by name. After this release that one plays nothing.'),
346
+ ...gone
347
+ };
348
+ case 'clip-added':
349
+ return {
350
+ message: say(key('message'), v, `The clip '${v.clip}' is new.`),
351
+ why: say(key('why'), {}, 'It stays still until an instance plays it.')
352
+ };
353
+ case 'binding-gone':
354
+ return {
355
+ message: say(key('message'), v, `The place '${v.binding}' is gone.`),
356
+ why: say(key('why'), {}, 'What sits in it on a placed instance loses its place.'),
357
+ ...gone
358
+ };
359
+ case 'binding-role-changed':
360
+ return {
361
+ message: say(key('message'), v, `The place '${v.binding}' changed from ${v.before} to ${v.after}.`),
362
+ why: say(key('why'), {}, 'What sits in it was placed for the old role.'),
363
+ ...gone
364
+ };
365
+ case 'binding-capacity-reduced':
366
+ return {
367
+ message: say(key('message'), v, `The place '${v.binding}' holds ${v.after} instead of ${v.before}.`),
368
+ why: say(key('why'), {}, 'What sits in the places that went has nowhere to go.'),
369
+ ...gone
370
+ };
371
+ case 'binding-accepts-narrowed':
372
+ return {
373
+ message: say(key('message'), v, `The place '${v.binding}' no longer accepts ${v.types}.`),
374
+ why: say(key('why'), {}, 'What already sits there stays. The next one of that type is refused.')
375
+ };
376
+ case 'binding-added':
377
+ return {
378
+ message: say(key('message'), v, `The place '${v.binding}' is new.`),
379
+ why: say(key('why'), {}, 'Placed instances do not know it yet and stand as they are.')
380
+ };
381
+ case 'capability-gone':
382
+ return {
383
+ message: say(key('message'), v, `The capability '${v.name}' is gone.`),
384
+ why: say(key('why'), {}, 'Scenes attach behaviour by this name. Placed instances lose what it did.'),
385
+ ...gone
386
+ };
387
+ case 'capability-added':
388
+ return {
389
+ message: say(key('message'), v, `The capability '${v.name}' is new.`),
390
+ why: say(key('why'), {}, 'It adds what the figure can do. Placed instances stand as they are.')
391
+ };
392
+ }
393
+ }
236
394
  /**
237
395
  * What a consumer is handed for a released figure.
238
396
  *
@@ -1 +1 @@
1
- {"version":3,"file":"figure-format.js","sourceRoot":"","sources":["../../../server/service/figure/figure-format.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Which format a stored source is written in, and one door per job that reads either.\n *\n * ## Why this exists\n *\n * The cutover (ADR-0087) converts assets to V3 and the `source` column holds one or the other while that\n * happens. Every server path that reads a source — saving, publishing, restoring a version, handing a\n * blueprint to a board — had exactly one implementation, the V2 one, so a V3 asset saved through\n * `saveFigureV3` could not be published, restored or drawn. Branching at each of those four places would put\n * the same question in four voices; it is asked once, here.\n *\n * ## How the format is known\n *\n * The asset says so. A V2 source carries `version: 2`; a V3 asset carries `version: 'figure-v3-asset-1'`.\n * Nothing is guessed from the shape of the object: a source that says neither is refused by name, because a\n * source the server cannot identify is one it must not save.\n *\n * ## What is not here\n *\n * No conversion. This door reads what is stored; turning a V2 asset into a V3 one is `convertV2ToV3`, run\n * deliberately against a recoverable copy (the designer's step 3), never as a side effect of a save.\n */\nimport { compile, compileV3Asset, inspectV3Asset, isPlaceholderType, parseV3Asset, serializeV3Asset, v3CostOf } from '@hatiolab/figure-model'\nimport type { V3ContractError, V3Violation } from '@hatiolab/figure-model'\n\nimport type { GateFinding, Inspection } from './figure-gate.js'\nimport { inspectSource } from './figure-gate.js'\nimport { FigureSourceError, parseSource } from './figure-source.js'\nimport type { ParsedSource } from './figure-source.js'\n\nexport type FigureFormat = 'v2' | 'v3'\n\n/** The version each format stamps on its own source. */\nexport const V3_ASSET_VERSION = 'figure-v3-asset-1'\n\n/**\n * Which format this source is written in, read from the `version` the asset carries.\n *\n * @throws {FigureSourceError} when the text is not JSON, or carries no version this server knows.\n */\nexport function formatOf(sourceJson: string): FigureFormat {\n let parsed: unknown\n try {\n parsed = JSON.parse(sourceJson)\n } catch (e) {\n throw new FigureSourceError([\n { code: 'source-not-json', message: `정본이 JSON 이 아니다: ${(e as Error).message}`, at: 'source' }\n ])\n }\n const version = (parsed as { version?: unknown } | null)?.version\n if (version === V3_ASSET_VERSION) return 'v3'\n if (typeof version === 'number') return 'v2'\n throw new FigureSourceError([\n {\n code: 'source-format-unknown',\n message: `정본의 판을 알 수 없다 — V2 는 숫자 version 을, V3 자산은 '${V3_ASSET_VERSION}' 을 적는다`,\n at: 'version'\n }\n ])\n}\n\n/**\n * Validates a source of either format and computes what the catalogue shows.\n *\n * A V3 asset is measured with `v3CostOf`, which charges the same per-shape estimate V2 charges, so the\n * budget columns mean one thing across the two formats. `score` is left null for V3: the V2 score is a\n * measure of V2 reuse and carrying it over would put a number in the column that nothing computed.\n *\n * @throws {FigureSourceError} when the source breaks its own format.\n */\nexport function parseAnySource(sourceJson: string): ParsedSource & { format: FigureFormat } {\n if (formatOf(sourceJson) === 'v2') return { ...parseSource(sourceJson), format: 'v2' }\n\n let asset\n try {\n asset = parseV3Asset(sourceJson)\n compileV3Asset(asset)\n } catch (e) {\n const error = e as V3ContractError & { path?: string }\n throw new FigureSourceError([\n { code: error.code ?? 'v3-asset-invalid', message: error.message ?? String(e), at: error.path }\n ])\n }\n const cost = v3CostOf(asset)\n return {\n format: 'v3',\n source: serializeV3Asset(asset),\n derived: { score: null as unknown as number, triangles: cost.triangles, groups: cost.groups },\n violations: []\n }\n}\n\n/**\n * Whether this source may be published, and why not.\n *\n * For V3 the judgement is `inspectV3Asset`: every part inside the volume the asset declares, over the whole\n * state range, and the face it says it is mounted on lying in that plane. A verdict reached by sampling is\n * reported as a finding of its own rather than hidden — an author releasing a figure the gate could only\n * sample should be told so, because nothing proved it holds between the states that were checked.\n */\n/** The request's translator. Always called with params — `t(key, undefined)` loses the locale. */\nexport type Translate = (key: string, options?: Record<string, unknown>) => string\n\n/**\n * A V3 finding in the words of the release panel.\n *\n * The gate says what it measured as a code and values (`part`, `axis`, `mm`, `degrees`) and an English `detail`\n * for logs. The sentence an author reads is made here, where the request's language is known (ADR-0054) — not in\n * figure-model, which knows no locale, and not on the screen, which would then have a second account of the same\n * refusal. The English line is the fallback when a key is missing, so a missing translation reads as English\n * rather than as the key printed raw.\n *\n * The words are the modeller's: 「공간 선언」 for the volume a figure takes up and 「장착면」 for the face it stands on\n * (`figure.text.mounting-face-explained`). The same register as the V2 blockers since 2026-09-19 — a fact, then\n * the way out — because at ADR-0087 step 5 these become the only findings an author sees.\n */\nfunction v3Wording(violation: V3Violation, say: Say, hasParts: boolean): Pick<GateFinding, 'message' | 'why' | 'how'> {\n const part = violation.part ?? ''\n const axis = (violation.axis ?? '').toUpperCase()\n const mm = violation.mm ?? 0\n const degrees = violation.degrees ?? 0\n const key = (field: string) => `figure.gate.${violation.code.toLowerCase().replace(/_/g, '-')}.${field}`\n\n switch (violation.code) {\n case 'PART_OUTSIDE_OCCUPANCY':\n return {\n message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the declared space on ${axis}.`),\n why: say(key('why'), {}, 'The declared space sets the scale a figure is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.'),\n how: say(key('how'), { part, axis, mm }, `Use \"fit to parts\" to declare the space from the parts again, or move '${part}' ${mm} mm inward on ${axis}.`)\n }\n case 'OCCUPANCY_UNDECLARED':\n return violation.axis\n ? {\n message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The declared space is empty on ${axis}.`),\n why: say(key('why'), {}, 'Without a declared space the scale a figure is placed at cannot be set.'),\n how: say(key('how'), {}, 'Use \"fit to parts\" to declare the space from the parts as they are now.')\n }\n : {\n message: say(key('message'), {}, 'This figure has not declared the space it takes up.'),\n why: say(key('why'), {}, 'Without a declared space the scale a figure is placed at cannot be set.'),\n /*\n A new figure is refused this way before it has a part, and 「fit to parts」 has nothing to measure then —\n the screen does not offer it. Telling the author to press it sent them looking for a button that was not\n there (:3300, 2026-09-24).\n */\n how: hasParts\n ? say(key('how'), {}, 'Use \"fit to parts\" to declare the space from the parts as they are now.')\n : say(key('how-no-parts'), {}, 'Add a shape first, then use \"fit to parts\" to declare the space from it.')\n }\n case 'PARTS_OFF_PLACEMENT_FACE':\n return {\n message: say(key('message'), { part, mm }, `The mounting face of '${part}' is ${mm} mm off the plane the figure stands on.`),\n why: say(key('why'), {}, 'The figure is declared to stand on that face. If the face does not reach the plane, the figure floats on the board by that much, and whatever is placed on it floats too.'),\n how: say(key('how'), { part, mm }, `Move '${part}' ${mm} mm on Y so the face meets the plane, or choose a different mounting face.`)\n }\n case 'PLACEMENT_FACE_TILTED':\n return {\n message: say(key('message'), { part, degrees }, `The mounting face of '${part}' is tilted ${degrees}° from the plane the figure stands on.`),\n why: say(key('why'), {}, 'A tilted face cannot lie on the plane, so how the figure stands is not settled.'),\n how: say(key('how'), { part }, `Choose a mounting face parallel to the plane, or turn '${part}' upright.`)\n }\n }\n}\n\ntype Say = (key: string, params: Record<string, string | number>, english: string) => string\n\nfunction sayerOf(t?: Translate): Say {\n return (key, params, english) => (t ? t(key, { ...params, defaultValue: english }) : english)\n}\n\nexport function inspectAnySource(\n sourceJson?: string | null,\n releasedJson?: string | null,\n t?: Translate,\n /** The row's `type` — for V3 the only place the name lives, since the asset carries none. */\n rowType?: string\n): Inspection {\n if (!sourceJson) return inspectSource(sourceJson, releasedJson)\n\n let format: FigureFormat\n try {\n format = formatOf(sourceJson)\n } catch (e) {\n const findings = (e as FigureSourceError).findings ?? [{ code: 'source-unreadable', message: String(e) }]\n return { blocked: true, findings: findings.map(f => ({ ...f, blocking: true })) }\n }\n if (format === 'v2') return inspectSource(sourceJson, releasedJson)\n\n let inspection\n let hasParts = true\n try {\n const asset = parseV3Asset(sourceJson)\n inspection = inspectV3Asset(asset)\n const cost = v3CostOf(asset)\n hasParts = cost.groups + cost.hidden > 0\n } catch (e) {\n const error = e as V3ContractError & { path?: string }\n return {\n blocked: true,\n findings: [{ code: error.code ?? 'v3-asset-invalid', message: error.message ?? String(e), at: error.path, blocking: true }]\n }\n }\n\n const say = sayerOf(t)\n const findings: GateFinding[] = inspection.violations.map(violation => ({\n code: violation.code.toLowerCase().replace(/_/g, '-'),\n ...v3Wording(violation, say, hasParts),\n at: violation.part,\n blocking: true\n }))\n /*\n V2 refuses a machine-made name at release (`type-is-placeholder`, from the source's own `type`). A V3 asset has\n no `type`, so that check never ran and a new V3 figure was published as `FIGURE_<uuid>` — seen on :3300,\n 2026-09-24. Publishing freezes the name (ADR-0063 decision 4) and boards call a figure by it, so the same\n refusal stands here, read off the row.\n */\n if (rowType && isPlaceholderType(rowType)) {\n findings.push({\n code: 'type-is-placeholder',\n message: say('figure.gate.type-is-placeholder.message', { type: rowType }, `'${rowType}' is a temporary type given when the figure was first saved.`),\n why: say('figure.gate.type-is-placeholder.why', {}, 'Boards call a figure by its type, and it cannot change once the figure is published.'),\n how: say('figure.gate.type-is-placeholder.how', {}, 'Set the type in the type field, then publish.'),\n blocking: true\n })\n }\n if (inspection.coverage.method === 'sampled') {\n const states = inspection.coverage.states\n findings.push({\n code: 'state-range-sampled',\n message: say('figure.gate.state-range-sampled.message', { states }, `The whole range of motion could not be proven, so ${states} poses were checked.`),\n why: say('figure.gate.state-range-sampled.why', {}, 'Between the poses that were checked, a part may still go past the declared space.'),\n how: say('figure.gate.state-range-sampled.how', {}, 'Re-author the motion that could not be proven, and the whole range can be proven.'),\n /* Which operations stopped the proof — a technical list, so it goes where the panel shows a location. */\n at: inspection.coverage.unprovable?.join(' · ') || undefined,\n blocking: false\n })\n }\n /*\n V2 compares a release with the one before it and names the changes that break placed instances (ADR-0063\n decision 3). Nothing does that for a V3 asset yet, and a V2→V3 conversion has no V3 release to compare with.\n The gate says so with the finding V2 uses when it cannot compare, rather than passing in silence — a\n release that was not checked against the last one is a different fact from one that was and was found safe.\n */\n if (releasedJson) {\n findings.push({\n code: 'released-unreadable',\n message: say('figure.gate.v3-released-not-compared.message', {}, 'Not compared with the last release — V3 figures have no comparison between releases yet.'),\n why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),\n blocking: false\n })\n }\n return { blocked: findings.some(finding => finding.blocking), findings }\n}\n\n/**\n * What a consumer is handed for a released figure.\n *\n * V2 hands over a compiled blueprint; V3 hands over the asset itself, because the thing that draws a V3 asset\n * — `registerFigureV3` in things-scene — takes the asset and compiles it with the kernel. The format travels\n * with it so the consumer calls the right registration rather than sniffing the payload a second time.\n */\nexport function consumablePayloadOf(sourceJson: string): { format: FigureFormat; blueprint?: unknown; asset?: unknown } {\n if (formatOf(sourceJson) === 'v2') return { format: 'v2', blueprint: compile(JSON.parse(sourceJson)) }\n const asset = parseV3Asset(sourceJson)\n compileV3Asset(asset)\n return { format: 'v3', asset }\n}\n"]}
1
+ {"version":3,"file":"figure-format.js","sourceRoot":"","sources":["../../../server/service/figure/figure-format.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Which format a stored source is written in, and one door per job that reads either.\n *\n * ## Why this exists\n *\n * The cutover (ADR-0087) converts assets to V3 and the `source` column holds one or the other while that\n * happens. Every server path that reads a source — saving, publishing, restoring a version, handing a\n * blueprint to a board — had exactly one implementation, the V2 one, so a V3 asset saved through\n * `saveFigureV3` could not be published, restored or drawn. Branching at each of those four places would put\n * the same question in four voices; it is asked once, here.\n *\n * ## How the format is known\n *\n * The asset says so. A V2 source carries `version: 2`; a V3 asset carries `version: 'figure-v3-asset-1'`.\n * Nothing is guessed from the shape of the object: a source that says neither is refused by name, because a\n * source the server cannot identify is one it must not save.\n *\n * ## What is not here\n *\n * No conversion. This door reads what is stored; turning a V2 asset into a V3 one is `convertV2ToV3`, run\n * deliberately against a recoverable copy (the designer's step 3), never as a side effect of a save.\n */\nimport { compile, compileV3Asset, inspectV3Asset, isPlaceholderType, parseV3Asset, serializeV3Asset, v3ContractChanges, v3CostOf } from '@hatiolab/figure-model'\nimport type { V3Asset, V3ContractChange, V3ContractError, V3Violation } from '@hatiolab/figure-model'\n\nimport type { GateFinding, Inspection } from './figure-gate.js'\nimport { inspectSource } from './figure-gate.js'\nimport { FigureSourceError, parseSource } from './figure-source.js'\nimport type { ParsedSource } from './figure-source.js'\n\nexport type FigureFormat = 'v2' | 'v3'\n\n/** The version each format stamps on its own source. */\nexport const V3_ASSET_VERSION = 'figure-v3-asset-1'\n\n/**\n * Which format this source is written in, read from the `version` the asset carries.\n *\n * @throws {FigureSourceError} when the text is not JSON, or carries no version this server knows.\n */\nexport function formatOf(sourceJson: string): FigureFormat {\n let parsed: unknown\n try {\n parsed = JSON.parse(sourceJson)\n } catch (e) {\n throw new FigureSourceError([\n { code: 'source-not-json', message: `정본이 JSON 이 아니다: ${(e as Error).message}`, at: 'source' }\n ])\n }\n const version = (parsed as { version?: unknown } | null)?.version\n if (version === V3_ASSET_VERSION) return 'v3'\n if (typeof version === 'number') return 'v2'\n throw new FigureSourceError([\n {\n code: 'source-format-unknown',\n message: `정본의 판을 알 수 없다 — V2 는 숫자 version 을, V3 자산은 '${V3_ASSET_VERSION}' 을 적는다`,\n at: 'version'\n }\n ])\n}\n\n/**\n * Validates a source of either format and computes what the catalogue shows.\n *\n * A V3 asset is measured with `v3CostOf`, which charges the same per-shape estimate V2 charges, so the\n * budget columns mean one thing across the two formats. `score` is left null for V3: the V2 score is a\n * measure of V2 reuse and carrying it over would put a number in the column that nothing computed.\n *\n * @throws {FigureSourceError} when the source breaks its own format.\n */\nexport function parseAnySource(sourceJson: string): ParsedSource & { format: FigureFormat } {\n if (formatOf(sourceJson) === 'v2') return { ...parseSource(sourceJson), format: 'v2' }\n\n let asset\n try {\n asset = parseV3Asset(sourceJson)\n compileV3Asset(asset)\n } catch (e) {\n const error = e as V3ContractError & { path?: string }\n throw new FigureSourceError([\n { code: error.code ?? 'v3-asset-invalid', message: error.message ?? String(e), at: error.path }\n ])\n }\n const cost = v3CostOf(asset)\n return {\n format: 'v3',\n source: serializeV3Asset(asset),\n derived: { score: null as unknown as number, triangles: cost.triangles, groups: cost.groups },\n violations: []\n }\n}\n\n/**\n * Whether this source may be published, and why not.\n *\n * For V3 the judgement is `inspectV3Asset`: every part inside the volume the asset declares, over the whole\n * state range, and the face it says it is mounted on lying in that plane. A verdict reached by sampling is\n * reported as a finding of its own rather than hidden — an author releasing a figure the gate could only\n * sample should be told so, because nothing proved it holds between the states that were checked.\n */\n/** The request's translator. Always called with params — `t(key, undefined)` loses the locale. */\nexport type Translate = (key: string, options?: Record<string, unknown>) => string\n\n/**\n * A V3 finding in the words of the release panel.\n *\n * The gate says what it measured as a code and values (`part`, `axis`, `mm`, `degrees`) and an English `detail`\n * for logs. The sentence an author reads is made here, where the request's language is known (ADR-0054) — not in\n * figure-model, which knows no locale, and not on the screen, which would then have a second account of the same\n * refusal. The English line is the fallback when a key is missing, so a missing translation reads as English\n * rather than as the key printed raw.\n *\n * The words are the modeller's: 「공간 선언」 for the volume a figure takes up and 「장착면」 for the face it stands on\n * (`figure.text.mounting-face-explained`). The same register as the V2 blockers since 2026-09-19 — a fact, then\n * the way out — because at ADR-0087 step 5 these become the only findings an author sees.\n */\nfunction v3Wording(violation: V3Violation, say: Say, hasParts: boolean): Pick<GateFinding, 'message' | 'why' | 'how'> {\n const part = violation.part ?? ''\n const axis = (violation.axis ?? '').toUpperCase()\n const mm = violation.mm ?? 0\n const degrees = violation.degrees ?? 0\n const key = (field: string) => `figure.gate.${violation.code.toLowerCase().replace(/_/g, '-')}.${field}`\n\n switch (violation.code) {\n case 'PART_OUTSIDE_OCCUPANCY':\n return {\n message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the declared space on ${axis}.`),\n why: say(key('why'), {}, 'The declared space sets the scale a figure is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.'),\n how: say(key('how'), { part, axis, mm }, `Use \"fit to parts\" to declare the space from the parts again, or move '${part}' ${mm} mm inward on ${axis}.`)\n }\n case 'OCCUPANCY_UNDECLARED':\n return violation.axis\n ? {\n message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The declared space is empty on ${axis}.`),\n why: say(key('why'), {}, 'Without a declared space the scale a figure is placed at cannot be set.'),\n how: say(key('how'), {}, 'Use \"fit to parts\" to declare the space from the parts as they are now.')\n }\n : {\n message: say(key('message'), {}, 'This figure has not declared the space it takes up.'),\n why: say(key('why'), {}, 'Without a declared space the scale a figure is placed at cannot be set.'),\n /*\n A new figure is refused this way before it has a part, and 「fit to parts」 has nothing to measure then —\n the screen does not offer it. Telling the author to press it sent them looking for a button that was not\n there (:3300, 2026-09-24).\n */\n how: hasParts\n ? say(key('how'), {}, 'Use \"fit to parts\" to declare the space from the parts as they are now.')\n : say(key('how-no-parts'), {}, 'Add a shape first, then use \"fit to parts\" to declare the space from it.')\n }\n case 'PARTS_OFF_PLACEMENT_FACE':\n return {\n message: say(key('message'), { part, mm }, `The mounting face of '${part}' is ${mm} mm off the plane the figure stands on.`),\n why: say(key('why'), {}, 'The figure is declared to stand on that face. If the face does not reach the plane, the figure floats on the board by that much, and whatever is placed on it floats too.'),\n how: say(key('how'), { part, mm }, `Move '${part}' ${mm} mm on Y so the face meets the plane, or choose a different mounting face.`)\n }\n case 'PLACEMENT_FACE_TILTED':\n return {\n message: say(key('message'), { part, degrees }, `The mounting face of '${part}' is tilted ${degrees}° from the plane the figure stands on.`),\n why: say(key('why'), {}, 'A tilted face cannot lie on the plane, so how the figure stands is not settled.'),\n how: say(key('how'), { part }, `Choose a mounting face parallel to the plane, or turn '${part}' upright.`)\n }\n case 'PARTS_SPLIT_ON_RESIZE': {\n /*\n What comes off is every piece but the largest — the figure as the author sees it is the big piece, and the\n small ones are what they will see hanging in the air (V2's sizing-splits, carried to V3 on 2026-09-24).\n */\n const resize = violation.resize!\n const pieces = [...resize.pieces].sort((a, b) => b.length - a.length)\n const parts = pieces.slice(1).flat().map(one => `'${one}'`).join(', ')\n const axes = resize.axes.map(one => one.toUpperCase()).join('·')\n const factor = resize.factor\n return {\n message: say(key('message'), { parts, axes, factor }, `At ${factor}x on ${axes}, ${parts} come away from the rest of the figure.`),\n why: say(key('why'), {}, 'A figure placed at another size shows those parts hanging in the air — to a person, it looks broken.'),\n how: say(key('how'), { parts }, `Fasten ${parts} to a face of the part they rest on, or press \"fix here\" under placement, so they stay put when the figure is resized.`)\n }\n }\n }\n}\n\ntype Say = (key: string, params: Record<string, string | number>, english: string) => string\n\n/*\n The sentence is shown as text, never as HTML, so the values go in as they are. i18next escapes interpolated\n values by default, and the screen then showed `'lamp-in'` as `&#39;lamp-in&#39;`.\n*/\nfunction sayerOf(t?: Translate): Say {\n return (key, params, english) =>\n t ? t(key, { ...params, defaultValue: english, interpolation: { escapeValue: false } }) : english\n}\n\nexport function inspectAnySource(\n sourceJson?: string | null,\n releasedJson?: string | null,\n t?: Translate,\n /** The row's `type` — for V3 the only place the name lives, since the asset carries none. */\n rowType?: string\n): Inspection {\n if (!sourceJson) return inspectSource(sourceJson, releasedJson)\n\n let format: FigureFormat\n try {\n format = formatOf(sourceJson)\n } catch (e) {\n const findings = (e as FigureSourceError).findings ?? [{ code: 'source-unreadable', message: String(e) }]\n return { blocked: true, findings: findings.map(f => ({ ...f, blocking: true })) }\n }\n if (format === 'v2') return inspectSource(sourceJson, releasedJson)\n\n let inspection\n let asset: V3Asset | undefined\n let hasParts = true\n try {\n asset = parseV3Asset(sourceJson)\n inspection = inspectV3Asset(asset)\n const cost = v3CostOf(asset)\n hasParts = cost.groups + cost.hidden > 0\n } catch (e) {\n const error = e as V3ContractError & { path?: string }\n return {\n blocked: true,\n findings: [{ code: error.code ?? 'v3-asset-invalid', message: error.message ?? String(e), at: error.path, blocking: true }]\n }\n }\n\n const say = sayerOf(t)\n const findings: GateFinding[] = inspection.violations.map(violation => ({\n code: violation.code.toLowerCase().replace(/_/g, '-'),\n ...v3Wording(violation, say, hasParts),\n at: violation.part,\n blocking: true\n }))\n /*\n V2 refuses a machine-made name at release (`type-is-placeholder`, from the source's own `type`). A V3 asset has\n no `type`, so that check never ran and a new V3 figure was published as `FIGURE_<uuid>` — seen on :3300,\n 2026-09-24. Publishing freezes the name (ADR-0063 decision 4) and boards call a figure by it, so the same\n refusal stands here, read off the row.\n */\n if (rowType && isPlaceholderType(rowType)) {\n findings.push({\n code: 'type-is-placeholder',\n message: say('figure.gate.type-is-placeholder.message', { type: rowType }, `'${rowType}' is a temporary type given when the figure was first saved.`),\n why: say('figure.gate.type-is-placeholder.why', {}, 'Boards call a figure by its type, and it cannot change once the figure is published.'),\n how: say('figure.gate.type-is-placeholder.how', {}, 'Set the type in the type field, then publish.'),\n blocking: true\n })\n }\n if (inspection.coverage.method === 'sampled') {\n const states = inspection.coverage.states\n findings.push({\n code: 'state-range-sampled',\n message: say('figure.gate.state-range-sampled.message', { states }, `The whole range of motion could not be proven, so ${states} poses were checked.`),\n why: say('figure.gate.state-range-sampled.why', {}, 'Between the poses that were checked, a part may still go past the declared space.'),\n how: say('figure.gate.state-range-sampled.how', {}, 'Re-author the motion that could not be proven, and the whole range can be proven.'),\n /* Which operations stopped the proof — a technical list, so it goes where the panel shows a location. */\n at: inspection.coverage.unprovable?.join(' · ') || undefined,\n blocking: false\n })\n }\n if (releasedJson) findings.push(...v3ContractFindings(asset!, releasedJson, say))\n return { blocked: findings.some(finding => finding.blocking), findings }\n}\n\n/**\n * The last release against this one — what the drawings that already hold this type lean on (ADR-0063 decision 3).\n *\n * figure-model names the changes and whether each blocks (`v3ContractChanges`); the sentence is made here. When\n * the last release cannot be compared — it was a V2 source, which is what every converted figure's last release\n * is, or it no longer parses — the gate says so rather than passing in silence: a release that was not checked\n * against the last one is a different fact from one that was checked and found safe.\n */\nfunction v3ContractFindings(asset: V3Asset, releasedJson: string, say: Say): GateFinding[] {\n let before: V3Asset\n try {\n if (formatOf(releasedJson) === 'v2') {\n return [\n {\n code: 'released-unreadable',\n message: say('figure.gate.v3-released-was-v2.message', {}, 'The last release is a V2 figure, so this release was not compared with it.'),\n why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),\n blocking: false\n }\n ]\n }\n before = parseV3Asset(releasedJson)\n } catch (e) {\n return [\n {\n code: 'released-unreadable',\n message: say('figure.gate.v3-released-not-compared.message', { reason: (e as Error).message }, `The last release could not be read, so this release was not compared with it: ${(e as Error).message}`),\n why: say('figure.gate.v3-released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),\n blocking: false\n }\n ]\n }\n return v3ContractChanges(before, asset).map(change => ({\n code: change.code,\n ...v3ContractWording(change, say),\n at: change.subject,\n blocking: change.blocking\n }))\n}\n\n/** One contract change in the words of the release panel. The English line is the fallback for a missing key. */\nfunction v3ContractWording(change: V3ContractChange, say: Say): Pick<GateFinding, 'message' | 'why' | 'how'> {\n const v = change.values\n const key = (field: string) => `figure.gate.contract.${change.code}.${field}`\n const gone = {\n how: say('figure.gate.contract.gone.how', {}, 'Put it back as it was, or publish this as a new figure type so the placed ones keep the old one.')\n }\n switch (change.code) {\n case 'placement-changed':\n return {\n message: say(key('message'), v, `Placement changed from ${v.before} to ${v.after}.`),\n why: say(key('why'), {}, 'Placement sets the height of every placed instance. They all move with it.'),\n ...gone\n }\n case 'size-inputs-gone':\n return {\n message: say(key('message'), v, 'The figure no longer takes its size from the place it is given.'),\n why: say(key('why'), {}, 'Placed instances were sized on the board. After this release they all draw at the authored size.'),\n ...gone\n }\n case 'size-inputs-added':\n return {\n message: say(key('message'), v, 'The figure now takes its size from the place it is given.'),\n why: say(key('why'), {}, 'Placed instances now follow their own size on the board.')\n }\n case 'size-range-narrowed':\n return {\n message: say(key('message'), { ...v, input: change.subject ?? '' }, `The size range of ${change.subject} narrowed from ${v.before} to ${v.after}.`),\n why: say(key('why'), {}, 'An instance placed at a size outside the new range will not draw. Which sizes are placed is not recorded.')\n }\n case 'authored-size-changed':\n return {\n message: say(key('message'), v, `The authored size changed from ${v.before} to ${v.after}.`),\n why: say(key('why'), {}, 'Placed instances keep their own size. An instance with no depth of its own takes its depth from this.')\n }\n case 'state-input-gone':\n return {\n message: say(key('message'), v, `The control '${v.id}' is gone.`),\n why: say(key('why'), {}, 'Placed instances and connectors give values under this name. After this release those values do nothing, and nothing on screen says why.'),\n ...gone\n }\n case 'state-unit-changed':\n return {\n message: say(key('message'), v, `The unit of the control '${v.id}' changed from ${v.before} to ${v.after}.`),\n why: say(key('why'), {}, 'Stored values keep their number. With another unit the same number means something else.'),\n ...gone\n }\n case 'state-range-narrowed':\n return {\n message: say(key('message'), v, `The range of the control '${v.id}' narrowed from ${v.before} to ${v.after}.`),\n why: say(key('why'), {}, 'A value outside the new range may already be stored on a placed instance.')\n }\n case 'state-input-added':\n return {\n message: say(key('message'), v, `The control '${v.id}' is new.`),\n why: say(key('why'), {}, 'Placed instances that give no value stand at its default.')\n }\n case 'clip-gone':\n return {\n message: say(key('message'), v, `The clip '${v.clip}' is gone.`),\n why: say(key('why'), {}, 'Placed instances play clips by name. After this release that one plays nothing.'),\n ...gone\n }\n case 'clip-added':\n return {\n message: say(key('message'), v, `The clip '${v.clip}' is new.`),\n why: say(key('why'), {}, 'It stays still until an instance plays it.')\n }\n case 'binding-gone':\n return {\n message: say(key('message'), v, `The place '${v.binding}' is gone.`),\n why: say(key('why'), {}, 'What sits in it on a placed instance loses its place.'),\n ...gone\n }\n case 'binding-role-changed':\n return {\n message: say(key('message'), v, `The place '${v.binding}' changed from ${v.before} to ${v.after}.`),\n why: say(key('why'), {}, 'What sits in it was placed for the old role.'),\n ...gone\n }\n case 'binding-capacity-reduced':\n return {\n message: say(key('message'), v, `The place '${v.binding}' holds ${v.after} instead of ${v.before}.`),\n why: say(key('why'), {}, 'What sits in the places that went has nowhere to go.'),\n ...gone\n }\n case 'binding-accepts-narrowed':\n return {\n message: say(key('message'), v, `The place '${v.binding}' no longer accepts ${v.types}.`),\n why: say(key('why'), {}, 'What already sits there stays. The next one of that type is refused.')\n }\n case 'binding-added':\n return {\n message: say(key('message'), v, `The place '${v.binding}' is new.`),\n why: say(key('why'), {}, 'Placed instances do not know it yet and stand as they are.')\n }\n case 'capability-gone':\n return {\n message: say(key('message'), v, `The capability '${v.name}' is gone.`),\n why: say(key('why'), {}, 'Scenes attach behaviour by this name. Placed instances lose what it did.'),\n ...gone\n }\n case 'capability-added':\n return {\n message: say(key('message'), v, `The capability '${v.name}' is new.`),\n why: say(key('why'), {}, 'It adds what the figure can do. Placed instances stand as they are.')\n }\n }\n}\n\n/**\n * What a consumer is handed for a released figure.\n *\n * V2 hands over a compiled blueprint; V3 hands over the asset itself, because the thing that draws a V3 asset\n * — `registerFigureV3` in things-scene — takes the asset and compiles it with the kernel. The format travels\n * with it so the consumer calls the right registration rather than sniffing the payload a second time.\n */\nexport function consumablePayloadOf(sourceJson: string): { format: FigureFormat; blueprint?: unknown; asset?: unknown } {\n if (formatOf(sourceJson) === 'v2') return { format: 'v2', blueprint: compile(JSON.parse(sourceJson)) }\n const asset = parseV3Asset(sourceJson)\n compileV3Asset(asset)\n return { format: 'v3', asset }\n}\n"]}