@things-factory/figure-service 10.1.171 → 10.1.172

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.171",
3
+ "version": "10.1.172",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -38,5 +38,5 @@
38
38
  "jszip": "^3.10.1",
39
39
  "koa-router": "^7.4.0"
40
40
  },
41
- "gitHead": "e88536db0bc3cbb759fe58f9efeef66a67fef2c3"
41
+ "gitHead": "6c653ce8573f7a0d64c7f7f930605f816c35d69f"
42
42
  }
@@ -177,7 +177,7 @@ describe('figure-format — 판을 알아보는 문', () => {
177
177
  it('넘은 부품은 이름 · 축 · 넘은 양을 화면 말투로 말하고, 왜와 어떻게를 함께 낸다', () => {
178
178
  const finding = inspectSource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
179
179
 
180
- expect(finding.message).toMatch(/^'arm' 부품이 공간 선언을 X 로 [\d.]+mm 넘습니다\.$/)
180
+ expect(finding.message).toMatch(/^도형이 차지하는 공간을 'arm' 부품이 X 로 [\d.]+mm 넘습니다\.$/)
181
181
  expect(finding.why).toMatch(/니다\.$/)
182
182
  expect(finding.how).toContain('부품에 맞추기')
183
183
  expect(finding.how).toMatch(/세요\.$/)
@@ -202,10 +202,18 @@ describe('figure-format — 판을 알아보는 문', () => {
202
202
  expect(said.findings.some(f => f.code === 'extension-gone')).toBe(false)
203
203
  })
204
204
 
205
- it('공간을 선언하지 않은 도형은 무엇을 누르면 되는지 말한다', () => {
205
+ /* 첫 15분 다시 재기(10-16 저녁): 판정 문구만 옛 말 「공간 선언」을 썼다. 화면 말은 ADR-0097 §9 Q2 A 의 표다. */
206
+ it('판정 문구는 화면 말을 쓴다 — 「차지하는 공간」, 「공간 선언」은 쓰지 않는다', () => {
207
+ const said = Object.entries(KO).filter(([key]) => key.startsWith('figure.gate.'))
208
+ expect(said.filter(([, text]) => /공간 선언|공간을[^.]*선언/.test(text)).map(([key]) => key)).toEqual([])
209
+ const finding = inspectSource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
210
+ expect(`${finding.message}${finding.why}${finding.how}`).toContain('차지하는 공간')
211
+ })
212
+
213
+ it('차지하는 공간을 정하지 않은 도형은 무엇을 누르면 되는지 말한다', () => {
206
214
  const finding = inspectSource(bare(), null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
207
215
 
208
- expect(finding.message).toBe('이 도형은 차지하는 공간을 아직 선언하지 않았습니다.')
216
+ expect(finding.message).toBe('이 도형은 차지하는 공간을 아직 정하지 않았습니다.')
209
217
  expect(finding.how).toContain('부품에 맞추기')
210
218
  })
211
219
 
@@ -213,7 +221,7 @@ describe('figure-format — 판을 알아보는 문', () => {
213
221
  const empty = serializeFigureAsset(createFigureAsset('Empty'))
214
222
  const finding = inspectSource(empty, null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
215
223
 
216
- expect(finding.how).toBe('먼저 도형을 추가한 뒤, 「부품에 맞추기」로 공간을 선언하세요.')
224
+ expect(finding.how).toBe('먼저 도형을 추가한 뒤, 「부품에 맞추기」로 차지하는 공간을 정하세요.')
217
225
  expect(inspectSource(empty).findings.find(f => f.code === 'occupancy-undeclared')!.how).toMatch(/^Add a shape first/)
218
226
  })
219
227
 
@@ -289,7 +297,7 @@ describe('figure-format — 판을 알아보는 문', () => {
289
297
  it('번역이 없으면 영어로 답한다 — 키를 찍지 않는다', () => {
290
298
  const finding = inspectSource(outside()).findings.find(f => f.code === 'part-outside-occupancy')!
291
299
 
292
- expect(finding.message).toMatch(/^'arm' goes [\d.]+ mm past the declared space on X\.$/)
300
+ expect(finding.message).toMatch(/^'arm' goes [\d.]+ mm past the space the figure takes on X\.$/)
293
301
  expect(finding.message).not.toContain('figure.gate.')
294
302
  })
295
303
 
@@ -119,8 +119,8 @@ export type Translate = (key: string, options?: Record<string, unknown>) => stri
119
119
  * refusal. The English line is the fallback when a key is missing, so a missing translation reads as English
120
120
  * rather than as the key printed raw.
121
121
  *
122
- * The words are the modeller's: 「공간 선언」 for the volume a figure takes up and 「장착면」 for the face it stands on
123
- * (`figure.text.mounting-face-explained`). A fact, then the way out.
122
+ * The words are the modeller's (ADR-0097 §9 Q2 A): 「차지하는 공간」 for the volume a figure takes up, 「부품에 맞추기」 for
123
+ * setting it from the parts, and 「장착면」 for the face it stands on (`figure.text.mounting-face-explained`). A fact, then the way out.
124
124
  */
125
125
  function figureWording(violation: FigureViolation, say: Say, hasParts: boolean): Pick<GateFinding, 'message' | 'why' | 'how'> {
126
126
  const part = violation.part ?? ''
@@ -132,28 +132,28 @@ function figureWording(violation: FigureViolation, say: Say, hasParts: boolean):
132
132
  switch (violation.code) {
133
133
  case 'PART_OUTSIDE_OCCUPANCY':
134
134
  return {
135
- message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the declared space on ${axis}.`),
136
- why: say(key('why'), {}, 'The declared space sets the scale a figure is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.'),
137
- how: say(key('how'), { part, axis, mm }, `Use "fit to parts" to declare the space from the parts again, or move '${part}' ${mm} mm inward on ${axis}.`)
135
+ message: say(key('message'), { part, axis, mm }, `'${part}' goes ${mm} mm past the space the figure takes on ${axis}.`),
136
+ why: say(key('why'), {}, 'The space the figure takes sets the scale it is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.'),
137
+ how: say(key('how'), { part, axis, mm }, `Use "fit to parts" to fit the space the figure takes to the parts again, or move '${part}' ${mm} mm inward on ${axis}.`)
138
138
  }
139
139
  case 'OCCUPANCY_UNDECLARED':
140
140
  return violation.axis
141
141
  ? {
142
- message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The declared space is empty on ${axis}.`),
143
- why: say(key('why'), {}, 'Without a declared space the scale a figure is placed at cannot be set.'),
144
- how: say(key('how'), {}, 'Use "fit to parts" to declare the space from the parts as they are now.')
142
+ message: say(`figure.gate.occupancy-empty-axis.message`, { axis }, `The space the figure takes is empty on ${axis}.`),
143
+ why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
144
+ how: say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
145
145
  }
146
146
  : {
147
- message: say(key('message'), {}, 'This figure has not declared the space it takes up.'),
148
- why: say(key('why'), {}, 'Without a declared space the scale a figure is placed at cannot be set.'),
147
+ message: say(key('message'), {}, 'The space this figure takes is not set yet.'),
148
+ why: say(key('why'), {}, 'The space a figure takes sets the scale it is placed at on a board.'),
149
149
  /*
150
150
  A new figure is refused this way before it has a part, and 「fit to parts」 has nothing to measure then —
151
151
  the screen does not offer it. Telling the author to press it sent them looking for a button that was not
152
152
  there (:3300, 2026-09-24).
153
153
  */
154
154
  how: hasParts
155
- ? say(key('how'), {}, 'Use "fit to parts" to declare the space from the parts as they are now.')
156
- : say(key('how-no-parts'), {}, 'Add a shape first, then use "fit to parts" to declare the space from it.')
155
+ ? say(key('how'), {}, 'Use "fit to parts" to set the space the figure takes from the parts as they are now.')
156
+ : say(key('how-no-parts'), {}, 'Add a shape first, then use "fit to parts" to set the space the figure takes from it.')
157
157
  }
158
158
  case 'PARTS_OFF_PLACEMENT_FACE':
159
159
  return {
@@ -269,7 +269,7 @@ export function inspectSource(
269
269
  code: 'state-range-sampled',
270
270
  /* Sizes as well as motion: the instance size range is swept for every figure (figure-model, designer 2026-09-27). */
271
271
  message: say('figure.gate.state-range-sampled.message', { states }, `The whole range of sizes and motion could not be proven, so ${states} combinations of size and pose were checked.`),
272
- why: say('figure.gate.state-range-sampled.why', {}, 'Between the combinations that were checked, a part may still go past the declared space.'),
272
+ why: say('figure.gate.state-range-sampled.why', {}, 'Between the combinations that were checked, a part may still go past the space the figure takes.'),
273
273
  how: say('figure.gate.state-range-sampled.how', {}, 'To cover the whole range, re-author what could not be proven.'),
274
274
  /*
275
275
  Which motions stopped the proof, by the name a person gave them: the kernel reports each operation it could
@@ -249,6 +249,21 @@ describe('규율이 도구와 함께 나간다', () => {
249
249
  expect(category.guidance).toMatch(/breaks the bindings/)
250
250
  })
251
251
 
252
+ /*
253
+ :3300, 2026-10-16, a real model: asked for "폭 600mm, 높이 800mm", the conversation's model passed the step writer
254
+ "폭 Y=600mm, 높이 Z=800mm" -- told to name sizes by axis, it guessed which was up, and the writer's Y is up. The
255
+ person's words go to the writer as they are; the writer knows the figure's axes.
256
+ */
257
+ it('넘기는 요청은 사람의 말 그대로 — 축을 붙여 옮기지 않는다', () => {
258
+ const category = getToolCategories().find((one: any) => one.name === 'figure-authoring')!
259
+ const prompt = category.specs.find((spec: any) => spec.name === 'proposeFigureCandidate').schema.properties.prompt.description
260
+
261
+ expect(category.guidance).toMatch(/pass the person's words as they are/)
262
+ expect(category.guidance).toMatch(/Y is up/)
263
+ expect(category.guidance).not.toMatch(/Name coordinates and sizes by their X\/Y\/Z axes/)
264
+ expect(prompt).toMatch(/do not turn sizes into axes/)
265
+ })
266
+
252
267
  it('타입 이름을 지어내지 말라고 말한다 — 저장되는 식별자다', () => {
253
268
  const category = getToolCategories().find((one: any) => one.name === 'figure-authoring')!
254
269
 
@@ -432,7 +432,7 @@ function figureToolSpecs(): ToolSpec[] {
432
432
  properties: {
433
433
  prompt: {
434
434
  type: 'string',
435
- description: 'What to make or change, in the words the author used.'
435
+ description: 'What to make or change, in the words the author used, with the sizes as they said them: do not turn sizes into axes.'
436
436
  },
437
437
  figureId: {
438
438
  type: 'string',
@@ -596,7 +596,7 @@ export function registerFigureToolCategories(): void {
596
596
  ' A proposal is a list of modeller commands. The modeller applies it as one step only when the person runs it from the dock card, and one undo takes it all back. Do not say the figure has changed before that.',
597
597
  ' A revision can be proposed only for the figure open in the modeller. Say what the result\'s `steps` do as they are; do not invent changes the commands do not make.',
598
598
  '- Never save the source yourself. Propose; the author decides on the screen whether to take it.',
599
- '- Name coordinates and sizes by their X/Y/Z axes. Do not attach width, depth or height meanings that have not been confirmed.',
599
+ '- In the prompt to `proposeFigureCandidate`, pass the person\'s words as they are: their sizes, counts and names. Do not turn them into axes; the writer knows the figure\'s axes. When you name an axis to the person, the figure\'s Y is up.',
600
600
  '- **For a request to change a figure, call `getFigureSource` first.** Revising without reading proposes against a guess, and a renamed part breaks the bindings of the boards it is already placed on.',
601
601
  '- On the catalog nothing is open. To read an existing figure, find the id of the name or type the person said with `listFigures`; ask which figure only when it is ambiguous. To revise an existing figure, tell the person to open it in the modeller and ask there. For a new figure, do not read existing sources: call `proposeFigureCandidate` with create: true.',
602
602
  '- Do not invent type names; check them with `listFigures`. **A type name is a stored identifier**, and boards find the figure by it. A new figure\'s type is set by a person at its first save.',
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "error.figure-proposal-failed": "The figure could not be made.",
3
3
  "error.figure-release-does-not-compile": "the released source of {type} (version {version}) does not compile. open it in the modeller, fix it and release it again before it draws on a board.",
4
- "figure.gate.occupancy-empty-axis.message": "The declared space is empty on {axis}.",
5
- "figure.gate.occupancy-undeclared.how": "Use \"fit to parts\" to declare the space from the parts as they are now.",
6
- "figure.gate.occupancy-undeclared.how-no-parts": "Add a shape first, then use \"fit to parts\" to declare the space from it.",
7
- "figure.gate.occupancy-undeclared.message": "This figure has not declared the space it takes up.",
8
- "figure.gate.occupancy-undeclared.why": "Without a declared space the scale a figure is placed at cannot be set.",
9
- "figure.gate.part-outside-occupancy.how": "Use \"fit to parts\" to declare the space from the parts again, or move '{part}' {mm} mm inward on {axis}.",
10
- "figure.gate.part-outside-occupancy.message": "'{part}' goes {mm} mm past the declared space on {axis}.",
11
- "figure.gate.part-outside-occupancy.why": "The declared space sets the scale a figure is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.",
4
+ "figure.gate.occupancy-empty-axis.message": "The space the figure takes is empty on {axis}.",
5
+ "figure.gate.occupancy-undeclared.how": "Use \"fit to parts\" to set the space the figure takes from the parts as they are now.",
6
+ "figure.gate.occupancy-undeclared.how-no-parts": "Add a shape first, then use \"fit to parts\" to set the space the figure takes from it.",
7
+ "figure.gate.occupancy-undeclared.message": "The space this figure takes is not set yet.",
8
+ "figure.gate.occupancy-undeclared.why": "The space a figure takes sets the scale it is placed at on a board.",
9
+ "figure.gate.part-outside-occupancy.how": "Use \"fit to parts\" to fit the space the figure takes to the parts again, or move '{part}' {mm} mm inward on {axis}.",
10
+ "figure.gate.part-outside-occupancy.message": "'{part}' goes {mm} mm past the space the figure takes on {axis}.",
11
+ "figure.gate.part-outside-occupancy.why": "The space the figure takes sets the scale it is placed at. What lies past it is left out of that scale, so on a board this part lands outside the place given to the figure.",
12
12
  "figure.gate.parts-off-placement-face.how": "Move '{part}' {mm} mm on Y so the face meets the plane, or choose a different mounting face.",
13
13
  "figure.gate.parts-off-placement-face.message": "The mounting face of '{part}' is {mm} mm off the plane the figure stands on.",
14
14
  "figure.gate.parts-off-placement-face.why": "The figure is declared to stand on that face. If the face does not reach the plane, the figure floats on the board by that much, and whatever is placed on it floats too.",
@@ -23,7 +23,7 @@
23
23
  "figure.gate.placement-face-tilted.why": "A tilted face cannot lie on the plane, so how the figure stands is not settled.",
24
24
  "figure.gate.state-range-sampled.how": "To cover the whole range, re-author what could not be proven.",
25
25
  "figure.gate.state-range-sampled.message": "The whole range of sizes and motion could not be proven, so {states} combinations of size and pose were checked.",
26
- "figure.gate.state-range-sampled.why": "Between the combinations that were checked, a part may still go past the declared space.",
26
+ "figure.gate.state-range-sampled.why": "Between the combinations that were checked, a part may still go past the space the figure takes.",
27
27
  "figure.gate.type-is-placeholder.how": "Set the type in the type field, then publish.",
28
28
  "figure.gate.type-is-placeholder.message": "'{type}' is a temporary type given when the figure was first saved.",
29
29
  "figure.gate.type-is-placeholder.why": "Boards call a figure by its type, and it cannot change once the figure is published.",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "error.figure-proposal-failed": "도형을 만들지 못했습니다.",
3
3
  "error.figure-release-does-not-compile": "{type} 의 발행판({version})이 지금 컴파일되지 않습니다. 저작 화면에서 열어 수정한 후 다시 발행해야 보드에 나옵니다.",
4
- "figure.gate.occupancy-empty-axis.message": "공간 선언의 {axis} 가 비어 있습니다.",
5
- "figure.gate.occupancy-undeclared.how": "「부품에 맞추기」를 누르면 지금 부품으로 공간을 선언합니다.",
6
- "figure.gate.occupancy-undeclared.how-no-parts": "먼저 도형을 추가한 뒤, 「부품에 맞추기」로 공간을 선언하세요.",
7
- "figure.gate.occupancy-undeclared.message": "이 도형은 차지하는 공간을 아직 선언하지 않았습니다.",
8
- "figure.gate.occupancy-undeclared.why": "공간 선언이 없으면 도면에 놓일 때의 배율을 정할 수 없습니다.",
9
- "figure.gate.part-outside-occupancy.how": "「부품에 맞추기」로 공간을 부품에서 다시 선언하거나, '{part}' 부품을 {axis} 로 {mm}mm 안으로 옮기세요.",
10
- "figure.gate.part-outside-occupancy.message": "'{part}' 부품이 공간 선언을 {axis} 로 {mm}mm 넘습니다.",
11
- "figure.gate.part-outside-occupancy.why": "공간 선언이 도면에 놓일 때의 배율을 정합니다. 넘은 만큼은 그 배율에 들어가지 않아서, 도면에서 이 부품이 도형에 주어진 자리 밖으로 나갑니다.",
4
+ "figure.gate.occupancy-empty-axis.message": "차지하는 공간의 {axis} 가 비어 있습니다.",
5
+ "figure.gate.occupancy-undeclared.how": "「부품에 맞추기」를 누르면 지금 부품으로 차지하는 공간을 정합니다.",
6
+ "figure.gate.occupancy-undeclared.how-no-parts": "먼저 도형을 추가한 뒤, 「부품에 맞추기」로 차지하는 공간을 정하세요.",
7
+ "figure.gate.occupancy-undeclared.message": "이 도형은 차지하는 공간을 아직 정하지 않았습니다.",
8
+ "figure.gate.occupancy-undeclared.why": "도면에 놓일 때의 배율은 차지하는 공간으로 정합니다.",
9
+ "figure.gate.part-outside-occupancy.how": "「부품에 맞추기」로 차지하는 공간을 지금 부품에 다시 맞추거나, '{part}' 부품을 {axis} 로 {mm}mm 안으로 옮기세요.",
10
+ "figure.gate.part-outside-occupancy.message": "도형이 차지하는 공간을 '{part}' 부품이 {axis} 로 {mm}mm 넘습니다.",
11
+ "figure.gate.part-outside-occupancy.why": "도면에 놓일 때의 배율은 차지하는 공간으로 정합니다. 넘은 만큼은 그 배율에 들어가지 않아서, 도면에서 이 부품이 도형에 주어진 자리 밖으로 나갑니다.",
12
12
  "figure.gate.parts-off-placement-face.how": "'{part}' 부품을 Y 로 {mm}mm 옮겨 그 면을 평면에 맞추거나, 장착면을 다른 면으로 다시 정하세요.",
13
13
  "figure.gate.parts-off-placement-face.message": "장착면으로 정한 '{part}' 부품의 면이 놓이는 평면에서 {mm}mm 떨어져 있습니다.",
14
14
  "figure.gate.parts-off-placement-face.why": "도형은 그 면으로 놓인다고 선언돼 있습니다. 닿지 않으면 도면에서 그만큼 떠 보이고, 자리에 놓인 물건도 함께 뜹니다.",
@@ -23,7 +23,7 @@
23
23
  "figure.gate.placement-face-tilted.why": "기운 면은 평면에 닿을 수 없어서, 도형이 어떤 자세로 놓이는지 정해지지 않습니다.",
24
24
  "figure.gate.state-range-sampled.how": "범위 전체를 보장하려면 증명하지 못한 항목을 다시 저작하세요.",
25
25
  "figure.gate.state-range-sampled.message": "크기와 움직임의 범위 전체를 증명하지 못해 크기·자세 조합 {states}개만 검사했습니다.",
26
- "figure.gate.state-range-sampled.why": "검사한 조합 사이에서 부품이 공간 선언을 넘을 수 있습니다.",
26
+ "figure.gate.state-range-sampled.why": "검사한 조합 사이에서 부품이 도형이 차지하는 공간 밖으로 나갈 수 있습니다.",
27
27
  "figure.gate.type-is-placeholder.how": "타입 항목에 도형 타입을 지정한 후 발행해 주세요.",
28
28
  "figure.gate.type-is-placeholder.message": "'{type}' 타입은 처음 저장할 때 붙은 임시 타입입니다.",
29
29
  "figure.gate.type-is-placeholder.why": "보드는 이 타입으로 도형을 부르고, 발행한 뒤에는 바꿀 수 없습니다.",