@things-factory/figure-ui 10.1.168 → 10.1.170

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 (123) hide show
  1. package/client/figure-assistant.ts +10 -1
  2. package/client/modeller/affected-parts.ts +11 -0
  3. package/client/modeller/ai-proposal.ts +40 -9
  4. package/client/modeller/canvas.ts +15 -1
  5. package/client/modeller/editor-styles.ts +98 -6
  6. package/client/modeller/editor.ts +212 -53
  7. package/client/modeller/fasten-geometry.ts +19 -0
  8. package/client/modeller/figure-fasten-drawing.ts +4 -3
  9. package/client/modeller/figure-links.ts +80 -37
  10. package/client/modeller/figure-material-panel.ts +20 -1
  11. package/client/modeller/figure-preview.ts +117 -8
  12. package/client/modeller/figure-properties.ts +1 -2
  13. package/client/modeller/figure-section-panel.ts +5 -0
  14. package/client/modeller/figure-verdict.ts +9 -2
  15. package/client/modeller/gate-findings.ts +12 -4
  16. package/client/modeller/group-menu.ts +57 -0
  17. package/client/modeller/links.ts +22 -1
  18. package/client/modeller/modeller-keys.ts +6 -0
  19. package/client/modeller/preview-size.ts +32 -0
  20. package/client/modeller/product-session.ts +23 -9
  21. package/client/modeller/refusals.ts +14 -0
  22. package/client/modeller/stage-model.ts +11 -0
  23. package/client/pages/figure-modeller-page.ts +15 -6
  24. package/dist-client/figure-assistant.d.ts +6 -0
  25. package/dist-client/figure-assistant.js +9 -1
  26. package/dist-client/figure-assistant.js.map +1 -1
  27. package/dist-client/modeller/affected-parts.d.ts +6 -0
  28. package/dist-client/modeller/affected-parts.js +12 -0
  29. package/dist-client/modeller/affected-parts.js.map +1 -0
  30. package/dist-client/modeller/ai-proposal.d.ts +15 -5
  31. package/dist-client/modeller/ai-proposal.js +39 -10
  32. package/dist-client/modeller/ai-proposal.js.map +1 -1
  33. package/dist-client/modeller/canvas.d.ts +3 -0
  34. package/dist-client/modeller/canvas.js +12 -1
  35. package/dist-client/modeller/canvas.js.map +1 -1
  36. package/dist-client/modeller/editor-styles.js +98 -6
  37. package/dist-client/modeller/editor-styles.js.map +1 -1
  38. package/dist-client/modeller/editor.d.ts +42 -1
  39. package/dist-client/modeller/editor.js +220 -51
  40. package/dist-client/modeller/editor.js.map +1 -1
  41. package/dist-client/modeller/fasten-geometry.d.ts +27 -0
  42. package/dist-client/modeller/fasten-geometry.js +14 -0
  43. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  44. package/dist-client/modeller/figure-fasten-drawing.js +4 -3
  45. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  46. package/dist-client/modeller/figure-links.d.ts +3 -1
  47. package/dist-client/modeller/figure-links.js +84 -38
  48. package/dist-client/modeller/figure-links.js.map +1 -1
  49. package/dist-client/modeller/figure-material-panel.d.ts +8 -1
  50. package/dist-client/modeller/figure-material-panel.js +24 -1
  51. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  52. package/dist-client/modeller/figure-preview.d.ts +12 -0
  53. package/dist-client/modeller/figure-preview.js +116 -5
  54. package/dist-client/modeller/figure-preview.js.map +1 -1
  55. package/dist-client/modeller/figure-properties.js +1 -2
  56. package/dist-client/modeller/figure-properties.js.map +1 -1
  57. package/dist-client/modeller/figure-section-panel.d.ts +1 -1
  58. package/dist-client/modeller/figure-section-panel.js +5 -0
  59. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  60. package/dist-client/modeller/figure-verdict.d.ts +1 -0
  61. package/dist-client/modeller/figure-verdict.js +8 -2
  62. package/dist-client/modeller/figure-verdict.js.map +1 -1
  63. package/dist-client/modeller/gate-findings.d.ts +3 -1
  64. package/dist-client/modeller/gate-findings.js +8 -4
  65. package/dist-client/modeller/gate-findings.js.map +1 -1
  66. package/dist-client/modeller/group-menu.d.ts +18 -0
  67. package/dist-client/modeller/group-menu.js +52 -0
  68. package/dist-client/modeller/group-menu.js.map +1 -0
  69. package/dist-client/modeller/links.d.ts +5 -0
  70. package/dist-client/modeller/links.js +21 -1
  71. package/dist-client/modeller/links.js.map +1 -1
  72. package/dist-client/modeller/modeller-keys.js +7 -0
  73. package/dist-client/modeller/modeller-keys.js.map +1 -1
  74. package/dist-client/modeller/preview-size.d.ts +23 -0
  75. package/dist-client/modeller/preview-size.js +32 -0
  76. package/dist-client/modeller/preview-size.js.map +1 -0
  77. package/dist-client/modeller/product-session.d.ts +0 -1
  78. package/dist-client/modeller/product-session.js +22 -9
  79. package/dist-client/modeller/product-session.js.map +1 -1
  80. package/dist-client/modeller/refusals.d.ts +1 -5
  81. package/dist-client/modeller/refusals.js +14 -1
  82. package/dist-client/modeller/refusals.js.map +1 -1
  83. package/dist-client/modeller/stage-model.d.ts +11 -0
  84. package/dist-client/modeller/stage-model.js +11 -0
  85. package/dist-client/modeller/stage-model.js.map +1 -1
  86. package/dist-client/pages/figure-modeller-page.d.ts +3 -0
  87. package/dist-client/pages/figure-modeller-page.js +18 -5
  88. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  89. package/dist-client/route.d.ts +1 -1
  90. package/dist-client/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +8 -8
  92. package/test/ai-proposal.test.ts +43 -4
  93. package/test/browser/editing-page.js +3 -1
  94. package/test/browser/editing.check.mjs +2 -1
  95. package/test/browser/links-page.js +1 -0
  96. package/test/browser/links.check.mjs +20 -5
  97. package/test/browser/modeller.check.mjs +6 -0
  98. package/test/browser/more-open.js +11 -0
  99. package/test/browser/parts.check.mjs +11 -1
  100. package/test/browser/places-mesh.check.mjs +9 -3
  101. package/test/browser/product-page.check.mjs +9 -4
  102. package/test/browser/props.check.mjs +224 -0
  103. package/test/browser/screen-page.js +17 -0
  104. package/test/browser/screen.check.mjs +171 -29
  105. package/test/browser/section-page.js +1 -0
  106. package/test/browser/section.check.mjs +6 -0
  107. package/test/command-preview.test.ts +2 -2
  108. package/test/fasten-geometry.test.ts +15 -0
  109. package/test/fields.test.ts +4 -2
  110. package/test/figure-assistant-surface.test.ts +38 -0
  111. package/test/first-minute.test.ts +15 -2
  112. package/test/layout.test.ts +1 -1
  113. package/test/preview-size.test.ts +33 -0
  114. package/test/refusals.test.ts +1 -1
  115. package/test/resize-with-placement.test.ts +1 -1
  116. package/test/stage-model.test.ts +11 -1
  117. package/test/verdict.test.ts +7 -0
  118. package/translations/en.json +43 -27
  119. package/translations/es.json +24 -8
  120. package/translations/ja.json +25 -9
  121. package/translations/ko.json +56 -40
  122. package/translations/ms.json +25 -9
  123. package/translations/zh.json +25 -9
@@ -134,7 +134,9 @@ export function partSummaryOf(
134
134
  if (f.of === 'own') continue
135
135
  if (f.of === 'instance') {
136
136
  key = f.times === 1 ? 'figure.text.summary-size-follows' : 'figure.text.summary-size-share'
137
- params = { pct: pct(f.times), whole: say(`figure.label.measure-${f.axis}`) }
137
+ /* A measure following the figure's own size on its own axis says no measure word, so the axes that do so are one
138
+ sentence -- 「너비 · 높이 · 깊이는 도형 크기를 따라 바뀝니다」, not three (user 2026-10-16). */
139
+ params = f.times === 1 && f.axis === axis && !f.plus ? {} : { pct: pct(f.times), whole: say(`figure.label.measure-${f.axis}`) }
138
140
  if (f.plus) (key = key + '-plus'), (params.plus = String(f.plus))
139
141
  } else if (f.of === 'part') {
140
142
  const other = say(`figure.label.measure-${f.axis}`)
@@ -675,6 +677,25 @@ export function motionFormRefusalOf(m: MotionForm, follow: string): 'multiplier'
675
677
  }
676
678
 
677
679
  /** The select's value for what a dimension follows now. */
680
+ /**
681
+ * What a measure that is not its own follows, in the words of the choice that set it -- 「도형 X 크기에 비례」, 「bed의 X」 --
682
+ * where the row once said only 「다른 규칙에 연결됨」 (user 2026-10-16: which rule?).
683
+ */
684
+ export function followWordsOf(follow: Follow, say: (key: string, params?: Record<string, string>) => string): string {
685
+ switch (follow.of) {
686
+ case 'own':
687
+ return say('figure.label.follows-nothing')
688
+ case 'instance':
689
+ return say(follow.times === 1 ? 'figure.label.follows-instance' : 'figure.label.follows-instance-share', { axis: follow.axis.toUpperCase() })
690
+ case 'part':
691
+ return say('figure.label.follows-part-axis', { part: follow.part, axis: follow.axis.toUpperCase() })
692
+ case 'input':
693
+ return follow.input
694
+ case 'expression':
695
+ return say('figure.label.follows-expression')
696
+ }
697
+ }
698
+
678
699
  export function choiceOf(follow: Follow): string {
679
700
  switch (follow.of) {
680
701
  case 'own':
@@ -35,6 +35,12 @@ export class ModellerKeys implements ReactiveController {
35
35
  if (!this.of.taking()) return
36
36
  const canvas = this.of.scene()
37
37
  const path = e.composedPath()
38
+ /*
39
+ A key for something open over the work -- a group's 「⋯」 menu, the view or the history, a question -- is that
40
+ thing's: heard here first, Esc deselected the part and was kept from the menu, which then could not be closed by
41
+ the keyboard (2026-10-16).
42
+ */
43
+ if (path.some(node => node instanceof Element && (node.localName.startsWith('ox-popup') || node.getAttribute('role') === 'dialog'))) return
38
44
  const action = keyActionOf(
39
45
  {
40
46
  key: e.key,
@@ -0,0 +1,32 @@
1
+ import { figureRowOf, FIGURE_INSTANCE_SIZE_INPUTS, type FigureAsset } from '@hatiolab/figure-model'
2
+
3
+ type Say = (key: string, params?: Record<string, string>) => string
4
+
5
+ /**
6
+ * What the preview's one figure is at the size being tried, in a line the author reads beside it -- its size in
7
+ * millimetres and how many copies each row holds there (first 15 minutes, 2026-10-16: the author stretched the conveyor
8
+ * and could not tell that the rollers grew in number). The counts are figure-model's reading of what the figure draws at
9
+ * that size (`figureRowOf`), never worked out again here.
10
+ */
11
+ export function previewSizeWordsOf(asset: FigureAsset, size: { x: number; y: number; z: number }, say: Say): string {
12
+ const mm = (v: number) => Math.round(v).toLocaleString()
13
+ const words = [`${mm(size.x)} × ${mm(size.y)} × ${mm(size.z)} mm`]
14
+ for (const row of rowCountsAt(asset, size)) words.push(say('figure.text.preview-copies', { part: row.part, n: String(row.count) }))
15
+ return words.join(' · ')
16
+ }
17
+
18
+ /** Each row's count at the size given, in the order the parts are listed. */
19
+ export function rowCountsAt(asset: FigureAsset, size: { x: number; y: number; z: number }): Array<{ part: string; count: number }> {
20
+ const values = { [FIGURE_INSTANCE_SIZE_INPUTS.x]: size.x, [FIGURE_INSTANCE_SIZE_INPUTS.y]: size.y, [FIGURE_INSTANCE_SIZE_INPUTS.z]: size.z }
21
+ const rows: Array<{ part: string; count: number }> = []
22
+ for (const look of asset.appearance) {
23
+ let reading: ReturnType<typeof figureRowOf>
24
+ try {
25
+ reading = figureRowOf(asset, look.target, values)
26
+ } catch {
27
+ reading = undefined
28
+ }
29
+ if (reading) rows.push({ part: reading.part, count: reading.count })
30
+ }
31
+ return rows
32
+ }
@@ -14,6 +14,15 @@ import {
14
14
  } from '@hatiolab/figure-model'
15
15
 
16
16
  /** Consent to let clips go, and the figure and version it was given on. */
17
+
18
+ /**
19
+ * A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the
20
+ * message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).
21
+ */
22
+ function refusal(code: string, message: string): Error & { code: string } {
23
+ return Object.assign(Error(message), { code })
24
+ }
25
+
17
26
  export interface FigureClipConsentFor extends FigureClipConsent {
18
27
  context: string
19
28
  revision: number
@@ -110,13 +119,18 @@ export function createFigureProductSession(store: FigureProductStore) {
110
119
  }
111
120
  return { dropClips: given.dropClips }
112
121
  }
113
- function writable() {
122
+ /* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */
123
+ function readable() {
114
124
  const view = workspace.read()
115
- if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')
116
- // Serializing writes also makes create retries safe: no second create while the first is pending.
117
- if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')
125
+ if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')
118
126
  return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }
119
127
  }
128
+ function writable() {
129
+ const open = readable()
130
+ // Serializing writes also makes create retries safe: no second create while the first is pending.
131
+ if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')
132
+ return open
133
+ }
120
134
  function clear() {
121
135
  workspace.close({ discardDesignChanges: true })
122
136
  id = undefined
@@ -179,7 +193,7 @@ export function createFigureProductSession(store: FigureProductStore) {
179
193
  const control = writable()
180
194
  // AI must supply the context AND revision captured when it started. Never substitute current values.
181
195
  if (origin === 'ai' && (context === undefined || expectedRevision === undefined))
182
- throw Error('AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')
196
+ throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')
183
197
  return tracked('do', () =>
184
198
  workspace.apply({
185
199
  ...control,
@@ -206,8 +220,8 @@ export function createFigureProductSession(store: FigureProductStore) {
206
220
  * new figure not yet touched -- one being made is the person's, and keeps its own.
207
221
  */
208
222
  createUnder(key: string) {
209
- if (id || workspace.read().dirty) throw Error('손대지 않은 새 모델에만 쓸 수 있습니다.')
210
- 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 Error('제안 식별자가 올바르지 않습니다.')
223
+ if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')
224
+ 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', '제안 식별자가 올바르지 않습니다.')
211
225
  creationId = key
212
226
  },
213
227
  /** The AI proposals whose steps are in the model now, each once. */
@@ -229,7 +243,7 @@ export function createFigureProductSession(store: FigureProductStore) {
229
243
  /** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */
230
244
  previewCommand(action: FigureAuthoringAction) {
231
245
  const view = workspace.read()
232
- if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')
246
+ if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')
233
247
  return workspace.previewCommand({ context: view.context, action })
234
248
  },
235
249
  /** One authoring command — what a person means, not a graph edit — as one step of the same history. */
@@ -256,7 +270,7 @@ export function createFigureProductSession(store: FigureProductStore) {
256
270
  return tracked('redo', () => workspace.redo(writable()))
257
271
  },
258
272
  source() {
259
- const { context } = writable()
273
+ const { context } = readable()
260
274
  return workspace.export({ context }).text
261
275
  },
262
276
  /**
@@ -226,8 +226,22 @@ function documentLimitRefusalOf(error: unknown, say: Say): string | null {
226
226
  * the kernel's English on the screen (the duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」,
227
227
  * 2026-09-27).
228
228
  */
229
+ /** The modeller session's own refusals (`product-session.ts`), by their code. Nothing for any other refusal. */
230
+ const SESSION_REFUSALS: Record<string, string> = {
231
+ SESSION_NOT_OPEN: 'figure.text.session-not-open',
232
+ SESSION_SAVING: 'figure.text.session-saving',
233
+ SESSION_VERSION_MISSING: 'figure.text.session-version-missing',
234
+ SESSION_NEW_FIGURE_TOUCHED: 'figure.text.session-new-figure-touched',
235
+ SESSION_PROPOSAL_KEY: 'figure.text.session-proposal-key'
236
+ }
237
+ export function sessionRefusalOf(error: unknown, say: Say): string | null {
238
+ const key = SESSION_REFUSALS[codeOf(error) ?? '']
239
+ return key ? say(key, {}) : null
240
+ }
241
+
229
242
  export function editRefusalOf(error: unknown, say: Say): string {
230
243
  return (
244
+ sessionRefusalOf(error, say) ??
231
245
  sizeRangeRefusalOf(error, say) ??
232
246
  inputRangeRefusalOf(error, say) ??
233
247
  repeatPitchRefusalOf(error, say) ??
@@ -191,6 +191,17 @@ export function boardFor(parts: readonly FigureRenderPart[], at: Board & { depth
191
191
  }
192
192
  }
193
193
 
194
+ /**
195
+ * Whether the figure's reach has changed so far since the view was last fitted that the camera should fit it again:
196
+ * more than twice, or less than half, on any axis (first 15 minutes, 2026-10-16: a 100 mm box typed to 3000 × 800 × 600
197
+ * put the camera inside it, and the author saw grey until pressing 「화면에 맞추기」). A drag that nudges a part does not
198
+ * move the camera.
199
+ */
200
+ export function needsRefit(fitted: (Board & { depth: number }) | undefined, now: Board & { depth: number }): boolean {
201
+ if (!fitted) return false
202
+ return (['width', 'height', 'depth'] as const).some(key => now[key] > fitted[key] * 2 || now[key] < fitted[key] / 2)
203
+ }
204
+
194
205
  /**
195
206
  * A body's placement as its fields would read it: its origin in mm and its turn in degrees (rigid@1's X·Y·Z order, as
196
207
  * `componentOf` reads it). For a field a rule sets -- a fastened part's height, a turn its joint fixes -- the screen shows
@@ -6,11 +6,13 @@ import { modellerPathOf } from '../modeller/modeller-route.js'
6
6
  import { createFigureProductSession } from '../modeller/product-session.js'
7
7
  import { figureProductStore } from '../graphql/store.js'
8
8
  import { i18next } from '@operato/i18n'
9
- import { fetchFigure, fetchFigureTypeNames, fetchProposalOutcome, updateFigure } from '../graphql/index.js'
9
+ import { fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
10
+ import { figureProposalOutcome } from '../figure-assistant.js'
10
11
  import { availableRecipes, recipeGuide } from '../modeller/recipe-source.js'
11
12
  import type { RecipeTemplate } from '../modeller/recipes.js'
12
13
  import { occupancyFromParts } from '../modeller/occupancy.js'
13
14
  import type { FigureAuthoringAction } from '@hatiolab/figure-model'
15
+ import type { FigureGateFinding } from '../types.js'
14
16
  import '../modeller/editor.js'
15
17
  import '@material/web/icon/icon.js'
16
18
  import '../modeller/figure-history-panel.js'
@@ -30,7 +32,7 @@ import {
30
32
  import type { FirstSave } from '../modeller/first-save.js'
31
33
  import { typeOf } from '../modeller/first-save.js'
32
34
  import { editRefusalOf } from '../modeller/refusals.js'
33
- import { proposalOutcomeFrom, takeFigureOffer, takePendingNewFigure } from '../modeller/ai-proposal.js'
35
+ import { askingToLeave, takeFigureOffer, takePendingNewFigure } from '../modeller/ai-proposal.js'
34
36
 
35
37
  /**
36
38
  * The product page has exactly one authoritative workspace for UI and AI input transactions.
@@ -118,6 +120,8 @@ export class FigureModellerPage extends PageView {
118
120
  @state() private typeError = ''
119
121
  /** Why a command from the side did nothing. Shown beside the tabs — the editor stays, since nothing in it was lost. */
120
122
  @state() private commandError = ''
123
+ /** What the verdict says blocks release now, handed to the editor's bar. */
124
+ @state() private releaseBlockers: FigureGateFinding[] = []
121
125
  /** The verdict's count, handed to the editor's verdict tab. */
122
126
  @state() private instances?: number
123
127
  @state() private budget?: FigureBudgetLine
@@ -152,8 +156,7 @@ export class FigureModellerPage extends PageView {
152
156
  detail: () => (this.session.read().dirty ? i18next.t('figure.text.unsaved-changes') : ''),
153
157
  anchor: () => ({ anchorType: 'figure', anchorId: id }),
154
158
  /* The card's "check the result": the server's record, never the dock's memory; nothing is applied again. */
155
- proposalOutcome: async (proposalKey: string) =>
156
- proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params)),
159
+ proposalOutcome: figureProposalOutcome,
157
160
  hostContext: () => ({
158
161
  surface: 'figure-modeller',
159
162
  editorMode: this.session.read().id ? 'existing' : 'new',
@@ -389,6 +392,8 @@ export class FigureModellerPage extends PageView {
389
392
  The shell asks this while the edited model is still on screen. The app's own prompt, as every operato screen
390
393
  asks -- not the browser's confirm box (user, 2026-09-24).
391
394
  */
395
+ /* A new figure the dock is opening waits for this answer, not against a clock (audit N4). */
396
+ const answered = askingToLeave()
392
397
  this.discardApproved = !!(await OxPrompt.open({
393
398
  type: 'warning',
394
399
  title: i18next.t('figure.text.unsaved-changes'),
@@ -396,6 +401,7 @@ export class FigureModellerPage extends PageView {
396
401
  confirmButton: { text: i18next.t('figure.button.leave-without-saving') },
397
402
  cancelButton: { text: i18next.t('figure.button.keep-editing') }
398
403
  }))
404
+ answered?.(this.discardApproved)
399
405
  return this.discardApproved
400
406
  }
401
407
  async pageUpdated(changes: Record<string, unknown>, lifecycle: { resourceId?: string }) {
@@ -471,6 +477,8 @@ export class FigureModellerPage extends PageView {
471
477
  the rail matches the page name and marks "new figure" while an existing one is being edited.
472
478
  */
473
479
  menu: view.id ? 'figure-list' : undefined,
480
+ /* The canvas needs the width: the rail folds while the modeller is open and comes back as it was on leaving. */
481
+ rail: 'collapsed',
474
482
  actions: [],
475
483
  /*
476
484
  No heading line: the canvas is what this page is for, and the name is already in the properties' first section
@@ -484,7 +492,7 @@ export class FigureModellerPage extends PageView {
484
492
  if (this.error) return html`<p role="alert" class="error">${this.error}</p>`
485
493
  const view = this.session.read()
486
494
  /* A new figure's third way to start: the app's assistant dock, with a request put in its input for the author to finish and send (B, U04). */
487
- return html`<figure-editor .session=${this.session} .releaseState=${this.row} .templates=${this.templates} .guideOf=${this.guideOf} .thumbnailOf=${this.thumbnailOf} .firstSave=${this.askFirstSave} .revision=${this.revision} .instances=${this.instances} .budget=${this.budget} aiDraft @ask-ai-draft=${() => askAssistant({ draft: i18next.t('figure.text.ai-draft-request') })} @changed=${this.changed}>
495
+ return html`<figure-editor .session=${this.session} .releaseState=${this.row} .releaseBlockers=${this.releaseBlockers} .templates=${this.templates} .guideOf=${this.guideOf} .thumbnailOf=${this.thumbnailOf} .firstSave=${this.askFirstSave} .revision=${this.revision} .instances=${this.instances} .budget=${this.budget} aiDraft @ask-ai-draft=${() => askAssistant({ draft: i18next.t('figure.text.ai-draft-request') })} @changed=${this.changed}>
488
496
  ${this.commandError ? html`<p slot="notice" fit-error role="alert">${this.commandError}</p>` : ''}
489
497
  <figure-properties
490
498
  slot="figure"
@@ -496,7 +504,7 @@ export class FigureModellerPage extends PageView {
496
504
  @catalog=${this.onCatalog}
497
505
  >
498
506
  <label slot="type" type-field>
499
- <span>${i18next.t('figure.label.figure-type')}</span>
507
+ <span>${i18next.t('figure.label.figure-identifier')}</span>
500
508
  ${this.row.version > 0
501
509
  ? html`<code>${this.row.type}</code><small>${i18next.t('figure.text.type-name-cannot-change')}</small>`
502
510
  : html`<input
@@ -517,6 +525,7 @@ export class FigureModellerPage extends PageView {
517
525
  this.budget = event.detail.budget
518
526
  }}
519
527
  @fit-base-to-parts=${this.fitToParts}
528
+ @release-verdict=${(event: CustomEvent<{ blocking: FigureGateFinding[] }>) => (this.releaseBlockers = event.detail.blocking)}
520
529
  ></figure-verdict>
521
530
  <figure-history-panel
522
531
  slot="history"
@@ -1 +1,7 @@
1
1
  export declare const FIGURE_CANDIDATE_TOOL = "proposeFigureCandidate";
2
+ /**
3
+ * The dock's "check the result" of a figure proposal, on every screen that offers its card: what the server recorded of
4
+ * its save (ADR-0097 §5.6 item 5). The modeller asks it, and so does an app's home where a new figure's card is made --
5
+ * there it was missing, and a card cut off by a reload stayed "no answer" for good (figure audit 2026-10-16 N8).
6
+ */
7
+ export declare function figureProposalOutcome(proposalKey: string): Promise<import("./modeller/ai-proposal.js").FigureProposalOutcome | undefined>;
@@ -11,8 +11,16 @@ import { navigate } from '@operato/shell';
11
11
  import { registerProposalHandler } from '@things-factory/ai-assistant/dist-client/index.js';
12
12
  import { fetchProposalOutcome } from './graphql/index.js';
13
13
  import { modellerPathOf } from './modeller/modeller-route.js';
14
- import { pendNewFigure, runFigureProposal } from './modeller/ai-proposal.js';
14
+ import { pendNewFigure, proposalOutcomeFrom, runFigureProposal } from './modeller/ai-proposal.js';
15
15
  export const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate';
16
+ /**
17
+ * The dock's "check the result" of a figure proposal, on every screen that offers its card: what the server recorded of
18
+ * its save (ADR-0097 §5.6 item 5). The modeller asks it, and so does an app's home where a new figure's card is made --
19
+ * there it was missing, and a card cut off by a reload stayed "no answer" for good (figure audit 2026-10-16 N8).
20
+ */
21
+ export async function figureProposalOutcome(proposalKey) {
22
+ return proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params));
23
+ }
16
24
  registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {
17
25
  const outcome = await runFigureProposal(proposal, {
18
26
  offer(detail) {
@@ -1 +1 @@
1
- {"version":3,"file":"figure-assistant.js","sourceRoot":"","sources":["../client/figure-assistant.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,mDAAmD,CAAA;AAE3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAA0B,MAAM,2BAA2B,CAAA;AAEpG,MAAM,CAAC,MAAM,qBAAqB,GAAG,wBAAwB,CAAA;AAE7D,uBAAuB,CAAC,qBAAqB,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IACzE,MAAM,OAAO,GAAG,MAAM,iBAAiB,CACrC,QAAQ,EACR;QACE,KAAK,CAAC,MAAyB;YAC7B,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YACjE,OAAO,MAAM,CAAA;QACf,CAAC;QACD,OAAO,CAAC,KAAK,EAAE,GAAG;YAChB,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACxC,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAA;YAC1B,OAAO,MAAM,CAAA;QACf,CAAC;QACD,SAAS,EAAE,oBAAoB;QAC/B,SAAS,CAAC,EAAE;YACV,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;QAClC,CAAC;KACF,EACD,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CACxC,CAAA;IACD,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACjG,OAAO,OAAO,CAAA;AAChB,CAAC,CAAC,CAAA","sourcesContent":["/*\n * What the AI dock does with a figure proposal - figure-ui declares it because figure-service declares the tool\n * (`proposeFigureCandidate`, design/ai/shared-dock.md §4).\n *\n * A proposal is a list of the modeller's own commands (ADR-0094). Running it applies them in the modeller as one step\n * of its history: a revision on the figure open there, at the version the model saw; a new figure on a new figure the\n * modeller opens. The outcome is said in words on every branch (`ai-proposal.ts`).\n */\nimport { i18next } from '@operato/i18n'\nimport { navigate } from '@operato/shell'\nimport { registerProposalHandler } from '@things-factory/ai-assistant/dist-client/index.js'\n\nimport { fetchProposalOutcome } from './graphql/index.js'\nimport { modellerPathOf } from './modeller/modeller-route.js'\nimport { pendNewFigure, runFigureProposal, type FigureApplyDetail } from './modeller/ai-proposal.js'\n\nexport const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'\n\nregisterProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {\n const outcome = await runFigureProposal(\n proposal,\n {\n offer(detail: FigureApplyDetail) {\n window.dispatchEvent(new CustomEvent('figure-apply', { detail }))\n return detail\n },\n openNew(steps, key) {\n const opened = pendNewFigure(steps, key)\n navigate(modellerPathOf())\n return opened\n },\n savedInto: fetchProposalOutcome,\n openSaved(id) {\n navigate(modellerPathOf({ id }))\n }\n },\n (key, params) => i18next.t(key, params)\n )\n if (outcome.note) context.notify(outcome.verdict === 'accepted' ? 'info' : 'error', outcome.note)\n return outcome\n})\n"]}
1
+ {"version":3,"file":"figure-assistant.js","sourceRoot":"","sources":["../client/figure-assistant.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,mDAAmD,CAAA;AAE3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,iBAAiB,EAA0B,MAAM,2BAA2B,CAAA;AAEzH,MAAM,CAAC,MAAM,qBAAqB,GAAG,wBAAwB,CAAA;AAE7D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,WAAmB;IAC7D,OAAO,mBAAmB,CAAC,MAAM,oBAAoB,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;AAC9G,CAAC;AAED,uBAAuB,CAAC,qBAAqB,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IACzE,MAAM,OAAO,GAAG,MAAM,iBAAiB,CACrC,QAAQ,EACR;QACE,KAAK,CAAC,MAAyB;YAC7B,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YACjE,OAAO,MAAM,CAAA;QACf,CAAC;QACD,OAAO,CAAC,KAAK,EAAE,GAAG;YAChB,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACxC,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAA;YAC1B,OAAO,MAAM,CAAA;QACf,CAAC;QACD,SAAS,EAAE,oBAAoB;QAC/B,SAAS,CAAC,EAAE;YACV,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;QAClC,CAAC;KACF,EACD,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CACxC,CAAA;IACD,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACjG,OAAO,OAAO,CAAA;AAChB,CAAC,CAAC,CAAA","sourcesContent":["/*\n * What the AI dock does with a figure proposal - figure-ui declares it because figure-service declares the tool\n * (`proposeFigureCandidate`, design/ai/shared-dock.md §4).\n *\n * A proposal is a list of the modeller's own commands (ADR-0094). Running it applies them in the modeller as one step\n * of its history: a revision on the figure open there, at the version the model saw; a new figure on a new figure the\n * modeller opens. The outcome is said in words on every branch (`ai-proposal.ts`).\n */\nimport { i18next } from '@operato/i18n'\nimport { navigate } from '@operato/shell'\nimport { registerProposalHandler } from '@things-factory/ai-assistant/dist-client/index.js'\n\nimport { fetchProposalOutcome } from './graphql/index.js'\nimport { modellerPathOf } from './modeller/modeller-route.js'\nimport { pendNewFigure, proposalOutcomeFrom, runFigureProposal, type FigureApplyDetail } from './modeller/ai-proposal.js'\n\nexport const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'\n\n/**\n * The dock's \"check the result\" of a figure proposal, on every screen that offers its card: what the server recorded of\n * its save (ADR-0097 §5.6 item 5). The modeller asks it, and so does an app's home where a new figure's card is made --\n * there it was missing, and a card cut off by a reload stayed \"no answer\" for good (figure audit 2026-10-16 N8).\n */\nexport async function figureProposalOutcome(proposalKey: string) {\n return proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params))\n}\n\nregisterProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {\n const outcome = await runFigureProposal(\n proposal,\n {\n offer(detail: FigureApplyDetail) {\n window.dispatchEvent(new CustomEvent('figure-apply', { detail }))\n return detail\n },\n openNew(steps, key) {\n const opened = pendNewFigure(steps, key)\n navigate(modellerPathOf())\n return opened\n },\n savedInto: fetchProposalOutcome,\n openSaved(id) {\n navigate(modellerPathOf({ id }))\n }\n },\n (key, params) => i18next.t(key, params)\n )\n if (outcome.note) context.notify(outcome.verdict === 'accepted' ? 'info' : 'error', outcome.note)\n return outcome\n})\n"]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The parts a release finding points at, by the author's names. The server's `at` lists the first word of each path it
3
+ * could not prove -- a part's name, or a section of the format such as 「occupancy」 -- and the screen showed it as it came
4
+ * (「occupancy · roller-row-1-roller」, user 2026-10-16). Only the figure's own part names are kept.
5
+ */
6
+ export declare function affectedPartsOf(at: string, parts: readonly string[]): string[];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The parts a release finding points at, by the author's names. The server's `at` lists the first word of each path it
3
+ * could not prove -- a part's name, or a section of the format such as 「occupancy」 -- and the screen showed it as it came
4
+ * (「occupancy · roller-row-1-roller」, user 2026-10-16). Only the figure's own part names are kept.
5
+ */
6
+ export function affectedPartsOf(at, parts) {
7
+ return at
8
+ .split('·')
9
+ .map(word => word.trim())
10
+ .filter(word => parts.includes(word));
11
+ }
12
+ //# sourceMappingURL=affected-parts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affected-parts.js","sourceRoot":"","sources":["../../client/modeller/affected-parts.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAAU,EAAE,KAAwB;IAClE,OAAO,EAAE;SACN,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SACxB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;AACzC,CAAC","sourcesContent":["/**\n * The parts a release finding points at, by the author's names. The server's `at` lists the first word of each path it\n * could not prove -- a part's name, or a section of the format such as 「occupancy」 -- and the screen showed it as it came\n * (「occupancy · roller-row-1-roller」, user 2026-10-16). Only the figure's own part names are kept.\n */\nexport function affectedPartsOf(at: string, parts: readonly string[]): string[] {\n return at\n .split('·')\n .map(word => word.trim())\n .filter(word => parts.includes(word))\n}\n"]}
@@ -19,6 +19,8 @@ export interface FigureApplyDetail extends FigureRefusalRecord {
19
19
  key?: string;
20
20
  steps: FigureProposalStep[];
21
21
  create: boolean;
22
+ /** The figure a revision was made for: the one the model read. */
23
+ figureId?: string;
22
24
  context?: string;
23
25
  revision?: number;
24
26
  applied?: boolean;
@@ -72,18 +74,26 @@ export declare function takeFigureOffer(session: FigureProductSession, detail: F
72
74
  say: Say;
73
75
  }): boolean;
74
76
  export declare function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome>;
77
+ type Settle = (result: {
78
+ applied: boolean;
79
+ error?: string;
80
+ code?: string;
81
+ }) => void;
75
82
  export declare function pendNewFigure(steps: FigureProposalStep[], key?: string, ms?: number): Promise<{
76
83
  applied: boolean;
77
84
  error?: string;
78
85
  code?: string;
79
86
  }>;
87
+ /**
88
+ * For a page about to ask the person whether to leave unsaved edits: the new figure waiting to open stops its clock
89
+ * while they answer -- an answer is no absent page (figure audit 2026-10-16 N4). Give it the answer: leaving runs the
90
+ * clock again for the page that opens; staying says now that it was not opened. Nothing waiting: undefined.
91
+ */
92
+ export declare function askingToLeave(): ((leaving: boolean) => void) | undefined;
80
93
  /** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */
81
94
  export declare function takePendingNewFigure(): {
82
95
  steps: FigureProposalStep[];
83
96
  key?: string;
84
- settle: (result: {
85
- applied: boolean;
86
- error?: string;
87
- code?: string;
88
- }) => void;
97
+ settle: Settle;
89
98
  } | undefined;
99
+ export {};
@@ -1,4 +1,4 @@
1
- import { clipLossOf, refusalRecordOf } from './refusals.js';
1
+ import { clipLossOf, editRefusalOf, refusalRecordOf } from './refusals.js';
2
2
  /**
3
3
  * The dock's "check the result" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:
4
4
  * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,
@@ -25,7 +25,8 @@ function refused(refusal, say) {
25
25
  ? (error ?? '')
26
26
  : clips
27
27
  ? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })
28
- : say('figure.text.proposal-refused', { reason: error ?? '' });
28
+ : /* A refusal with a code is said from it, as the modeller says an edit it refuses; never its message (audit N6). */
29
+ say('figure.text.proposal-refused', { reason: code ? editRefusalOf(refusal, (key, params) => say(key, params)) : (error ?? '') });
29
30
  return { verdict: 'refused', errorCode: code ?? 'refused', note };
30
31
  }
31
32
  /**
@@ -38,6 +39,13 @@ export function takeFigureOffer(session, detail, { onSavedFigure, say }) {
38
39
  const view = session.read();
39
40
  if (detail.create && onSavedFigure)
40
41
  return false;
42
+ /*
43
+ A revision is for the figure the model read; with another open it is left untaken, and the handler says it needs that
44
+ figure open. The version is no proof of the figure: two figures opened in two page loads stood at the same one
45
+ (figure audit 2026-10-16 N3).
46
+ */
47
+ if (!detail.create && typeof detail.figureId === 'string' && detail.figureId !== (view.id ?? view.creationId))
48
+ return false;
41
49
  if (detail.create && view.dirty) {
42
50
  /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */
43
51
  detail.applied = false;
@@ -71,7 +79,7 @@ export function takeFigureOffer(session, detail, { onSavedFigure, say }) {
71
79
  export async function runFigureProposal(card, target, say) {
72
80
  const steps = card?.steps;
73
81
  if (!Array.isArray(steps) || !steps.length)
74
- return { verdict: 'unavailable', errorCode: 'no-steps' };
82
+ return { verdict: 'unavailable', errorCode: 'no-steps', note: say('figure.text.proposal-empty') };
75
83
  const create = card.create === true;
76
84
  /*
77
85
  A new figure's proposal already saved into a figure is not made again: that figure is opened (ADR-0061 decision 3,
@@ -89,7 +97,7 @@ export async function runFigureProposal(card, target, say) {
89
97
  applied: steps.length,
90
98
  note: say('figure.text.proposal-applied', { n: String(steps.length) })
91
99
  });
92
- const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision });
100
+ const detail = target.offer({ key: card.key, steps, create, figureId: create ? undefined : card.figureId, context: card.context, revision: card.revision });
93
101
  if (detail.applied)
94
102
  return accepted();
95
103
  if (detail.applied === false)
@@ -104,15 +112,12 @@ export async function runFigureProposal(card, target, say) {
104
112
  ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }
105
113
  : refused(opened, say);
106
114
  }
107
- /*
108
- A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the
109
- modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new
110
- figure must not receive steps nobody is waiting for.
111
- */
112
115
  let pending;
113
116
  export function pendNewFigure(steps, key, ms = 10_000) {
114
117
  pending?.settle({ applied: false, code: 'not-opened' });
115
118
  return new Promise(resolve => {
119
+ let timer;
120
+ const lapse = () => mine.settle({ applied: false, code: 'not-opened' });
116
121
  const mine = {
117
122
  steps,
118
123
  key,
@@ -121,12 +126,36 @@ export function pendNewFigure(steps, key, ms = 10_000) {
121
126
  if (pending === mine)
122
127
  pending = undefined;
123
128
  resolve(result);
129
+ },
130
+ hold: () => clearTimeout(timer),
131
+ resume: () => {
132
+ clearTimeout(timer);
133
+ timer = setTimeout(lapse, ms);
124
134
  }
125
135
  };
126
- const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms);
136
+ mine.resume();
127
137
  pending = mine;
128
138
  });
129
139
  }
140
+ /**
141
+ * For a page about to ask the person whether to leave unsaved edits: the new figure waiting to open stops its clock
142
+ * while they answer -- an answer is no absent page (figure audit 2026-10-16 N4). Give it the answer: leaving runs the
143
+ * clock again for the page that opens; staying says now that it was not opened. Nothing waiting: undefined.
144
+ */
145
+ export function askingToLeave() {
146
+ const waiting = pending;
147
+ if (!waiting)
148
+ return undefined;
149
+ waiting.hold();
150
+ return leaving => {
151
+ if (pending !== waiting)
152
+ return;
153
+ if (leaving)
154
+ waiting.resume();
155
+ else
156
+ waiting.settle({ applied: false, code: 'not-opened' });
157
+ };
158
+ }
130
159
  /** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */
131
160
  export function takePendingNewFigure() {
132
161
  const taken = pending;
@@ -1 +1 @@
1
- {"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\nimport { clipLossOf, refusalRecordOf, type FigureRefusalRecord } from './refusals.js'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */\n key?: string\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/**\n * The `figure-apply` event's detail. The page that takes it sets `applied`, and when refused `error` and the refusal in\n * figure-model's shape (`code`, `details`, the `clips` it would stop).\n */\nexport interface FigureApplyDetail extends FigureRefusalRecord {\n key?: string\n steps: FigureProposalStep[]\n create: boolean\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[], key?: string): Promise<{ applied: boolean; error?: string; code?: string }>\n /** The figures the server recorded a save of this proposal into (`figureProposalOutcome`), first saved first. */\n savedInto(key: string): Promise<ReadonlyArray<{ figureId: string; type?: string; name?: string }>>\n /** Opens a saved figure in the modeller. */\n openSaved(figureId: string): void\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/**\n * The dock's \"check the result\" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:\n * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,\n * which the dock keeps as \"not known\" and never turns into a refusal. Nothing is run again.\n */\nexport function proposalOutcomeFrom(saved: ReadonlyArray<{ figureId: string; type?: string; name?: string }>, say: Say): FigureProposalOutcome | undefined {\n if (!saved.length) return undefined\n const names = saved.map(f => f.name || f.type || f.figureId).join(', ')\n return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) }\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(refusal: FigureRefusalRecord & { error?: string }, say: Say): FigureProposalOutcome {\n const { code, error } = refusal\n const clips = clipLossOf(refusal)\n /*\n A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not\n asked about -- the person edits by hand if they mean it (designer 2026-09-27).\n */\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : clips\n ? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })\n : say('figure.text.proposal-refused', { reason: error ?? '' })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\n/**\n * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands\n * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new\n * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a\n * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.\n */\nexport function takeFigureOffer(\n session: FigureProductSession,\n detail: FigureApplyDetail & { changes?: unknown },\n { onSavedFigure, say }: { onSavedFigure: boolean; say: Say }\n): boolean {\n const view = session.read()\n if (detail.create && onSavedFigure) return false\n if (detail.create && view.dirty) {\n /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */\n detail.applied = false\n detail.code = 'NEW_FIGURE_UNSAVED'\n detail.error = say('figure.text.proposal-new-figure-unsaved')\n return false\n }\n try {\n if (detail.create) {\n /*\n Taken on the new figure the modeller has open, whose conversation asked for it: the figure keeps its own creation\n id, the anchor of that conversation (ADR-0061 decision 3, ruled 2026-10-16). One proposal stays one figure by the\n save's record of it, which `runFigureProposal` reads before the card runs again.\n */\n session.applyProposal(detail.steps, view.revision!, view.context!, detail.key)\n } else if (detail.steps) session.applyProposal(detail.steps, detail.revision!, detail.context!, detail.key)\n else session.apply(detail.changes as never, 'ai', detail.revision, detail.context)\n detail.applied = true\n return true\n } catch (error) {\n detail.applied = false\n Object.assign(detail, refusalRecordOf(error))\n detail.error = error instanceof Error ? error.message : String(error)\n return false\n }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }\n const create = card.create === true\n /*\n A new figure's proposal already saved into a figure is not made again: that figure is opened (ADR-0061 decision 3,\n ruled 2026-10-16). Taken on a new figure that kept its own creation id, the save's record is what says it was made.\n */\n if (create && card.key) {\n const saved = await target.savedInto(card.key)\n if (saved.length) {\n target.openSaved(saved[0]!.figureId)\n return proposalOutcomeFrom(saved, say)!\n }\n }\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail, say)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps, card.key)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\nlet pending: { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n const mine = {\n steps,\n key,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n }\n }\n const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)\n pending = mine\n })\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure():\n | { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void }\n | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
1
+ {"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\nimport { clipLossOf, editRefusalOf, refusalRecordOf, type FigureRefusalRecord } from './refusals.js'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */\n key?: string\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/**\n * The `figure-apply` event's detail. The page that takes it sets `applied`, and when refused `error` and the refusal in\n * figure-model's shape (`code`, `details`, the `clips` it would stop).\n */\nexport interface FigureApplyDetail extends FigureRefusalRecord {\n key?: string\n steps: FigureProposalStep[]\n create: boolean\n /** The figure a revision was made for: the one the model read. */\n figureId?: string\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[], key?: string): Promise<{ applied: boolean; error?: string; code?: string }>\n /** The figures the server recorded a save of this proposal into (`figureProposalOutcome`), first saved first. */\n savedInto(key: string): Promise<ReadonlyArray<{ figureId: string; type?: string; name?: string }>>\n /** Opens a saved figure in the modeller. */\n openSaved(figureId: string): void\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/**\n * The dock's \"check the result\" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:\n * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,\n * which the dock keeps as \"not known\" and never turns into a refusal. Nothing is run again.\n */\nexport function proposalOutcomeFrom(saved: ReadonlyArray<{ figureId: string; type?: string; name?: string }>, say: Say): FigureProposalOutcome | undefined {\n if (!saved.length) return undefined\n const names = saved.map(f => f.name || f.type || f.figureId).join(', ')\n return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) }\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(refusal: FigureRefusalRecord & { error?: string }, say: Say): FigureProposalOutcome {\n const { code, error } = refusal\n const clips = clipLossOf(refusal)\n /*\n A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not\n asked about -- the person edits by hand if they mean it (designer 2026-09-27).\n */\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : clips\n ? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })\n : /* A refusal with a code is said from it, as the modeller says an edit it refuses; never its message (audit N6). */\n say('figure.text.proposal-refused', { reason: code ? editRefusalOf(refusal, (key, params) => say(key, params)) : (error ?? '') })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\n/**\n * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands\n * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new\n * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a\n * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.\n */\nexport function takeFigureOffer(\n session: FigureProductSession,\n detail: FigureApplyDetail & { changes?: unknown },\n { onSavedFigure, say }: { onSavedFigure: boolean; say: Say }\n): boolean {\n const view = session.read()\n if (detail.create && onSavedFigure) return false\n /*\n A revision is for the figure the model read; with another open it is left untaken, and the handler says it needs that\n figure open. The version is no proof of the figure: two figures opened in two page loads stood at the same one\n (figure audit 2026-10-16 N3).\n */\n if (!detail.create && typeof detail.figureId === 'string' && detail.figureId !== (view.id ?? view.creationId)) return false\n if (detail.create && view.dirty) {\n /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */\n detail.applied = false\n detail.code = 'NEW_FIGURE_UNSAVED'\n detail.error = say('figure.text.proposal-new-figure-unsaved')\n return false\n }\n try {\n if (detail.create) {\n /*\n Taken on the new figure the modeller has open, whose conversation asked for it: the figure keeps its own creation\n id, the anchor of that conversation (ADR-0061 decision 3, ruled 2026-10-16). One proposal stays one figure by the\n save's record of it, which `runFigureProposal` reads before the card runs again.\n */\n session.applyProposal(detail.steps, view.revision!, view.context!, detail.key)\n } else if (detail.steps) session.applyProposal(detail.steps, detail.revision!, detail.context!, detail.key)\n else session.apply(detail.changes as never, 'ai', detail.revision, detail.context)\n detail.applied = true\n return true\n } catch (error) {\n detail.applied = false\n Object.assign(detail, refusalRecordOf(error))\n detail.error = error instanceof Error ? error.message : String(error)\n return false\n }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps', note: say('figure.text.proposal-empty') }\n const create = card.create === true\n /*\n A new figure's proposal already saved into a figure is not made again: that figure is opened (ADR-0061 decision 3,\n ruled 2026-10-16). Taken on a new figure that kept its own creation id, the save's record is what says it was made.\n */\n if (create && card.key) {\n const saved = await target.savedInto(card.key)\n if (saved.length) {\n target.openSaved(saved[0]!.figureId)\n return proposalOutcomeFrom(saved, say)!\n }\n }\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ key: card.key, steps, create, figureId: create ? undefined : card.figureId, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail, say)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps, card.key)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\ntype Settle = (result: { applied: boolean; error?: string; code?: string }) => void\nlet pending: { steps: FigureProposalStep[]; key?: string; settle: Settle; hold(): void; resume(): void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const lapse = () => mine.settle({ applied: false, code: 'not-opened' })\n const mine = {\n steps,\n key,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n },\n hold: () => clearTimeout(timer),\n resume: () => {\n clearTimeout(timer)\n timer = setTimeout(lapse, ms)\n }\n }\n mine.resume()\n pending = mine\n })\n}\n\n/**\n * For a page about to ask the person whether to leave unsaved edits: the new figure waiting to open stops its clock\n * while they answer -- an answer is no absent page (figure audit 2026-10-16 N4). Give it the answer: leaving runs the\n * clock again for the page that opens; staying says now that it was not opened. Nothing waiting: undefined.\n */\nexport function askingToLeave(): ((leaving: boolean) => void) | undefined {\n const waiting = pending\n if (!waiting) return undefined\n waiting.hold()\n return leaving => {\n if (pending !== waiting) return\n if (leaving) waiting.resume()\n else waiting.settle({ applied: false, code: 'not-opened' })\n }\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure(): { steps: FigureProposalStep[]; key?: string; settle: Settle } | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
@@ -34,6 +34,9 @@ export declare class FigureCanvas extends LitElement {
34
34
  private stage;
35
35
  private scene?;
36
36
  private board?;
37
+ /** The figure's reach when the view was last fitted to it. */
38
+ private fittedFor?;
39
+ private refitTimer?;
37
40
  private standingFor?;
38
41
  private watcher?;
39
42
  /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */