@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.
- package/README.md +5 -8
- package/dist-server/service/figure/figure-format.d.ts +49 -31
- package/dist-server/service/figure/figure-format.js +47 -90
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/service/figure/figure-history.js +1 -1
- package/dist-server/service/figure/figure-history.js.map +1 -1
- package/dist-server/service/figure/figure-mutation.js +5 -5
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-propose-v3.d.ts +1 -1
- package/dist-server/service/figure/figure-propose-v3.js +3 -1
- package/dist-server/service/figure/figure-propose-v3.js.map +1 -1
- package/dist-server/service/figure/figure-query.d.ts +6 -11
- package/dist-server/service/figure/figure-query.js +12 -40
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/service/figure/figure-rename.js +2 -11
- package/dist-server/service/figure/figure-rename.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +32 -112
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/service/figure/figure-type.d.ts +0 -17
- package/dist-server/service/figure/figure-type.js +4 -71
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/service/figure/figure-v3-write.d.ts +1 -1
- package/dist-server/service/figure/figure-v3-write.js +2 -3
- package/dist-server/service/figure/figure-v3-write.js.map +1 -1
- package/dist-server/service/figure/figure.d.ts +5 -10
- package/dist-server/service/figure/figure.js +6 -11
- package/dist-server/service/figure/figure.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/index.js +1 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/figure/figure-blueprint-query.test.ts +29 -48
- package/server/service/figure/figure-format.test.ts +54 -110
- package/server/service/figure/figure-format.ts +77 -87
- package/server/service/figure/figure-history.ts +1 -1
- package/server/service/figure/figure-inspect-db.test.ts +1 -11
- package/server/service/figure/figure-mutation.ts +7 -7
- package/server/service/figure/figure-propose-v3.test.ts +6 -2
- package/server/service/figure/figure-propose-v3.ts +3 -1
- package/server/service/figure/figure-query.ts +14 -36
- package/server/service/figure/figure-rename-db.test.ts +1 -1
- package/server/service/figure/figure-rename.test.ts +4 -18
- package/server/service/figure/figure-rename.ts +2 -12
- package/server/service/figure/figure-tools.test.ts +17 -80
- package/server/service/figure/figure-tools.ts +30 -125
- package/server/service/figure/figure-type.ts +3 -52
- package/server/service/figure/figure-v3-write.ts +3 -4
- package/server/service/figure/figure.ts +7 -12
- package/server/service/index.ts +1 -1
- package/dist-server/service/figure/figure-gate.d.ts +0 -93
- package/dist-server/service/figure/figure-gate.js +0 -302
- package/dist-server/service/figure/figure-gate.js.map +0 -1
- package/dist-server/service/figure/figure-source.d.ts +0 -45
- package/dist-server/service/figure/figure-source.js +0 -59
- package/dist-server/service/figure/figure-source.js.map +0 -1
- package/server/service/figure/figure-gate.test.ts +0 -155
- package/server/service/figure/figure-gate.ts +0 -392
- package/server/service/figure/figure-source.test.ts +0 -149
- 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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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('
|
|
401
|
-
const out = await toolNamed('proposeFigureCandidate').builder({ prompt: '고쳐줘' }, ctx({ figureId: 'fig-
|
|
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
|
-
|
|
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('
|
|
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('형식을 어긴
|
|
431
|
+
it('형식을 어긴 초안은 저장 문의 말로 거절한다', async () => {
|
|
494
432
|
const broken = { ...v3Draft(), appearance: [] }
|
|
495
|
-
await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
*
|
|
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,
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
(
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
*
|
|
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
|
|
156
|
-
*
|
|
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
|
|
177
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
323
|
-
|
|
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:
|
|
349
|
-
score:
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
-
* ## 무엇을 담나 —
|
|
22
|
+
* ## 무엇을 담나 — 도형 자산이다, 씬 모델이 아니다
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* 자리가 형식에 없어 따로 관리해야 한다.
|
|
24
|
+
* 형식은 `@hatiolab/figure-model` 이 정한다(`figure-v3-asset-1`). 씬 컴포넌트 트리를 담지 않는다.
|
|
25
|
+
* 컴파일한 것도 저장하지 않는다 — 원본에서 파생되므로 소비하는 쪽이 커널로 컴파일한다. 저장하면 원본과 갈린다.
|
|
27
26
|
*
|
|
28
|
-
*
|
|
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 —
|
|
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
|
-
* `
|
|
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
|
|
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
|
// ── 파생값 — 저장하는 이유가 하나뿐이다 ──────────────────────────────────
|
package/server/service/index.ts
CHANGED
|
@@ -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;
|