@things-factory/figure-service 10.1.75 → 10.1.79

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 (185) hide show
  1. package/README.md +1 -1
  2. package/dist-server/service/figure/figure-format.d.ts +6 -6
  3. package/dist-server/service/figure/figure-format.js +32 -32
  4. package/dist-server/service/figure/figure-format.js.map +1 -1
  5. package/dist-server/service/figure/figure-history.js +1 -1
  6. package/dist-server/service/figure/figure-history.js.map +1 -1
  7. package/dist-server/service/figure/figure-mutation.d.ts +1 -1
  8. package/dist-server/service/figure/figure-mutation.js +12 -12
  9. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  10. package/dist-server/service/figure/{figure-propose-v3.d.ts → figure-propose-steps.d.ts} +16 -16
  11. package/dist-server/service/figure/{figure-propose-v3.js → figure-propose-steps.js} +27 -27
  12. package/dist-server/service/figure/figure-propose-steps.js.map +1 -0
  13. package/dist-server/service/figure/figure-propose-type.js +3 -3
  14. package/dist-server/service/figure/figure-propose-type.js.map +1 -1
  15. package/dist-server/service/figure/figure-query.d.ts +2 -2
  16. package/dist-server/service/figure/figure-query.js +5 -5
  17. package/dist-server/service/figure/figure-query.js.map +1 -1
  18. package/dist-server/service/figure/figure-tools.js +14 -14
  19. package/dist-server/service/figure/figure-tools.js.map +1 -1
  20. package/dist-server/service/figure/figure-type.js +2 -2
  21. package/dist-server/service/figure/figure-type.js.map +1 -1
  22. package/dist-server/service/figure/{figure-v3-write.d.ts → figure-write.d.ts} +3 -3
  23. package/dist-server/service/figure/{figure-v3-write.js → figure-write.js} +10 -10
  24. package/dist-server/service/figure/figure-write.js.map +1 -0
  25. package/dist-server/service/figure/figure.d.ts +2 -2
  26. package/dist-server/service/figure/figure.js +2 -2
  27. package/dist-server/service/figure/figure.js.map +1 -1
  28. package/dist-server/service/figure-recipe/figure-recipe-templates.d.ts +2 -2
  29. package/dist-server/service/figure-recipe/figure-recipe-templates.js +1 -1
  30. package/dist-server/service/figure-recipe/figure-recipe-templates.js.map +1 -1
  31. package/dist-server/service/figure-recipe/figure-recipe-type.js +1 -1
  32. package/dist-server/service/figure-recipe/figure-recipe-type.js.map +1 -1
  33. package/dist-server/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +5 -5
  35. package/recipes/_lib/model.mts +19 -19
  36. package/recipes/air-cylinder/models/default.json +1 -1
  37. package/recipes/air-shower/models/default.json +1 -1
  38. package/recipes/barrier-gate/models/default.json +1 -1
  39. package/recipes/base-frame/models/default.json +1 -1
  40. package/recipes/belt-surface/models/default.json +1 -1
  41. package/recipes/bin-row/models/default.json +1 -1
  42. package/recipes/bollard-row/models/default.json +1 -1
  43. package/recipes/buzzer/models/default.json +1 -1
  44. package/recipes/cabinet-doors/models/default.json +1 -1
  45. package/recipes/cabinet-doors/models/doors=1.json +1 -1
  46. package/recipes/cabinet-doors/models/doors=2.json +1 -1
  47. package/recipes/cable-tray/models/default.json +1 -1
  48. package/recipes/camera-mount/models/default.json +1 -1
  49. package/recipes/canopy/models/default.json +1 -1
  50. package/recipes/cart/models/default.json +1 -1
  51. package/recipes/cart-handle/models/default.json +1 -1
  52. package/recipes/casters-four/models/default.json +1 -1
  53. package/recipes/ceiling-rail-carriage/models/default.json +1 -1
  54. package/recipes/chain-rails/models/default.json +1 -1
  55. package/recipes/charging-station/models/default.json +1 -1
  56. package/recipes/column-row/models/default.json +1 -1
  57. package/recipes/control-cabinet/models/default.json +1 -1
  58. package/recipes/conveyor-skirts/models/default.json +1 -1
  59. package/recipes/counter-display/models/default.json +1 -1
  60. package/recipes/dock-bumpers/models/default.json +1 -1
  61. package/recipes/dock-leveler/models/default.json +1 -1
  62. package/recipes/double-hinged-door/models/default.json +1 -1
  63. package/recipes/drawer-stack/models/default.json +1 -1
  64. package/recipes/drawer-stack/models/drawers=2.json +1 -1
  65. package/recipes/drawer-stack/models/drawers=3.json +1 -1
  66. package/recipes/drawer-stack/models/drawers=4.json +1 -1
  67. package/recipes/drip-tray/models/default.json +1 -1
  68. package/recipes/duct-fan/models/default.json +1 -1
  69. package/recipes/emergency-stop/models/default.json +1 -1
  70. package/recipes/end-covers/models/default.json +1 -1
  71. package/recipes/ffu-row/models/default.json +1 -1
  72. package/recipes/fire-extinguisher/models/default.json +1 -1
  73. package/recipes/first-aid-box/models/default.json +1 -1
  74. package/recipes/floor-zone/models/default.json +1 -1
  75. package/recipes/folding-door/models/default.json +1 -1
  76. package/recipes/fork/models/default.json +1 -1
  77. package/recipes/gantry-frame/models/default.json +1 -1
  78. package/recipes/gripper-2f/models/default.json +1 -1
  79. package/recipes/guard-cover/models/default.json +1 -1
  80. package/recipes/handrail/models/default.json +1 -1
  81. package/recipes/hatch/models/default.json +1 -1
  82. package/recipes/hinged-door/models/default.json +1 -1
  83. package/recipes/hinged-door/models/hinge=left.json +1 -1
  84. package/recipes/hinged-door/models/hinge=right.json +1 -1
  85. package/recipes/hmi-monitor/models/default.json +1 -1
  86. package/recipes/hopper/models/default.json +1 -1
  87. package/recipes/interlock-switch/models/default.json +1 -1
  88. package/recipes/junction-box/models/default.json +1 -1
  89. package/recipes/ladder/models/default.json +1 -1
  90. package/recipes/legs-four/models/default.json +1 -1
  91. package/recipes/leveling-feet/models/default.json +1 -1
  92. package/recipes/lift-updown/models/default.json +1 -1
  93. package/recipes/light-curtain/models/default.json +1 -1
  94. package/recipes/limit-switch/models/default.json +1 -1
  95. package/recipes/linear-guide/models/default.json +1 -1
  96. package/recipes/locker-row/models/default.json +1 -1
  97. package/recipes/motor-box/models/default.json +1 -1
  98. package/recipes/observation-window/models/default.json +1 -1
  99. package/recipes/operator-panel/models/buttons=2.json +1 -1
  100. package/recipes/operator-panel/models/buttons=3.json +1 -1
  101. package/recipes/operator-panel/models/buttons=4.json +1 -1
  102. package/recipes/operator-panel/models/buttons=5.json +1 -1
  103. package/recipes/operator-panel/models/default.json +1 -1
  104. package/recipes/pallet/models/default.json +1 -1
  105. package/recipes/partition-wall/models/default.json +1 -1
  106. package/recipes/pass-box/models/default.json +1 -1
  107. package/recipes/peg-board/models/default.json +1 -1
  108. package/recipes/photo-sensor-pair/models/default.json +1 -1
  109. package/recipes/pipe-run/models/default.json +1 -1
  110. package/recipes/platform/models/default.json +1 -1
  111. package/recipes/power-box/models/default.json +1 -1
  112. package/recipes/pressure-gauge/models/default.json +1 -1
  113. package/recipes/pull-cord/models/default.json +1 -1
  114. package/recipes/pump/models/default.json +1 -1
  115. package/recipes/pusher/models/default.json +1 -1
  116. package/recipes/robot-plinth/models/default.json +1 -1
  117. package/recipes/roll-up-shutter/models/default.json +1 -1
  118. package/recipes/roller-row/models/default.json +1 -1
  119. package/recipes/room-shell/models/default.json +1 -1
  120. package/recipes/rotating-beacon/models/default.json +1 -1
  121. package/recipes/safety-fence/models/default.json +1 -1
  122. package/recipes/safety-mat/models/default.json +1 -1
  123. package/recipes/safety-sign-stand/models/default.json +1 -1
  124. package/recipes/scanner-mount/models/default.json +1 -1
  125. package/recipes/selector-switch/models/default.json +1 -1
  126. package/recipes/shelf-tiers/models/default.json +1 -1
  127. package/recipes/shelf-tiers/models/tiers=2.json +1 -1
  128. package/recipes/shelf-tiers/models/tiers=3.json +1 -1
  129. package/recipes/shelf-tiers/models/tiers=4.json +1 -1
  130. package/recipes/shelf-tiers/models/tiers=5.json +1 -1
  131. package/recipes/side-guides/models/default.json +1 -1
  132. package/recipes/sign-board/models/default.json +1 -1
  133. package/recipes/signal-tower-3/models/default.json +1 -1
  134. package/recipes/sliding-door/models/default.json +1 -1
  135. package/recipes/sliding-door/models/leaves=1,lamp=false.json +1 -1
  136. package/recipes/sliding-door/models/leaves=1,lamp=true.json +1 -1
  137. package/recipes/sliding-door/models/leaves=2,lamp=false.json +1 -1
  138. package/recipes/sliding-door/models/leaves=2,lamp=true.json +1 -1
  139. package/recipes/stairs/models/default.json +1 -1
  140. package/recipes/status-lamp/models/default.json +1 -1
  141. package/recipes/stopper/models/default.json +1 -1
  142. package/recipes/suction-pads/models/default.json +1 -1
  143. package/recipes/tank/models/default.json +1 -1
  144. package/recipes/telescopic-arm/models/default.json +1 -1
  145. package/recipes/toe-board/models/default.json +1 -1
  146. package/recipes/tote/models/default.json +1 -1
  147. package/recipes/traffic-cones/models/default.json +1 -1
  148. package/recipes/transfer-belts/models/default.json +1 -1
  149. package/recipes/tray-stack/models/default.json +1 -1
  150. package/recipes/tray-stack/models/trays=3.json +1 -1
  151. package/recipes/tray-stack/models/trays=4.json +1 -1
  152. package/recipes/tray-stack/models/trays=5.json +1 -1
  153. package/recipes/trough/models/default.json +1 -1
  154. package/recipes/turntable/models/default.json +1 -1
  155. package/recipes/valve-handle/models/default.json +1 -1
  156. package/recipes/waste-bin/models/default.json +1 -1
  157. package/recipes/work-light/models/default.json +1 -1
  158. package/recipes/workbench-top/models/default.json +1 -1
  159. package/scripts/build-templates.mts +7 -7
  160. package/scripts/templates.test.mts +16 -16
  161. package/server/service/figure/figure-blueprint-query.test.ts +3 -3
  162. package/server/service/figure/figure-format.test.ts +57 -57
  163. package/server/service/figure/figure-format.ts +39 -39
  164. package/server/service/figure/figure-history.ts +1 -1
  165. package/server/service/figure/figure-inspect-db.test.ts +14 -14
  166. package/server/service/figure/figure-llm-smoke.test.ts +7 -7
  167. package/server/service/figure/figure-mutation.ts +12 -12
  168. package/server/service/figure/{figure-propose-v3.test.ts → figure-propose-steps.test.ts} +18 -18
  169. package/server/service/figure/{figure-propose-v3.ts → figure-propose-steps.ts} +33 -33
  170. package/server/service/figure/figure-propose-type.ts +3 -3
  171. package/server/service/figure/figure-query.ts +5 -5
  172. package/server/service/figure/figure-rename-db.test.ts +5 -5
  173. package/server/service/figure/figure-rename.test.ts +7 -7
  174. package/server/service/figure/figure-tools.test.ts +22 -22
  175. package/server/service/figure/figure-tools.ts +14 -14
  176. package/server/service/figure/figure-type.ts +2 -2
  177. package/server/service/figure/{figure-v3-write.ts → figure-write.ts} +10 -10
  178. package/server/service/figure/figure.ts +3 -3
  179. package/server/service/figure-recipe/figure-recipe-templates.test.ts +2 -2
  180. package/server/service/figure-recipe/figure-recipe-templates.ts +4 -4
  181. package/server/service/figure-recipe/figure-recipe-type.ts +1 -1
  182. package/translations/en.json +2 -3
  183. package/translations/ko.json +2 -3
  184. package/dist-server/service/figure/figure-propose-v3.js.map +0 -1
  185. package/dist-server/service/figure/figure-v3-write.js.map +0 -1
@@ -6,12 +6,12 @@
6
6
  * 하려면 어시스턴트가 **지금 무엇이 있는지 읽을 수 있어야** 한다. 읽지 못하면 매번 사람이 정본을
7
7
  * 붙여 넣어야 하고, 그러면 대화가 아니다.
8
8
  *
9
- * 능력을 복제하지 않는다. figure-service 가 이미 가진 진입점(`inspectSource` · `proposeFigureV3` ·
9
+ * 능력을 복제하지 않는다. figure-service 가 이미 가진 진입점(`inspectSource` · `proposeFigureSteps` ·
10
10
  * 조회)을 레지스트리에 투영한다.
11
11
  *
12
12
  * ── 전부 조회·제안 전용이다 ─────────────────────────────────────────────────
13
13
  * 정본을 고치는 도구를 넣지 않는다. 형식의 설계가 그렇게 정해져 있다 — 어시스턴트는 **후보**를
14
- * 내고, 받을지 버릴지는 저작자가 정한다(`figure-propose-v3.ts`, ADR-0094). 되돌릴 수 없는 조작이 대화면으로
14
+ * 내고, 받을지 버릴지는 저작자가 정한다(`figure-propose-steps.ts`, ADR-0094). 되돌릴 수 없는 조작이 대화면으로
15
15
  * 새지 않으므로 전부 `kind: 'read'` 다.
16
16
  *
17
17
  * 저장은 저작면이 `updateFigure` 로 한다. 그 mutation 은 `figure:mutation` 이 지키고, 대화는
@@ -53,16 +53,16 @@ function getDeps(): any {
53
53
  // eslint-disable-next-line @typescript-eslint/no-var-requires
54
54
  const format = require('./figure-format.js')
55
55
  // eslint-disable-next-line @typescript-eslint/no-var-requires
56
- const proposeV3 = require('./figure-propose-v3.js')
56
+ const proposer = require('./figure-propose-steps.js')
57
57
  _deps = {
58
58
  getRepository: shell.getRepository,
59
59
  Figure: entity.Figure,
60
60
  /* The same doors a save and a release go through. The tool reads; it never saves. */
61
61
  inspectSource: format.inspectSource,
62
62
  parseSource: format.parseSource,
63
- v3CostOf: model.v3CostOf,
64
- v3ScoreOf: model.v3ScoreOf,
65
- proposeFigureV3: proposeV3.proposeFigureV3,
63
+ figureCostOf: model.figureCostOf,
64
+ figureScoreOf: model.figureScoreOf,
65
+ proposeFigureSteps: proposer.proposeFigureSteps,
66
66
  isPlaceholderType: model.isPlaceholderType
67
67
  }
68
68
  }
@@ -119,7 +119,7 @@ async function resolveFigureTarget(figureId: string | undefined, domainId: strin
119
119
  * The host context is the browser's, so it is no ground for access: the row is found in the domain first, and the
120
120
  * draft is put through the same door a save goes through before anything reads it.
121
121
  */
122
- const isV3 = (source: any) => source?.version === 'figure-v3-asset-1'
122
+ const isFigure = (source: any) => source?.version === 'figure-asset-1'
123
123
 
124
124
  /**
125
125
  * A revision is applied by the modeller on the version the model saw (ADR-0094), so only the figure open there can be
@@ -242,7 +242,7 @@ function figureToolSpecs(): ToolSpec[] {
242
242
  }
243
243
 
244
244
  /* Measured as the catalogue measures it: triangles, draw groups, and its grade against one screen. */
245
- const { v3CostOf, v3ScoreOf } = getDeps()
245
+ const { figureCostOf, figureScoreOf } = getDeps()
246
246
  return {
247
247
  id: row.id,
248
248
  type: row.type,
@@ -250,8 +250,8 @@ function figureToolSpecs(): ToolSpec[] {
250
250
  state: row.state,
251
251
  source: current.source,
252
252
  draft: current.draft,
253
- cost: v3CostOf(current.source),
254
- score: v3ScoreOf(current.source) ?? null
253
+ cost: figureCostOf(current.source),
254
+ score: figureScoreOf(current.source) ?? null
255
255
  }
256
256
  }
257
257
  },
@@ -335,7 +335,7 @@ function figureToolSpecs(): ToolSpec[] {
335
335
  const row = await resolveFigureTarget(open, domainId, host)
336
336
  if (!row) return missingFigureTarget()
337
337
  base = sourceForTool(row, host).source ?? undefined
338
- if (!isV3(base)) return figureNotOpen()
338
+ if (!isFigure(base)) return figureNotOpen()
339
339
  if (typeof host?.context !== 'string' || !Number.isInteger(host?.revision)) return figureNotOpen()
340
340
  figureId = open
341
341
  }
@@ -348,10 +348,10 @@ function figureToolSpecs(): ToolSpec[] {
348
348
  hostImage = { data: buf, mediaType: rawImage.mediaType || 'image/png' }
349
349
  }
350
350
 
351
- const { proposeFigureV3 } = getDeps()
351
+ const { proposeFigureSteps } = getDeps()
352
352
  let result: any
353
353
  try {
354
- result = await proposeFigureV3({
354
+ result = await proposeFigureSteps({
355
355
  prompt,
356
356
  base,
357
357
  name: typeof args?.name === 'string' && args.name.trim() ? args.name.trim() : undefined,
@@ -359,7 +359,7 @@ function figureToolSpecs(): ToolSpec[] {
359
359
  })
360
360
  } catch (error) {
361
361
  /*
362
- * 세 번 거절된 끝의 사유를 ProposeV3Failure.reasons 에만 두면 agentic-loop 은 「만들지 못했습니다」만
362
+ * 세 번 거절된 끝의 사유를 ProposeFigureFailure.reasons 에만 두면 agentic-loop 은 「만들지 못했습니다」만
363
363
  * 받아 화면에는 「응답이 비어있습니다」가 된다. 다시 시도할 수 있는 말로 경계를 넘긴다.
364
364
  * 다른 예외(권한·통신)는 원문을 보존한다.
365
365
  */
@@ -25,7 +25,7 @@ export class NewFigure {
25
25
  @Field(type => [String], { nullable: true, description: 'Free-form tags. A figure may carry several.' })
26
26
  tags?: string[]
27
27
 
28
- @Field({ description: 'The figure asset JSON (figure-v3-asset-1).' })
28
+ @Field({ description: 'The figure asset JSON (figure-asset-1).' })
29
29
  source: string
30
30
 
31
31
  @Field({ nullable: true, description: 'JSON definition of the master-data fields an instance carries.' })
@@ -70,7 +70,7 @@ export class FigurePatch {
70
70
  @Field(type => [String], { nullable: true, description: 'The new tags. An empty list clears them; omitting the field leaves them.' })
71
71
  tags?: string[]
72
72
 
73
- @Field({ nullable: true, description: 'The new figure asset JSON (figure-v3-asset-1).' })
73
+ @Field({ nullable: true, description: 'The new figure asset JSON (figure-asset-1).' })
74
74
  source?: string
75
75
 
76
76
  @Field({ nullable: true, description: 'The new master-data field definition.' })
@@ -1,12 +1,12 @@
1
- import { parseV3Asset, serializeV3Asset, compileV3Asset, v3CostOf, v3ScoreOf } from '@hatiolab/figure-model'
1
+ import { parseFigureAsset, serializeFigureAsset, compileFigureAsset, figureCostOf, figureScoreOf } from '@hatiolab/figure-model'
2
2
 
3
- export interface V3WriteCriteria {
3
+ export interface FigureWriteCriteria {
4
4
  id: string
5
5
  domain: { id: string }
6
6
  source: string
7
7
  }
8
- export async function compareAndSwapV3(
9
- update: (criteria: V3WriteCriteria) => Promise<{ affected?: number | null }>,
8
+ export async function compareAndSwapFigure(
9
+ update: (criteria: FigureWriteCriteria) => Promise<{ affected?: number | null }>,
10
10
  id: string,
11
11
  domainId: string,
12
12
  expectedSource: string | undefined
@@ -17,20 +17,20 @@ export async function compareAndSwapV3(
17
17
  }
18
18
 
19
19
  /** Early validation only. The repository must also compare expectedSource in its atomic UPDATE. */
20
- export function prepareV3Write(source: string, existing?: { source?: string }, expectedSource?: string) {
20
+ export function prepareFigureWrite(source: string, existing?: { source?: string }, expectedSource?: string) {
21
21
  if (existing && (expectedSource === undefined || existing.source !== expectedSource)) {
22
22
  throw Error('다른 편집에서 모델이 변경되었습니다. 최신 모델을 다시 연 뒤 적용해 주세요.')
23
23
  }
24
- const asset = parseV3Asset(source)
25
- compileV3Asset(asset)
24
+ const asset = parseFigureAsset(source)
25
+ compileFigureAsset(asset)
26
26
  /*
27
27
  The same measure `parseSource` uses, so the catalogue's columns do not depend on which mutation wrote the row.
28
28
  */
29
- const cost = v3CostOf(asset)
29
+ const cost = figureCostOf(asset)
30
30
  return {
31
- source: serializeV3Asset(asset),
31
+ source: serializeFigureAsset(asset),
32
32
  name: asset.name,
33
- score: v3ScoreOf(asset)?.score ?? null,
33
+ score: figureScoreOf(asset)?.score ?? null,
34
34
  triangles: cost.triangles,
35
35
  groups: cost.groups,
36
36
  state: 'draft' as const
@@ -21,7 +21,7 @@ import { Domain, longTextColumn } from '@things-factory/shell'
21
21
  *
22
22
  * ## 무엇을 담나 — 도형 자산이다, 씬 모델이 아니다
23
23
  *
24
- * 형식은 `@hatiolab/figure-model` 이 정한다(`figure-v3-asset-1`). 씬 컴포넌트 트리를 담지 않는다.
24
+ * 형식은 `@hatiolab/figure-model` 이 정한다(`figure-asset-1`). 씬 컴포넌트 트리를 담지 않는다.
25
25
  * 컴파일한 것도 저장하지 않는다 — 원본에서 파생되므로 소비하는 쪽이 커널로 컴파일한다. 저장하면 원본과 갈린다.
26
26
  *
27
27
  * 그래서 부품·움직임·자리 같은 것을 컬럼으로 두지 않는다 — 전부 `source` 안에 이미 있다. 두 벌이면 갈린다.
@@ -114,13 +114,13 @@ export class Figure {
114
114
  state?: 'draft' | 'released'
115
115
 
116
116
  /**
117
- * The figure asset JSON (`figure-v3-asset-1`).
117
+ * The figure asset JSON (`figure-asset-1`).
118
118
  *
119
119
  * `Board.model` 자리이지만 이름을 바꿨다 — 「씬 모델이 아니다」를 필드 이름이
120
120
  * 드러내야 한다.
121
121
  */
122
122
  @Column({ nullable: true, ...longTextColumn() })
123
- @Field({ nullable: true, description: 'The figure asset JSON (figure-v3-asset-1) authored by the modeller.' })
123
+ @Field({ nullable: true, description: 'The figure asset JSON (figure-asset-1) authored by the modeller.' })
124
124
  source?: string
125
125
 
126
126
  // ── 파생값 — 저장하는 이유가 하나뿐이다 ──────────────────────────────────
@@ -6,7 +6,7 @@ import { mkdtempSync, mkdirSync, writeFileSync } from 'fs'
6
6
  import { tmpdir } from 'os'
7
7
  import { join } from 'path'
8
8
 
9
- import { checkV3Recipe } from '@hatiolab/figure-model'
9
+ import { checkFigureRecipe } from '@hatiolab/figure-model'
10
10
 
11
11
  import { inLanguage, recipeTemplates, RECIPE_DIR } from './figure-recipe-templates'
12
12
 
@@ -18,7 +18,7 @@ describe('조각 템플릿 — 정적 파일', () => {
18
18
  expect(templates.map(t => t.recipe.name)).toEqual(folders)
19
19
  expect(templates.length).toBeGreaterThanOrEqual(22)
20
20
  for (const t of templates) {
21
- expect(() => checkV3Recipe(structuredClone(t.recipe))).not.toThrow()
21
+ expect(() => checkFigureRecipe(structuredClone(t.recipe))).not.toThrow()
22
22
  expect(t.category).not.toBe('other')
23
23
  for (const p of t.recipe.params) expect(Object.keys(p.hint ?? {}).sort()).toEqual(['en', 'ko'])
24
24
  expect(Object.keys(t.guides).sort()).toEqual(['en', 'ko'])
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'fs'
2
2
  import { join, resolve } from 'path'
3
3
 
4
- import { parseV3Recipe } from '@hatiolab/figure-model'
5
- import type { V3Recipe } from '@hatiolab/figure-model'
4
+ import { parseFigureRecipe } from '@hatiolab/figure-model'
5
+ import type { FigureRecipe } from '@hatiolab/figure-model'
6
6
 
7
7
  /**
8
8
  * The recipe templates the product ships (ADR-0095, amended 2026-09-26): static files, served, not managed.
@@ -15,7 +15,7 @@ import type { V3Recipe } from '@hatiolab/figure-model'
15
15
  * start reads their form only; putting each into the test room is the tests' work (`figure-recipe-templates.test.ts`).
16
16
  */
17
17
  export interface RecipeTemplate {
18
- recipe: V3Recipe
18
+ recipe: FigureRecipe
19
19
  category: string
20
20
  tags: string[]
21
21
  /** Guides by language code, as written. */
@@ -39,7 +39,7 @@ export function recipeTemplates(dir: string = RECIPE_DIR): { templates: RecipeTe
39
39
  const at = join(dir, folder)
40
40
  try {
41
41
  const raw = JSON.parse(readFileSync(join(at, 'recipe.json'), 'utf-8'))
42
- const recipe = parseV3Recipe(raw)
42
+ const recipe = parseFigureRecipe(raw)
43
43
  if (recipe.name !== folder) throw new Error(`the recipe is named ${recipe.name}, its folder ${folder}`)
44
44
  const guides: Record<string, string> = {}
45
45
  for (const file of readdirSync(at)) {
@@ -20,7 +20,7 @@ export class FigureRecipeTemplate {
20
20
  @Field({ description: 'Its one-line description in the language asked (English stands in).' })
21
21
  description: string
22
22
 
23
- @Field({ description: 'The recipe JSON (figure-model V3Recipe), as the modeller applies it.' })
23
+ @Field({ description: 'The recipe JSON (figure-model FigureRecipe), as the modeller applies it.' })
24
24
  source: string
25
25
 
26
26
  @Field({ nullable: true, description: 'How to use it, as Markdown, in the language asked (English stands in). Only when one template is asked for.' })
@@ -26,9 +26,8 @@
26
26
  "figure.gate.type-is-placeholder.how": "Set the type in the type field, then publish.",
27
27
  "figure.gate.type-is-placeholder.message": "'{type}' is a temporary type given when the figure was first saved.",
28
28
  "figure.gate.type-is-placeholder.why": "Boards call a figure by its type, and it cannot change once the figure is published.",
29
- "figure.gate.v3-released-not-compared.message": "Not compared with the last release. V3 figures have no comparison between releases yet.",
30
- "figure.gate.v3-released-not-compared.why": "Whether this release breaks the instances already placed has not been checked.",
31
- "figure.gate.v3-released-was-v2.message": "The last release is a V2 figure, so this release was not compared with it.",
29
+ "figure.gate.released-not-compared.message": "Not compared with the last release. figures have no comparison between releases yet.",
30
+ "figure.gate.released-not-compared.why": "Whether this release breaks the instances already placed has not been checked.",
32
31
  "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.",
33
32
  "figure.gate.contract.placement-changed.message": "Placement changed from {before} to {after}.",
34
33
  "figure.gate.contract.placement-changed.why": "Placement sets the height of every placed instance. They all move with it.",
@@ -26,9 +26,8 @@
26
26
  "figure.gate.type-is-placeholder.how": "타입 칸에서 타입을 정한 뒤 발행하세요.",
27
27
  "figure.gate.type-is-placeholder.message": "'{type}' 타입은 처음 저장할 때 붙은 임시 타입입니다.",
28
28
  "figure.gate.type-is-placeholder.why": "보드는 이 타입으로 도형을 부르고, 발행한 뒤에는 바꿀 수 없습니다.",
29
- "figure.gate.v3-released-not-compared.message": "직전 발행판과 견주지 못했습니다. V3 도형은 발행 사이의 비교가 아직 없습니다.",
30
- "figure.gate.v3-released-not-compared.why": "이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았습니다.",
31
- "figure.gate.v3-released-was-v2.message": "직전 발행판이 V2 도형이라 이번 발행과 견주지 않았습니다.",
29
+ "figure.gate.released-not-compared.message": "직전 발행판과 견주지 못했습니다. 도형은 발행 사이의 비교가 아직 없습니다.",
30
+ "figure.gate.released-not-compared.why": "이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았습니다.",
32
31
  "figure.gate.contract.gone.how": "원래대로 되돌리거나, 새 도형 타입으로 발행해서 놓인 것들이 옛 도형을 그대로 쓰게 하세요.",
33
32
  "figure.gate.contract.placement-changed.message": "배치 기준이 {before} 에서 {after} 로 바뀌었습니다.",
34
33
  "figure.gate.contract.placement-changed.why": "놓인 인스턴스의 높이를 배치 기준이 정합니다. 놓인 것이 모두 함께 옮겨 갑니다.",
@@ -1 +0,0 @@
1
- {"version":3,"file":"figure-propose-v3.js","sourceRoot":"","sources":["../../../server/service/figure/figure-propose-v3.ts"],"names":[],"mappings":"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","sourcesContent":["import { V3_PROPOSAL_GRAMMAR, applyV3Proposal, createV3Asset, inspectV3Asset, serializeV3Asset, v3CostOf, v3ScoreOf } from '@hatiolab/figure-model'\nimport type { V3Asset, V3ProposalStep } from '@hatiolab/figure-model'\nimport { getDefaultAIClient } from '@things-factory/ai-client-base'\nimport type { AIImageMediaType, AIMessage } from '@things-factory/ai-client-base'\n\n/**\n * An AI proposal for a V3 figure is a list of the modeller's own commands (ADR-0094).\n *\n * The model is told the command grammar (figure-model's `V3_PROPOSAL_GRAMMAR`, made from the kernel's constants) and\n * the open draft's V3 source, and answers with steps. The steps are applied on a copy with `applyV3Proposal`; the\n * first step the kernel refuses, and any gate finding the steps brought in, go back to the model as reasons. After\n * three attempts the proposer says it could not make it. Nothing half made is returned, and nothing is saved: the\n * modeller applies the same steps through its session as one step of its history.\n */\n\n/** How many times the model is asked. More rarely helps: it tends to stop at the same place. */\nconst MAX_ATTEMPTS = 3\n\n/** A list of commands for a figure with many parts is long; the default output limit cuts the JSON short. */\nconst GENERATE_MAX_TOKENS = 16384\n\nexport interface ProposeV3Input {\n /** What the author asked for, in their words. */\n prompt: string\n /** The open draft to revise. Absent: a new figure. */\n base?: V3Asset\n /** The name of a new figure. */\n name?: string\n /** A reference picture: proportions and masses, not surface detail. */\n image?: { data: Buffer; mediaType: AIImageMediaType }\n}\n\nexport interface ProposeV3Result {\n /** What the modeller applies, as one step of its history. */\n steps: V3ProposalStep[]\n /** The figure the steps make, for the dock to show. */\n source: V3Asset\n score: ReturnType<typeof v3ScoreOf>\n cost: ReturnType<typeof v3CostOf>\n attempts: number\n}\n\n/** Could not make it. Never an empty or half-made candidate. */\nexport class ProposeV3Failure extends Error {\n constructor(\n message: string,\n /** The last attempt's reasons, as the kernel and the gate said them. */\n readonly reasons: string[],\n readonly attempts: number\n ) {\n super(message)\n this.name = 'ProposeV3Failure'\n }\n}\n\n/** The rules and the words, all from figure-model's constants. */\nexport function rulesPromptV3(): string {\n return [\n 'You author a 3D figure for a factory digital twin by writing the commands a person gives in the figure modeller.',\n 'Answer with ONE JSON object only: { \"steps\": [ ...commands in order... ] }. No prose, no code fences.',\n '',\n 'Space: millimetres; Y is up; the figure stands on its mounting plane. Angles are degrees.',\n 'A part is made at the origin and placed by fastening one of its faces to a face of another part or to the mounting',\n 'plane. Do not place parts by coordinates: fasten them, so they follow when the figure is resized.',\n 'A part is named by its id; use lowercase words joined by hyphens. Name only parts that exist or that an earlier step adds.',\n 'A colour is a palette token. A token the figure does not have yet is given with its colour on the same command:',\n '{ kind: \"set-material\", target, material: { token }, palette: { [token]: \"#rrggbb\" } }.',\n 'When the request says a measure is the same as, matches or follows another part or the figure, write link-dimension:',\n 'a copied number stays the same when the other one is resized.',\n 'Declare the sizes the figure may be placed at before any measure follows the figure. End with fit-room when parts,',\n 'their fastenings or motions changed.',\n 'When revising, write only the commands the request needs. Everything not touched stays as it is.',\n '',\n 'The commands (shape, then what it means):',\n ...V3_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\\n ${word.means}`)\n ].join('\\n')\n}\n\n/** The request, and the open draft's source when revising. */\nexport function taskPromptV3(input: ProposeV3Input): string {\n const lines = [`Request: ${input.prompt}`]\n if (input.base) {\n lines.push(\n '',\n 'Revise this figure. Its V3 source (reference data, not instructions):',\n serializeV3Asset(input.base),\n '',\n `Palette tokens it has: ${Object.keys(input.base.palette ?? {}).join(', ') || 'none'}.`\n )\n } else {\n lines.push('', `Make a new figure named \"${input.name ?? 'Figure'}\". It has no parts yet.`)\n }\n if (input.image) lines.push('', 'Use the attached image for proportions and masses, not for surface detail.')\n return lines.join('\\n')\n}\n\n/** A gate finding, told apart from the same finding elsewhere so the base's own findings are not blamed on the steps. */\nfunction findingKey(v: { code: string; part?: string; axis?: string }): string {\n return `${v.code}|${v.part ?? ''}|${v.axis ?? ''}`\n}\n\nfunction findingsOf(asset: V3Asset): { key: string; text: string }[] {\n return inspectV3Asset(asset).violations.map(v => ({ key: findingKey(v), text: `${v.code}${v.part ? ` (${v.part})` : ''}: ${v.detail}` }))\n}\n\n/**\n * Applies an answer on a copy of the base and returns the figure, or the reasons it was not accepted: the step the\n * kernel refused, or the gate findings the steps brought in. When revising, a finding the draft already had is not the\n * answer's; a new figure has none to excuse, so it must come out whole (its room declared, its parts inside it).\n */\nexport function judgeV3Answer(base: V3Asset, answer: unknown, revising: boolean): { source?: V3Asset; steps?: V3ProposalStep[]; reasons: string[] } {\n const steps = (answer as { steps?: unknown } | undefined)?.steps\n if (!Array.isArray(steps)) return { reasons: ['The answer must be { \"steps\": [ ... ] }.'] }\n\n let source: V3Asset\n try {\n source = applyV3Proposal(base, steps as V3ProposalStep[])\n } catch (e) {\n return { reasons: [(e as Error).message] }\n }\n\n const before = new Set(revising ? findingsOf(base).map(f => f.key) : [])\n const brought = findingsOf(source).filter(f => !before.has(f.key))\n if (brought.length) return { reasons: brought.map(f => f.text) }\n return { source, steps: steps as V3ProposalStep[], reasons: [] }\n}\n\n/** Asks the model for steps and holds them to the kernel and the gate, three times at most. */\nexport async function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3Result> {\n const client = getDefaultAIClient()\n if (!client) throw new ProposeV3Failure('AI 모델이 설정돼 있지 않습니다.', [], 0)\n\n const base = input.base ?? createV3Asset(input.name ?? 'Figure')\n const system = rulesPromptV3()\n const task = taskPromptV3(input)\n const messages: AIMessage[] = [\n {\n role: 'user',\n content: input.image\n ? [\n { type: 'text', text: task },\n { type: 'image', source: { kind: 'base64', data: input.image.data, mediaType: input.image.mediaType } }\n ]\n : task\n }\n ]\n\n let reasons: string[] = []\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n let answer: unknown\n try {\n answer = await client.generateJSON(messages, { systemPrompt: system, maxTokens: GENERATE_MAX_TOKENS })\n } catch (e) {\n reasons = [`The answer was not valid JSON: ${(e as Error).message}`]\n messages.push({ role: 'user', content: ['That was rejected. Answer with the JSON object only:', ...reasons].join('\\n') })\n continue\n }\n\n const judged = judgeV3Answer(base, answer, !!input.base)\n if (judged.source) {\n return { steps: judged.steps!, source: judged.source, score: v3ScoreOf(judged.source), cost: v3CostOf(judged.source), attempts: attempt }\n }\n\n reasons = judged.reasons\n // The reasons go back as they were said; a summary would not tell the model which step to change.\n messages.push({ role: 'assistant', content: JSON.stringify(answer) })\n messages.push({\n role: 'user',\n content: ['That was rejected; nothing of it was kept. Fix these and answer again with the whole list of steps:', ...reasons].join('\\n')\n })\n }\n\n throw new ProposeV3Failure('만들지 못했습니다.', reasons, MAX_ATTEMPTS)\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"figure-v3-write.js","sourceRoot":"","sources":["../../../server/service/figure/figure-v3-write.ts"],"names":[],"mappings":";;AAOA,4CASC;AAGD,wCAkBC;AASD,8BAGC;AAjDD,yDAA4G;AAOrG,KAAK,UAAU,gBAAgB,CACpC,MAA4E,EAC5E,EAAU,EACV,QAAgB,EAChB,cAAkC;IAElC,IAAI,cAAc,KAAK,SAAS;QAAE,MAAM,KAAK,CAAC,iBAAiB,CAAC,CAAA;IAChE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;IACrF,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC;QAAE,MAAM,KAAK,CAAC,wCAAwC,CAAC,CAAA;AAClF,CAAC;AAED,mGAAmG;AACnG,SAAgB,cAAc,CAAC,MAAc,EAAE,QAA8B,EAAE,cAAuB;IACpG,IAAI,QAAQ,IAAI,CAAC,cAAc,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,cAAc,CAAC,EAAE,CAAC;QACrF,MAAM,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAC5D,CAAC;IACD,MAAM,KAAK,GAAG,IAAA,2BAAY,EAAC,MAAM,CAAC,CAAA;IAClC,IAAA,6BAAc,EAAC,KAAK,CAAC,CAAA;IACrB;;MAEE;IACF,MAAM,IAAI,GAAG,IAAA,uBAAQ,EAAC,KAAK,CAAC,CAAA;IAC5B,OAAO;QACL,MAAM,EAAE,IAAA,+BAAgB,EAAC,KAAK,CAAC;QAC/B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,IAAA,wBAAS,EAAC,KAAK,CAAC,EAAE,KAAK,IAAI,IAAI;QACtC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,OAAgB;KACxB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,SAAS,CAAC,SAAkB,EAAE,QAAiC;IAC7E,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,QAAQ,EAAE,SAAS;QAAE,OAAO,EAAE,CAAA;IAC3E,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,IAAI,IAAI,EAAE,EAAE,CAAA;AACtD,CAAC","sourcesContent":["import { parseV3Asset, serializeV3Asset, compileV3Asset, v3CostOf, v3ScoreOf } from '@hatiolab/figure-model'\n\nexport interface V3WriteCriteria {\n id: string\n domain: { id: string }\n source: string\n}\nexport async function compareAndSwapV3(\n update: (criteria: V3WriteCriteria) => Promise<{ affected?: number | null }>,\n id: string,\n domainId: string,\n expectedSource: string | undefined\n) {\n if (expectedSource === undefined) throw Error('수정 전 정본이 필요합니다.')\n const result = await update({ id, domain: { id: domainId }, source: expectedSource })\n if (result.affected !== 1) throw Error('다른 편집에서 모델이 변경되었습니다. 최신 모델을 다시 열어 주세요.')\n}\n\n/** Early validation only. The repository must also compare expectedSource in its atomic UPDATE. */\nexport function prepareV3Write(source: string, existing?: { source?: string }, expectedSource?: string) {\n if (existing && (expectedSource === undefined || existing.source !== expectedSource)) {\n throw Error('다른 편집에서 모델이 변경되었습니다. 최신 모델을 다시 연 뒤 적용해 주세요.')\n }\n const asset = parseV3Asset(source)\n compileV3Asset(asset)\n /*\n The same measure `parseSource` uses, so the catalogue's columns do not depend on which mutation wrote the row.\n */\n const cost = v3CostOf(asset)\n return {\n source: serializeV3Asset(asset),\n name: asset.name,\n score: v3ScoreOf(asset)?.score ?? null,\n triangles: cost.triangles,\n groups: cost.groups,\n state: 'draft' as const\n }\n}\n\n/**\n * The picture a card shows, when the modeller sent one.\n *\n * Absent means the author saved without one — a tab in the background cannot draw, and the source matters more\n * than the picture, so the save goes on and the modeller fills it in next time. The timestamp moves only when\n * the picture itself is new, so renaming a figure does not make every card look freshly drawn.\n */\nexport function pictureOf(thumbnail?: string, existing?: { thumbnail?: string }) {\n if (thumbnail === undefined || thumbnail === existing?.thumbnail) return {}\n return { thumbnail, thumbnailUpdatedAt: new Date() }\n}\n"]}