@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
@@ -33,20 +33,13 @@ function stubRepository(rows: any[]) {
33
33
  const MINE = { id: 'domain-mine' }
34
34
  const THEIRS = { id: 'domain-theirs' }
35
35
 
36
- const SOURCE = {
37
- version: 2,
38
- type: 'MIXER',
39
- base: { x: 300, y: 500, z: 300 },
40
- detailLevel: 'M',
41
- parts: [
42
- {
43
- name: 'body',
44
- primitive: 'cube',
45
- transform: { position: { x: 0, y: 250, z: 0 }, size: { x: 300, y: 400, z: 300 } },
46
- material: { token: 'palette.primary' }
47
- }
48
- ]
49
- }
36
+ /** A stored figure: one body on the floor, its room declared. */
37
+ const SOURCE = (() => {
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' })
41
+ return asset
42
+ })()
50
43
 
51
44
  const ROWS = [
52
45
  { id: 'fig-1', type: 'MIXER', name: '믹서', state: 'draft', domain: MINE, source: JSON.stringify(SOURCE) },
@@ -74,12 +67,7 @@ function install() {
74
67
  attempts: 1
75
68
  }
76
69
  },
77
- compile: model.compile,
78
- costOf: model.costOf,
79
- scoreOf: model.scoreOf,
80
- validate: model.validate,
81
- isPlaceholderType: model.isPlaceholderType,
82
- parseAnySource: require('./figure-format').parseAnySource,
70
+ parseSource: require('./figure-format').parseSource,
83
71
  v3CostOf: model.v3CostOf,
84
72
  v3ScoreOf: model.v3ScoreOf
85
73
  })
@@ -251,7 +239,7 @@ describe('대화면 문맥에서 대상을 해소한다', () => {
251
239
  const result = await toolNamed('getFigureSource').builder({}, ctx())
252
240
 
253
241
  expect(result.id).toBe('fig-1')
254
- expect(result.source.type).toBe('MIXER')
242
+ expect(result.source.name).toBe('Mixer')
255
243
  })
256
244
 
257
245
  it.each(['getFigureSource', 'inspectFigure'])('%s: 목록의 대상 누락은 채팅을 끊지 않고 다음 동작을 안내한다', async name => {
@@ -267,7 +255,7 @@ describe('대화면 문맥에서 대상을 해소한다', () => {
267
255
  const target = catalog.figures.find((figure: any) => figure.name === '믹서')
268
256
  const result = await toolNamed('getFigureSource').builder({ figureId: target.id }, ctx({}))
269
257
  expect(result.id).toBe('fig-1')
270
- expect(result.source.type).toBe('MIXER')
258
+ expect(result.source.name).toBe('Mixer')
271
259
  })
272
260
 
273
261
  it('정본이 없는 도형은 없다고 말한다 — 지어내지 않는다', async () => {
@@ -281,56 +269,7 @@ describe('대화면 문맥에서 대상을 해소한다', () => {
281
269
  const result = await toolNamed('getFigureSource').builder({ figureId: 'fig-1' }, ctx())
282
270
 
283
271
  expect(result.cost.triangles).toBeGreaterThan(0)
284
- expect(result.cost.drawCalls).toBeGreaterThan(0)
285
- })
286
-
287
- it('저장 전 초안이 있으면 그것을 읽고, 초안이라는 사실도 함께 낸다', async () => {
288
- const draft = {
289
- ...SOURCE,
290
- parts: [
291
- ...SOURCE.parts,
292
- {
293
- name: 'lid',
294
- primitive: 'cube',
295
- transform: { position: { x: 0, y: 500, z: 0 }, size: { x: 100, y: 20, z: 100 } },
296
- material: { token: 'palette.primary' }
297
- }
298
- ]
299
- }
300
- const result = await toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', palette: ['palette.primary'], draftSource: draft }))
301
-
302
- expect(result.draft).toBe(true)
303
- expect(result.source.parts).toHaveLength(2)
304
- })
305
-
306
- it('다른 type의 초안은 거절한다 — 브라우저 문맥으로 저장 식별자를 바꾸지 않는다', async () => {
307
- await expect(
308
- toolNamed('getFigureSource').builder(
309
- {},
310
- ctx({ figureId: 'fig-1', palette: ['palette.primary'], draftSource: { ...SOURCE, type: 'FORKLIFT' } })
311
- )
312
- ).rejects.toThrow(/does not match Figure/)
313
- })
314
-
315
- it('재현: 기존 도형 편집 중 미저장 초안의 type이 FIGURE여도 현재 도형 초안으로 허용해야 한다', async () => {
316
- // DB의 row는 MIXER(또는 OHT)이지만, 캔버스/도크의 초안 type은 일반 'FIGURE'일 수 있음
317
- const result = await toolNamed('getFigureSource').builder(
318
- {},
319
- ctx({ figureId: 'fig-1', palette: ['palette.primary'], draftSource: { ...SOURCE, type: 'FIGURE' } })
320
- )
321
- expect(result.draft).toBe(true)
322
- expect(result.type).toBe('MIXER')
323
- })
324
-
325
- it('재현: LLM이 새 미저장 모델의 임시 타입(FIGURE_MTZO5R10)을 figureId로 전달해도 초안을 읽어야 한다', async () => {
326
- const tempDraft = { ...SOURCE, type: 'FIGURE_MTZO5R10' }
327
- const host = { editorMode: 'new', draftSource: tempDraft, palette: ['palette.primary'] }
328
- const result = await toolNamed('getFigureSource').builder(
329
- { figureId: 'FIGURE_MTZO5R10' },
330
- ctx(host)
331
- )
332
- expect(result.draft).toBe(true)
333
- expect(result.type).toBe('FIGURE_MTZO5R10')
272
+ expect(result.cost.groups).toBeGreaterThan(0)
334
273
  })
335
274
  })
336
275
 
@@ -397,8 +336,8 @@ describe('제안을 요청할 때', () => {
397
336
  expect(proposed).toHaveLength(0)
398
337
  })
399
338
 
400
- it('V3 가 아닌 원본은 고치지 않는다 — V2 를 제안기에 건네지 않는다', async () => {
401
- const out = await toolNamed('proposeFigureCandidate').builder({ prompt: '고쳐줘' }, ctx({ figureId: 'fig-1', context: 'c', revision: 1 }))
339
+ it('원본이 없는 도형은 고치지 않는다 — 빈 것을 제안기에 건네지 않는다', async () => {
340
+ const out = await toolNamed('proposeFigureCandidate').builder({ prompt: '고쳐줘' }, ctx({ figureId: 'fig-blank', context: 'c', revision: 1 }))
402
341
  expect(out.error).toBe('FIGURE_NOT_OPEN')
403
342
  expect(proposed).toHaveLength(0)
404
343
  })
@@ -439,7 +378,7 @@ describe('제안을 요청할 때', () => {
439
378
  __setFigureToolDepsForTest({
440
379
  getRepository: () => stubRepository(ROWS),
441
380
  Figure: class {},
442
- parseAnySource: require('./figure-format').parseAnySource,
381
+ parseSource: require('./figure-format').parseSource,
443
382
  proposeFigureV3: async () => {
444
383
  throw new ProposeV3Failure('만들지 못했습니다.', ['ATTACH_FACE: steps[4]: attach: faces along x'], 3)
445
384
  },
@@ -480,19 +419,17 @@ describe('제안이 카드가 된다', () => {
480
419
  })
481
420
  })
482
421
 
483
- describe('V3 도형이 열려 있을 때', () => {
422
+ describe('도형이 모델러에 열려 있을 때', () => {
484
423
  it('저장 전 V3 초안을 V3 로 읽고, V3 셈으로 잰다', async () => {
485
424
  const draft = v3Draft()
486
425
  const got = await toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: draft, palette: ['palette.primary'] }))
487
- expect(got.format).toBe('v3')
488
426
  expect(got.draft).toBe(true)
489
427
  expect(got.cost.triangles).toBe(12)
490
428
  expect(got.score.grade).toBe('A')
491
429
  })
492
430
 
493
- it('형식을 어긴 V3 초안은 V3 의 말로 거절한다 — V2 파서의 말이 아니다', async () => {
431
+ it('형식을 어긴 초안은 저장 문의 말로 거절한다', async () => {
494
432
  const broken = { ...v3Draft(), appearance: [] }
495
- await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken, palette: ['palette.primary'] }))).rejects.toThrow(/V3 draft/)
496
- await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken, palette: ['palette.primary'] }))).rejects.not.toThrow(/base 가 없다/)
433
+ await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken }))).rejects.toThrow(/unsaved draft is invalid/)
497
434
  })
498
435
  })
@@ -57,19 +57,12 @@ function getDeps(): any {
57
57
  _deps = {
58
58
  getRepository: shell.getRepository,
59
59
  Figure: entity.Figure,
60
- /*
61
- The format-aware door, so an AI asked about a V3 model gets the V3 judgement rather than a V2 parser's
62
- complaint. The tool's own contract is unchanged: it reads, it never saves (ADR-0087).
63
- */
64
- inspectSource: format.inspectAnySource,
65
- parseAnySource: format.parseAnySource,
60
+ /* The same doors a save and a release go through. The tool reads; it never saves. */
61
+ inspectSource: format.inspectSource,
62
+ parseSource: format.parseSource,
66
63
  v3CostOf: model.v3CostOf,
67
64
  v3ScoreOf: model.v3ScoreOf,
68
65
  proposeFigureV3: proposeV3.proposeFigureV3,
69
- compile: model.compile,
70
- costOf: model.costOf,
71
- scoreOf: model.scoreOf,
72
- validate: model.validate,
73
66
  isPlaceholderType: model.isPlaceholderType
74
67
  }
75
68
  }
@@ -108,55 +101,24 @@ async function figureInDomain(figureId: string, domainId: string): Promise<any>
108
101
  }
109
102
 
110
103
  /**
111
- * figureId 또는 hostContext로부터 대상 Figure row(또는 미저장 초안 가상 row)를 해소한다.
104
+ * The figure a tool is about: by id, or by its type name, in the caller's domain. None named: none.
112
105
  */
113
- async function resolveFigureTarget(figureId: string | undefined, domainId: string, host: any): Promise<any> {
114
- if (
115
- host?.editorMode === 'new' &&
116
- host?.draftSource &&
117
- (!figureId || figureId === host.draftSource.type || figureId.startsWith('FIGURE'))
118
- ) {
119
- return unsavedFigure(host)
120
- }
121
-
122
- if (figureId) {
123
- const { getRepository, Figure } = getDeps()
124
- let found = await getRepository(Figure).findOne({
125
- where: { id: figureId, domain: { id: domainId } }
126
- })
127
- if (!found) {
128
- found = await getRepository(Figure).findOne({
129
- where: { type: figureId, domain: { id: domainId } }
130
- })
131
- }
132
- if (!found) {
133
- if (
134
- host?.editorMode === 'new' ||
135
- (host?.draftSource &&
136
- (host.draftSource.type === figureId ||
137
- figureId.startsWith('FIGURE') ||
138
- !host.figureId ||
139
- host.figureId === figureId))
140
- ) {
141
- const unsaved = unsavedFigure(host)
142
- if (unsaved) return unsaved
143
- }
144
- throw new Error(`figure not found in this tenant: ${figureId}`)
145
- }
146
- return found
147
- }
148
-
149
- return unsavedFigure(host)
106
+ async function resolveFigureTarget(figureId: string | undefined, domainId: string, _host: any): Promise<any> {
107
+ if (!figureId) return undefined
108
+ const { getRepository, Figure } = getDeps()
109
+ const found =
110
+ (await getRepository(Figure).findOne({ where: { id: figureId, domain: { id: domainId } } })) ??
111
+ (await getRepository(Figure).findOne({ where: { type: figureId, domain: { id: domainId } } }))
112
+ if (!found) throw new Error(`figure not found in this tenant: ${figureId}`)
113
+ return found
150
114
  }
151
115
 
152
116
  /**
153
- * 모델러가 아직 저장하지 않은 source를 도구가 읽는 유일한 길.
117
+ * The source a tool reads: the draft on screen when the modeller has this figure open, else the stored one.
154
118
  *
155
- * host context는 브라우저가 보낸 값이므로 권한 근거가 될 수 없다. 먼저 `row`를 현재 도메인에서
156
- * 찾은 뒤에만 보며, Figure의 불변 type과 형식 검증을 다시 확인한다. 검증되지 않은 큰 객체를 LLM
157
- * 프롬프트로 옮기면 비용만 태우고, DB에 있던 Figure와 다른 것을 고친다고 잘못 말할 수 있다.
119
+ * The host context is the browser's, so it is no ground for access: the row is found in the domain first, and the
120
+ * draft is put through the same door a save goes through before anything reads it.
158
121
  */
159
- /** A V3 asset names its format in `version`; a V2 FigureSource carries a number there. */
160
122
  const isV3 = (source: any) => source?.version === 'figure-v3-asset-1'
161
123
 
162
124
  /**
@@ -173,51 +135,19 @@ function figureNotOpen() {
173
135
  }
174
136
 
175
137
  function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
176
- const isMatch = host?.figureId === row.id || (!row.id && host?.editorMode === 'new') || (!row.id && !host?.figureId)
177
- const draft = isMatch ? host?.draftSource : undefined
178
- if (!draft) {
179
- return { source: row.source ? JSON.parse(row.source) : null, draft: false }
180
- }
181
-
182
- if (typeof draft !== 'object' || Array.isArray(draft)) {
183
- throw new Error('the unsaved draft is not a FigureSource object')
184
- }
138
+ const draft = host?.figureId === row.id ? host?.draftSource : undefined
139
+ if (!draft) return { source: row.source ? JSON.parse(row.source) : null, draft: false }
185
140
 
141
+ if (typeof draft !== 'object' || Array.isArray(draft)) throw new Error('the unsaved draft is not a figure asset')
186
142
  const serialized = JSON.stringify(draft)
187
- if (serialized.length > 100_000) {
188
- throw new Error('the unsaved draft is too large to safely review')
143
+ if (serialized.length > 100_000) throw new Error('the unsaved draft is too large to safely review')
144
+ try {
145
+ getDeps().parseSource(serialized)
146
+ } catch (e: any) {
147
+ const findings = Array.isArray(e?.findings) ? e.findings.map((one: any) => one.message).join(' · ') : String(e?.message ?? e)
148
+ throw new Error(`the unsaved draft is invalid: ${findings}`)
189
149
  }
190
-
191
- /* A V3 draft is checked as V3 -- the same door a save goes through. It names no type; the row does. */
192
- if (isV3(draft)) {
193
- try {
194
- getDeps().parseAnySource(serialized)
195
- } catch (e: any) {
196
- const findings = Array.isArray(e?.findings) ? e.findings.map((one: any) => one.message).join(' · ') : String(e?.message ?? e)
197
- throw new Error(`the unsaved V3 draft is invalid: ${findings}`)
198
- }
199
- return { source: draft, draft: true }
200
- }
201
-
202
- const { validate, isPlaceholderType } = getDeps()
203
- const checked = validate(draft)
204
- if (checked.errors.length > 0) {
205
- throw new Error(`the unsaved draft is invalid: ${checked.errors.map((one: any) => one.message).join(' · ')}`)
206
- }
207
-
208
- let normalizedDraft = draft
209
- if (draft.type !== row.type) {
210
- // 템플릿 기본값 'FIGURE', 임시 draftType('FIGURE_...'), 또는 빈 값인 경우
211
- // 현재 열려 있는 Figure의 실제 type으로 보정/정규화한다.
212
- const isGenericOrTempType = !draft.type || draft.type === 'FIGURE' || isPlaceholderType(draft.type)
213
- if (isGenericOrTempType && row.type) {
214
- normalizedDraft = { ...draft, type: row.type }
215
- } else {
216
- throw new Error(`the unsaved draft type '${draft.type}' does not match Figure '${row.type}'`)
217
- }
218
- }
219
-
220
- return { source: normalizedDraft, draft: true }
150
+ return { source: draft, draft: true }
221
151
  }
222
152
 
223
153
  /**
@@ -244,14 +174,6 @@ function missingFigureTarget() {
244
174
  }
245
175
  }
246
176
 
247
- function unsavedFigure(host: any): any | undefined {
248
- if (host?.editorMode !== 'new' || !host?.draftSource) return undefined
249
- const row = { id: host.figureId, type: host.draftSource.type || 'FIGURE', name: '', state: 'draft' }
250
- // Reuse size, shape and vocabulary validation before trusting browser content.
251
- const current = sourceForTool(row, host)
252
- return { ...row, source: JSON.stringify(current.source) }
253
- }
254
-
255
177
  const FIGURE_ID_ARG = {
256
178
  figureId: {
257
179
  type: 'string',
@@ -303,7 +225,7 @@ function figureToolSpecs(): ToolSpec[] {
303
225
  * 그 갈래에는 닿는 문이 없지만 `doors: []` 로 적지 않는다. 그러면 저장된 도형을 읽는 권한 검사가 선언에서 사라진다. */
304
226
  doors: ['Query.figure'],
305
227
  description:
306
- 'Returns the canonical FigureSource of one figure, with its measured cost ' +
228
+ 'Returns the source (the figure asset) of one figure, with its measured cost ' +
307
229
  '(triangles, part groups, and how many of it hold 60fps on one screen). Call it before proposing a revision — ' +
308
230
  'revising without reading it means proposing against a guess.',
309
231
  schema: { type: 'object', properties: { ...FIGURE_ID_ARG } },
@@ -319,25 +241,8 @@ function figureToolSpecs(): ToolSpec[] {
319
241
  return { id: row.id, type: row.type, name: row.name, source: null, cost: null }
320
242
  }
321
243
 
322
- if (isV3(current.source)) {
323
- /* A V3 asset is measured as V3 -- triangles, draw groups, and its grade against one screen. */
324
- const { v3CostOf, v3ScoreOf } = getDeps()
325
- return {
326
- id: row.id,
327
- type: row.type,
328
- name: row.name,
329
- state: row.state,
330
- format: 'v3',
331
- source: current.source,
332
- draft: current.draft,
333
- cost: v3CostOf(current.source),
334
- score: v3ScoreOf(current.source) ?? null
335
- }
336
- }
337
-
338
- const { compile, costOf, scoreOf } = getDeps()
339
- const blueprint = compile(current.source)
340
-
244
+ /* Measured as the catalogue measures it: triangles, draw groups, and its grade against one screen. */
245
+ const { v3CostOf, v3ScoreOf } = getDeps()
341
246
  return {
342
247
  id: row.id,
343
248
  type: row.type,
@@ -345,8 +250,8 @@ function figureToolSpecs(): ToolSpec[] {
345
250
  state: row.state,
346
251
  source: current.source,
347
252
  draft: current.draft,
348
- cost: costOf(blueprint, 100),
349
- score: scoreOf(blueprint)
253
+ cost: v3CostOf(current.source),
254
+ score: v3ScoreOf(current.source) ?? null
350
255
  }
351
256
  }
352
257
  },
@@ -25,7 +25,7 @@ export class NewFigure {
25
25
  @Field(type => [String], { nullable: true, description: 'Free-form tags. A figure may carry several.' })
26
26
  tags?: string[]
27
27
 
28
- @Field({ description: 'The canonical FigureSource JSON.' })
28
+ @Field({ description: 'The figure asset JSON (figure-v3-asset-1).' })
29
29
  source: string
30
30
 
31
31
  @Field({ nullable: true, description: 'JSON definition of the master-data fields an instance carries.' })
@@ -70,7 +70,7 @@ export class FigurePatch {
70
70
  @Field(type => [String], { nullable: true, description: 'The new tags. An empty list clears them; omitting the field leaves them.' })
71
71
  tags?: string[]
72
72
 
73
- @Field({ nullable: true, description: 'The new FigureSource JSON.' })
73
+ @Field({ nullable: true, description: 'The new figure asset JSON (figure-v3-asset-1).' })
74
74
  source?: string
75
75
 
76
76
  @Field({ nullable: true, description: 'The new master-data field definition.' })
@@ -136,46 +136,9 @@ export class FigureSaveResult {
136
136
  * 것들이라 그 칸이 필요 없었다. 발행 판정은 막는 것과 알리는 것을 함께 내므로, 어느 쪽인지를 값이
137
137
  * 스스로 말해야 한다 — 코드를 보고 화면이 다시 분류하면 규칙이 두 벌이 된다.
138
138
  */
139
- /** A measured, one-anchor repair candidate for a release-gate sizing split. */
140
- @ObjectType({ description: 'A reversible anchor change measured to reduce a sizing split.' })
141
- export class FigureGateFix {
142
- @Field({ description: 'Sizing test that this candidate improves, for example y or x-y.' })
143
- findingWay: string
144
-
145
- @Field({ description: 'Stable FigureSource part name to change.' })
146
- part: string
147
-
148
- @Field({ description: 'Axis whose anchor will change.' })
149
- axis: string
150
-
151
- @Field({ nullable: true, description: 'Current explicit anchor, if one is present.' })
152
- from?: string
153
-
154
- @Field({ description: 'Suggested explicit anchor: min, center, max, or span.' })
155
- to: string
156
-
157
- @Field(type => Int, { description: 'Number of blocking sizing-split checks removed by this one change.' })
158
- resolved: number
159
-
160
- @Field({ description: 'True only when every blocking sizing check is clear after remeasurement.' })
161
- safe: boolean
162
- }
163
-
164
- @ObjectType({ description: 'A coordinated, fully remeasured set of anchor changes that clears release-blocking sizing splits.' })
165
- export class FigureGateFixPlan {
166
- @Field(type => [FigureGateFix], { description: 'The draft-only anchor changes to apply together.' })
167
- fixes: FigureGateFix[]
168
-
169
- @Field(type => Int, { description: 'Number of blocking sizing checks cleared by the complete plan.' })
170
- resolved: number
171
-
172
- @Field({ description: 'True only when the full plan was remeasured with no sizing split left.' })
173
- safe: boolean
174
- }
175
-
176
139
  @ObjectType({ description: 'One finding from the release gate.' })
177
140
  export class FigureGateFinding {
178
- @Field({ description: 'Stable code — figure-model validation codes, or sizing-splits / sizing-loose.' })
141
+ @Field({ description: 'Stable code of the gate finding, for example part-outside-occupancy or parts-split-on-resize.' })
179
142
  code: string
180
143
 
181
144
  @Field({ description: 'Human-readable message.' })
@@ -190,18 +153,6 @@ export class FigureGateFinding {
190
153
  @Field({ nullable: true, description: 'Where it points: part names that came apart, or a path in the source.' })
191
154
  at?: string
192
155
 
193
- @Field(type => [FigureGateFix], {
194
- nullable: true,
195
- description: 'Measured repair candidates. They are previews until the author explicitly applies one.'
196
- })
197
- fixes?: FigureGateFix[]
198
-
199
- @Field(type => FigureGateFixPlan, {
200
- nullable: true,
201
- description: 'A coordinated recommended repair, available only when the complete sizing suite clears.'
202
- })
203
- fixPlan?: FigureGateFixPlan
204
-
205
156
  @Field({ description: 'True when this finding blocks the release.' })
206
157
  blocking: boolean
207
158
  }
@@ -1,4 +1,4 @@
1
- import { parseV3Asset, serializeV3Asset, compileV3Asset, v3CostOf } from '@hatiolab/figure-model'
1
+ import { parseV3Asset, serializeV3Asset, compileV3Asset, v3CostOf, v3ScoreOf } from '@hatiolab/figure-model'
2
2
 
3
3
  export interface V3WriteCriteria {
4
4
  id: string
@@ -24,14 +24,13 @@ export function prepareV3Write(source: string, existing?: { source?: string }, e
24
24
  const asset = parseV3Asset(source)
25
25
  compileV3Asset(asset)
26
26
  /*
27
- The same measure `parseAnySource` uses, so the catalogue's budget columns do not depend on which mutation
28
- wrote the row. `score` stays null: it measures V2 reuse and nothing has measured V3 (ADR-0087).
27
+ The same measure `parseSource` uses, so the catalogue's columns do not depend on which mutation wrote the row.
29
28
  */
30
29
  const cost = v3CostOf(asset)
31
30
  return {
32
31
  source: serializeV3Asset(asset),
33
32
  name: asset.name,
34
- score: null,
33
+ score: v3ScoreOf(asset)?.score ?? null,
35
34
  triangles: cost.triangles,
36
35
  groups: cost.groups,
37
36
  state: 'draft' as const
@@ -19,17 +19,12 @@ import { Domain, longTextColumn } from '@things-factory/shell'
19
19
  *
20
20
  * `Board` 를 준용한다. 다른 점만 주석으로 적는다.
21
21
  *
22
- * ## 무엇을 담나 — `FigureSource` 다, 씬 모델이 아니다
22
+ * ## 무엇을 담나 — 도형 자산이다, 씬 모델이 아니다
23
23
  *
24
- * 정본 형식은 `@hatiolab/figure-model` 이 정한다(`FigureSource`). 씬 컴포넌트 트리를
25
- * 담지 않는다. 담으면 저작도구가 씬 편집기가 되고, 부품 이름·상태 슬롯·애니메이션
26
- * 자리가 형식에 없어 따로 관리해야 한다.
24
+ * 형식은 `@hatiolab/figure-model` 이 정한다(`figure-v3-asset-1`). 씬 컴포넌트 트리를 담지 않는다.
25
+ * 컴파일한 것도 저장하지 않는다 — 원본에서 파생되므로 소비하는 쪽이 커널로 컴파일한다. 저장하면 원본과 갈린다.
27
26
  *
28
- * 청사진(`FigureBlueprint`)은 **저장하지 않는다.** 정본에서 파생되는 캐시이므로
29
- * 소비하는 쪽이 `compile()` 로 만든다. 저장하면 정본과 갈린다.
30
- *
31
- * 그래서 `baseWidth`·`anchor`·`statusSlots`·`parts`·`animations` 같은 것을 컬럼으로
32
- * 두지 않는다 — 전부 `source` 안에 이미 있는 자리다. 두 벌이면 갈린다.
27
+ * 그래서 부품·움직임·자리 같은 것을 컬럼으로 두지 않는다 — 전부 `source` 안에 이미 있다. 두 벌이면 갈린다.
33
28
  *
34
29
  * 설계: `docs/design/figure-modules.md` §2
35
30
  */
@@ -39,7 +34,7 @@ import { Domain, longTextColumn } from '@things-factory/shell'
39
34
  where: '"deleted_at" IS NULL'
40
35
  })
41
36
  @Index('ix_figure_2', (figure: Figure) => [figure.domain, figure.category])
42
- @ObjectType({ description: 'An authored 3D component type — the canonical FigureSource plus its metadata.' })
37
+ @ObjectType({ description: 'An authored 3D component type — its figure asset plus its metadata.' })
43
38
  export class Figure {
44
39
  @PrimaryGeneratedColumn('uuid')
45
40
  @Field(type => ID, { nullable: true, description: 'Unique identifier for the figure.' })
@@ -119,13 +114,13 @@ export class Figure {
119
114
  state?: 'draft' | 'released'
120
115
 
121
116
  /**
122
- * `FigureSource` JSON.
117
+ * The figure asset JSON (`figure-v3-asset-1`).
123
118
  *
124
119
  * `Board.model` 자리이지만 이름을 바꿨다 — 「씬 모델이 아니다」를 필드 이름이
125
120
  * 드러내야 한다.
126
121
  */
127
122
  @Column({ nullable: true, ...longTextColumn() })
128
- @Field({ nullable: true, description: 'The canonical FigureSource JSON authored by the modeller.' })
123
+ @Field({ nullable: true, description: 'The figure asset JSON (figure-v3-asset-1) authored by the modeller.' })
129
124
  source?: string
130
125
 
131
126
  // ── 파생값 — 저장하는 이유가 하나뿐이다 ──────────────────────────────────
@@ -1,7 +1,7 @@
1
1
  // Entity types
2
2
  export * from './figure/figure.js'
3
3
  export * from './figure/figure-type.js'
4
- export * from './figure/figure-source.js'
4
+ export * from './figure/figure-format.js'
5
5
  export * from './figure/figure-history.js'
6
6
 
7
7
  // Entities, resolvers
@@ -1,93 +0,0 @@
1
- import type { Axis } from '@hatiolab/figure-model';
2
- /** Service transport shape. It deliberately contains only an explicit, reversible FigureSource edit. */
3
- export interface GateFix {
4
- findingWay: string;
5
- part: string;
6
- axis: Axis;
7
- from?: string;
8
- to: 'min' | 'center' | 'max' | 'span';
9
- resolved: number;
10
- /** True only when rechecking every sizing case leaves no release-blocking split. */
11
- safe: boolean;
12
- }
13
- /**
14
- * A coordinated repair, measured from the current source all the way through the sizing suite.
15
- * A split often involves several independently anchored parts, so no single anchor edit can be
16
- * called a solution. This remains a draft-only edit; it is never saved or released by the gate.
17
- */
18
- export interface GateFixPlan {
19
- fixes: GateFix[];
20
- resolved: number;
21
- /** True only after the complete plan leaves no release-blocking sizing split. */
22
- safe: boolean;
23
- }
24
- /**
25
- * 발행해도 되나 — **서버가 판정한다.**
26
- *
27
- * ## 왜 서버인가
28
- *
29
- * 이 검사는 브라우저 하네스에만 있었다(`test/browser/check-figure-scaling.mjs`). 사람이 돌릴 때만
30
- * 돌았고, 발행 경로에는 아무 검사도 없었다 — 형식만 맞으면 무엇이든 발행되었다. 발행은 소비처에게
31
- * 하는 약속이므로, 그 약속을 지킬 수 있는지는 약속하는 자리가 재야 한다.
32
- *
33
- * ## 무엇을 막고 무엇을 알리나
34
- *
35
- * 판정은 `figure-model` 의 `gate.ts` 가 이미 문서로 정해 두었다. 여기서 새로 정하지 않는다.
36
- *
37
- * sizing-splits 크기를 바꾸면 조각이 갈라진다 **막는다**
38
- * part-outside-base 부품이 기준 상자를 넘는다 **막는다**
39
- * parts-off-placement-face 기준면에 안 닿는다 **막는다**
40
- * sizing-loose 1배에서 이미 여러 조각이다 알린다
41
- * 그 밖의 violations 형식이 권하지 않는 것 알린다 (저장 때 이미 보여 준 것들)
42
- *
43
- * 갈라짐을 막는 이유는 사람 눈에 「부서졌다」로 보이기 때문이다 — 시그널타워를 3배로 세우니 램프가
44
- * 기둥 위 공중에 떴던 그것이고, 앵커 규칙이 생긴 이유다. 소비처가 형상을 늘려 쓰는 것은 막을 수
45
- * 없으니(그것이 이 형식의 값이다), 늘려도 견디는지를 발행 전에 본다.
46
- *
47
- * ## 몇 개가 걸리나는 여기 안 적는다
48
- *
49
- * 한동안 「표본 15개 중 갈라짐 1 · 조용함 14」를 적어 두고 **게이트가 벽이 아니라는 근거**로
50
- * 삼았다. 수는 굳고 카탈로그는 움직인다 — 기준 상자 판정이 붙자 그 비율이 하루 만에 뒤집혔다.
51
- * 굳은 수를 근거로 든 문장은 그때부터 사실이 아닌 채로 사람을 안심시킨다.
52
- *
53
- * 무엇이 걸리고 얼마나 넘쳤나는 **저작 도구가 재서 보여 준다.** 그것을 볼 사람은 고치는
54
- * 사람이고, 그 사람이 보는 자리는 여기가 아니다.
55
- */
56
- /** 게이트가 낸 한 가지. `blocking` 이 참이면 발행이 안 된다. */
57
- export interface GateFinding {
58
- code: string;
59
- message: string;
60
- /**
61
- * **왜 그것이 문제인가.** 잰 것만 보여 주면 읽는 사람이 「그래서?」에서 멈춘다 — 화면에
62
- * 「1 조각이 2 조각으로 갈라진다」만 떴을 때 실제로 그랬다.
63
- */
64
- why?: string;
65
- /** **무엇을 바꾸면 되나.** 부품 이름과 값으로. 형식이 만든 문장을 그대로 옮긴다. */
66
- how?: string;
67
- /** 어디를 가리키나 — 갈라짐은 갈라진 부품 이름들, 형식 위반은 정본 안의 경로. */
68
- at?: string;
69
- /**
70
- * 실제로 재 본, 되돌릴 수 있는 한 앵커 수정안들. 문장을 파싱해서 고치는 것이 아니라
71
- * FigureSource 에 적용할 정확한 값만 내보낸다. 화면은 이것을 후보로만 보여 주고 저작자가
72
- * 수락하기 전에는 정본을 바꾸지 않는다.
73
- */
74
- fixes?: GateFix[];
75
- /** The one-click, fully remeasured repair when individual candidates are insufficient. */
76
- fixPlan?: GateFixPlan;
77
- blocking: boolean;
78
- }
79
- export interface Inspection {
80
- findings: GateFinding[];
81
- /** 막는 것이 하나라도 있나. 화면이 이것으로 단추를 잠근다. */
82
- blocked: boolean;
83
- }
84
- /**
85
- * 정본 하나를 판정한다.
86
- *
87
- * 정본이 없거나(아직 저장 전) JSON 이 아니면 **막는다** — 발행할 대상이 없는 것이다. 그 경우를
88
- * 조용히 통과시키면 빈 것이 발행된다.
89
- *
90
- * `releasedJson` 은 **직전 발행판의 정본**이다. 주면 그것과 견주어 「이미 놓인 인스턴스를 깨는
91
- * 발행인가」를 함께 낸다(ADR-0063 결정 3). 한 번도 발행된 적 없으면 견줄 것이 없으므로 안 준다.
92
- */
93
- export declare function inspectSource(sourceJson?: string | null, releasedJson?: string | null): Inspection;