@things-factory/figure-service 10.1.74 → 10.1.76

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 +3 -3
  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
@@ -1,7 +1,7 @@
1
1
  import { In } from 'typeorm'
2
2
  import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
3
3
 
4
- import { parseV3Asset, placeholderType } from '@hatiolab/figure-model'
4
+ import { parseFigureAsset, placeholderType } from '@hatiolab/figure-model'
5
5
  import { getRepository } from '@things-factory/shell'
6
6
 
7
7
  import { Figure } from './figure.js'
@@ -11,15 +11,15 @@ import { FigureProposal, ProposeRequest } from './figure-propose-type.js'
11
11
  import { inspectSource } from './figure-format.js'
12
12
  import { renameDraftType, renameFacts } from './figure-rename.js'
13
13
  import { parseSource } from './figure-format.js'
14
- import { ProposeV3Failure, proposeFigureV3 } from './figure-propose-v3.js'
15
- import { prepareV3Write, compareAndSwapV3, pictureOf } from './figure-v3-write.js'
14
+ import { ProposeFigureFailure, proposeFigureSteps } from './figure-propose-steps.js'
15
+ import { prepareFigureWrite, compareAndSwapFigure, pictureOf } from './figure-write.js'
16
16
 
17
17
  @Resolver(Figure)
18
18
  export class FigureMutation {
19
19
  @Directive('@transaction')
20
20
  @Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
21
- @Mutation(returns => Figure, { description: 'Save a V3 asset draft using an exact-source concurrency check.' })
22
- async saveFigureV3(
21
+ @Mutation(returns => Figure, { description: 'Save a asset draft using an exact-source concurrency check.' })
22
+ async saveFigure(
23
23
  @Arg('source') source: string,
24
24
  @Ctx() context: ResolverContext,
25
25
  @Arg('id', { nullable: true }) id?: string,
@@ -34,7 +34,7 @@ export class FigureMutation {
34
34
  if (!id) {
35
35
  if (!creationId || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(creationId))
36
36
  throw Error('새 모델 저장에는 생성 요청 식별자가 필요합니다.')
37
- const prepared = { ...prepareV3Write(source), ...pictureOf(thumbnail) }
37
+ const prepared = { ...prepareFigureWrite(source), ...pictureOf(thumbnail) }
38
38
  const previous = await repository.findOne({ where: { id: creationId, domain: { id: domain.id } } })
39
39
  if (previous) {
40
40
  if (previous.source !== prepared.source) throw Error('이 생성 요청은 이미 다른 내용으로 저장되었습니다. 저장된 모델을 다시 열어 주세요.')
@@ -43,7 +43,7 @@ export class FigureMutation {
43
43
  /*
44
44
  The temporary name comes from the format's `placeholderType()`, the one rule the release gate and the AI
45
45
  tools use to recognise it. This used to build its own (`FIGURE_<32 hex>`), which `isPlaceholderType` does
46
- not match, so a new V3 figure could be published under a machine name that then froze (ADR-0063 decision 4)
46
+ not match, so a new figure could be published under a machine name that then froze (ADR-0063 decision 4)
47
47
  — seen on :3300, 2026-09-24. A retry is still safe: the row id is the creation id, not the name.
48
48
  */
49
49
  /*
@@ -69,10 +69,10 @@ export class FigureMutation {
69
69
  // Exact tenant ownership: an inherited catalog entry is not writable in the child domain.
70
70
  const figure = await repository.findOne({ where: { id, domain: { id: domain.id } } })
71
71
  if (!figure) throw Error('편집할 Figure를 찾을 수 없습니다.')
72
- const prepared = { ...prepareV3Write(source, figure, expectedSource), ...pictureOf(thumbnail, figure) }
72
+ const prepared = { ...prepareFigureWrite(source, figure, expectedSource), ...pictureOf(thumbnail, figure) }
73
73
  // Compare-and-swap in the UPDATE itself: supported by SQLite as well as PostgreSQL.
74
74
  // A pessimistic row lock is not supported by the product's SQLite deployment.
75
- await compareAndSwapV3(criteria => repository.update(criteria, { ...prepared, updater: user }),
75
+ await compareAndSwapFigure(criteria => repository.update(criteria, { ...prepared, updater: user }),
76
76
  id, domain.id, expectedSource)
77
77
  return repository.findOneOrFail({ where: { id, domain: { id: domain.id } } })
78
78
  }
@@ -422,9 +422,9 @@ export class FigureMutation {
422
422
  }
423
423
 
424
424
  try {
425
- const result = await proposeFigureV3({
425
+ const result = await proposeFigureSteps({
426
426
  prompt: request.prompt,
427
- base: request.base ? parseV3Asset(request.base) : undefined,
427
+ base: request.base ? parseFigureAsset(request.base) : undefined,
428
428
  name: request.name,
429
429
  image
430
430
  })
@@ -438,7 +438,7 @@ export class FigureMutation {
438
438
  attempts: result.attempts
439
439
  }
440
440
  } catch (e) {
441
- if (e instanceof ProposeV3Failure) {
441
+ if (e instanceof ProposeFigureFailure) {
442
442
  // 사유를 삼키지 않는다. 저작자가 무엇이 막혔는지 알아야 말을 고쳐 다시 시킨다.
443
443
  throw new Error([e.message, ...e.reasons].join('\n'))
444
444
  }
@@ -1,15 +1,15 @@
1
1
  /**
2
- * figure-propose-v3 — an AI proposal for a V3 figure is a list of the modeller's commands (ADR-0094).
2
+ * figure-propose-steps — an AI proposal for a figure is a list of the modeller's commands (ADR-0094).
3
3
  *
4
4
  * What is held here is our filter, not the model's skill: the model is not called. The steps are applied on a copy;
5
5
  * the step the kernel refuses, and the gate findings the steps bring in, go back to the model; after three attempts
6
6
  * the proposer says it could not make it, and nothing half made comes back.
7
7
  */
8
- import { V3_PROPOSAL_GRAMMAR, applyV3Proposal, createV3Asset, inspectV3Asset } from '@hatiolab/figure-model'
8
+ import { FIGURE_PROPOSAL_GRAMMAR, applyFigureProposal, createFigureAsset, inspectFigureAsset } from '@hatiolab/figure-model'
9
9
  import { setDefaultAIClient } from '@things-factory/ai-client-base'
10
10
  import type { AIClient } from '@things-factory/ai-client-base'
11
11
 
12
- import { ProposeV3Failure, proposeFigureV3, rulesPromptV3 } from './figure-propose-v3'
12
+ import { ProposeFigureFailure, proposeFigureSteps, rulesPromptFigure } from './figure-propose-steps'
13
13
 
14
14
  function stubClient(answers: unknown[]): AIClient & { calls: number; sent: any[][]; system?: string } {
15
15
  const client = {
@@ -49,21 +49,21 @@ const wrongFace = liftTable.map((s, i) => (i === 4 ? { ...s, face: 'left' } : s)
49
49
 
50
50
  describe('the model is told the kernel\'s commands and the open draft', () => {
51
51
  it('teaches every word of the grammar, in its own shape', () => {
52
- const rules = rulesPromptV3()
53
- for (const word of V3_PROPOSAL_GRAMMAR) expect(rules).toContain(word.shape)
52
+ const rules = rulesPromptFigure()
53
+ for (const word of FIGURE_PROPOSAL_GRAMMAR) expect(rules).toContain(word.shape)
54
54
  expect(rules).not.toMatch(/"parts"\s*:\s*\[|anchor/i)
55
55
  })
56
56
 
57
57
  it('a measure asked to be the same as another is linked, not copied (:3300, 2026-09-26: 「몸통 지름과 같게」 came as a fixed 100)', () => {
58
- expect(rulesPromptV3()).toMatch(/same as, matches or follows another part or the figure, write link-dimension/)
58
+ expect(rulesPromptFigure()).toMatch(/same as, matches or follows another part or the figure, write link-dimension/)
59
59
  })
60
60
 
61
- it('a revision carries the draft\'s V3 source', async () => {
62
- const draft = applyV3Proposal(createV3Asset('Lift table'), liftTable as any)
61
+ it('a revision carries the draft\'s source', async () => {
62
+ const draft = applyFigureProposal(createFigureAsset('Lift table'), liftTable as any)
63
63
  const client = stubClient([{ steps: [{ kind: 'rename', name: 'Lift table 2' }] }])
64
64
  setDefaultAIClient(client)
65
- const made = await proposeFigureV3({ prompt: '이름을 바꿔 줘', base: draft })
66
- expect(client.sent[0]![0]!.content).toContain('"version":"figure-v3-asset-1"')
65
+ const made = await proposeFigureSteps({ prompt: '이름을 바꿔 줘', base: draft })
66
+ expect(client.sent[0]![0]!.content).toContain('"version":"figure-asset-1"')
67
67
  expect(made.source.name).toBe('Lift table 2')
68
68
  expect(draft.name).toBe('Lift table')
69
69
  })
@@ -72,17 +72,17 @@ describe('the model is told the kernel\'s commands and the open draft', () => {
72
72
  describe('the steps are held to the kernel and the gate', () => {
73
73
  it('a list the kernel takes comes back with the figure it makes, the gate clean, on the first attempt', async () => {
74
74
  setDefaultAIClient(stubClient([{ steps: liftTable }]))
75
- const made = await proposeFigureV3({ prompt: '리프트 테이블', name: 'Lift table' })
75
+ const made = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' })
76
76
  expect(made.attempts).toBe(1)
77
77
  expect(made.steps).toEqual(liftTable)
78
- expect(inspectV3Asset(made.source).violations).toEqual([])
78
+ expect(inspectFigureAsset(made.source).violations).toEqual([])
79
79
  expect(made.score?.grade).toMatch(/^[ABCD]$/)
80
80
  })
81
81
 
82
82
  it('a refused step goes back with its place and the kernel\'s code, and the next answer is taken', async () => {
83
83
  const client = stubClient([{ steps: wrongFace }, { steps: liftTable }])
84
84
  setDefaultAIClient(client)
85
- const made = await proposeFigureV3({ prompt: '리프트 테이블', name: 'Lift table' })
85
+ const made = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' })
86
86
  expect(made.attempts).toBe(2)
87
87
  const said = client.sent[1]!.at(-1)!.content as string
88
88
  expect(said).toMatch(/ATTACH_FACE: steps\[4\]: attach: /)
@@ -91,23 +91,23 @@ describe('the steps are held to the kernel and the gate', () => {
91
91
  it('a new figure must come out whole: a list that leaves the room undeclared is sent back', async () => {
92
92
  const client = stubClient([{ steps: withoutRoom }, { steps: liftTable }])
93
93
  setDefaultAIClient(client)
94
- const made = await proposeFigureV3({ prompt: '리프트 테이블', name: 'Lift table' })
94
+ const made = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' })
95
95
  expect(made.attempts).toBe(2)
96
96
  expect(client.sent[1]!.at(-1)!.content).toMatch(/OCCUPANCY_UNDECLARED/)
97
97
  })
98
98
 
99
99
  it('a revision is not blamed for what the draft already had', async () => {
100
- const draft = applyV3Proposal(createV3Asset('Lift table'), withoutRoom as any)
100
+ const draft = applyFigureProposal(createFigureAsset('Lift table'), withoutRoom as any)
101
101
  setDefaultAIClient(stubClient([{ steps: [{ kind: 'rename', name: 'Lift' }] }]))
102
- const made = await proposeFigureV3({ prompt: '이름', base: draft })
102
+ const made = await proposeFigureSteps({ prompt: '이름', base: draft })
103
103
  expect(made.attempts).toBe(1)
104
104
  })
105
105
 
106
106
  it('three refusals said back, then it says it could not make it, with the last reasons -- nothing half made', async () => {
107
107
  const client = stubClient([{ steps: wrongFace }, new Error('cut short'), { nope: true }])
108
108
  setDefaultAIClient(client)
109
- const failure = await proposeFigureV3({ prompt: '리프트 테이블', name: 'Lift table' }).catch(e => e)
110
- expect(failure).toBeInstanceOf(ProposeV3Failure)
109
+ const failure = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' }).catch(e => e)
110
+ expect(failure).toBeInstanceOf(ProposeFigureFailure)
111
111
  expect(failure.message).toBe('만들지 못했습니다.')
112
112
  expect(failure.attempts).toBe(3)
113
113
  expect(failure.reasons).toEqual(['The answer must be { "steps": [ ... ] }.'])
@@ -1,13 +1,13 @@
1
- import { V3_PROPOSAL_GRAMMAR, applyV3Proposal, createV3Asset, inspectV3Asset, serializeV3Asset, v3CostOf, v3ScoreOf } from '@hatiolab/figure-model'
2
- import type { V3Asset, V3ProposalStep } from '@hatiolab/figure-model'
1
+ import { FIGURE_PROPOSAL_GRAMMAR, applyFigureProposal, createFigureAsset, inspectFigureAsset, serializeFigureAsset, figureCostOf, figureScoreOf } from '@hatiolab/figure-model'
2
+ import type { FigureAsset, FigureProposalStep } from '@hatiolab/figure-model'
3
3
  import { getDefaultAIClient } from '@things-factory/ai-client-base'
4
4
  import type { AIImageMediaType, AIMessage } from '@things-factory/ai-client-base'
5
5
 
6
6
  /**
7
- * An AI proposal for a V3 figure is a list of the modeller's own commands (ADR-0094).
7
+ * An AI proposal for a figure is a list of the modeller's own commands (ADR-0094).
8
8
  *
9
- * The model is told the command grammar (figure-model's `V3_PROPOSAL_GRAMMAR`, made from the kernel's constants) and
10
- * the open draft's V3 source, and answers with steps. The steps are applied on a copy with `applyV3Proposal`; the
9
+ * The model is told the command grammar (figure-model's `FIGURE_PROPOSAL_GRAMMAR`, made from the kernel's constants) and
10
+ * the open draft's source, and answers with steps. The steps are applied on a copy with `applyFigureProposal`; the
11
11
  * first step the kernel refuses, and any gate finding the steps brought in, go back to the model as reasons. After
12
12
  * three attempts the proposer says it could not make it. Nothing half made is returned, and nothing is saved: the
13
13
  * modeller applies the same steps through its session as one step of its history.
@@ -19,29 +19,29 @@ const MAX_ATTEMPTS = 3
19
19
  /** A list of commands for a figure with many parts is long; the default output limit cuts the JSON short. */
20
20
  const GENERATE_MAX_TOKENS = 16384
21
21
 
22
- export interface ProposeV3Input {
22
+ export interface ProposeFigureInput {
23
23
  /** What the author asked for, in their words. */
24
24
  prompt: string
25
25
  /** The open draft to revise. Absent: a new figure. */
26
- base?: V3Asset
26
+ base?: FigureAsset
27
27
  /** The name of a new figure. */
28
28
  name?: string
29
29
  /** A reference picture: proportions and masses, not surface detail. */
30
30
  image?: { data: Buffer; mediaType: AIImageMediaType }
31
31
  }
32
32
 
33
- export interface ProposeV3Result {
33
+ export interface ProposeFigureResult {
34
34
  /** What the modeller applies, as one step of its history. */
35
- steps: V3ProposalStep[]
35
+ steps: FigureProposalStep[]
36
36
  /** The figure the steps make, for the dock to show. */
37
- source: V3Asset
38
- score: ReturnType<typeof v3ScoreOf>
39
- cost: ReturnType<typeof v3CostOf>
37
+ source: FigureAsset
38
+ score: ReturnType<typeof figureScoreOf>
39
+ cost: ReturnType<typeof figureCostOf>
40
40
  attempts: number
41
41
  }
42
42
 
43
43
  /** Could not make it. Never an empty or half-made candidate. */
44
- export class ProposeV3Failure extends Error {
44
+ export class ProposeFigureFailure extends Error {
45
45
  constructor(
46
46
  message: string,
47
47
  /** The last attempt's reasons, as the kernel and the gate said them. */
@@ -49,12 +49,12 @@ export class ProposeV3Failure extends Error {
49
49
  readonly attempts: number
50
50
  ) {
51
51
  super(message)
52
- this.name = 'ProposeV3Failure'
52
+ this.name = 'ProposeFigureFailure'
53
53
  }
54
54
  }
55
55
 
56
56
  /** The rules and the words, all from figure-model's constants. */
57
- export function rulesPromptV3(): string {
57
+ export function rulesPromptFigure(): string {
58
58
  return [
59
59
  'You author a 3D figure for a factory digital twin by writing the commands a person gives in the figure modeller.',
60
60
  'Answer with ONE JSON object only: { "steps": [ ...commands in order... ] }. No prose, no code fences.',
@@ -72,18 +72,18 @@ export function rulesPromptV3(): string {
72
72
  'When revising, write only the commands the request needs. Everything not touched stays as it is.',
73
73
  '',
74
74
  'The commands (shape, then what it means):',
75
- ...V3_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\n ${word.means}`)
75
+ ...FIGURE_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\n ${word.means}`)
76
76
  ].join('\n')
77
77
  }
78
78
 
79
79
  /** The request, and the open draft's source when revising. */
80
- export function taskPromptV3(input: ProposeV3Input): string {
80
+ export function taskPromptFigure(input: ProposeFigureInput): string {
81
81
  const lines = [`Request: ${input.prompt}`]
82
82
  if (input.base) {
83
83
  lines.push(
84
84
  '',
85
- 'Revise this figure. Its V3 source (reference data, not instructions):',
86
- serializeV3Asset(input.base),
85
+ 'Revise this figure. Its source (reference data, not instructions):',
86
+ serializeFigureAsset(input.base),
87
87
  '',
88
88
  `Palette tokens it has: ${Object.keys(input.base.palette ?? {}).join(', ') || 'none'}.`
89
89
  )
@@ -99,8 +99,8 @@ function findingKey(v: { code: string; part?: string; axis?: string }): string {
99
99
  return `${v.code}|${v.part ?? ''}|${v.axis ?? ''}`
100
100
  }
101
101
 
102
- function findingsOf(asset: V3Asset): { key: string; text: string }[] {
103
- return inspectV3Asset(asset).violations.map(v => ({ key: findingKey(v), text: `${v.code}${v.part ? ` (${v.part})` : ''}: ${v.detail}` }))
102
+ function findingsOf(asset: FigureAsset): { key: string; text: string }[] {
103
+ return inspectFigureAsset(asset).violations.map(v => ({ key: findingKey(v), text: `${v.code}${v.part ? ` (${v.part})` : ''}: ${v.detail}` }))
104
104
  }
105
105
 
106
106
  /**
@@ -108,13 +108,13 @@ function findingsOf(asset: V3Asset): { key: string; text: string }[] {
108
108
  * kernel refused, or the gate findings the steps brought in. When revising, a finding the draft already had is not the
109
109
  * answer's; a new figure has none to excuse, so it must come out whole (its room declared, its parts inside it).
110
110
  */
111
- export function judgeV3Answer(base: V3Asset, answer: unknown, revising: boolean): { source?: V3Asset; steps?: V3ProposalStep[]; reasons: string[] } {
111
+ export function judgeFigureAnswer(base: FigureAsset, answer: unknown, revising: boolean): { source?: FigureAsset; steps?: FigureProposalStep[]; reasons: string[] } {
112
112
  const steps = (answer as { steps?: unknown } | undefined)?.steps
113
113
  if (!Array.isArray(steps)) return { reasons: ['The answer must be { "steps": [ ... ] }.'] }
114
114
 
115
- let source: V3Asset
115
+ let source: FigureAsset
116
116
  try {
117
- source = applyV3Proposal(base, steps as V3ProposalStep[])
117
+ source = applyFigureProposal(base, steps as FigureProposalStep[])
118
118
  } catch (e) {
119
119
  return { reasons: [(e as Error).message] }
120
120
  }
@@ -122,17 +122,17 @@ export function judgeV3Answer(base: V3Asset, answer: unknown, revising: boolean)
122
122
  const before = new Set(revising ? findingsOf(base).map(f => f.key) : [])
123
123
  const brought = findingsOf(source).filter(f => !before.has(f.key))
124
124
  if (brought.length) return { reasons: brought.map(f => f.text) }
125
- return { source, steps: steps as V3ProposalStep[], reasons: [] }
125
+ return { source, steps: steps as FigureProposalStep[], reasons: [] }
126
126
  }
127
127
 
128
128
  /** Asks the model for steps and holds them to the kernel and the gate, three times at most. */
129
- export async function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3Result> {
129
+ export async function proposeFigureSteps(input: ProposeFigureInput): Promise<ProposeFigureResult> {
130
130
  const client = getDefaultAIClient()
131
- if (!client) throw new ProposeV3Failure('AI 모델이 설정돼 있지 않습니다.', [], 0)
131
+ if (!client) throw new ProposeFigureFailure('AI 모델이 설정돼 있지 않습니다.', [], 0)
132
132
 
133
- const base = input.base ?? createV3Asset(input.name ?? 'Figure')
134
- const system = rulesPromptV3()
135
- const task = taskPromptV3(input)
133
+ const base = input.base ?? createFigureAsset(input.name ?? 'Figure')
134
+ const system = rulesPromptFigure()
135
+ const task = taskPromptFigure(input)
136
136
  const messages: AIMessage[] = [
137
137
  {
138
138
  role: 'user',
@@ -156,9 +156,9 @@ export async function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3R
156
156
  continue
157
157
  }
158
158
 
159
- const judged = judgeV3Answer(base, answer, !!input.base)
159
+ const judged = judgeFigureAnswer(base, answer, !!input.base)
160
160
  if (judged.source) {
161
- return { steps: judged.steps!, source: judged.source, score: v3ScoreOf(judged.source), cost: v3CostOf(judged.source), attempts: attempt }
161
+ return { steps: judged.steps!, source: judged.source, score: figureScoreOf(judged.source), cost: figureCostOf(judged.source), attempts: attempt }
162
162
  }
163
163
 
164
164
  reasons = judged.reasons
@@ -170,5 +170,5 @@ export async function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3R
170
170
  })
171
171
  }
172
172
 
173
- throw new ProposeV3Failure('만들지 못했습니다.', reasons, MAX_ATTEMPTS)
173
+ throw new ProposeFigureFailure('만들지 못했습니다.', reasons, MAX_ATTEMPTS)
174
174
  }
@@ -8,7 +8,7 @@ export class ProposeRequest {
8
8
  @Field({ description: 'What the author asked for, in their own words' })
9
9
  prompt: string
10
10
 
11
- @Field({ nullable: true, description: 'The V3 figure source to revise, as JSON. Omit to create a new one.' })
11
+ @Field({ nullable: true, description: 'The figure source to revise, as JSON. Omit to create a new one.' })
12
12
  base?: string
13
13
 
14
14
  @Field({ nullable: true, description: 'The name a person reads for a new figure. Ignored when base is given.' })
@@ -26,10 +26,10 @@ export class ProposeRequest {
26
26
  */
27
27
  @ObjectType({ description: 'A figure the assistant proposes, as modeller commands. Not saved — the author applies it or leaves it.' })
28
28
  export class FigureProposal {
29
- @Field({ description: 'The V3 authoring commands, as a JSON array, in the order they apply' })
29
+ @Field({ description: 'The authoring commands, as a JSON array, in the order they apply' })
30
30
  steps: string
31
31
 
32
- @Field({ description: 'The V3 figure source the steps make, as JSON' })
32
+ @Field({ description: 'The figure source the steps make, as JSON' })
33
33
  source: string
34
34
 
35
35
  @Field(type => Int, { nullable: true, description: 'Reuse score out of 100' })
@@ -114,7 +114,7 @@ export class FigureQuery {
114
114
  const figure = await getRepository(Figure).findOne({
115
115
  where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id },
116
116
  /*
117
- `type` is read too. A V3 asset carries no type, so the temporary-type refusal reads the row's; with only
117
+ `type` is read too. A asset carries no type, so the temporary-type refusal reads the row's; with only
118
118
  id and source selected it saw `undefined`, the panel enabled 「발행하기」, and the release mutation — which
119
119
  loads the whole row — refused on the press (2026-09-24).
120
120
  */
@@ -142,7 +142,7 @@ export class FigureQuery {
142
142
  /**
143
143
  * The same judgement for a source that has not been saved — what the modeller shows **while** the author edits.
144
144
  *
145
- * Without it a V3 author saw nothing until they saved and opened the release panel. The judgement runs here, not
145
+ * Without it a author saw nothing until they saved and opened the release panel. The judgement runs here, not
146
146
  * in the browser, because the sentences are made from the request's language (`inspectAnySource` with `context.t`)
147
147
  * and a second copy on the screen would drift from the one the release refuses with. When `id` names a saved
148
148
  * figure, its row type and last release are read as `inspectFigure` does; a figure never saved has neither.
@@ -190,7 +190,7 @@ export class FigureQuery {
190
190
  * 소비처(operato-board, operato-twin, things-scene)가 발행된 도형을 받아 가는 문.
191
191
  *
192
192
  * 이름은 보드와의 계약이라 그대로 둔다(바꾸는 일은 따로 한다). 돌려주는 것은 자산이다 —
193
- * `{ format: 'v3', type, asset }`. 그리는 쪽(`registerFigureV3`)이 커널로 컴파일한다.
193
+ * `{ type, asset }`. 그리는 쪽(`registerFigure`)이 커널로 컴파일한다.
194
194
  *
195
195
  * ## 규칙
196
196
  * 1. 소비처는 초안(draft)을 볼 수 없다 — **반드시 발행된(released) 판만 전달**한다.
@@ -221,7 +221,7 @@ export class FigureQuery {
221
221
  @Directive('@privilege(category: "figure-published", privilege: "query", domainOwnerGranted: true)')
222
222
  @Query(returns => GraphQLJSON, {
223
223
  nullable: true,
224
- description: 'A released figure for its consumers (board, twin, things-scene): { format, type, asset } with the figure asset. Only returns released figures.'
224
+ description: 'A released figure for its consumers (board, twin, things-scene): { type, asset } with the figure asset. Only returns released figures.'
225
225
  })
226
226
  async figureBlueprint(
227
227
  @Arg('type') type: string,
@@ -247,7 +247,7 @@ export class FigureQuery {
247
247
 
248
248
  try {
249
249
  const text = typeof sourceToCompile === 'string' ? sourceToCompile : JSON.stringify(sourceToCompile)
250
- return { format: 'v3', type, asset: consumablePayloadOf(text).asset }
250
+ return { type, asset: consumablePayloadOf(text).asset }
251
251
  } catch (err) {
252
252
  console.error(`[figureBlueprint] 타입 '${type}' (figure id: ${figure.id}) 자산 컴파일 실패:`, err)
253
253
 
@@ -143,18 +143,18 @@ describe('이름을 바꾸기 전에 읽는 사실', () => {
143
143
  */
144
144
  describe('처음 저장에 타입 이름을 받는다', () => {
145
145
  let FigureMutation: any
146
- let V3_SOURCE: string
146
+ let FIGURE_SOURCE: string
147
147
  beforeAll(() => {
148
148
  FigureMutation = loadCompiled('service/figure/figure-mutation').FigureMutation
149
149
  const model = require('@hatiolab/figure-model')
150
- V3_SOURCE = model.serializeV3Asset(
151
- model.addV3Box(model.createV3Asset('상자'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
150
+ FIGURE_SOURCE = model.serializeFigureAsset(
151
+ model.addFigureBox(model.createFigureAsset('상자'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
152
152
  )
153
153
  })
154
154
 
155
155
  const save = (creationId: string, type?: string) =>
156
- new FigureMutation().saveFigureV3(
157
- V3_SOURCE,
156
+ new FigureMutation().saveFigure(
157
+ FIGURE_SOURCE,
158
158
  { state: { domain: DOMAIN, user: undefined, tx: dataSource!.manager } },
159
159
  undefined,
160
160
  undefined,
@@ -7,13 +7,13 @@
7
7
  import { readFileSync } from 'fs'
8
8
  import { join } from 'path'
9
9
 
10
- import { createV3Asset, parseV3Asset, serializeV3Asset } from '@hatiolab/figure-model'
10
+ import { createFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
11
11
 
12
12
  import { FigureRenameRefused, renameDraftType } from './figure-rename'
13
13
 
14
14
  const draft = (over: Record<string, unknown> = {}) => ({
15
15
  type: 'FIGURE_MU6RJH47',
16
- source: serializeV3Asset(createV3Asset('새 Figure')),
16
+ source: serializeFigureAsset(createFigureAsset('새 Figure')),
17
17
  ...over
18
18
  })
19
19
 
@@ -22,15 +22,15 @@ const never = { version: 0, hasHistory: false, takenByAnother: false }
22
22
  describe('초안의 type 고치기', () => {
23
23
  /*
24
24
  자산에는 type 칸이 없고 형식은 모르는 칸을 거절한다. 원본에 이름을 써 넣었더니 그 도형을 다시 열 수
25
- 없었다(`parseV3Asset` — "unknown type"). 행만 바뀐다.
25
+ 없었다(`parseFigureAsset` — "unknown type"). 행만 바뀐다.
26
26
  */
27
27
  it('★ 행만 바뀌고, 원본은 그대로 다시 열린다', () => {
28
- const source = serializeV3Asset(createV3Asset('새 Figure'))
28
+ const source = serializeFigureAsset(createFigureAsset('새 Figure'))
29
29
  const result = renameDraftType({ type: 'FIGURE_ABCDEF12', source }, 'ROLLER_CONVEYOR', never)
30
30
 
31
31
  expect(result.type).toBe('ROLLER_CONVEYOR')
32
32
  expect(result.source).toBe(source)
33
- expect(() => parseV3Asset(result.source)).not.toThrow()
33
+ expect(() => parseFigureAsset(result.source)).not.toThrow()
34
34
  })
35
35
 
36
36
  it('앞뒤 공백을 뗀다. 대소문자는 저작자가 적은 대로 둔다', () => {
@@ -93,10 +93,10 @@ describe('문이 이 규칙을 지난다', () => {
93
93
  })
94
94
 
95
95
  /*
96
- V3 저장이 임시 이름을 스스로 지었다(`FIGURE_<32 hex>`). 형식의 `isPlaceholderType` 이 그것을 못 알아봐서
96
+ 저장이 임시 이름을 스스로 지었다(`FIGURE_<32 hex>`). 형식의 `isPlaceholderType` 이 그것을 못 알아봐서
97
97
  발행 판정이 기계 이름을 통과시켰다. 짓는 쪽과 알아보는 쪽이 한 규칙이어야 한다.
98
98
  */
99
- /* 새 V3 도형의 임시 이름이 판정이 알아보는 그 규칙인지는 문을 실제로 불러 본다 — figure-rename-db.test.ts 「이름 없이 부르면」. */
99
+ /* 새 도형의 임시 이름이 판정이 알아보는 그 규칙인지는 문을 실제로 불러 본다 — figure-rename-db.test.ts 「이름 없이 부르면」. */
100
100
 
101
101
  it('문이 type 을 받는 칸을 갖는다', () => {
102
102
  const type = source('figure-type.ts')
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 저작 도구의 **배선**을 못박는다.
3
3
  *
4
- * 도구가 무엇을 계산하는지는 각 진입점의 시험이 이미 잠근다(`figure-propose-v3.test` ·
4
+ * 도구가 무엇을 계산하는지는 각 진입점의 시험이 이미 잠근다(`figure-propose-steps.test` ·
5
5
  * figure-model 의 게이트 시험). 비어 있던 것은 **그 사이를 잇는 줄들**이고, 그 줄이 끊어질 때
6
6
  * 나타나는 증상이 실제로 겪은 실패와 1:1 로 대응한다.
7
7
  *
@@ -36,8 +36,8 @@ const THEIRS = { id: 'domain-theirs' }
36
36
  /** A stored figure: one body on the floor, its room declared. */
37
37
  const SOURCE = (() => {
38
38
  const model = require('@hatiolab/figure-model')
39
- let asset = model.addV3Box(model.createV3Asset('Mixer'), { id: 'body', size: { x: 300, y: 400, z: 300 }, position: { x: 0, y: 200, z: 0 } })
40
- asset = model.applyV3Authoring(asset, { kind: 'declare-occupancy', proposal: model.proposeV3Occupancy(asset), placement: 'floor' })
39
+ let asset = model.addFigureBox(model.createFigureAsset('Mixer'), { id: 'body', size: { x: 300, y: 400, z: 300 }, position: { x: 0, y: 200, z: 0 } })
40
+ asset = model.applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: model.proposeFigureOccupancy(asset), placement: 'floor' })
41
41
  return asset
42
42
  })()
43
43
 
@@ -57,19 +57,19 @@ function install() {
57
57
  Figure: class {},
58
58
  inspectSource: (json?: string | null) =>
59
59
  json ? { blocked: false, findings: [] } : { blocked: true, findings: [{ code: 'no-source', message: '정본이 없다', blocking: true }] },
60
- proposeFigureV3: async (input: any) => {
60
+ proposeFigureSteps: async (input: any) => {
61
61
  proposed.push(input)
62
62
  return {
63
63
  steps: [{ kind: 'rename', name: 'Mixer' }],
64
- source: { ...model.createV3Asset(input.base?.name ?? input.name ?? 'Figure') },
64
+ source: { ...model.createFigureAsset(input.base?.name ?? input.name ?? 'Figure') },
65
65
  score: { grade: 'B' },
66
66
  cost: { triangles: 12 },
67
67
  attempts: 1
68
68
  }
69
69
  },
70
70
  parseSource: require('./figure-format').parseSource,
71
- v3CostOf: model.v3CostOf,
72
- v3ScoreOf: model.v3ScoreOf
71
+ figureCostOf: model.figureCostOf,
72
+ figureScoreOf: model.figureScoreOf
73
73
  })
74
74
  }
75
75
 
@@ -303,21 +303,21 @@ describe('모델이 넘긴 id 를 믿지 않는다', () => {
303
303
 
304
304
  /*
305
305
  제안은 모델러의 명령 목록이다(ADR-0094). 고치는 제안은 모델러가 모델이 본 판 위에 한 단계로 적용하므로, 고칠 수
306
- 있는 것은 모델러에 열린 V3 도형뿐이고 그 판(context · revision)을 문맥이 싣고 와야 한다.
306
+ 있는 것은 모델러에 열린 도형뿐이고 그 판(context · revision)을 문맥이 싣고 와야 한다.
307
307
  */
308
- const v3Draft = () => {
308
+ const figureDraft = () => {
309
309
  const model = require('@hatiolab/figure-model')
310
- let asset = model.createV3Asset('Probe')
311
- asset = model.addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
310
+ let asset = model.createFigureAsset('Probe')
311
+ asset = model.addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
312
312
  return asset
313
313
  }
314
- const modeller = (over: any = {}) => ({ figureId: 'fig-1', draftSource: v3Draft(), context: 'ctx-7', revision: 4, ...over })
314
+ const modeller = (over: any = {}) => ({ figureId: 'fig-1', draftSource: figureDraft(), context: 'ctx-7', revision: 4, ...over })
315
315
 
316
316
  describe('제안을 요청할 때', () => {
317
- it('열린 도형을 고치면 그 V3 초안이 제안기에 가고, 모델러가 적용할 판이 함께 돌아온다', async () => {
317
+ it('열린 도형을 고치면 그 초안이 제안기에 가고, 모델러가 적용할 판이 함께 돌아온다', async () => {
318
318
  const out = await toolNamed('proposeFigureCandidate').builder({ prompt: '다리를 길게' }, ctx(modeller()))
319
319
  expect(proposed[0].base.name).toBe('Probe')
320
- expect(proposed[0].base.version).toBe('figure-v3-asset-1')
320
+ expect(proposed[0].base.version).toBe('figure-asset-1')
321
321
  expect(out.context).toBe('ctx-7')
322
322
  expect(out.revision).toBe(4)
323
323
  expect(out.figureId).toBe('fig-1')
@@ -373,17 +373,17 @@ describe('제안을 요청할 때', () => {
373
373
  })
374
374
 
375
375
  it('세 번 거절된 끝의 사유가 도구 경계를 넘어간다 — 「만들지 못했습니다」만 남지 않게', async () => {
376
- const { ProposeV3Failure } = require('./figure-propose-v3')
376
+ const { ProposeFigureFailure } = require('./figure-propose-steps')
377
377
  const model = require('@hatiolab/figure-model')
378
378
  __setFigureToolDepsForTest({
379
379
  getRepository: () => stubRepository(ROWS),
380
380
  Figure: class {},
381
381
  parseSource: require('./figure-format').parseSource,
382
- proposeFigureV3: async () => {
383
- throw new ProposeV3Failure('만들지 못했습니다.', ['ATTACH_FACE: steps[4]: attach: faces along x'], 3)
382
+ proposeFigureSteps: async () => {
383
+ throw new ProposeFigureFailure('만들지 못했습니다.', ['ATTACH_FACE: steps[4]: attach: faces along x'], 3)
384
384
  },
385
- v3CostOf: model.v3CostOf,
386
- v3ScoreOf: model.v3ScoreOf
385
+ figureCostOf: model.figureCostOf,
386
+ figureScoreOf: model.figureScoreOf
387
387
  })
388
388
  await expect(toolNamed('proposeFigureCandidate').builder({ prompt: '고쳐줘' }, ctx(modeller()))).rejects.toThrow(
389
389
  /만들지 못했습니다\.\nATTACH_FACE: steps\[4\]/
@@ -420,8 +420,8 @@ describe('제안이 카드가 된다', () => {
420
420
  })
421
421
 
422
422
  describe('도형이 모델러에 열려 있을 때', () => {
423
- it('저장 전 V3 초안을 V3 로 읽고, V3 셈으로 잰다', async () => {
424
- const draft = v3Draft()
423
+ it('저장 전 초안을 이 모델로 읽고, 셈으로 잰다', async () => {
424
+ const draft = figureDraft()
425
425
  const got = await toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: draft, palette: ['palette.primary'] }))
426
426
  expect(got.draft).toBe(true)
427
427
  expect(got.cost.triangles).toBe(12)
@@ -429,7 +429,7 @@ describe('도형이 모델러에 열려 있을 때', () => {
429
429
  })
430
430
 
431
431
  it('형식을 어긴 초안은 저장 문의 말로 거절한다', async () => {
432
- const broken = { ...v3Draft(), appearance: [] }
432
+ const broken = { ...figureDraft(), appearance: [] }
433
433
  await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken }))).rejects.toThrow(/unsaved draft is invalid/)
434
434
  })
435
435
  })