@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { In } from 'typeorm'
|
|
2
2
|
import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { parseFigureAsset, placeholderType } from '@hatiolab/figure-model'
|
|
5
5
|
import { getRepository } from '@things-factory/shell'
|
|
6
6
|
|
|
7
7
|
import { Figure } from './figure.js'
|
|
@@ -11,15 +11,15 @@ import { FigureProposal, ProposeRequest } from './figure-propose-type.js'
|
|
|
11
11
|
import { inspectSource } from './figure-format.js'
|
|
12
12
|
import { renameDraftType, renameFacts } from './figure-rename.js'
|
|
13
13
|
import { parseSource } from './figure-format.js'
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { ProposeFigureFailure, proposeFigureSteps } from './figure-propose-steps.js'
|
|
15
|
+
import { prepareFigureWrite, compareAndSwapFigure, pictureOf } from './figure-write.js'
|
|
16
16
|
|
|
17
17
|
@Resolver(Figure)
|
|
18
18
|
export class FigureMutation {
|
|
19
19
|
@Directive('@transaction')
|
|
20
20
|
@Directive('@privilege(category: "figure", privilege: "mutation", domainOwnerGranted: true)')
|
|
21
|
-
@Mutation(returns => Figure, { description: 'Save a
|
|
22
|
-
async
|
|
21
|
+
@Mutation(returns => Figure, { description: 'Save a asset draft using an exact-source concurrency check.' })
|
|
22
|
+
async saveFigure(
|
|
23
23
|
@Arg('source') source: string,
|
|
24
24
|
@Ctx() context: ResolverContext,
|
|
25
25
|
@Arg('id', { nullable: true }) id?: string,
|
|
@@ -34,7 +34,7 @@ export class FigureMutation {
|
|
|
34
34
|
if (!id) {
|
|
35
35
|
if (!creationId || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(creationId))
|
|
36
36
|
throw Error('새 모델 저장에는 생성 요청 식별자가 필요합니다.')
|
|
37
|
-
const prepared = { ...
|
|
37
|
+
const prepared = { ...prepareFigureWrite(source), ...pictureOf(thumbnail) }
|
|
38
38
|
const previous = await repository.findOne({ where: { id: creationId, domain: { id: domain.id } } })
|
|
39
39
|
if (previous) {
|
|
40
40
|
if (previous.source !== prepared.source) throw Error('이 생성 요청은 이미 다른 내용으로 저장되었습니다. 저장된 모델을 다시 열어 주세요.')
|
|
@@ -43,7 +43,7 @@ export class FigureMutation {
|
|
|
43
43
|
/*
|
|
44
44
|
The temporary name comes from the format's `placeholderType()`, the one rule the release gate and the AI
|
|
45
45
|
tools use to recognise it. This used to build its own (`FIGURE_<32 hex>`), which `isPlaceholderType` does
|
|
46
|
-
not match, so a new
|
|
46
|
+
not match, so a new figure could be published under a machine name that then froze (ADR-0063 decision 4)
|
|
47
47
|
— seen on :3300, 2026-09-24. A retry is still safe: the row id is the creation id, not the name.
|
|
48
48
|
*/
|
|
49
49
|
/*
|
|
@@ -69,10 +69,10 @@ export class FigureMutation {
|
|
|
69
69
|
// Exact tenant ownership: an inherited catalog entry is not writable in the child domain.
|
|
70
70
|
const figure = await repository.findOne({ where: { id, domain: { id: domain.id } } })
|
|
71
71
|
if (!figure) throw Error('편집할 Figure를 찾을 수 없습니다.')
|
|
72
|
-
const prepared = { ...
|
|
72
|
+
const prepared = { ...prepareFigureWrite(source, figure, expectedSource), ...pictureOf(thumbnail, figure) }
|
|
73
73
|
// Compare-and-swap in the UPDATE itself: supported by SQLite as well as PostgreSQL.
|
|
74
74
|
// A pessimistic row lock is not supported by the product's SQLite deployment.
|
|
75
|
-
await
|
|
75
|
+
await compareAndSwapFigure(criteria => repository.update(criteria, { ...prepared, updater: user }),
|
|
76
76
|
id, domain.id, expectedSource)
|
|
77
77
|
return repository.findOneOrFail({ where: { id, domain: { id: domain.id } } })
|
|
78
78
|
}
|
|
@@ -422,9 +422,9 @@ export class FigureMutation {
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
try {
|
|
425
|
-
const result = await
|
|
425
|
+
const result = await proposeFigureSteps({
|
|
426
426
|
prompt: request.prompt,
|
|
427
|
-
base: request.base ?
|
|
427
|
+
base: request.base ? parseFigureAsset(request.base) : undefined,
|
|
428
428
|
name: request.name,
|
|
429
429
|
image
|
|
430
430
|
})
|
|
@@ -438,7 +438,7 @@ export class FigureMutation {
|
|
|
438
438
|
attempts: result.attempts
|
|
439
439
|
}
|
|
440
440
|
} catch (e) {
|
|
441
|
-
if (e instanceof
|
|
441
|
+
if (e instanceof ProposeFigureFailure) {
|
|
442
442
|
// 사유를 삼키지 않는다. 저작자가 무엇이 막혔는지 알아야 말을 고쳐 다시 시킨다.
|
|
443
443
|
throw new Error([e.message, ...e.reasons].join('\n'))
|
|
444
444
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* figure-propose-
|
|
2
|
+
* figure-propose-steps — an AI proposal for a figure is a list of the modeller's commands (ADR-0094).
|
|
3
3
|
*
|
|
4
4
|
* What is held here is our filter, not the model's skill: the model is not called. The steps are applied on a copy;
|
|
5
5
|
* the step the kernel refuses, and the gate findings the steps bring in, go back to the model; after three attempts
|
|
6
6
|
* the proposer says it could not make it, and nothing half made comes back.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { FIGURE_PROPOSAL_GRAMMAR, applyFigureProposal, createFigureAsset, inspectFigureAsset } from '@hatiolab/figure-model'
|
|
9
9
|
import { setDefaultAIClient } from '@things-factory/ai-client-base'
|
|
10
10
|
import type { AIClient } from '@things-factory/ai-client-base'
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { ProposeFigureFailure, proposeFigureSteps, rulesPromptFigure } from './figure-propose-steps'
|
|
13
13
|
|
|
14
14
|
function stubClient(answers: unknown[]): AIClient & { calls: number; sent: any[][]; system?: string } {
|
|
15
15
|
const client = {
|
|
@@ -49,21 +49,21 @@ const wrongFace = liftTable.map((s, i) => (i === 4 ? { ...s, face: 'left' } : s)
|
|
|
49
49
|
|
|
50
50
|
describe('the model is told the kernel\'s commands and the open draft', () => {
|
|
51
51
|
it('teaches every word of the grammar, in its own shape', () => {
|
|
52
|
-
const rules =
|
|
53
|
-
for (const word of
|
|
52
|
+
const rules = rulesPromptFigure()
|
|
53
|
+
for (const word of FIGURE_PROPOSAL_GRAMMAR) expect(rules).toContain(word.shape)
|
|
54
54
|
expect(rules).not.toMatch(/"parts"\s*:\s*\[|anchor/i)
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
it('a measure asked to be the same as another is linked, not copied (:3300, 2026-09-26: 「몸통 지름과 같게」 came as a fixed 100)', () => {
|
|
58
|
-
expect(
|
|
58
|
+
expect(rulesPromptFigure()).toMatch(/same as, matches or follows another part or the figure, write link-dimension/)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
it('a revision carries the draft\'s
|
|
62
|
-
const draft =
|
|
61
|
+
it('a revision carries the draft\'s source', async () => {
|
|
62
|
+
const draft = applyFigureProposal(createFigureAsset('Lift table'), liftTable as any)
|
|
63
63
|
const client = stubClient([{ steps: [{ kind: 'rename', name: 'Lift table 2' }] }])
|
|
64
64
|
setDefaultAIClient(client)
|
|
65
|
-
const made = await
|
|
66
|
-
expect(client.sent[0]![0]!.content).toContain('"version":"figure-
|
|
65
|
+
const made = await proposeFigureSteps({ prompt: '이름을 바꿔 줘', base: draft })
|
|
66
|
+
expect(client.sent[0]![0]!.content).toContain('"version":"figure-asset-1"')
|
|
67
67
|
expect(made.source.name).toBe('Lift table 2')
|
|
68
68
|
expect(draft.name).toBe('Lift table')
|
|
69
69
|
})
|
|
@@ -72,17 +72,17 @@ describe('the model is told the kernel\'s commands and the open draft', () => {
|
|
|
72
72
|
describe('the steps are held to the kernel and the gate', () => {
|
|
73
73
|
it('a list the kernel takes comes back with the figure it makes, the gate clean, on the first attempt', async () => {
|
|
74
74
|
setDefaultAIClient(stubClient([{ steps: liftTable }]))
|
|
75
|
-
const made = await
|
|
75
|
+
const made = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' })
|
|
76
76
|
expect(made.attempts).toBe(1)
|
|
77
77
|
expect(made.steps).toEqual(liftTable)
|
|
78
|
-
expect(
|
|
78
|
+
expect(inspectFigureAsset(made.source).violations).toEqual([])
|
|
79
79
|
expect(made.score?.grade).toMatch(/^[ABCD]$/)
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
it('a refused step goes back with its place and the kernel\'s code, and the next answer is taken', async () => {
|
|
83
83
|
const client = stubClient([{ steps: wrongFace }, { steps: liftTable }])
|
|
84
84
|
setDefaultAIClient(client)
|
|
85
|
-
const made = await
|
|
85
|
+
const made = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' })
|
|
86
86
|
expect(made.attempts).toBe(2)
|
|
87
87
|
const said = client.sent[1]!.at(-1)!.content as string
|
|
88
88
|
expect(said).toMatch(/ATTACH_FACE: steps\[4\]: attach: /)
|
|
@@ -91,23 +91,23 @@ describe('the steps are held to the kernel and the gate', () => {
|
|
|
91
91
|
it('a new figure must come out whole: a list that leaves the room undeclared is sent back', async () => {
|
|
92
92
|
const client = stubClient([{ steps: withoutRoom }, { steps: liftTable }])
|
|
93
93
|
setDefaultAIClient(client)
|
|
94
|
-
const made = await
|
|
94
|
+
const made = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' })
|
|
95
95
|
expect(made.attempts).toBe(2)
|
|
96
96
|
expect(client.sent[1]!.at(-1)!.content).toMatch(/OCCUPANCY_UNDECLARED/)
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
it('a revision is not blamed for what the draft already had', async () => {
|
|
100
|
-
const draft =
|
|
100
|
+
const draft = applyFigureProposal(createFigureAsset('Lift table'), withoutRoom as any)
|
|
101
101
|
setDefaultAIClient(stubClient([{ steps: [{ kind: 'rename', name: 'Lift' }] }]))
|
|
102
|
-
const made = await
|
|
102
|
+
const made = await proposeFigureSteps({ prompt: '이름', base: draft })
|
|
103
103
|
expect(made.attempts).toBe(1)
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
it('three refusals said back, then it says it could not make it, with the last reasons -- nothing half made', async () => {
|
|
107
107
|
const client = stubClient([{ steps: wrongFace }, new Error('cut short'), { nope: true }])
|
|
108
108
|
setDefaultAIClient(client)
|
|
109
|
-
const failure = await
|
|
110
|
-
expect(failure).toBeInstanceOf(
|
|
109
|
+
const failure = await proposeFigureSteps({ prompt: '리프트 테이블', name: 'Lift table' }).catch(e => e)
|
|
110
|
+
expect(failure).toBeInstanceOf(ProposeFigureFailure)
|
|
111
111
|
expect(failure.message).toBe('만들지 못했습니다.')
|
|
112
112
|
expect(failure.attempts).toBe(3)
|
|
113
113
|
expect(failure.reasons).toEqual(['The answer must be { "steps": [ ... ] }.'])
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import { FIGURE_PROPOSAL_GRAMMAR, applyFigureProposal, createFigureAsset, inspectFigureAsset, serializeFigureAsset, figureCostOf, figureScoreOf } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAsset, FigureProposalStep } from '@hatiolab/figure-model'
|
|
3
3
|
import { getDefaultAIClient } from '@things-factory/ai-client-base'
|
|
4
4
|
import type { AIImageMediaType, AIMessage } from '@things-factory/ai-client-base'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* An AI proposal for a
|
|
7
|
+
* An AI proposal for a figure is a list of the modeller's own commands (ADR-0094).
|
|
8
8
|
*
|
|
9
|
-
* The model is told the command grammar (figure-model's `
|
|
10
|
-
* the open draft's
|
|
9
|
+
* The model is told the command grammar (figure-model's `FIGURE_PROPOSAL_GRAMMAR`, made from the kernel's constants) and
|
|
10
|
+
* the open draft's source, and answers with steps. The steps are applied on a copy with `applyFigureProposal`; the
|
|
11
11
|
* first step the kernel refuses, and any gate finding the steps brought in, go back to the model as reasons. After
|
|
12
12
|
* three attempts the proposer says it could not make it. Nothing half made is returned, and nothing is saved: the
|
|
13
13
|
* modeller applies the same steps through its session as one step of its history.
|
|
@@ -19,29 +19,29 @@ const MAX_ATTEMPTS = 3
|
|
|
19
19
|
/** A list of commands for a figure with many parts is long; the default output limit cuts the JSON short. */
|
|
20
20
|
const GENERATE_MAX_TOKENS = 16384
|
|
21
21
|
|
|
22
|
-
export interface
|
|
22
|
+
export interface ProposeFigureInput {
|
|
23
23
|
/** What the author asked for, in their words. */
|
|
24
24
|
prompt: string
|
|
25
25
|
/** The open draft to revise. Absent: a new figure. */
|
|
26
|
-
base?:
|
|
26
|
+
base?: FigureAsset
|
|
27
27
|
/** The name of a new figure. */
|
|
28
28
|
name?: string
|
|
29
29
|
/** A reference picture: proportions and masses, not surface detail. */
|
|
30
30
|
image?: { data: Buffer; mediaType: AIImageMediaType }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export interface
|
|
33
|
+
export interface ProposeFigureResult {
|
|
34
34
|
/** What the modeller applies, as one step of its history. */
|
|
35
|
-
steps:
|
|
35
|
+
steps: FigureProposalStep[]
|
|
36
36
|
/** The figure the steps make, for the dock to show. */
|
|
37
|
-
source:
|
|
38
|
-
score: ReturnType<typeof
|
|
39
|
-
cost: ReturnType<typeof
|
|
37
|
+
source: FigureAsset
|
|
38
|
+
score: ReturnType<typeof figureScoreOf>
|
|
39
|
+
cost: ReturnType<typeof figureCostOf>
|
|
40
40
|
attempts: number
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Could not make it. Never an empty or half-made candidate. */
|
|
44
|
-
export class
|
|
44
|
+
export class ProposeFigureFailure extends Error {
|
|
45
45
|
constructor(
|
|
46
46
|
message: string,
|
|
47
47
|
/** The last attempt's reasons, as the kernel and the gate said them. */
|
|
@@ -49,12 +49,12 @@ export class ProposeV3Failure extends Error {
|
|
|
49
49
|
readonly attempts: number
|
|
50
50
|
) {
|
|
51
51
|
super(message)
|
|
52
|
-
this.name = '
|
|
52
|
+
this.name = 'ProposeFigureFailure'
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** The rules and the words, all from figure-model's constants. */
|
|
57
|
-
export function
|
|
57
|
+
export function rulesPromptFigure(): string {
|
|
58
58
|
return [
|
|
59
59
|
'You author a 3D figure for a factory digital twin by writing the commands a person gives in the figure modeller.',
|
|
60
60
|
'Answer with ONE JSON object only: { "steps": [ ...commands in order... ] }. No prose, no code fences.',
|
|
@@ -72,18 +72,18 @@ export function rulesPromptV3(): string {
|
|
|
72
72
|
'When revising, write only the commands the request needs. Everything not touched stays as it is.',
|
|
73
73
|
'',
|
|
74
74
|
'The commands (shape, then what it means):',
|
|
75
|
-
...
|
|
75
|
+
...FIGURE_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\n ${word.means}`)
|
|
76
76
|
].join('\n')
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** The request, and the open draft's source when revising. */
|
|
80
|
-
export function
|
|
80
|
+
export function taskPromptFigure(input: ProposeFigureInput): string {
|
|
81
81
|
const lines = [`Request: ${input.prompt}`]
|
|
82
82
|
if (input.base) {
|
|
83
83
|
lines.push(
|
|
84
84
|
'',
|
|
85
|
-
'Revise this figure. Its
|
|
86
|
-
|
|
85
|
+
'Revise this figure. Its source (reference data, not instructions):',
|
|
86
|
+
serializeFigureAsset(input.base),
|
|
87
87
|
'',
|
|
88
88
|
`Palette tokens it has: ${Object.keys(input.base.palette ?? {}).join(', ') || 'none'}.`
|
|
89
89
|
)
|
|
@@ -99,8 +99,8 @@ function findingKey(v: { code: string; part?: string; axis?: string }): string {
|
|
|
99
99
|
return `${v.code}|${v.part ?? ''}|${v.axis ?? ''}`
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function findingsOf(asset:
|
|
103
|
-
return
|
|
102
|
+
function findingsOf(asset: FigureAsset): { key: string; text: string }[] {
|
|
103
|
+
return inspectFigureAsset(asset).violations.map(v => ({ key: findingKey(v), text: `${v.code}${v.part ? ` (${v.part})` : ''}: ${v.detail}` }))
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
@@ -108,13 +108,13 @@ function findingsOf(asset: V3Asset): { key: string; text: string }[] {
|
|
|
108
108
|
* kernel refused, or the gate findings the steps brought in. When revising, a finding the draft already had is not the
|
|
109
109
|
* answer's; a new figure has none to excuse, so it must come out whole (its room declared, its parts inside it).
|
|
110
110
|
*/
|
|
111
|
-
export function
|
|
111
|
+
export function judgeFigureAnswer(base: FigureAsset, answer: unknown, revising: boolean): { source?: FigureAsset; steps?: FigureProposalStep[]; reasons: string[] } {
|
|
112
112
|
const steps = (answer as { steps?: unknown } | undefined)?.steps
|
|
113
113
|
if (!Array.isArray(steps)) return { reasons: ['The answer must be { "steps": [ ... ] }.'] }
|
|
114
114
|
|
|
115
|
-
let source:
|
|
115
|
+
let source: FigureAsset
|
|
116
116
|
try {
|
|
117
|
-
source =
|
|
117
|
+
source = applyFigureProposal(base, steps as FigureProposalStep[])
|
|
118
118
|
} catch (e) {
|
|
119
119
|
return { reasons: [(e as Error).message] }
|
|
120
120
|
}
|
|
@@ -122,17 +122,17 @@ export function judgeV3Answer(base: V3Asset, answer: unknown, revising: boolean)
|
|
|
122
122
|
const before = new Set(revising ? findingsOf(base).map(f => f.key) : [])
|
|
123
123
|
const brought = findingsOf(source).filter(f => !before.has(f.key))
|
|
124
124
|
if (brought.length) return { reasons: brought.map(f => f.text) }
|
|
125
|
-
return { source, steps: steps as
|
|
125
|
+
return { source, steps: steps as FigureProposalStep[], reasons: [] }
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/** Asks the model for steps and holds them to the kernel and the gate, three times at most. */
|
|
129
|
-
export async function
|
|
129
|
+
export async function proposeFigureSteps(input: ProposeFigureInput): Promise<ProposeFigureResult> {
|
|
130
130
|
const client = getDefaultAIClient()
|
|
131
|
-
if (!client) throw new
|
|
131
|
+
if (!client) throw new ProposeFigureFailure('AI 모델이 설정돼 있지 않습니다.', [], 0)
|
|
132
132
|
|
|
133
|
-
const base = input.base ??
|
|
134
|
-
const system =
|
|
135
|
-
const task =
|
|
133
|
+
const base = input.base ?? createFigureAsset(input.name ?? 'Figure')
|
|
134
|
+
const system = rulesPromptFigure()
|
|
135
|
+
const task = taskPromptFigure(input)
|
|
136
136
|
const messages: AIMessage[] = [
|
|
137
137
|
{
|
|
138
138
|
role: 'user',
|
|
@@ -156,9 +156,9 @@ export async function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3R
|
|
|
156
156
|
continue
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
const judged =
|
|
159
|
+
const judged = judgeFigureAnswer(base, answer, !!input.base)
|
|
160
160
|
if (judged.source) {
|
|
161
|
-
return { steps: judged.steps!, source: judged.source, score:
|
|
161
|
+
return { steps: judged.steps!, source: judged.source, score: figureScoreOf(judged.source), cost: figureCostOf(judged.source), attempts: attempt }
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
reasons = judged.reasons
|
|
@@ -170,5 +170,5 @@ export async function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3R
|
|
|
170
170
|
})
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
throw new
|
|
173
|
+
throw new ProposeFigureFailure('만들지 못했습니다.', reasons, MAX_ATTEMPTS)
|
|
174
174
|
}
|
|
@@ -8,7 +8,7 @@ export class ProposeRequest {
|
|
|
8
8
|
@Field({ description: 'What the author asked for, in their own words' })
|
|
9
9
|
prompt: string
|
|
10
10
|
|
|
11
|
-
@Field({ nullable: true, description: 'The
|
|
11
|
+
@Field({ nullable: true, description: 'The figure source to revise, as JSON. Omit to create a new one.' })
|
|
12
12
|
base?: string
|
|
13
13
|
|
|
14
14
|
@Field({ nullable: true, description: 'The name a person reads for a new figure. Ignored when base is given.' })
|
|
@@ -26,10 +26,10 @@ export class ProposeRequest {
|
|
|
26
26
|
*/
|
|
27
27
|
@ObjectType({ description: 'A figure the assistant proposes, as modeller commands. Not saved — the author applies it or leaves it.' })
|
|
28
28
|
export class FigureProposal {
|
|
29
|
-
@Field({ description: 'The
|
|
29
|
+
@Field({ description: 'The authoring commands, as a JSON array, in the order they apply' })
|
|
30
30
|
steps: string
|
|
31
31
|
|
|
32
|
-
@Field({ description: 'The
|
|
32
|
+
@Field({ description: 'The figure source the steps make, as JSON' })
|
|
33
33
|
source: string
|
|
34
34
|
|
|
35
35
|
@Field(type => Int, { nullable: true, description: 'Reuse score out of 100' })
|
|
@@ -114,7 +114,7 @@ export class FigureQuery {
|
|
|
114
114
|
const figure = await getRepository(Figure).findOne({
|
|
115
115
|
where: { domain: { id: In([domain.id, domain.parentId].filter(Boolean)) }, id },
|
|
116
116
|
/*
|
|
117
|
-
`type` is read too. A
|
|
117
|
+
`type` is read too. A asset carries no type, so the temporary-type refusal reads the row's; with only
|
|
118
118
|
id and source selected it saw `undefined`, the panel enabled 「발행하기」, and the release mutation — which
|
|
119
119
|
loads the whole row — refused on the press (2026-09-24).
|
|
120
120
|
*/
|
|
@@ -142,7 +142,7 @@ export class FigureQuery {
|
|
|
142
142
|
/**
|
|
143
143
|
* The same judgement for a source that has not been saved — what the modeller shows **while** the author edits.
|
|
144
144
|
*
|
|
145
|
-
* Without it a
|
|
145
|
+
* Without it a author saw nothing until they saved and opened the release panel. The judgement runs here, not
|
|
146
146
|
* in the browser, because the sentences are made from the request's language (`inspectAnySource` with `context.t`)
|
|
147
147
|
* and a second copy on the screen would drift from the one the release refuses with. When `id` names a saved
|
|
148
148
|
* figure, its row type and last release are read as `inspectFigure` does; a figure never saved has neither.
|
|
@@ -190,7 +190,7 @@ export class FigureQuery {
|
|
|
190
190
|
* 소비처(operato-board, operato-twin, things-scene)가 발행된 도형을 받아 가는 문.
|
|
191
191
|
*
|
|
192
192
|
* 이름은 보드와의 계약이라 그대로 둔다(바꾸는 일은 따로 한다). 돌려주는 것은 자산이다 —
|
|
193
|
-
* `{
|
|
193
|
+
* `{ type, asset }`. 그리는 쪽(`registerFigure`)이 커널로 컴파일한다.
|
|
194
194
|
*
|
|
195
195
|
* ## 규칙
|
|
196
196
|
* 1. 소비처는 초안(draft)을 볼 수 없다 — **반드시 발행된(released) 판만 전달**한다.
|
|
@@ -221,7 +221,7 @@ export class FigureQuery {
|
|
|
221
221
|
@Directive('@privilege(category: "figure-published", privilege: "query", domainOwnerGranted: true)')
|
|
222
222
|
@Query(returns => GraphQLJSON, {
|
|
223
223
|
nullable: true,
|
|
224
|
-
description: 'A released figure for its consumers (board, twin, things-scene): {
|
|
224
|
+
description: 'A released figure for its consumers (board, twin, things-scene): { type, asset } with the figure asset. Only returns released figures.'
|
|
225
225
|
})
|
|
226
226
|
async figureBlueprint(
|
|
227
227
|
@Arg('type') type: string,
|
|
@@ -247,7 +247,7 @@ export class FigureQuery {
|
|
|
247
247
|
|
|
248
248
|
try {
|
|
249
249
|
const text = typeof sourceToCompile === 'string' ? sourceToCompile : JSON.stringify(sourceToCompile)
|
|
250
|
-
return {
|
|
250
|
+
return { type, asset: consumablePayloadOf(text).asset }
|
|
251
251
|
} catch (err) {
|
|
252
252
|
console.error(`[figureBlueprint] 타입 '${type}' (figure id: ${figure.id}) 자산 컴파일 실패:`, err)
|
|
253
253
|
|
|
@@ -143,18 +143,18 @@ describe('이름을 바꾸기 전에 읽는 사실', () => {
|
|
|
143
143
|
*/
|
|
144
144
|
describe('처음 저장에 타입 이름을 받는다', () => {
|
|
145
145
|
let FigureMutation: any
|
|
146
|
-
let
|
|
146
|
+
let FIGURE_SOURCE: string
|
|
147
147
|
beforeAll(() => {
|
|
148
148
|
FigureMutation = loadCompiled('service/figure/figure-mutation').FigureMutation
|
|
149
149
|
const model = require('@hatiolab/figure-model')
|
|
150
|
-
|
|
151
|
-
model.
|
|
150
|
+
FIGURE_SOURCE = model.serializeFigureAsset(
|
|
151
|
+
model.addFigureBox(model.createFigureAsset('상자'), { id: 'box', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
152
152
|
)
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
const save = (creationId: string, type?: string) =>
|
|
156
|
-
new FigureMutation().
|
|
157
|
-
|
|
156
|
+
new FigureMutation().saveFigure(
|
|
157
|
+
FIGURE_SOURCE,
|
|
158
158
|
{ state: { domain: DOMAIN, user: undefined, tx: dataSource!.manager } },
|
|
159
159
|
undefined,
|
|
160
160
|
undefined,
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
import { readFileSync } from 'fs'
|
|
8
8
|
import { join } from 'path'
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { createFigureAsset, parseFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import { FigureRenameRefused, renameDraftType } from './figure-rename'
|
|
13
13
|
|
|
14
14
|
const draft = (over: Record<string, unknown> = {}) => ({
|
|
15
15
|
type: 'FIGURE_MU6RJH47',
|
|
16
|
-
source:
|
|
16
|
+
source: serializeFigureAsset(createFigureAsset('새 Figure')),
|
|
17
17
|
...over
|
|
18
18
|
})
|
|
19
19
|
|
|
@@ -22,15 +22,15 @@ const never = { version: 0, hasHistory: false, takenByAnother: false }
|
|
|
22
22
|
describe('초안의 type 고치기', () => {
|
|
23
23
|
/*
|
|
24
24
|
자산에는 type 칸이 없고 형식은 모르는 칸을 거절한다. 원본에 이름을 써 넣었더니 그 도형을 다시 열 수
|
|
25
|
-
없었다(`
|
|
25
|
+
없었다(`parseFigureAsset` — "unknown type"). 행만 바뀐다.
|
|
26
26
|
*/
|
|
27
27
|
it('★ 행만 바뀌고, 원본은 그대로 다시 열린다', () => {
|
|
28
|
-
const source =
|
|
28
|
+
const source = serializeFigureAsset(createFigureAsset('새 Figure'))
|
|
29
29
|
const result = renameDraftType({ type: 'FIGURE_ABCDEF12', source }, 'ROLLER_CONVEYOR', never)
|
|
30
30
|
|
|
31
31
|
expect(result.type).toBe('ROLLER_CONVEYOR')
|
|
32
32
|
expect(result.source).toBe(source)
|
|
33
|
-
expect(() =>
|
|
33
|
+
expect(() => parseFigureAsset(result.source)).not.toThrow()
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
it('앞뒤 공백을 뗀다. 대소문자는 저작자가 적은 대로 둔다', () => {
|
|
@@ -93,10 +93,10 @@ describe('문이 이 규칙을 지난다', () => {
|
|
|
93
93
|
})
|
|
94
94
|
|
|
95
95
|
/*
|
|
96
|
-
|
|
96
|
+
저장이 임시 이름을 스스로 지었다(`FIGURE_<32 hex>`). 형식의 `isPlaceholderType` 이 그것을 못 알아봐서
|
|
97
97
|
발행 판정이 기계 이름을 통과시켰다. 짓는 쪽과 알아보는 쪽이 한 규칙이어야 한다.
|
|
98
98
|
*/
|
|
99
|
-
/* 새
|
|
99
|
+
/* 새 도형의 임시 이름이 판정이 알아보는 그 규칙인지는 문을 실제로 불러 본다 — figure-rename-db.test.ts 「이름 없이 부르면」. */
|
|
100
100
|
|
|
101
101
|
it('문이 type 을 받는 칸을 갖는다', () => {
|
|
102
102
|
const type = source('figure-type.ts')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 저작 도구의 **배선**을 못박는다.
|
|
3
3
|
*
|
|
4
|
-
* 도구가 무엇을 계산하는지는 각 진입점의 시험이 이미 잠근다(`figure-propose-
|
|
4
|
+
* 도구가 무엇을 계산하는지는 각 진입점의 시험이 이미 잠근다(`figure-propose-steps.test` ·
|
|
5
5
|
* figure-model 의 게이트 시험). 비어 있던 것은 **그 사이를 잇는 줄들**이고, 그 줄이 끊어질 때
|
|
6
6
|
* 나타나는 증상이 실제로 겪은 실패와 1:1 로 대응한다.
|
|
7
7
|
*
|
|
@@ -36,8 +36,8 @@ const THEIRS = { id: 'domain-theirs' }
|
|
|
36
36
|
/** A stored figure: one body on the floor, its room declared. */
|
|
37
37
|
const SOURCE = (() => {
|
|
38
38
|
const model = require('@hatiolab/figure-model')
|
|
39
|
-
let asset = model.
|
|
40
|
-
asset = model.
|
|
39
|
+
let asset = model.addFigureBox(model.createFigureAsset('Mixer'), { id: 'body', size: { x: 300, y: 400, z: 300 }, position: { x: 0, y: 200, z: 0 } })
|
|
40
|
+
asset = model.applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: model.proposeFigureOccupancy(asset), placement: 'floor' })
|
|
41
41
|
return asset
|
|
42
42
|
})()
|
|
43
43
|
|
|
@@ -57,19 +57,19 @@ function install() {
|
|
|
57
57
|
Figure: class {},
|
|
58
58
|
inspectSource: (json?: string | null) =>
|
|
59
59
|
json ? { blocked: false, findings: [] } : { blocked: true, findings: [{ code: 'no-source', message: '정본이 없다', blocking: true }] },
|
|
60
|
-
|
|
60
|
+
proposeFigureSteps: async (input: any) => {
|
|
61
61
|
proposed.push(input)
|
|
62
62
|
return {
|
|
63
63
|
steps: [{ kind: 'rename', name: 'Mixer' }],
|
|
64
|
-
source: { ...model.
|
|
64
|
+
source: { ...model.createFigureAsset(input.base?.name ?? input.name ?? 'Figure') },
|
|
65
65
|
score: { grade: 'B' },
|
|
66
66
|
cost: { triangles: 12 },
|
|
67
67
|
attempts: 1
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
parseSource: require('./figure-format').parseSource,
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
figureCostOf: model.figureCostOf,
|
|
72
|
+
figureScoreOf: model.figureScoreOf
|
|
73
73
|
})
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -303,21 +303,21 @@ describe('모델이 넘긴 id 를 믿지 않는다', () => {
|
|
|
303
303
|
|
|
304
304
|
/*
|
|
305
305
|
제안은 모델러의 명령 목록이다(ADR-0094). 고치는 제안은 모델러가 모델이 본 판 위에 한 단계로 적용하므로, 고칠 수
|
|
306
|
-
있는 것은 모델러에 열린
|
|
306
|
+
있는 것은 모델러에 열린 도형뿐이고 그 판(context · revision)을 문맥이 싣고 와야 한다.
|
|
307
307
|
*/
|
|
308
|
-
const
|
|
308
|
+
const figureDraft = () => {
|
|
309
309
|
const model = require('@hatiolab/figure-model')
|
|
310
|
-
let asset = model.
|
|
311
|
-
asset = model.
|
|
310
|
+
let asset = model.createFigureAsset('Probe')
|
|
311
|
+
asset = model.addFigureBox(asset, { id: 'body', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: 50, z: 0 } })
|
|
312
312
|
return asset
|
|
313
313
|
}
|
|
314
|
-
const modeller = (over: any = {}) => ({ figureId: 'fig-1', draftSource:
|
|
314
|
+
const modeller = (over: any = {}) => ({ figureId: 'fig-1', draftSource: figureDraft(), context: 'ctx-7', revision: 4, ...over })
|
|
315
315
|
|
|
316
316
|
describe('제안을 요청할 때', () => {
|
|
317
|
-
it('열린 도형을 고치면 그
|
|
317
|
+
it('열린 도형을 고치면 그 초안이 제안기에 가고, 모델러가 적용할 판이 함께 돌아온다', async () => {
|
|
318
318
|
const out = await toolNamed('proposeFigureCandidate').builder({ prompt: '다리를 길게' }, ctx(modeller()))
|
|
319
319
|
expect(proposed[0].base.name).toBe('Probe')
|
|
320
|
-
expect(proposed[0].base.version).toBe('figure-
|
|
320
|
+
expect(proposed[0].base.version).toBe('figure-asset-1')
|
|
321
321
|
expect(out.context).toBe('ctx-7')
|
|
322
322
|
expect(out.revision).toBe(4)
|
|
323
323
|
expect(out.figureId).toBe('fig-1')
|
|
@@ -373,17 +373,17 @@ describe('제안을 요청할 때', () => {
|
|
|
373
373
|
})
|
|
374
374
|
|
|
375
375
|
it('세 번 거절된 끝의 사유가 도구 경계를 넘어간다 — 「만들지 못했습니다」만 남지 않게', async () => {
|
|
376
|
-
const {
|
|
376
|
+
const { ProposeFigureFailure } = require('./figure-propose-steps')
|
|
377
377
|
const model = require('@hatiolab/figure-model')
|
|
378
378
|
__setFigureToolDepsForTest({
|
|
379
379
|
getRepository: () => stubRepository(ROWS),
|
|
380
380
|
Figure: class {},
|
|
381
381
|
parseSource: require('./figure-format').parseSource,
|
|
382
|
-
|
|
383
|
-
throw new
|
|
382
|
+
proposeFigureSteps: async () => {
|
|
383
|
+
throw new ProposeFigureFailure('만들지 못했습니다.', ['ATTACH_FACE: steps[4]: attach: faces along x'], 3)
|
|
384
384
|
},
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
figureCostOf: model.figureCostOf,
|
|
386
|
+
figureScoreOf: model.figureScoreOf
|
|
387
387
|
})
|
|
388
388
|
await expect(toolNamed('proposeFigureCandidate').builder({ prompt: '고쳐줘' }, ctx(modeller()))).rejects.toThrow(
|
|
389
389
|
/만들지 못했습니다\.\nATTACH_FACE: steps\[4\]/
|
|
@@ -420,8 +420,8 @@ describe('제안이 카드가 된다', () => {
|
|
|
420
420
|
})
|
|
421
421
|
|
|
422
422
|
describe('도형이 모델러에 열려 있을 때', () => {
|
|
423
|
-
it('저장 전
|
|
424
|
-
const draft =
|
|
423
|
+
it('저장 전 초안을 이 모델로 읽고, 셈으로 잰다', async () => {
|
|
424
|
+
const draft = figureDraft()
|
|
425
425
|
const got = await toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: draft, palette: ['palette.primary'] }))
|
|
426
426
|
expect(got.draft).toBe(true)
|
|
427
427
|
expect(got.cost.triangles).toBe(12)
|
|
@@ -429,7 +429,7 @@ describe('도형이 모델러에 열려 있을 때', () => {
|
|
|
429
429
|
})
|
|
430
430
|
|
|
431
431
|
it('형식을 어긴 초안은 저장 문의 말로 거절한다', async () => {
|
|
432
|
-
const broken = { ...
|
|
432
|
+
const broken = { ...figureDraft(), appearance: [] }
|
|
433
433
|
await expect(toolNamed('getFigureSource').builder({}, ctx({ figureId: 'fig-1', draftSource: broken }))).rejects.toThrow(/unsaved draft is invalid/)
|
|
434
434
|
})
|
|
435
435
|
})
|