@things-factory/figure-service 10.1.65 → 10.1.67

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 (60) hide show
  1. package/README.md +5 -8
  2. package/dist-server/service/figure/figure-format.d.ts +49 -31
  3. package/dist-server/service/figure/figure-format.js +47 -90
  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.js +5 -5
  8. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  9. package/dist-server/service/figure/figure-propose-v3.d.ts +1 -1
  10. package/dist-server/service/figure/figure-propose-v3.js +3 -1
  11. package/dist-server/service/figure/figure-propose-v3.js.map +1 -1
  12. package/dist-server/service/figure/figure-query.d.ts +6 -11
  13. package/dist-server/service/figure/figure-query.js +12 -40
  14. package/dist-server/service/figure/figure-query.js.map +1 -1
  15. package/dist-server/service/figure/figure-rename.js +2 -11
  16. package/dist-server/service/figure/figure-rename.js.map +1 -1
  17. package/dist-server/service/figure/figure-tools.js +32 -112
  18. package/dist-server/service/figure/figure-tools.js.map +1 -1
  19. package/dist-server/service/figure/figure-type.d.ts +0 -17
  20. package/dist-server/service/figure/figure-type.js +4 -71
  21. package/dist-server/service/figure/figure-type.js.map +1 -1
  22. package/dist-server/service/figure/figure-v3-write.d.ts +1 -1
  23. package/dist-server/service/figure/figure-v3-write.js +2 -3
  24. package/dist-server/service/figure/figure-v3-write.js.map +1 -1
  25. package/dist-server/service/figure/figure.d.ts +5 -10
  26. package/dist-server/service/figure/figure.js +6 -11
  27. package/dist-server/service/figure/figure.js.map +1 -1
  28. package/dist-server/service/index.d.ts +1 -1
  29. package/dist-server/service/index.js +1 -1
  30. package/dist-server/service/index.js.map +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +3 -3
  33. package/server/service/figure/figure-blueprint-query.test.ts +29 -48
  34. package/server/service/figure/figure-format.test.ts +54 -110
  35. package/server/service/figure/figure-format.ts +77 -87
  36. package/server/service/figure/figure-history.ts +1 -1
  37. package/server/service/figure/figure-inspect-db.test.ts +1 -11
  38. package/server/service/figure/figure-mutation.ts +7 -7
  39. package/server/service/figure/figure-propose-v3.test.ts +6 -2
  40. package/server/service/figure/figure-propose-v3.ts +3 -1
  41. package/server/service/figure/figure-query.ts +14 -36
  42. package/server/service/figure/figure-rename-db.test.ts +1 -1
  43. package/server/service/figure/figure-rename.test.ts +4 -18
  44. package/server/service/figure/figure-rename.ts +2 -12
  45. package/server/service/figure/figure-tools.test.ts +17 -80
  46. package/server/service/figure/figure-tools.ts +30 -125
  47. package/server/service/figure/figure-type.ts +3 -52
  48. package/server/service/figure/figure-v3-write.ts +3 -4
  49. package/server/service/figure/figure.ts +7 -12
  50. package/server/service/index.ts +1 -1
  51. package/dist-server/service/figure/figure-gate.d.ts +0 -93
  52. package/dist-server/service/figure/figure-gate.js +0 -302
  53. package/dist-server/service/figure/figure-gate.js.map +0 -1
  54. package/dist-server/service/figure/figure-source.d.ts +0 -45
  55. package/dist-server/service/figure/figure-source.js +0 -59
  56. package/dist-server/service/figure/figure-source.js.map +0 -1
  57. package/server/service/figure/figure-gate.test.ts +0 -155
  58. package/server/service/figure/figure-gate.ts +0 -392
  59. package/server/service/figure/figure-source.test.ts +0 -149
  60. package/server/service/figure/figure-source.ts +0 -94
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.65",
3
+ "version": "10.1.67",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -23,7 +23,7 @@
23
23
  "migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
24
24
  },
25
25
  "dependencies": {
26
- "@hatiolab/figure-model": "^0.1.76",
26
+ "@hatiolab/figure-model": "^0.1.78",
27
27
  "@operato/typeorm-history": "^10.0.0",
28
28
  "@things-factory/ai-client-base": "^10.1.53",
29
29
  "@things-factory/auth-base": "^10.1.47",
@@ -33,5 +33,5 @@
33
33
  "graphql-upload": "^15.0.2",
34
34
  "koa-router": "^7.4.0"
35
35
  },
36
- "gitHead": "87b95ef312f11d3d646f6e2247ee202b68d23e3f"
36
+ "gitHead": "cd3f81fbf667f513dfcfb90906cb97ba935373f5"
37
37
  }
@@ -26,25 +26,19 @@ function loadCompiled(modulePath: string): any {
26
26
  return require(resolved)
27
27
  }
28
28
 
29
- const sampleSource = JSON.stringify({
30
- version: 2,
31
- type: 'TEST_BOX',
32
- base: { x: 100, y: 100, z: 100 },
33
- parts: [
34
- {
35
- name: 'box-1',
36
- primitive: 'cube',
37
- transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 100, y: 100, z: 100 } },
38
- material: { token: 'palette.primary' }
39
- }
40
- ]
41
- })
29
+ const model = require('@hatiolab/figure-model')
42
30
 
43
- /** The same box under a different type and part name, so a test can tell the sources apart. */
44
- const boxNamed = (type: string, part: string) => {
45
- const parsed = JSON.parse(sampleSource)
46
- return JSON.stringify({ ...parsed, type, parts: [{ ...parsed.parts[0], name: part }] })
47
- }
31
+ /** A released figure's source: one box, named `part`, `size` mm on each side. */
32
+ const box = (part = 'box-1', size = 100) =>
33
+ model.serializeV3Asset(model.addV3Box(model.createV3Asset('Test box'), { id: part, size: { x: size, y: size, z: size }, position: { x: 0, y: size / 2, z: 0 } }))
34
+ const sampleSource = box()
35
+
36
+ /** The same box under a different part name, so a test can tell the sources apart. */
37
+ const boxNamed = (_type: string, part: string) => box(part)
38
+
39
+ /** What a consumer was handed: the part ids of the asset, and a part's width. */
40
+ const partsOf = (payload: any): string[] => payload.asset.document.model.nodes.filter((n: any) => n.op === 'place@1').map((n: any) => n.id)
41
+ const widthOf = (payload: any, part: string): number => payload.asset.designInputs[`${part}.size.x`]
48
42
 
49
43
  beforeAll(async () => {
50
44
  Figure = loadCompiled('service/figure/figure').Figure
@@ -91,7 +85,7 @@ function makeContext() {
91
85
  } as any
92
86
  }
93
87
 
94
- describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위한 청사진 전달', () => {
88
+ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)에 발행된 자산 전달', () => {
95
89
  it('존재하지 않는 타입은 null을 반환한다', async () => {
96
90
  const query = new FigureQuery()
97
91
  const result = await query.figureBlueprint('NON_EXISTENT', makeContext())
@@ -114,7 +108,7 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
114
108
  expect(result).toBeNull()
115
109
  })
116
110
 
117
- it('발행된(released) Figure는 올바르게 컴파일된 Blueprint를 반환한다', async () => {
111
+ it('발행된(released) Figure는 자산을 format · type 과 함께 반환한다', async () => {
118
112
  const repo = dataSource!.getRepository(Figure)
119
113
  await repo.save({
120
114
  domain: testDomain,
@@ -128,37 +122,25 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
128
122
  const query = new FigureQuery()
129
123
  const result = await query.figureBlueprint('RELEASED_FIGURE', makeContext())
130
124
  expect(result).not.toBeNull()
131
- expect(result.type).toBe('TEST_BOX')
132
- expect(result.groups).toBeDefined()
133
- expect(result.groups.length).toBe(1)
134
- expect(result.groups[0].members[0].name).toBe('box-1')
125
+ expect(result.format).toBe('v3')
126
+ expect(result.type).toBe('RELEASED_FIGURE')
127
+ expect(result.asset.version).toBe('figure-v3-asset-1')
128
+ expect(partsOf(result)).toEqual(['box-1'])
135
129
  })
136
130
 
137
- it('현재 저작 중(draft)이더라도 과거 발행판(FigureHistory)이 있으면 최신 발행판을 컴파일해 반환한다', async () => {
131
+ it('현재 저작 중(draft)이더라도 과거 발행판(FigureHistory)이 있으면 최신 발행판을 반환한다', async () => {
138
132
  const repo = dataSource!.getRepository(Figure)
139
133
  const historyRepo = dataSource!.getRepository(FigureHistory)
140
134
 
141
135
  const v1Source = sampleSource
142
- const v2DraftSource = JSON.stringify({
143
- version: 2,
144
- type: 'UPDATING_FIGURE',
145
- base: { x: 200, y: 200, z: 200 },
146
- parts: [
147
- {
148
- name: 'box-modified',
149
- primitive: 'cube',
150
- transform: { position: { x: 0, y: 100, z: 0 }, size: { x: 200, y: 200, z: 200 } },
151
- material: { token: 'palette.primary' }
152
- }
153
- ]
154
- })
136
+ const draftSource = box('box-modified', 200)
155
137
 
156
138
  const figure = await repo.save({
157
139
  domain: testDomain,
158
140
  type: 'UPDATING_FIGURE',
159
141
  name: '수정 중인 도형',
160
142
  state: 'draft',
161
- source: v2DraftSource,
143
+ source: draftSource,
162
144
  version: 1
163
145
  })
164
146
 
@@ -178,8 +160,8 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
178
160
 
179
161
  expect(result).not.toBeNull()
180
162
  // 소비처는 수정 중인 초안(box-modified, size 200)이 아니라 기발행된 판(box-1, size 100)을 본다!
181
- expect(result.groups[0].members[0].name).toBe('box-1')
182
- expect(result.groups[0].members[0].transform.size.x).toBe(100)
163
+ expect(partsOf(result)).toEqual(['box-1'])
164
+ expect(widthOf(result, 'box-1')).toBe(100)
183
165
  })
184
166
 
185
167
  /*
@@ -217,7 +199,7 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
217
199
  const result = await new FigureQuery().figureBlueprint('MULTI', makeContext())
218
200
 
219
201
  expect(result).not.toBeNull()
220
- expect(result.groups[0].members[0].name).toBe('v3')
202
+ expect(partsOf(result)).toEqual(['v3'])
221
203
  })
222
204
 
223
205
  /*
@@ -261,7 +243,7 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
261
243
  const result = await new FigureQuery().figureBlueprint('WIP', makeContext())
262
244
 
263
245
  expect(result).not.toBeNull()
264
- expect(result.groups[0].members[0].name).toBe('released-v2')
246
+ expect(partsOf(result)).toEqual(['released-v2'])
265
247
  })
266
248
 
267
249
  /*
@@ -393,8 +375,7 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
393
375
  })
394
376
 
395
377
  describe('a child domain shadows its parent only with a released version', () => {
396
- const boxOf = (type: string, part: string) =>
397
- JSON.stringify({ ...JSON.parse(sampleSource), type, parts: [{ ...JSON.parse(sampleSource).parts[0], name: part }] })
378
+ const boxOf = (_type: string, part: string) => box(part)
398
379
 
399
380
  let childSeq = 0
400
381
 
@@ -419,7 +400,7 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
419
400
  const result = await new FigureQuery().figureBlueprint('CONVEYOR', contextIn(child))
420
401
 
421
402
  expect(result).not.toBeNull()
422
- expect(result.groups[0].members[0].name).toBe('standard')
403
+ expect(partsOf(result)).toEqual(['standard'])
423
404
  })
424
405
 
425
406
  it('uses the child release when the child has one', async () => {
@@ -431,7 +412,7 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
431
412
 
432
413
  const result = await new FigureQuery().figureBlueprint('CONVEYOR', contextIn(child))
433
414
 
434
- expect(result.groups[0].members[0].name).toBe('tenant')
415
+ expect(partsOf(result)).toEqual(['tenant'])
435
416
  })
436
417
 
437
418
  /*
@@ -462,7 +443,7 @@ describe('figureBlueprint 질의 — 소비처(board, twin, things-scene)를 위
462
443
  const result = await new FigureQuery().figureBlueprint('CONVEYOR', contextIn(child))
463
444
 
464
445
  expect(result).not.toBeNull()
465
- expect(result.groups[0].members[0].name).toBe('tenant-v1')
446
+ expect(partsOf(result)).toEqual(['tenant-v1'])
466
447
  })
467
448
 
468
449
  /*
@@ -1,38 +1,15 @@
1
1
  /**
2
- * figure-format — 한 문이 두 판을 읽는다.
2
+ * figure-format — the doors a stored source goes through: save, release, hand to a board.
3
3
  *
4
- * 전환 중에는 `source` 칸에 V2 정본과 V3 자산이 섞여 있다(ADR-0087). 저장·발행·되돌리기·소비처가
5
- * 저마다 판을 따지면 네 자리에서 답이 갈린다. 여기서 한 번 묻는다.
6
- *
7
- * 이 파일이 지키는 것:
8
- * - 판은 자산이 적은 `version` 으로 안다. 생김새로 짐작하지 않는다.
9
- * - 판을 모르는 정본은 거절한다 — 알아보지 못하는 것을 저장하면 안 된다.
10
- * - V3 자산도 저장·발행·되돌리기·소비처를 지난다. 이 넷은 전에 V2 만 지날 수 있었다.
11
- * - 예산 칸(`triangles`·`groups`)은 두 판에서 같은 뜻이다.
4
+ * What this file holds:
5
+ * - the format is known from the `version` the asset carries, never guessed from its shape;
6
+ * - a source that does not name the format is refused -- what cannot be identified is not stored;
7
+ * - the budget columns (`triangles`, `groups`) and the score are the kernel's.
12
8
  */
13
9
  import { createV3Asset, addV3Box, addV3Primitive, serializeV3Asset, applyV3Authoring, proposeV3Occupancy, placeholderType, v3ScoreOf } from '@hatiolab/figure-model'
14
10
  import type { V3Asset } from '@hatiolab/figure-model'
15
11
 
16
- import { consumablePayloadOf, formatOf, inspectAnySource, parseAnySource } from './figure-format'
17
- import { FigureSourceError } from './figure-source'
18
-
19
- /** 온전한 V2 정본 하나. 바닥에 선 상자 한 부품. */
20
- const v2Source = (over: Record<string, unknown> = {}) =>
21
- JSON.stringify({
22
- version: 2,
23
- type: 'MIXER',
24
- base: { x: 300, y: 800, z: 300 },
25
- detailLevel: 'M',
26
- parts: [
27
- {
28
- name: 'body',
29
- primitive: 'cube',
30
- transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 100, y: 100, z: 100 } },
31
- material: { token: 'palette.primary' }
32
- }
33
- ],
34
- ...over
35
- })
12
+ import { FigureSourceError, consumablePayloadOf, inspectSource, parseSource } from './figure-format'
36
13
 
37
14
  /** 온전한 V3 자산 하나: 바닥에 선 상자와, 부품에서 제안받아 확정한 점유 부피. */
38
15
  function v3Asset(): V3Asset {
@@ -50,68 +27,56 @@ const v3Source = () => serializeV3Asset(v3Asset())
50
27
 
51
28
  describe('figure-format — 판을 알아보는 문', () => {
52
29
  describe('판은 자산이 적는다', () => {
53
- it('숫자 version 은 V2, figure-v3-asset-1 은 V3 다', () => {
54
- expect(formatOf(v2Source())).toBe('v2')
55
- expect(formatOf(v3Source())).toBe('v3')
56
- })
57
-
58
30
  it('판을 안 적은 정본은 거절한다 — 생김새로 짐작하지 않는다', () => {
59
- expect(() => formatOf(JSON.stringify({ type: 'MIXER', parts: [] }))).toThrow(FigureSourceError)
60
- try {
61
- formatOf(JSON.stringify({ type: 'MIXER' }))
62
- } catch (e) {
63
- expect((e as FigureSourceError).findings[0].code).toBe('source-format-unknown')
31
+ for (const text of [JSON.stringify({ type: 'MIXER', parts: [] }), JSON.stringify({ version: 2, type: 'MIXER', parts: [] })]) {
32
+ expect(() => parseSource(text)).toThrow(FigureSourceError)
33
+ try {
34
+ parseSource(text)
35
+ } catch (e) {
36
+ expect((e as FigureSourceError).findings[0].code).toBe('source-format-unknown')
37
+ }
64
38
  }
65
39
  })
66
40
 
67
41
  it('JSON 이 아니면 그 말로 거절한다', () => {
68
42
  try {
69
- formatOf('{')
43
+ parseSource('{')
70
44
  } catch (e) {
71
45
  expect((e as FigureSourceError).findings[0].code).toBe('source-not-json')
72
46
  }
73
47
  })
74
48
  })
75
49
 
76
- describe('저장 — 두 판 다 지난다', () => {
77
- it('V2 정본은 전과 같이 점수·삼각형·묶음을 받는다', () => {
78
- const parsed = parseAnySource(v2Source())
79
- expect(parsed.format).toBe('v2')
80
- expect(parsed.derived.triangles).toBe(12)
81
- expect(parsed.derived.groups).toBeGreaterThan(0)
82
- expect(typeof parsed.derived.score).toBe('number')
83
- })
84
-
85
- it('V3 자산은 같은 예산 칸을 받는다 — 상자 하나는 두 판에서 다 삼각형 12 다', () => {
86
- const parsed = parseAnySource(v3Source())
87
- expect(parsed.format).toBe('v3')
50
+ describe('저장', () => {
51
+ it('예산 칸을 커널이 잰다 — 상자 하나는 삼각형 12, 묶음 1', () => {
52
+ const parsed = parseSource(v3Source())
88
53
  expect(parsed.derived.triangles).toBe(12)
89
54
  expect(parsed.derived.groups).toBe(1)
90
55
  })
91
56
 
92
- it('V3 의 점수 칸은 v3ScoreOf 다 — 저작면의 성숙도 탭과 같은 수(수석 아키텍트 판정 09-24)', () => {
93
- const score = parseAnySource(v3Source()).derived.score
57
+ it('점수 칸은 v3ScoreOf 다 — 저작면의 성숙도 탭과 같은 수(수석 아키텍트 판정 09-24)', () => {
58
+ const score = parseSource(v3Source()).derived.score
94
59
  expect(score).toBe(v3ScoreOf(v3Asset())!.score)
95
60
  expect(score).toBeGreaterThan(0)
96
61
  })
97
62
 
98
- it('형식을 어긴 V3 자산은 거절한다', () => {
63
+ it('형식을 어긴 자산은 거절한다', () => {
99
64
  const broken = JSON.parse(v3Source())
100
65
  broken.appearance = []
101
- expect(() => parseAnySource(JSON.stringify(broken))).toThrow(FigureSourceError)
66
+ expect(() => parseSource(JSON.stringify(broken))).toThrow(FigureSourceError)
102
67
  })
103
68
  })
104
69
 
105
- describe('발행 판정 — 두 판 다 지난다', () => {
70
+ describe('발행 판정', () => {
106
71
  it('제 부피 안에 든 V3 자산은 막지 않는다', () => {
107
- const inspection = inspectAnySource(v3Source())
72
+ const inspection = inspectSource(v3Source())
108
73
  expect(inspection.blocked).toBe(false)
109
74
  })
110
75
 
111
76
  it('선언한 부피 밖으로 나간 부품은 이름과 함께 막는다', () => {
112
77
  let asset = v3Asset()
113
78
  asset = addV3Box(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
114
- const inspection = inspectAnySource(serializeV3Asset(asset))
79
+ const inspection = inspectSource(serializeV3Asset(asset))
115
80
  expect(inspection.blocked).toBe(true)
116
81
  expect(inspection.findings.some(f => f.code === 'part-outside-occupancy' && f.at === 'arm')).toBe(true)
117
82
  })
@@ -119,7 +84,7 @@ describe('figure-format — 판을 알아보는 문', () => {
119
84
  it('부피를 선언하지 않은 V3 자산은 막는다', () => {
120
85
  let asset = createV3Asset('Bare')
121
86
  asset = addV3Box(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
122
- const inspection = inspectAnySource(serializeV3Asset(asset))
87
+ const inspection = inspectSource(serializeV3Asset(asset))
123
88
  expect(inspection.blocked).toBe(true)
124
89
  expect(inspection.findings.some(f => f.code === 'occupancy-undeclared')).toBe(true)
125
90
  })
@@ -149,7 +114,7 @@ describe('figure-format — 판을 알아보는 문', () => {
149
114
  placement: 'floor'
150
115
  })
151
116
 
152
- const inspection = inspectAnySource(serializeV3Asset(asset))
117
+ const inspection = inspectSource(serializeV3Asset(asset))
153
118
  const sampled = inspection.findings.find(f => f.code === 'state-range-sampled')
154
119
  expect(sampled).toBeDefined()
155
120
  expect(sampled!.blocking).toBe(false)
@@ -159,11 +124,11 @@ describe('figure-format — 판을 알아보는 문', () => {
159
124
  })
160
125
 
161
126
  it('정본이 없으면 전과 같이 그 말로 막는다', () => {
162
- expect(inspectAnySource(null).findings[0].code).toBe('no-source')
127
+ expect(inspectSource(null).findings[0].code).toBe('no-source')
163
128
  })
164
129
 
165
130
  it('바뀐 것 없는 V3 재발행은 아무 말도 없이 지난다', () => {
166
- const again = inspectAnySource(v3Source(), v3Source())
131
+ const again = inspectSource(v3Source(), v3Source())
167
132
  expect(again.blocked).toBe(false)
168
133
  expect(again.findings.filter(f => f.code !== 'state-range-sampled')).toEqual([])
169
134
  })
@@ -171,7 +136,7 @@ describe('figure-format — 판을 알아보는 문', () => {
171
136
  it('놓인 인스턴스가 기대던 것을 없앤 V3 재발행은 막는다 — 없어진 능력을 이름으로 댄다', () => {
172
137
  const before = v3Asset()
173
138
  before.document.capabilities.names = ['Holdable']
174
- const inspection = inspectAnySource(v3Source(), serializeV3Asset(before))
139
+ const inspection = inspectSource(v3Source(), serializeV3Asset(before))
175
140
  const gone = inspection.findings.find(f => f.code === 'capability-gone')
176
141
  expect(gone).toBeDefined()
177
142
  expect(gone!.blocking).toBe(true)
@@ -180,24 +145,11 @@ describe('figure-format — 판을 알아보는 문', () => {
180
145
  expect(gone!.how).toBeTruthy()
181
146
  expect(inspection.blocked).toBe(true)
182
147
  })
183
-
184
- it('직전 발행판이 V2 면 견주지 못했다고 말한다 — 막지는 않는다', () => {
185
- /* 전환한 도형의 직전 발행판은 모두 V2 다. 두 판을 견주는 길은 없으니 그 사실을 적는다. */
186
- const converted = inspectAnySource(v3Source(), v2Source())
187
- const warning = converted.findings.find(f => f.code === 'released-unreadable')
188
- expect(warning).toBeDefined()
189
- expect(warning!.blocking).toBe(false)
190
- expect(converted.blocked).toBe(false)
191
-
192
- /* 첫 발행에는 견줄 것이 없으니 그 말도 없다. */
193
- expect(inspectAnySource(v3Source()).findings.some(f => f.code === 'released-unreadable')).toBe(false)
194
- })
195
148
  })
196
149
 
197
150
  /*
198
- ADR-0087 5단계에서 V3 판정이 저작자가 보는 유일한 발행 판정이 된다. 그때까지 이 판정은 발행 패널에 영어 문장
199
- (`detail`)을 그대로 냈고 왜 · 어떻게가 없었다 — 사용자가 2026-09-19 에 「가장 큰 문제」로 짚고 V2 에서 고친 바로
200
- 그 모양이다. 문장은 요청의 언어를 아는 서버가 코드와 값에서 만든다(ADR-0054).
151
+ 이 판정은 한때 발행 패널에 영어 문장(`detail`)을 그대로 냈고 왜 · 어떻게가 없었다 — 사용자가 2026-09-19 에
152
+ 「가장 큰 문제」로 짚은 모양이다. 문장은 요청의 언어를 아는 서버가 코드와 값에서 만든다(ADR-0054).
201
153
  */
202
154
  describe('V3 판정의 문장 — 발행 패널의 말로', () => {
203
155
  const KO = require('../../../translations/ko.json') as Record<string, string>
@@ -223,7 +175,7 @@ describe('figure-format — 판을 알아보는 문', () => {
223
175
  }
224
176
 
225
177
  it('넘은 부품은 이름 · 축 · 넘은 양을 화면 말투로 말하고, 왜와 어떻게를 함께 낸다', () => {
226
- const finding = inspectAnySource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
178
+ const finding = inspectSource(outside(), null, translatorOf(KO)).findings.find(f => f.code === 'part-outside-occupancy')!
227
179
 
228
180
  expect(finding.message).toMatch(/^'arm' 부품이 공간 선언을 X 로 [\d.]+mm 넘습니다\.$/)
229
181
  expect(finding.why).toMatch(/니다\.$/)
@@ -234,7 +186,7 @@ describe('figure-format — 판을 알아보는 문', () => {
234
186
  })
235
187
 
236
188
  it('공간을 선언하지 않은 도형은 무엇을 누르면 되는지 말한다', () => {
237
- const finding = inspectAnySource(bare(), null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
189
+ const finding = inspectSource(bare(), null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
238
190
 
239
191
  expect(finding.message).toBe('이 도형은 차지하는 공간을 아직 선언하지 않았습니다.')
240
192
  expect(finding.how).toContain('부품에 맞추기')
@@ -242,14 +194,14 @@ describe('figure-format — 판을 알아보는 문', () => {
242
194
 
243
195
  it('부품이 없는 새 도형에는 도형을 먼저 추가하라고 한다 — 화면은 그때 맞추기 단추를 내지 않는다', () => {
244
196
  const empty = serializeV3Asset(createV3Asset('Empty'))
245
- const finding = inspectAnySource(empty, null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
197
+ const finding = inspectSource(empty, null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
246
198
 
247
199
  expect(finding.how).toBe('먼저 도형을 추가한 뒤, 「부품에 맞추기」로 공간을 선언하세요.')
248
- expect(inspectAnySource(empty).findings.find(f => f.code === 'occupancy-undeclared')!.how).toMatch(/^Add a shape first/)
200
+ expect(inspectSource(empty).findings.find(f => f.code === 'occupancy-undeclared')!.how).toMatch(/^Add a shape first/)
249
201
  })
250
202
 
251
203
  /*
252
- V2 의 sizing-splits 가 V3 판정에 온 것(2026-09-24). 드라이룸 램프처럼 — 붙어 있던 부품이 도형을 키우면 떨어진다.
204
+ (2026-09-24) 드라이룸 램프처럼 — 붙어 있던 부품이 도형을 키우면 떨어진다.
253
205
  여기서는 손으로 만든다: 램프의 높이가 도형 높이(size.y)를 따라 올라가고, 몸통은 그대로다.
254
206
  */
255
207
  const splitting = () => {
@@ -266,7 +218,7 @@ describe('figure-format — 판을 알아보는 문', () => {
266
218
  }
267
219
 
268
220
  it('크기를 바꾸면 떨어지는 부품을 막고, 어느 부품이 어느 축에서인지 · 왜 · 어떻게를 말한다', () => {
269
- const finding = inspectAnySource(splitting(), null, translatorOf(KO)).findings.find(f => f.code === 'parts-split-on-resize')
221
+ const finding = inspectSource(splitting(), null, translatorOf(KO)).findings.find(f => f.code === 'parts-split-on-resize')
270
222
 
271
223
  expect(finding?.blocking).toBe(true)
272
224
  expect(finding!.message).toBe("크기를 Y 로 2배로 하면 'lamp' 부품이 나머지에서 떨어집니다.")
@@ -285,7 +237,7 @@ describe('figure-format — 판을 알아보는 문', () => {
285
237
  asset.designInputs[`size.${axis}`] = 100
286
238
  }
287
239
  asset = applyV3Authoring(asset, { kind: 'scale-uniformly' } as never)
288
- const finding = inspectAnySource(serializeV3Asset(asset), null, translatorOf(KO)).findings.find(f => f.code === 'sizing-not-uniform')
240
+ const finding = inspectSource(serializeV3Asset(asset), null, translatorOf(KO)).findings.find(f => f.code === 'sizing-not-uniform')
289
241
 
290
242
  expect(finding?.blocking).toBe(true)
291
243
  expect(finding!.message).toBe("이 도형은 한 비율로 커지는데, 크기를 X 로 2배로 하면 'body' 부품은 같이 커지지 않습니다.")
@@ -303,7 +255,7 @@ describe('figure-format — 판을 알아보는 문', () => {
303
255
  interpolation: { prefix: '{', suffix: '}' },
304
256
  keySeparator: false
305
257
  })
306
- const finding = inspectAnySource(splitting(), null, i18next.t.bind(i18next)).findings.find(f => f.code === 'parts-split-on-resize')!
258
+ const finding = inspectSource(splitting(), null, i18next.t.bind(i18next)).findings.find(f => f.code === 'parts-split-on-resize')!
307
259
 
308
260
  expect(finding.message).toBe("크기를 Y 로 2배로 하면 'lamp' 부품이 나머지에서 떨어집니다.")
309
261
  expect(`${finding.message}${finding.how}`).not.toContain('&#39;')
@@ -318,36 +270,36 @@ describe('figure-format — 판을 알아보는 문', () => {
318
270
  })
319
271
 
320
272
  it('번역이 없으면 영어로 답한다 — 키를 찍지 않는다', () => {
321
- const finding = inspectAnySource(outside()).findings.find(f => f.code === 'part-outside-occupancy')!
273
+ const finding = inspectSource(outside()).findings.find(f => f.code === 'part-outside-occupancy')!
322
274
 
323
275
  expect(finding.message).toMatch(/^'arm' goes [\d.]+ mm past the declared space on X\.$/)
324
276
  expect(finding.message).not.toContain('figure.gate.')
325
277
  })
326
278
 
327
279
  /*
328
- V3 자산에는 type 이 없어 V2 의 임시 이름 검사가 돌지 않았고, 새 V3 도형이 `FIGURE_<uuid>` 로 발행됐다
329
- (:3300, 2026-09-24). 발행하면 이름이 굳으므로 행의 type 으로 같은 거절을 한다.
280
+ 자산에는 type 이 없어 임시 이름 검사가 돌지 않았고, 새 도형이 `FIGURE_<uuid>` 로 발행됐다(:3300, 2026-09-24).
281
+ 발행하면 이름이 굳으므로 행의 type 으로 거절한다.
330
282
  */
331
- it('임시 이름인 V3 도형은 발행을 막는다 — 이름은 행에 있다', () => {
332
- /* The name a new V3 figure gets — the format's rule, the one the gate recognises. */
283
+ it('임시 이름인 도형은 발행을 막는다 — 이름은 행에 있다', () => {
284
+ /* The name a new figure gets — the format's rule, the one the gate recognises. */
333
285
  const placeholder = placeholderType()
334
- const finding = inspectAnySource(v3Source(), null, translatorOf(KO), placeholder).findings.find(f => f.code === 'type-is-placeholder')
286
+ const finding = inspectSource(v3Source(), null, translatorOf(KO), placeholder).findings.find(f => f.code === 'type-is-placeholder')
335
287
 
336
288
  expect(finding?.blocking).toBe(true)
337
289
  expect(finding!.message).toBe(`'${placeholder}' 타입은 처음 저장할 때 붙은 임시 타입입니다.`)
338
290
  /* 화면의 칸 이름은 「타입」이다(`figure.label.figure-type`). */
339
291
  expect(finding!.how).toContain('타입 칸')
340
- expect(inspectAnySource(v3Source(), null, translatorOf(KO), 'ROLLER_CONVEYOR').blocked).toBe(false)
292
+ expect(inspectSource(v3Source(), null, translatorOf(KO), 'ROLLER_CONVEYOR').blocked).toBe(false)
341
293
  })
342
294
 
343
295
  /* 코드가 묻는 키가 두 언어에 다 있어야 한다 — 없으면 그 언어의 화면에 영어가 섞인다. */
344
296
  it('판정이 묻는 키는 ko · en 에 다 있다', () => {
345
297
  const asked: string[] = []
346
298
  const t = translatorOf(KO, asked)
347
- inspectAnySource(outside(), null, t)
348
- inspectAnySource(bare(), null, t)
349
- inspectAnySource(v3Source(), v3Source(), t)
350
- inspectAnySource(v3Source(), null, t, 'FIGURE_ABCDEF12')
299
+ inspectSource(outside(), null, t)
300
+ inspectSource(bare(), null, t)
301
+ inspectSource(v3Source(), v3Source(), t)
302
+ inspectSource(v3Source(), null, t, 'FIGURE_ABCDEF12')
351
303
 
352
304
  const keys = [...new Set(asked)]
353
305
  expect(keys.length).toBeGreaterThan(5)
@@ -356,21 +308,13 @@ describe('figure-format — 판을 알아보는 문', () => {
356
308
  })
357
309
  })
358
310
 
359
- describe('소비처 — 판마다 받을 것이 다르다', () => {
360
- it('V2 는 전과 같이 청사진을 받는다', () => {
361
- const payload = consumablePayloadOf(v2Source())
362
- expect(payload.format).toBe('v2')
363
- expect((payload.blueprint as { groups?: unknown[] }).groups!.length).toBeGreaterThan(0)
364
- expect((payload.blueprint as Record<string, unknown>).format).toBeUndefined()
365
- })
366
-
367
- it('V3 는 자산을 그대로 받는다 — 그리는 쪽이 커널로 컴파일한다', () => {
311
+ describe('소비처', () => {
312
+ it('자산을 그대로 받는다 — 그리는 쪽이 커널로 컴파일한다', () => {
368
313
  const payload = consumablePayloadOf(v3Source())
369
- expect(payload.format).toBe('v3')
370
- expect((payload.asset as V3Asset).version).toBe('figure-v3-asset-1')
314
+ expect(payload.asset.version).toBe('figure-v3-asset-1')
371
315
  })
372
316
 
373
- it('컴파일되지 않는 V3 자산은 소비처에 넘기지 않고 던진다', () => {
317
+ it('컴파일되지 않는 자산은 소비처에 넘기지 않고 던진다', () => {
374
318
  const broken = JSON.parse(v3Source())
375
319
  broken.designInputs = {}
376
320
  expect(() => consumablePayloadOf(JSON.stringify(broken))).toThrow()