@things-factory/figure-service 10.1.75 → 10.1.79
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 +1 -1
- package/dist-server/service/figure/figure-format.d.ts +6 -6
- package/dist-server/service/figure/figure-format.js +32 -32
- 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.d.ts +1 -1
- package/dist-server/service/figure/figure-mutation.js +12 -12
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/{figure-propose-v3.d.ts → figure-propose-steps.d.ts} +16 -16
- package/dist-server/service/figure/{figure-propose-v3.js → figure-propose-steps.js} +27 -27
- package/dist-server/service/figure/figure-propose-steps.js.map +1 -0
- package/dist-server/service/figure/figure-propose-type.js +3 -3
- package/dist-server/service/figure/figure-propose-type.js.map +1 -1
- package/dist-server/service/figure/figure-query.d.ts +2 -2
- package/dist-server/service/figure/figure-query.js +5 -5
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +14 -14
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/service/figure/figure-type.js +2 -2
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/service/figure/{figure-v3-write.d.ts → figure-write.d.ts} +3 -3
- package/dist-server/service/figure/{figure-v3-write.js → figure-write.js} +10 -10
- package/dist-server/service/figure/figure-write.js.map +1 -0
- package/dist-server/service/figure/figure.d.ts +2 -2
- package/dist-server/service/figure/figure.js +2 -2
- package/dist-server/service/figure/figure.js.map +1 -1
- package/dist-server/service/figure-recipe/figure-recipe-templates.d.ts +2 -2
- package/dist-server/service/figure-recipe/figure-recipe-templates.js +1 -1
- package/dist-server/service/figure-recipe/figure-recipe-templates.js.map +1 -1
- package/dist-server/service/figure-recipe/figure-recipe-type.js +1 -1
- package/dist-server/service/figure-recipe/figure-recipe-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/recipes/_lib/model.mts +19 -19
- package/recipes/air-cylinder/models/default.json +1 -1
- package/recipes/air-shower/models/default.json +1 -1
- package/recipes/barrier-gate/models/default.json +1 -1
- package/recipes/base-frame/models/default.json +1 -1
- package/recipes/belt-surface/models/default.json +1 -1
- package/recipes/bin-row/models/default.json +1 -1
- package/recipes/bollard-row/models/default.json +1 -1
- package/recipes/buzzer/models/default.json +1 -1
- package/recipes/cabinet-doors/models/default.json +1 -1
- package/recipes/cabinet-doors/models/doors=1.json +1 -1
- package/recipes/cabinet-doors/models/doors=2.json +1 -1
- package/recipes/cable-tray/models/default.json +1 -1
- package/recipes/camera-mount/models/default.json +1 -1
- package/recipes/canopy/models/default.json +1 -1
- package/recipes/cart/models/default.json +1 -1
- package/recipes/cart-handle/models/default.json +1 -1
- package/recipes/casters-four/models/default.json +1 -1
- package/recipes/ceiling-rail-carriage/models/default.json +1 -1
- package/recipes/chain-rails/models/default.json +1 -1
- package/recipes/charging-station/models/default.json +1 -1
- package/recipes/column-row/models/default.json +1 -1
- package/recipes/control-cabinet/models/default.json +1 -1
- package/recipes/conveyor-skirts/models/default.json +1 -1
- package/recipes/counter-display/models/default.json +1 -1
- package/recipes/dock-bumpers/models/default.json +1 -1
- package/recipes/dock-leveler/models/default.json +1 -1
- package/recipes/double-hinged-door/models/default.json +1 -1
- package/recipes/drawer-stack/models/default.json +1 -1
- package/recipes/drawer-stack/models/drawers=2.json +1 -1
- package/recipes/drawer-stack/models/drawers=3.json +1 -1
- package/recipes/drawer-stack/models/drawers=4.json +1 -1
- package/recipes/drip-tray/models/default.json +1 -1
- package/recipes/duct-fan/models/default.json +1 -1
- package/recipes/emergency-stop/models/default.json +1 -1
- package/recipes/end-covers/models/default.json +1 -1
- package/recipes/ffu-row/models/default.json +1 -1
- package/recipes/fire-extinguisher/models/default.json +1 -1
- package/recipes/first-aid-box/models/default.json +1 -1
- package/recipes/floor-zone/models/default.json +1 -1
- package/recipes/folding-door/models/default.json +1 -1
- package/recipes/fork/models/default.json +1 -1
- package/recipes/gantry-frame/models/default.json +1 -1
- package/recipes/gripper-2f/models/default.json +1 -1
- package/recipes/guard-cover/models/default.json +1 -1
- package/recipes/handrail/models/default.json +1 -1
- package/recipes/hatch/models/default.json +1 -1
- package/recipes/hinged-door/models/default.json +1 -1
- package/recipes/hinged-door/models/hinge=left.json +1 -1
- package/recipes/hinged-door/models/hinge=right.json +1 -1
- package/recipes/hmi-monitor/models/default.json +1 -1
- package/recipes/hopper/models/default.json +1 -1
- package/recipes/interlock-switch/models/default.json +1 -1
- package/recipes/junction-box/models/default.json +1 -1
- package/recipes/ladder/models/default.json +1 -1
- package/recipes/legs-four/models/default.json +1 -1
- package/recipes/leveling-feet/models/default.json +1 -1
- package/recipes/lift-updown/models/default.json +1 -1
- package/recipes/light-curtain/models/default.json +1 -1
- package/recipes/limit-switch/models/default.json +1 -1
- package/recipes/linear-guide/models/default.json +1 -1
- package/recipes/locker-row/models/default.json +1 -1
- package/recipes/motor-box/models/default.json +1 -1
- package/recipes/observation-window/models/default.json +1 -1
- package/recipes/operator-panel/models/buttons=2.json +1 -1
- package/recipes/operator-panel/models/buttons=3.json +1 -1
- package/recipes/operator-panel/models/buttons=4.json +1 -1
- package/recipes/operator-panel/models/buttons=5.json +1 -1
- package/recipes/operator-panel/models/default.json +1 -1
- package/recipes/pallet/models/default.json +1 -1
- package/recipes/partition-wall/models/default.json +1 -1
- package/recipes/pass-box/models/default.json +1 -1
- package/recipes/peg-board/models/default.json +1 -1
- package/recipes/photo-sensor-pair/models/default.json +1 -1
- package/recipes/pipe-run/models/default.json +1 -1
- package/recipes/platform/models/default.json +1 -1
- package/recipes/power-box/models/default.json +1 -1
- package/recipes/pressure-gauge/models/default.json +1 -1
- package/recipes/pull-cord/models/default.json +1 -1
- package/recipes/pump/models/default.json +1 -1
- package/recipes/pusher/models/default.json +1 -1
- package/recipes/robot-plinth/models/default.json +1 -1
- package/recipes/roll-up-shutter/models/default.json +1 -1
- package/recipes/roller-row/models/default.json +1 -1
- package/recipes/room-shell/models/default.json +1 -1
- package/recipes/rotating-beacon/models/default.json +1 -1
- package/recipes/safety-fence/models/default.json +1 -1
- package/recipes/safety-mat/models/default.json +1 -1
- package/recipes/safety-sign-stand/models/default.json +1 -1
- package/recipes/scanner-mount/models/default.json +1 -1
- package/recipes/selector-switch/models/default.json +1 -1
- package/recipes/shelf-tiers/models/default.json +1 -1
- package/recipes/shelf-tiers/models/tiers=2.json +1 -1
- package/recipes/shelf-tiers/models/tiers=3.json +1 -1
- package/recipes/shelf-tiers/models/tiers=4.json +1 -1
- package/recipes/shelf-tiers/models/tiers=5.json +1 -1
- package/recipes/side-guides/models/default.json +1 -1
- package/recipes/sign-board/models/default.json +1 -1
- package/recipes/signal-tower-3/models/default.json +1 -1
- package/recipes/sliding-door/models/default.json +1 -1
- package/recipes/sliding-door/models/leaves=1,lamp=false.json +1 -1
- package/recipes/sliding-door/models/leaves=1,lamp=true.json +1 -1
- package/recipes/sliding-door/models/leaves=2,lamp=false.json +1 -1
- package/recipes/sliding-door/models/leaves=2,lamp=true.json +1 -1
- package/recipes/stairs/models/default.json +1 -1
- package/recipes/status-lamp/models/default.json +1 -1
- package/recipes/stopper/models/default.json +1 -1
- package/recipes/suction-pads/models/default.json +1 -1
- package/recipes/tank/models/default.json +1 -1
- package/recipes/telescopic-arm/models/default.json +1 -1
- package/recipes/toe-board/models/default.json +1 -1
- package/recipes/tote/models/default.json +1 -1
- package/recipes/traffic-cones/models/default.json +1 -1
- package/recipes/transfer-belts/models/default.json +1 -1
- package/recipes/tray-stack/models/default.json +1 -1
- package/recipes/tray-stack/models/trays=3.json +1 -1
- package/recipes/tray-stack/models/trays=4.json +1 -1
- package/recipes/tray-stack/models/trays=5.json +1 -1
- package/recipes/trough/models/default.json +1 -1
- package/recipes/turntable/models/default.json +1 -1
- package/recipes/valve-handle/models/default.json +1 -1
- package/recipes/waste-bin/models/default.json +1 -1
- package/recipes/work-light/models/default.json +1 -1
- package/recipes/workbench-top/models/default.json +1 -1
- package/scripts/build-templates.mts +7 -7
- package/scripts/templates.test.mts +16 -16
- package/server/service/figure/figure-blueprint-query.test.ts +3 -3
- package/server/service/figure/figure-format.test.ts +57 -57
- package/server/service/figure/figure-format.ts +39 -39
- package/server/service/figure/figure-history.ts +1 -1
- package/server/service/figure/figure-inspect-db.test.ts +14 -14
- package/server/service/figure/figure-llm-smoke.test.ts +7 -7
- package/server/service/figure/figure-mutation.ts +12 -12
- package/server/service/figure/{figure-propose-v3.test.ts → figure-propose-steps.test.ts} +18 -18
- package/server/service/figure/{figure-propose-v3.ts → figure-propose-steps.ts} +33 -33
- package/server/service/figure/figure-propose-type.ts +3 -3
- package/server/service/figure/figure-query.ts +5 -5
- package/server/service/figure/figure-rename-db.test.ts +5 -5
- package/server/service/figure/figure-rename.test.ts +7 -7
- package/server/service/figure/figure-tools.test.ts +22 -22
- package/server/service/figure/figure-tools.ts +14 -14
- package/server/service/figure/figure-type.ts +2 -2
- package/server/service/figure/{figure-v3-write.ts → figure-write.ts} +10 -10
- package/server/service/figure/figure.ts +3 -3
- package/server/service/figure-recipe/figure-recipe-templates.test.ts +2 -2
- package/server/service/figure-recipe/figure-recipe-templates.ts +4 -4
- package/server/service/figure-recipe/figure-recipe-type.ts +1 -1
- package/translations/en.json +2 -3
- package/translations/ko.json +2 -3
- package/dist-server/service/figure/figure-propose-v3.js.map +0 -1
- package/dist-server/service/figure/figure-v3-write.js.map +0 -1
|
@@ -6,24 +6,24 @@
|
|
|
6
6
|
* - a source that does not name the format is refused -- what cannot be identified is not stored;
|
|
7
7
|
* - the budget columns (`triangles`, `groups`) and the score are the kernel's.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
9
|
+
import { createFigureAsset, addFigureBox, addFigurePrimitive, serializeFigureAsset, applyFigureAuthoring, proposeFigureOccupancy, placeholderType, figureScoreOf } from '@hatiolab/figure-model'
|
|
10
|
+
import type { FigureAsset } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import { FigureSourceError, consumablePayloadOf, inspectSource, parseSource } from './figure-format'
|
|
13
13
|
|
|
14
|
-
/** 온전한
|
|
15
|
-
function
|
|
16
|
-
let asset =
|
|
17
|
-
asset =
|
|
18
|
-
asset =
|
|
19
|
-
asset =
|
|
14
|
+
/** 온전한 자산 하나: 바닥에 선 상자와, 부품에서 제안받아 확정한 점유 부피. */
|
|
15
|
+
function figureAsset(): FigureAsset {
|
|
16
|
+
let asset = createFigureAsset('Mixer')
|
|
17
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
18
|
+
asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'body', face: 'bottom', to: { plane: 'mounting-plane' } })
|
|
19
|
+
asset = applyFigureAuthoring(asset, {
|
|
20
20
|
kind: 'declare-occupancy',
|
|
21
|
-
proposal:
|
|
21
|
+
proposal: proposeFigureOccupancy(asset, { over: 'range' }),
|
|
22
22
|
placement: 'floor'
|
|
23
23
|
})
|
|
24
24
|
return asset
|
|
25
25
|
}
|
|
26
|
-
const
|
|
26
|
+
const figureSource = () => serializeFigureAsset(figureAsset())
|
|
27
27
|
|
|
28
28
|
describe('figure-format — 판을 알아보는 문', () => {
|
|
29
29
|
describe('판은 자산이 적는다', () => {
|
|
@@ -49,50 +49,50 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
49
49
|
|
|
50
50
|
describe('저장', () => {
|
|
51
51
|
it('예산 칸을 커널이 잰다 — 상자 하나는 삼각형 12, 묶음 1', () => {
|
|
52
|
-
const parsed = parseSource(
|
|
52
|
+
const parsed = parseSource(figureSource())
|
|
53
53
|
expect(parsed.derived.triangles).toBe(12)
|
|
54
54
|
expect(parsed.derived.groups).toBe(1)
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
it('점수 칸은
|
|
58
|
-
const score = parseSource(
|
|
59
|
-
expect(score).toBe(
|
|
57
|
+
it('점수 칸은 figureScoreOf 다 — 저작면의 성숙도 탭과 같은 수(수석 아키텍트 판정 09-24)', () => {
|
|
58
|
+
const score = parseSource(figureSource()).derived.score
|
|
59
|
+
expect(score).toBe(figureScoreOf(figureAsset())!.score)
|
|
60
60
|
expect(score).toBeGreaterThan(0)
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('형식을 어긴 자산은 거절한다', () => {
|
|
64
|
-
const broken = JSON.parse(
|
|
64
|
+
const broken = JSON.parse(figureSource())
|
|
65
65
|
broken.appearance = []
|
|
66
66
|
expect(() => parseSource(JSON.stringify(broken))).toThrow(FigureSourceError)
|
|
67
67
|
})
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
describe('발행 판정', () => {
|
|
71
|
-
it('제 부피 안에 든
|
|
72
|
-
const inspection = inspectSource(
|
|
71
|
+
it('제 부피 안에 든 자산은 막지 않는다', () => {
|
|
72
|
+
const inspection = inspectSource(figureSource())
|
|
73
73
|
expect(inspection.blocked).toBe(false)
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
it('선언한 부피 밖으로 나간 부품은 이름과 함께 막는다', () => {
|
|
77
|
-
let asset =
|
|
78
|
-
asset =
|
|
79
|
-
const inspection = inspectSource(
|
|
77
|
+
let asset = figureAsset()
|
|
78
|
+
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
|
|
79
|
+
const inspection = inspectSource(serializeFigureAsset(asset))
|
|
80
80
|
expect(inspection.blocked).toBe(true)
|
|
81
81
|
expect(inspection.findings.some(f => f.code === 'part-outside-occupancy' && f.at === 'arm')).toBe(true)
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
it('부피를 선언하지 않은
|
|
85
|
-
let asset =
|
|
86
|
-
asset =
|
|
87
|
-
const inspection = inspectSource(
|
|
84
|
+
it('부피를 선언하지 않은 자산은 막는다', () => {
|
|
85
|
+
let asset = createFigureAsset('Bare')
|
|
86
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
87
|
+
const inspection = inspectSource(serializeFigureAsset(asset))
|
|
88
88
|
expect(inspection.blocked).toBe(true)
|
|
89
89
|
expect(inspection.findings.some(f => f.code === 'occupancy-undeclared')).toBe(true)
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
it('전 범위를 증명하지 못한 판정은 막지 않고 그 사실을 말한다', () => {
|
|
93
93
|
/* 상태에 따라 도는 부품은 모서리만으로 증명되지 않는다 — 표본 검사였음을 발행 화면이 읽는다. */
|
|
94
|
-
let asset =
|
|
95
|
-
asset =
|
|
94
|
+
let asset = figureAsset()
|
|
95
|
+
asset = addFigurePrimitive(asset, { id: 'wheel', kind: 'cylinder', dimensions: { radius: 20, length: 20 }, position: { x: 0, y: 20, z: 0 } })
|
|
96
96
|
const m = asset.document.model as any
|
|
97
97
|
m.inputs.push({ id: 'spin', unit: 'deg', min: -180, max: 180, role: 'state' })
|
|
98
98
|
asset.stateDefaults['spin'] = 0
|
|
@@ -108,13 +108,13 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
108
108
|
pose.params = { ...pose.params, to: 'wheel.spun' }
|
|
109
109
|
m.nodes.push({ id: 'wheel.world', op: 'compose@1', args: ['wheel.turn.p', pose.outputs.pose], outputs: { pose: 'wheel.wp' } })
|
|
110
110
|
m.nodes.find((n: any) => n.id === 'wheel').args = [m.nodes.find((n: any) => n.id === 'wheel.shape').outputs.shape, 'wheel.wp']
|
|
111
|
-
asset =
|
|
111
|
+
asset = applyFigureAuthoring(asset, {
|
|
112
112
|
kind: 'declare-occupancy',
|
|
113
|
-
proposal:
|
|
113
|
+
proposal: proposeFigureOccupancy(asset, { over: 'range' }),
|
|
114
114
|
placement: 'floor'
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
const inspection = inspectSource(
|
|
117
|
+
const inspection = inspectSource(serializeFigureAsset(asset))
|
|
118
118
|
const sampled = inspection.findings.find(f => f.code === 'state-range-sampled')
|
|
119
119
|
expect(sampled).toBeDefined()
|
|
120
120
|
expect(sampled!.blocking).toBe(false)
|
|
@@ -127,16 +127,16 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
127
127
|
expect(inspectSource(null).findings[0].code).toBe('no-source')
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
-
it('바뀐 것 없는
|
|
131
|
-
const again = inspectSource(
|
|
130
|
+
it('바뀐 것 없는 재발행은 아무 말도 없이 지난다', () => {
|
|
131
|
+
const again = inspectSource(figureSource(), figureSource())
|
|
132
132
|
expect(again.blocked).toBe(false)
|
|
133
133
|
expect(again.findings.filter(f => f.code !== 'state-range-sampled')).toEqual([])
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
it('놓인 인스턴스가 기대던 것을 없앤
|
|
137
|
-
const before =
|
|
136
|
+
it('놓인 인스턴스가 기대던 것을 없앤 재발행은 막는다 — 없어진 능력을 이름으로 댄다', () => {
|
|
137
|
+
const before = figureAsset()
|
|
138
138
|
before.document.capabilities.names = ['Holdable']
|
|
139
|
-
const inspection = inspectSource(
|
|
139
|
+
const inspection = inspectSource(figureSource(), serializeFigureAsset(before))
|
|
140
140
|
const gone = inspection.findings.find(f => f.code === 'capability-gone')
|
|
141
141
|
expect(gone).toBeDefined()
|
|
142
142
|
expect(gone!.blocking).toBe(true)
|
|
@@ -151,7 +151,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
151
151
|
이 판정은 한때 발행 패널에 영어 문장(`detail`)을 그대로 냈고 왜 · 어떻게가 없었다 — 사용자가 2026-09-19 에
|
|
152
152
|
「가장 큰 문제」로 짚은 모양이다. 문장은 요청의 언어를 아는 서버가 코드와 값에서 만든다(ADR-0054).
|
|
153
153
|
*/
|
|
154
|
-
describe('
|
|
154
|
+
describe('판정의 문장 — 발행 패널의 말로', () => {
|
|
155
155
|
const KO = require('../../../translations/ko.json') as Record<string, string>
|
|
156
156
|
const EN = require('../../../translations/en.json') as Record<string, string>
|
|
157
157
|
|
|
@@ -164,14 +164,14 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
const outside = () => {
|
|
167
|
-
let asset =
|
|
168
|
-
asset =
|
|
169
|
-
return
|
|
167
|
+
let asset = figureAsset()
|
|
168
|
+
asset = addFigureBox(asset, { id: 'arm', size: { x: 100, y: 100, z: 100 }, position: { x: 900, y: 50, z: 0 } })
|
|
169
|
+
return serializeFigureAsset(asset)
|
|
170
170
|
}
|
|
171
171
|
const bare = () => {
|
|
172
|
-
let asset =
|
|
173
|
-
asset =
|
|
174
|
-
return
|
|
172
|
+
let asset = createFigureAsset('Bare')
|
|
173
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
174
|
+
return serializeFigureAsset(asset)
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
it('넘은 부품은 이름 · 축 · 넘은 양을 화면 말투로 말하고, 왜와 어떻게를 함께 낸다', () => {
|
|
@@ -193,7 +193,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
193
193
|
})
|
|
194
194
|
|
|
195
195
|
it('부품이 없는 새 도형에는 도형을 먼저 추가하라고 한다 — 화면은 그때 맞추기 단추를 내지 않는다', () => {
|
|
196
|
-
const empty =
|
|
196
|
+
const empty = serializeFigureAsset(createFigureAsset('Empty'))
|
|
197
197
|
const finding = inspectSource(empty, null, translatorOf(KO)).findings.find(f => f.code === 'occupancy-undeclared')!
|
|
198
198
|
|
|
199
199
|
expect(finding.how).toBe('먼저 도형을 추가한 뒤, 「부품에 맞추기」로 공간을 선언하세요.')
|
|
@@ -205,16 +205,16 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
205
205
|
여기서는 손으로 만든다: 램프의 높이가 도형 높이(size.y)를 따라 올라가고, 몸통은 그대로다.
|
|
206
206
|
*/
|
|
207
207
|
const splitting = () => {
|
|
208
|
-
let asset =
|
|
209
|
-
asset =
|
|
210
|
-
asset =
|
|
208
|
+
let asset = createFigureAsset('Lamp post')
|
|
209
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
210
|
+
asset = addFigureBox(asset, { id: 'lamp', size: { x: 40, y: 20, z: 40 }, position: { x: 0, y: 10, z: 0 } })
|
|
211
211
|
const model = asset.document.model
|
|
212
212
|
model.inputs.push({ id: 'size.y', unit: 'mm', min: 50, max: 20000, role: 'design' } as never)
|
|
213
213
|
asset.designInputs['size.y'] = 100
|
|
214
214
|
model.nodes.push({ id: 'lamp.lift', op: 'add@1', args: ['size.y', 'lamp.position.y'], outputs: { value: 'lamp.lift.value' } } as never)
|
|
215
215
|
const pose = model.nodes.find(n => n.id === 'lamp.pose')!
|
|
216
216
|
pose.args[1] = 'lamp.lift.value'
|
|
217
|
-
return
|
|
217
|
+
return serializeFigureAsset(asset)
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
it('크기를 바꾸면 떨어지는 부품을 막고, 어느 부품이 어느 축에서인지 · 왜 · 어떻게를 말한다', () => {
|
|
@@ -230,14 +230,14 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
230
230
|
|
|
231
231
|
/* 한 비율로 커진다고 선언한 도형에서 제 숫자를 가진 부품(수석 아키텍트 판정 2026-09-24 조건 ②). */
|
|
232
232
|
it('한 비율로 커진다고 선언한 도형에서 따라 커지지 않는 부품을 막고 이름을 댄다', () => {
|
|
233
|
-
let asset =
|
|
234
|
-
asset =
|
|
233
|
+
let asset = createFigureAsset('Uniform')
|
|
234
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
235
235
|
for (const axis of ['x', 'y', 'z']) {
|
|
236
236
|
asset.document.model.inputs.push({ id: `size.${axis}`, unit: 'mm', min: 1, max: 20000, role: 'design' } as never)
|
|
237
237
|
asset.designInputs[`size.${axis}`] = 100
|
|
238
238
|
}
|
|
239
|
-
asset =
|
|
240
|
-
const finding = inspectSource(
|
|
239
|
+
asset = applyFigureAuthoring(asset, { kind: 'scale-uniformly' } as never)
|
|
240
|
+
const finding = inspectSource(serializeFigureAsset(asset), null, translatorOf(KO)).findings.find(f => f.code === 'sizing-not-uniform')
|
|
241
241
|
|
|
242
242
|
expect(finding?.blocking).toBe(true)
|
|
243
243
|
expect(finding!.message).toBe("이 도형은 한 비율로 커지는데, 크기를 X 로 2배로 하면 'body' 부품은 같이 커지지 않습니다.")
|
|
@@ -283,13 +283,13 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
283
283
|
it('임시 이름인 도형은 발행을 막는다 — 이름은 행에 있다', () => {
|
|
284
284
|
/* The name a new figure gets — the format's rule, the one the gate recognises. */
|
|
285
285
|
const placeholder = placeholderType()
|
|
286
|
-
const finding = inspectSource(
|
|
286
|
+
const finding = inspectSource(figureSource(), null, translatorOf(KO), placeholder).findings.find(f => f.code === 'type-is-placeholder')
|
|
287
287
|
|
|
288
288
|
expect(finding?.blocking).toBe(true)
|
|
289
289
|
expect(finding!.message).toBe(`'${placeholder}' 타입은 처음 저장할 때 붙은 임시 타입입니다.`)
|
|
290
290
|
/* 화면의 칸 이름은 「타입」이다(`figure.label.figure-type`). */
|
|
291
291
|
expect(finding!.how).toContain('타입 칸')
|
|
292
|
-
expect(inspectSource(
|
|
292
|
+
expect(inspectSource(figureSource(), null, translatorOf(KO), 'ROLLER_CONVEYOR').blocked).toBe(false)
|
|
293
293
|
})
|
|
294
294
|
|
|
295
295
|
/* 코드가 묻는 키가 두 언어에 다 있어야 한다 — 없으면 그 언어의 화면에 영어가 섞인다. */
|
|
@@ -298,8 +298,8 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
298
298
|
const t = translatorOf(KO, asked)
|
|
299
299
|
inspectSource(outside(), null, t)
|
|
300
300
|
inspectSource(bare(), null, t)
|
|
301
|
-
inspectSource(
|
|
302
|
-
inspectSource(
|
|
301
|
+
inspectSource(figureSource(), figureSource(), t)
|
|
302
|
+
inspectSource(figureSource(), null, t, 'FIGURE_ABCDEF12')
|
|
303
303
|
|
|
304
304
|
const keys = [...new Set(asked)]
|
|
305
305
|
expect(keys.length).toBeGreaterThan(5)
|
|
@@ -310,12 +310,12 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
310
310
|
|
|
311
311
|
describe('소비처', () => {
|
|
312
312
|
it('자산을 그대로 받는다 — 그리는 쪽이 커널로 컴파일한다', () => {
|
|
313
|
-
const payload = consumablePayloadOf(
|
|
314
|
-
expect(payload.asset.version).toBe('figure-
|
|
313
|
+
const payload = consumablePayloadOf(figureSource())
|
|
314
|
+
expect(payload.asset.version).toBe('figure-asset-1')
|
|
315
315
|
})
|
|
316
316
|
|
|
317
317
|
it('컴파일되지 않는 자산은 소비처에 넘기지 않고 던진다', () => {
|
|
318
|
-
const broken = JSON.parse(
|
|
318
|
+
const broken = JSON.parse(figureSource())
|
|
319
319
|
broken.designInputs = {}
|
|
320
320
|
expect(() => consumablePayloadOf(JSON.stringify(broken))).toThrow()
|
|
321
321
|
})
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The doors a stored source goes through: saving (`parseSource`), publishing (`inspectSource`) and handing it to a
|
|
3
|
-
* board (`consumablePayloadOf`). The format is `figure-
|
|
3
|
+
* board (`consumablePayloadOf`). The format is `figure-asset-1`, and the asset says so in its `version`; a source
|
|
4
4
|
* that does not is refused by name, because a source the server cannot identify is one it must not save.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
6
|
+
import { compileFigureAsset, inspectFigureAsset, isPlaceholderType, parseFigureAsset, serializeFigureAsset, figureContractChanges, figureCostOf, figureScoreOf } from '@hatiolab/figure-model'
|
|
7
|
+
import type { FigureAsset, FigureContractChange, FigureContractError, FigureViolation } from '@hatiolab/figure-model'
|
|
8
8
|
|
|
9
9
|
import type { FigureFinding } from './figure-type.js'
|
|
10
10
|
|
|
11
11
|
/** The version the format stamps on its own source. */
|
|
12
|
-
export const
|
|
12
|
+
export const FIGURE_ASSET_VERSION = 'figure-asset-1'
|
|
13
13
|
|
|
14
14
|
/** A source that passed the save door: what is stored, and the numbers the catalogue shows, computed here. */
|
|
15
15
|
export interface ParsedSource {
|
|
@@ -54,7 +54,7 @@ export interface Inspection {
|
|
|
54
54
|
/**
|
|
55
55
|
* That the source is JSON and names the format.
|
|
56
56
|
*
|
|
57
|
-
* @throws {FigureSourceError} when the text is not JSON, or does not carry `version: 'figure-
|
|
57
|
+
* @throws {FigureSourceError} when the text is not JSON, or does not carry `version: 'figure-asset-1'`.
|
|
58
58
|
*/
|
|
59
59
|
function assertFormat(sourceJson: string): void {
|
|
60
60
|
let parsed: unknown
|
|
@@ -65,14 +65,14 @@ function assertFormat(sourceJson: string): void {
|
|
|
65
65
|
{ code: 'source-not-json', message: `정본이 JSON 이 아니다: ${(e as Error).message}`, at: 'source' }
|
|
66
66
|
])
|
|
67
67
|
}
|
|
68
|
-
if ((parsed as { version?: unknown } | null)?.version !==
|
|
68
|
+
if ((parsed as { version?: unknown } | null)?.version !== FIGURE_ASSET_VERSION)
|
|
69
69
|
throw new FigureSourceError([
|
|
70
|
-
{ code: 'source-format-unknown', message: `정본의 판을 알 수 없다 — 도형 자산은 version '${
|
|
70
|
+
{ code: 'source-format-unknown', message: `정본의 판을 알 수 없다 — 도형 자산은 version '${FIGURE_ASSET_VERSION}' 을 적는다`, at: 'version' }
|
|
71
71
|
])
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Validates a source and computes what the catalogue shows: the cost from `
|
|
75
|
+
* Validates a source and computes what the catalogue shows: the cost from `figureCostOf`, and the score from `figureScoreOf`
|
|
76
76
|
* -- the headroom against placing `LIMITS.screenInstances` of it on one screen (chief architect, 2026-09-24), the
|
|
77
77
|
* same number the modeller's maturity tab shows. An asset that draws nothing has no score, not a full one.
|
|
78
78
|
*
|
|
@@ -82,18 +82,18 @@ export function parseSource(sourceJson: string): ParsedSource {
|
|
|
82
82
|
assertFormat(sourceJson)
|
|
83
83
|
let asset
|
|
84
84
|
try {
|
|
85
|
-
asset =
|
|
86
|
-
|
|
85
|
+
asset = parseFigureAsset(sourceJson)
|
|
86
|
+
compileFigureAsset(asset)
|
|
87
87
|
} catch (e) {
|
|
88
|
-
const error = e as
|
|
88
|
+
const error = e as FigureContractError & { path?: string }
|
|
89
89
|
throw new FigureSourceError([
|
|
90
|
-
{ code: error.code ?? '
|
|
90
|
+
{ code: error.code ?? 'asset-invalid', message: error.message ?? String(e), at: error.path }
|
|
91
91
|
])
|
|
92
92
|
}
|
|
93
|
-
const cost =
|
|
94
|
-
const score =
|
|
93
|
+
const cost = figureCostOf(asset)
|
|
94
|
+
const score = figureScoreOf(asset)
|
|
95
95
|
return {
|
|
96
|
-
source:
|
|
96
|
+
source: serializeFigureAsset(asset),
|
|
97
97
|
derived: { score: (score?.score ?? null) as unknown as number, triangles: cost.triangles, groups: cost.groups },
|
|
98
98
|
violations: []
|
|
99
99
|
}
|
|
@@ -102,7 +102,7 @@ export function parseSource(sourceJson: string): ParsedSource {
|
|
|
102
102
|
/**
|
|
103
103
|
* Whether this source may be published, and why not.
|
|
104
104
|
*
|
|
105
|
-
* The judgement is `
|
|
105
|
+
* The judgement is `inspectFigureAsset`: every part inside the volume the asset declares, over the whole
|
|
106
106
|
* state range, and the face it says it is mounted on lying in that plane. A verdict reached by sampling is
|
|
107
107
|
* reported as a finding of its own rather than hidden — an author releasing a figure the gate could only
|
|
108
108
|
* sample should be told so, because nothing proved it holds between the states that were checked.
|
|
@@ -111,7 +111,7 @@ export function parseSource(sourceJson: string): ParsedSource {
|
|
|
111
111
|
export type Translate = (key: string, options?: Record<string, unknown>) => string
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* A
|
|
114
|
+
* A finding in the words of the release panel.
|
|
115
115
|
*
|
|
116
116
|
* The gate says what it measured as a code and values (`part`, `axis`, `mm`, `degrees`) and an English `detail`
|
|
117
117
|
* for logs. The sentence an author reads is made here, where the request's language is known (ADR-0054) — not in
|
|
@@ -122,7 +122,7 @@ export type Translate = (key: string, options?: Record<string, unknown>) => stri
|
|
|
122
122
|
* The words are the modeller's: 「공간 선언」 for the volume a figure takes up and 「장착면」 for the face it stands on
|
|
123
123
|
* (`figure.text.mounting-face-explained`). A fact, then the way out.
|
|
124
124
|
*/
|
|
125
|
-
function
|
|
125
|
+
function figureWording(violation: FigureViolation, say: Say, hasParts: boolean): Pick<GateFinding, 'message' | 'why' | 'how'> {
|
|
126
126
|
const part = violation.part ?? ''
|
|
127
127
|
const axis = (violation.axis ?? '').toUpperCase()
|
|
128
128
|
const mm = violation.mm ?? 0
|
|
@@ -227,25 +227,25 @@ export function inspectSource(
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
let inspection
|
|
230
|
-
let asset:
|
|
230
|
+
let asset: FigureAsset | undefined
|
|
231
231
|
let hasParts = true
|
|
232
232
|
try {
|
|
233
|
-
asset =
|
|
234
|
-
inspection =
|
|
235
|
-
const cost =
|
|
233
|
+
asset = parseFigureAsset(sourceJson)
|
|
234
|
+
inspection = inspectFigureAsset(asset)
|
|
235
|
+
const cost = figureCostOf(asset)
|
|
236
236
|
hasParts = cost.groups + cost.hidden > 0
|
|
237
237
|
} catch (e) {
|
|
238
|
-
const error = e as
|
|
238
|
+
const error = e as FigureContractError & { path?: string }
|
|
239
239
|
return {
|
|
240
240
|
blocked: true,
|
|
241
|
-
findings: [{ code: error.code ?? '
|
|
241
|
+
findings: [{ code: error.code ?? 'asset-invalid', message: error.message ?? String(e), at: error.path, blocking: true }]
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
const say = sayerOf(t)
|
|
246
246
|
const findings: GateFinding[] = inspection.violations.map(violation => ({
|
|
247
247
|
code: violation.code.toLowerCase().replace(/_/g, '-'),
|
|
248
|
-
...
|
|
248
|
+
...figureWording(violation, say, hasParts),
|
|
249
249
|
at: violation.part,
|
|
250
250
|
blocking: true
|
|
251
251
|
}))
|
|
@@ -279,41 +279,41 @@ export function inspectSource(
|
|
|
279
279
|
blocking: false
|
|
280
280
|
})
|
|
281
281
|
}
|
|
282
|
-
if (releasedJson) findings.push(...
|
|
282
|
+
if (releasedJson) findings.push(...figureContractFindings(asset!, releasedJson, say))
|
|
283
283
|
return { blocked: findings.some(finding => finding.blocking), findings }
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
287
|
* The last release against this one — what the drawings that already hold this type lean on (ADR-0063 decision 3).
|
|
288
288
|
*
|
|
289
|
-
* figure-model names the changes and whether each blocks (`
|
|
289
|
+
* figure-model names the changes and whether each blocks (`figureContractChanges`); the sentence is made here. When
|
|
290
290
|
* the last release no longer parses, the gate says so rather than passing in silence: a release that was not checked
|
|
291
291
|
* against the last one is a different fact from one that was checked and found safe.
|
|
292
292
|
*/
|
|
293
|
-
function
|
|
294
|
-
let before:
|
|
293
|
+
function figureContractFindings(asset: FigureAsset, releasedJson: string, say: Say): GateFinding[] {
|
|
294
|
+
let before: FigureAsset
|
|
295
295
|
try {
|
|
296
|
-
before =
|
|
296
|
+
before = parseFigureAsset(releasedJson)
|
|
297
297
|
} catch (e) {
|
|
298
298
|
return [
|
|
299
299
|
{
|
|
300
300
|
code: 'released-unreadable',
|
|
301
|
-
message: say('figure.gate.
|
|
302
|
-
why: say('figure.gate.
|
|
301
|
+
message: say('figure.gate.released-not-compared.message', { reason: (e as Error).message }, `The last release could not be read, so this release was not compared with it: ${(e as Error).message}`),
|
|
302
|
+
why: say('figure.gate.released-not-compared.why', {}, 'Whether this release breaks the instances already placed has not been checked.'),
|
|
303
303
|
blocking: false
|
|
304
304
|
}
|
|
305
305
|
]
|
|
306
306
|
}
|
|
307
|
-
return
|
|
307
|
+
return figureContractChanges(before, asset).map(change => ({
|
|
308
308
|
code: change.code,
|
|
309
|
-
...
|
|
309
|
+
...figureContractWording(change, say),
|
|
310
310
|
at: change.subject,
|
|
311
311
|
blocking: change.blocking
|
|
312
312
|
}))
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
/** One contract change in the words of the release panel. The English line is the fallback for a missing key. */
|
|
316
|
-
function
|
|
316
|
+
function figureContractWording(change: FigureContractChange, say: Say): Pick<GateFinding, 'message' | 'why' | 'how'> {
|
|
317
317
|
const v = change.values
|
|
318
318
|
const key = (field: string) => `figure.gate.contract.${change.code}.${field}`
|
|
319
319
|
const gone = {
|
|
@@ -423,12 +423,12 @@ function v3ContractWording(change: V3ContractChange, say: Say): Pick<GateFinding
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
/**
|
|
426
|
-
* What a consumer is handed for a released figure: the asset itself, because what draws it -- `
|
|
426
|
+
* What a consumer is handed for a released figure: the asset itself, because what draws it -- `registerFigure` in
|
|
427
427
|
* things-scene -- takes the asset and compiles it with the kernel.
|
|
428
428
|
*/
|
|
429
|
-
export function consumablePayloadOf(sourceJson: string): { asset:
|
|
429
|
+
export function consumablePayloadOf(sourceJson: string): { asset: FigureAsset } {
|
|
430
430
|
assertFormat(sourceJson)
|
|
431
|
-
const asset =
|
|
432
|
-
|
|
431
|
+
const asset = parseFigureAsset(sourceJson)
|
|
432
|
+
compileFigureAsset(asset)
|
|
433
433
|
return { asset }
|
|
434
434
|
}
|
|
@@ -64,7 +64,7 @@ export class FigureHistory implements HistoryEntityInterface<Figure> {
|
|
|
64
64
|
state?: 'draft' | 'released'
|
|
65
65
|
|
|
66
66
|
@Column({ nullable: true, ...longTextColumn() })
|
|
67
|
-
@Field({ nullable: true, description: 'The figure asset JSON (figure-
|
|
67
|
+
@Field({ nullable: true, description: 'The figure asset JSON (figure-asset-1) at this version.' })
|
|
68
68
|
source?: string
|
|
69
69
|
|
|
70
70
|
@Column({ nullable: true, ...longTextColumn() })
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { DataSource } from 'typeorm'
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
addFigureBox,
|
|
13
|
+
applyFigureAuthoring,
|
|
14
|
+
createFigureAsset,
|
|
15
15
|
placeholderType,
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
proposeFigureOccupancy,
|
|
17
|
+
serializeFigureAsset
|
|
18
18
|
} from '@hatiolab/figure-model'
|
|
19
19
|
|
|
20
20
|
const { DataSource: TypeOrmDataSource } = require('typeorm')
|
|
@@ -37,12 +37,12 @@ function loadCompiled(modulePath: string): any {
|
|
|
37
37
|
return require(resolved)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** A
|
|
40
|
+
/** A figure that passes the gate on its own: a box standing on the floor, with its space declared. */
|
|
41
41
|
function sound(): string {
|
|
42
|
-
let asset =
|
|
43
|
-
asset =
|
|
44
|
-
asset =
|
|
45
|
-
return
|
|
42
|
+
let asset = createFigureAsset('Probe')
|
|
43
|
+
asset = addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
44
|
+
asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }), placement: 'floor' })
|
|
45
|
+
return serializeFigureAsset(asset)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
beforeAll(async () => {
|
|
@@ -82,7 +82,7 @@ const save = (type: string, source = sound()) =>
|
|
|
82
82
|
dataSource!.getRepository(Figure).save({ domain, type, name: 'probe', state: 'draft', source, version: 0 })
|
|
83
83
|
|
|
84
84
|
describe('inspectFigure — the release panel', () => {
|
|
85
|
-
it('refuses a
|
|
85
|
+
it('refuses a figure still under its temporary type — the type comes from the row', async () => {
|
|
86
86
|
const figure = await save(placeholderType())
|
|
87
87
|
|
|
88
88
|
const inspection = await new FigureQuery().inspectFigure(figure.id, context())
|
|
@@ -101,10 +101,10 @@ describe('inspectFigure — the release panel', () => {
|
|
|
101
101
|
describe('inspectFigureDraft — the verdict while editing', () => {
|
|
102
102
|
it('judges the unsaved source, not the stored one, and still reads the row type', async () => {
|
|
103
103
|
const figure = await save(placeholderType())
|
|
104
|
-
let edited =
|
|
105
|
-
edited =
|
|
104
|
+
let edited = createFigureAsset('Probe')
|
|
105
|
+
edited = addFigureBox(edited, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
106
106
|
|
|
107
|
-
const inspection = await new FigureQuery().inspectFigureDraft(
|
|
107
|
+
const inspection = await new FigureQuery().inspectFigureDraft(serializeFigureAsset(edited), context(), figure.id)
|
|
108
108
|
const codes = inspection.findings.map((f: any) => f.code)
|
|
109
109
|
|
|
110
110
|
expect(codes).toContain('occupancy-undeclared')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 실제 LLM Figure 후보 생성 smoke test (기본 비활성).
|
|
3
3
|
*
|
|
4
|
-
* 결정적 시험은 프롬프트·도구·검사 배선을 고정하지만, 실제 모델이
|
|
4
|
+
* 결정적 시험은 프롬프트·도구·검사 배선을 고정하지만, 실제 모델이 명령 목록(ADR-0094)을 써서 게이트를
|
|
5
5
|
* 지나는 도형을 만드는지는 대신할 수 없다. 이 시험은 `RUN_FIGURE_LLM_SMOKE=1`과 공급자 API 키가 모두 있을 때만 실행한다.
|
|
6
6
|
* DB·GraphQL·Figure 저장을 건드리지 않고 후보 생성 함수만 호출하므로, 외부 영향은 API 비용뿐이다.
|
|
7
7
|
*
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createAIClient, setDefaultAIClient } from '@things-factory/ai-client-base'
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { applyFigureProposal, createFigureAsset, inspectFigureAsset } from '@hatiolab/figure-model'
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { proposeFigureSteps } from './figure-propose-steps'
|
|
17
17
|
|
|
18
18
|
// Provider modules register themselves on import. Keep this separate from the deterministic tests.
|
|
19
19
|
import '@things-factory/ai-client-base'
|
|
@@ -50,13 +50,13 @@ describeIf('Figure 실제 LLM smoke', () => {
|
|
|
50
50
|
test(
|
|
51
51
|
'새 Figure 는 게이트를 지나는 명령 목록으로 온다',
|
|
52
52
|
async () => {
|
|
53
|
-
const result = await
|
|
53
|
+
const result = await proposeFigureSteps({
|
|
54
54
|
prompt: 'two cylindrical rollers inside a simple rectangular press frame; make the rollers visually distinct',
|
|
55
55
|
name: 'Roll press'
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
expect(result.steps.length).toBeGreaterThan(0)
|
|
59
|
-
expect(
|
|
59
|
+
expect(inspectFigureAsset(result.source).violations).toEqual([])
|
|
60
60
|
expect(result.cost.triangles).toBeGreaterThan(0)
|
|
61
61
|
// eslint-disable-next-line no-console
|
|
62
62
|
console.log(`[figure-llm-smoke] new: ${result.steps.length} steps, ${result.attempts} attempt(s)`)
|
|
@@ -67,14 +67,14 @@ describeIf('Figure 실제 LLM smoke', () => {
|
|
|
67
67
|
test(
|
|
68
68
|
'고치는 제안은 건드리지 않은 부품을 그대로 둔다',
|
|
69
69
|
async () => {
|
|
70
|
-
const draft =
|
|
70
|
+
const draft = applyFigureProposal(createFigureAsset('Mixer'), [
|
|
71
71
|
{ kind: 'add-part', part: { id: 'body', kind: 'cylinder', dimensions: { radius: 140, length: 320 } } },
|
|
72
72
|
{ kind: 'add-part', part: { id: 'port', kind: 'box', dimensions: { width: 80, height: 60, depth: 30 } } },
|
|
73
73
|
{ kind: 'attach', part: 'body', face: 'bottom', to: { plane: 'mounting-plane' } },
|
|
74
74
|
{ kind: 'attach', part: 'port', face: 'back', to: { part: 'body', face: 'front' } },
|
|
75
75
|
{ kind: 'fit-room', placement: 'floor' }
|
|
76
76
|
] as any)
|
|
77
|
-
const result = await
|
|
77
|
+
const result = await proposeFigureSteps({ prompt: 'make the port twice as wide and give it an accent colour', base: draft })
|
|
78
78
|
|
|
79
79
|
const ids = result.source.document.model.nodes.map((n: any) => n.id)
|
|
80
80
|
expect(ids.some((id: string) => id.includes('body'))).toBe(true)
|