@things-factory/figure-service 10.1.64 → 10.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist-server/service/figure/figure-mutation.d.ts +4 -8
  2. package/dist-server/service/figure/figure-mutation.js +13 -27
  3. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  4. package/dist-server/service/figure/figure-propose-type.d.ts +8 -59
  5. package/dist-server/service/figure/figure-propose-type.js +15 -150
  6. package/dist-server/service/figure/figure-propose-type.js.map +1 -1
  7. package/dist-server/service/figure/figure-propose-v3.d.ts +50 -0
  8. package/dist-server/service/figure/figure-propose-v3.js +143 -0
  9. package/dist-server/service/figure/figure-propose-v3.js.map +1 -0
  10. package/dist-server/service/figure/figure-tools.js +64 -90
  11. package/dist-server/service/figure/figure-tools.js.map +1 -1
  12. package/dist-server/tsconfig.tsbuildinfo +1 -1
  13. package/package.json +3 -3
  14. package/server/service/figure/figure-llm-smoke.test.ts +28 -32
  15. package/server/service/figure/figure-mutation.ts +14 -28
  16. package/server/service/figure/figure-propose-type.ts +16 -124
  17. package/server/service/figure/figure-propose-v3.test.ts +117 -0
  18. package/server/service/figure/figure-propose-v3.ts +174 -0
  19. package/server/service/figure/figure-tools.test.ts +86 -133
  20. package/server/service/figure/figure-tools.ts +60 -91
  21. package/dist-server/service/figure/figure-propose.d.ts +0 -91
  22. package/dist-server/service/figure/figure-propose.js +0 -439
  23. package/dist-server/service/figure/figure-propose.js.map +0 -1
  24. package/dist-server/service/figure/figure-quality.d.ts +0 -29
  25. package/dist-server/service/figure/figure-quality.js +0 -62
  26. package/dist-server/service/figure/figure-quality.js.map +0 -1
  27. package/server/service/figure/figure-ai-flow.test.ts +0 -93
  28. package/server/service/figure/figure-e2e-smoke.test.ts +0 -72
  29. package/server/service/figure/figure-propose.test.ts +0 -719
  30. package/server/service/figure/figure-propose.ts +0 -551
  31. package/server/service/figure/figure-quality.test.ts +0 -43
  32. package/server/service/figure/figure-quality.ts +0 -98
@@ -1 +0,0 @@
1
- {"version":3,"file":"figure-quality.js","sourceRoot":"","sources":["../../../server/service/figure/figure-quality.ts"],"names":[],"mappings":"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","sourcesContent":["import { sizingReport, topViewEvidence } from '@hatiolab/figure-model'\nimport type { FigureBlueprint, FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'\n\n/**\n * A deterministic design review for an AI candidate.\n *\n * This deliberately does not claim to recognise beauty from JSON. It reports\n * only what the Figure model can prove: stable part names, palette focus,\n * declared runtime budget, and whether the assembly survives resizing.\n */\nexport type FigureQualityStatus = 'ready' | 'review'\nexport type FigureQualityDimension = 'identity' | 'silhouette' | 'palette' | 'performance' | 'resilience'\n\nexport interface FigureQualityFinding {\n dimension: FigureQualityDimension\n code: string\n message: string\n}\n\nexport interface FigureQualityReview {\n /** Clear measurable checks, not an aesthetic certificate. */\n status: FigureQualityStatus\n summary: string\n findings: FigureQualityFinding[]\n /** Reproducible top-view evidence for a later render/vision critique loop. */\n visual: ReturnType<typeof topViewEvidence>\n}\n\nconst PLACEHOLDER_PART_NAME = /^(?:part|item|shape|cube|box)[-_]?\\d+$/\n\n/**\n * Reviews a valid, compiled source. It never rejects a candidate: a crate can\n * intentionally be a set of boxes, so the author retains the decision.\n */\nexport function reviewFigureQuality(\n source: FigureSource,\n blueprint: FigureBlueprint,\n score: FigureScore,\n cost: FigureCost\n): FigureQualityReview {\n const findings: FigureQualityFinding[] = []\n const visual = topViewEvidence(source)\n const placeholderNames = source.parts.filter(part => PLACEHOLDER_PART_NAME.test(part.name)).map(part => part.name)\n\n if (placeholderNames.length > 0) {\n findings.push({\n dimension: 'identity',\n code: 'generic-part-name',\n message: `Rename ${placeholderNames.join(', ')} for the real component; part names are stable binding identifiers.`\n })\n }\n\n const primitives = new Set(source.parts.map(part => part.primitive))\n if (source.parts.length >= 4 && [...primitives].every(primitive => primitive === 'cube' || primitive === 'wall' || primitive === 'rect')) {\n findings.push({\n dimension: 'silhouette',\n code: 'box-only-silhouette',\n message: 'This candidate is built only from rectangular primitives. Confirm that its top-view silhouette remains recognisable at board scale.'\n })\n }\n\n const tokens = new Set(source.parts.map(part => part.material.token))\n if (tokens.size > 3) {\n findings.push({\n dimension: 'palette',\n code: 'palette-too-broad',\n message: `The candidate uses ${tokens.size} material tokens. Confirm each colour carries meaning; low-poly figures usually read better with a focused palette.`\n })\n }\n\n if (score.grade === 'C' || score.grade === 'D') {\n findings.push({\n dimension: 'performance',\n code: 'budget-tight',\n message: `Runtime budget grade ${score.grade}: ${score.tightest.label} uses ${score.tightest.used}/${score.tightest.limit}. Simplify before broad placement if possible.`\n })\n }\n\n for (const finding of sizingReport(blueprint).findings) {\n findings.push({ dimension: 'resilience', code: finding.code, message: finding.message })\n }\n\n if (findings.length === 0) {\n return {\n status: 'ready',\n summary: `Model-grounded review is clear: grade ${score.grade}, ${cost.triangles} triangles, ${cost.drawCalls} draw calls per instance.`,\n findings,\n visual\n }\n }\n\n return {\n status: 'review',\n summary: `Needs author review: ${findings.length} measurable design consideration${findings.length === 1 ? '' : 's'} found.`,\n findings,\n visual\n }\n}\n"]}
@@ -1,93 +0,0 @@
1
- /**
2
- * Figure AI의 결정적 종단 흐름.
3
- *
4
- * 외부 공급자·DB를 띄우지 않되, 도크 host context → 도구 → 실제 후보 생성/검사 → 후보 카드라는
5
- * 경계를 한 번에 지난다. 단위 시험만으로는 피드백이 도구 입력에서 끊기거나, 검증된 후보가 카드
6
- * 표시를 잃는 연결 결함을 놓칠 수 있다.
7
- */
8
- import { clearToolRegistry, getToolCategories, setDefaultAIClient } from '@things-factory/ai-client-base'
9
- import type { AIClient } from '@things-factory/ai-client-base'
10
-
11
- import { __setFigureToolDepsForTest, registerFigureToolCategories } from './figure-tools'
12
- import { proposeFigure } from './figure-propose'
13
-
14
- const DOMAIN = { id: 'domain-mine' }
15
- const SOURCE = {
16
- version: 2, type: 'MIXER', base: { x: 300, y: 500, z: 300 }, detailLevel: 'M',
17
- parts: [{
18
- name: 'body', primitive: 'cylinder', segments: 12,
19
- transform: { position: { x: 0, y: 250, z: 0 }, size: { x: 300, y: 400, z: 300 } },
20
- material: { token: 'palette.primary' }
21
- }]
22
- }
23
- const ROW = { id: 'fig-1', type: 'MIXER', name: '믹서', state: 'draft', domain: DOMAIN, source: JSON.stringify(SOURCE) }
24
-
25
- function client(): AIClient & { sent: any[][] } {
26
- const fake = {
27
- id: 'deterministic-figure-flow',
28
- sent: [] as any[][],
29
- async chat() { return '' },
30
- async generateJSON(messages: any[]) {
31
- fake.sent.push(JSON.parse(JSON.stringify(messages)))
32
- return SOURCE
33
- }
34
- }
35
- return fake as unknown as AIClient & { sent: any[][] }
36
- }
37
-
38
- beforeEach(() => {
39
- clearToolRegistry()
40
- const model = require('@hatiolab/figure-model')
41
- __setFigureToolDepsForTest({
42
- getRepository: () => ({
43
- async findOne({ where }: any) { return where.id === ROW.id && where.domain?.id === DOMAIN.id ? ROW : null },
44
- async find({ where }: any) { return where.domain?.id === DOMAIN.id ? [ROW] : [] }
45
- }),
46
- Figure: class {},
47
- inspectSource: () => ({ blocked: false, findings: [] }),
48
- proposeFigure,
49
- compile: model.compile,
50
- costOf: model.costOf,
51
- scoreOf: model.scoreOf,
52
- validate: model.validate
53
- })
54
- registerFigureToolCategories()
55
- })
56
-
57
- afterEach(() => {
58
- clearToolRegistry()
59
- setDefaultAIClient(undefined)
60
- })
61
-
62
- test('초안·세션 피드백을 반영한 후보가 검증·품질 근거와 카드 표시를 거쳐 돌아온다', async () => {
63
- const fake = client()
64
- setDefaultAIClient(fake)
65
- const propose = getToolCategories()
66
- .find((category: any) => category.name === 'figure-authoring')!
67
- .specs.find((spec: any) => spec.name === 'proposeFigureCandidate') as any
68
-
69
- const result = await propose.builder(
70
- { prompt: '포트가 더 잘 읽히도록 고쳐줘' },
71
- {
72
- state: { domain: DOMAIN },
73
- host: {
74
- figureId: 'fig-1',
75
- palette: ['palette.primary'],
76
- draftSource: SOURCE,
77
- proposalFeedback: [{
78
- outcome: 'discarded', selectedChanges: 0, totalChanges: 3,
79
- note: '실루엣은 유지하고 포트의 대비를 높여줘',
80
- quality: { status: 'review', findingCodes: ['top-view-readability'] }
81
- }]
82
- }
83
- }
84
- )
85
-
86
- expect(fake.sent).toHaveLength(1)
87
- expect(fake.sent[0]![0]!.content).toContain('Revise this existing figure')
88
- expect(fake.sent[0]![0]!.content).toContain('실루엣은 유지하고 포트의 대비를 높여줘')
89
- expect(result.proposed).toBe(true)
90
- expect(result.source.type).toBe('MIXER')
91
- expect(result.quality).toEqual(expect.objectContaining({ status: expect.stringMatching(/^(ready|review)$/) }))
92
- expect(result.cost.triangles).toBeGreaterThan(0)
93
- })
@@ -1,72 +0,0 @@
1
- /**
2
- * AI 로 figure 를 완성하는 길에 막히는 데가 있나 — 끝까지 걸어 본다.
3
- *
4
- * 개별 단계는 각자 시험이 있다. 없던 것은 **이어 붙였을 때**다. 만드는 데까지 되는데
5
- * 발행에서 막히면 저작자는 AI 로 시작해서 손으로 끝내야 하고, 그것은 이 기능이 있으나 마나다.
6
- */
7
- import { createAIClient, setDefaultAIClient } from '@things-factory/ai-client-base'
8
- import '@things-factory/ai-client-base'
9
-
10
- import { proposeFigure } from './figure-propose'
11
- import { inspectSource } from './figure-gate'
12
- import { parseSource } from './figure-source'
13
-
14
- const PALETTE = ['palette.primary', 'palette.accent', 'palette.neutral']
15
- const on = process.env.RUN_FIGURE_LLM_SMOKE === '1'
16
- const describeIf = on ? describe : describe.skip
17
-
18
- describeIf('AI 로 만든 figure 가 발행까지 가나', () => {
19
- beforeAll(() => {
20
- setDefaultAIClient(createAIClient({ provider: 'gemini', apiKey: process.env.GEMINI_API_KEY!, model: 'gemini-3.5-flash' } as any))
21
- })
22
- afterAll(() => setDefaultAIClient(undefined))
23
-
24
- const walk = async (label: string, prompt: string, type: string) => {
25
- const made = await proposeFigure({ prompt, type, palette: PALETTE, refine: true })
26
-
27
- /* 1. 저장 문을 지나는가 — 형식 오류면 여기서 던진다. */
28
- const saved = parseSource(JSON.stringify(made.source))
29
-
30
- /* 2. 발행 게이트를 지나는가 — 여기서 막히면 AI 로 시작해 손으로 끝내야 한다. */
31
- const gate = inspectSource(saved.source)
32
- const blocking = gate.findings.filter(f => f.blocking)
33
-
34
- // eslint-disable-next-line no-console
35
- console.log(
36
- `[${label}] 부품 ${made.source.parts.length} · 삼각형 ${made.cost.triangles} · 등급 ${made.score.grade}` +
37
- ` · 위반 ${made.violations.length} · 발행 ${gate.blocked ? '막힘: ' + blocking.map(f => f.code + '(' + (f.at ?? '') + ')').join(', ') : '통과'}`
38
- )
39
- return { made, gate, blocking }
40
- }
41
-
42
- test('늘려도 안 부서지는 것을 만든다 — 앵커를 가르친 뒤', async () => {
43
- const { gate, blocking } = await walk(
44
- '신호탑',
45
- 'a signal tower: a tall slim pole standing on a foot, with three stacked lamps at the very top. the lamps must stay on top of the pole when the tower is made taller.',
46
- 'SIGNAL_TOWER_E2E'
47
- )
48
- expect(gate.blocked).toBe(false)
49
- expect(blocking).toEqual([])
50
- }, 120_000)
51
-
52
- test('반복하는 것을 만든다 — 컨베이어', async () => {
53
- const { made, gate } = await walk(
54
- '컨베이어',
55
- 'a roller conveyor: a frame with rollers repeating along its length. when the conveyor is made longer the number of rollers must grow, not their size.',
56
- 'CONVEYOR_E2E'
57
- )
58
- expect(gate.blocked).toBe(false)
59
- expect(made.source.parts.some((p: any) => p.sizing === 'repeat')).toBe(true)
60
- }, 120_000)
61
-
62
- test('움직이는 것을 만든다 — 문', async () => {
63
- const { made, gate } = await walk(
64
- '자동문',
65
- 'a two-panel automatic door in a frame. add an animation clip that opens both panels together by sliding them apart.',
66
- 'AUTO_DOOR_E2E'
67
- )
68
- expect(gate.blocked).toBe(false)
69
- expect(made.source.animations?.length).toBeGreaterThan(0)
70
- expect(made.source.animations![0]!.channels.length).toBeGreaterThan(0)
71
- }, 120_000)
72
- })