@things-factory/figure-service 10.1.6 → 10.1.8

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 (42) hide show
  1. package/dist-server/service/figure/figure-gate.d.ts +18 -0
  2. package/dist-server/service/figure/figure-gate.js +52 -1
  3. package/dist-server/service/figure/figure-gate.js.map +1 -1
  4. package/dist-server/service/figure/figure-mutation.js +8 -0
  5. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  6. package/dist-server/service/figure/figure-propose-type.d.ts +30 -0
  7. package/dist-server/service/figure/figure-propose-type.js +112 -1
  8. package/dist-server/service/figure/figure-propose-type.js.map +1 -1
  9. package/dist-server/service/figure/figure-propose.d.ts +25 -1
  10. package/dist-server/service/figure/figure-propose.js +102 -4
  11. package/dist-server/service/figure/figure-propose.js.map +1 -1
  12. package/dist-server/service/figure/figure-quality.d.ts +29 -0
  13. package/dist-server/service/figure/figure-quality.js +62 -0
  14. package/dist-server/service/figure/figure-quality.js.map +1 -0
  15. package/dist-server/service/figure/figure-query.js +8 -1
  16. package/dist-server/service/figure/figure-query.js.map +1 -1
  17. package/dist-server/service/figure/figure-tools.js +55 -9
  18. package/dist-server/service/figure/figure-tools.js.map +1 -1
  19. package/dist-server/service/figure/figure-type.d.ts +13 -0
  20. package/dist-server/service/figure/figure-type.js +51 -1
  21. package/dist-server/service/figure/figure-type.js.map +1 -1
  22. package/dist-server/service/figure/figure.d.ts +17 -1
  23. package/dist-server/service/figure/figure.js +6 -1
  24. package/dist-server/service/figure/figure.js.map +1 -1
  25. package/dist-server/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +6 -6
  27. package/server/service/figure/catalog-db.test.ts +162 -0
  28. package/server/service/figure/figure-ai-flow.test.ts +95 -0
  29. package/server/service/figure/figure-gate.test.ts +32 -0
  30. package/server/service/figure/figure-gate.ts +71 -2
  31. package/server/service/figure/figure-llm-smoke.test.ts +91 -0
  32. package/server/service/figure/figure-mutation.ts +8 -0
  33. package/server/service/figure/figure-propose-type.ts +80 -0
  34. package/server/service/figure/figure-propose.test.ts +77 -0
  35. package/server/service/figure/figure-propose.ts +137 -4
  36. package/server/service/figure/figure-quality.test.ts +43 -0
  37. package/server/service/figure/figure-quality.ts +98 -0
  38. package/server/service/figure/figure-query.ts +8 -1
  39. package/server/service/figure/figure-tools.test.ts +62 -1
  40. package/server/service/figure/figure-tools.ts +60 -8
  41. package/server/service/figure/figure-type.ts +41 -0
  42. package/server/service/figure/figure.ts +22 -2
@@ -55,7 +55,8 @@ function getDeps(): any {
55
55
  proposeFigure: propose.proposeFigure,
56
56
  compile: model.compile,
57
57
  costOf: model.costOf,
58
- scoreOf: model.scoreOf
58
+ scoreOf: model.scoreOf,
59
+ validate: model.validate
59
60
  }
60
61
  }
61
62
  return _deps
@@ -92,6 +93,51 @@ async function figureInDomain(figureId: string, domainId: string): Promise<any>
92
93
  return found
93
94
  }
94
95
 
96
+ /**
97
+ * 모델러가 아직 저장하지 않은 source를 도구가 읽는 유일한 길.
98
+ *
99
+ * host context는 브라우저가 보낸 값이므로 권한 근거가 될 수 없다. 먼저 `row`를 현재 도메인에서
100
+ * 찾은 뒤에만 보며, Figure의 불변 type과 형식 검증을 다시 확인한다. 검증되지 않은 큰 객체를 LLM
101
+ * 프롬프트로 옮기면 비용만 태우고, DB에 있던 Figure와 다른 것을 고친다고 잘못 말할 수 있다.
102
+ */
103
+ function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
104
+ const draft = host?.draftSource
105
+ if (!draft) {
106
+ return { source: row.source ? JSON.parse(row.source) : null, draft: false }
107
+ }
108
+
109
+ if (typeof draft !== 'object' || Array.isArray(draft)) {
110
+ throw new Error('the unsaved draft is not a FigureSource object')
111
+ }
112
+
113
+ const serialized = JSON.stringify(draft)
114
+ if (serialized.length > 100_000) {
115
+ throw new Error('the unsaved draft is too large to safely review')
116
+ }
117
+
118
+ const { validate } = getDeps()
119
+ const checked = validate(draft)
120
+ if (checked.errors.length > 0) {
121
+ throw new Error(`the unsaved draft is invalid: ${checked.errors.map((one: any) => one.message).join(' · ')}`)
122
+ }
123
+ if (draft.type !== row.type) {
124
+ throw new Error(`the unsaved draft type '${draft.type}' does not match Figure '${row.type}'`)
125
+ }
126
+
127
+ return { source: draft, draft: true }
128
+ }
129
+
130
+ /**
131
+ * 후보를 냈던 바로 그 Figure의 세션 반응만 후보 생성기에 넘긴다.
132
+ *
133
+ * 도크의 host context는 브라우저 값이므로 이 함수는 접근 권한을 판단하지 않는다. 이미 현재
134
+ * 도메인에서 Figure를 찾은 뒤, `proposeFigure`가 다시 길이·형식을 정리할 참고 데이터만 건넨다.
135
+ */
136
+ function feedbackForTool(host: any, figureId?: string): unknown[] | undefined {
137
+ if (!figureId || host?.figureId !== figureId || !Array.isArray(host?.proposalFeedback)) return undefined
138
+ return host.proposalFeedback.slice(-5)
139
+ }
140
+
95
141
  /**
96
142
  * 무엇을 고치라는 것인지 대화면 문맥에서 해소한다.
97
143
  *
@@ -163,21 +209,22 @@ function figureToolSpecs(): ToolSpec[] {
163
209
  const figureId = figureIdFrom(args, ctx?.state?.host)
164
210
  const row = await figureInDomain(figureId, domainId)
165
211
 
166
- if (!row.source) {
212
+ const current = sourceForTool(row, ctx?.state?.host)
213
+ if (!current.source) {
167
214
  // 저장된 정본이 없는 상태가 실제로 있다(새로 만들고 저장 안 한 것). 지어내지 않는다.
168
215
  return { id: row.id, type: row.type, name: row.name, source: null, cost: null }
169
216
  }
170
217
 
171
218
  const { compile, costOf, scoreOf } = getDeps()
172
- const source = JSON.parse(row.source)
173
- const blueprint = compile(source)
219
+ const blueprint = compile(current.source)
174
220
 
175
221
  return {
176
222
  id: row.id,
177
223
  type: row.type,
178
224
  name: row.name,
179
225
  state: row.state,
180
- source,
226
+ source: current.source,
227
+ draft: current.draft,
181
228
  cost: costOf(blueprint, 100),
182
229
  score: scoreOf(blueprint)
183
230
  }
@@ -196,8 +243,9 @@ function figureToolSpecs(): ToolSpec[] {
196
243
  const figureId = figureIdFrom(args, ctx?.state?.host)
197
244
  const row = await figureInDomain(figureId, domainId)
198
245
 
246
+ const current = sourceForTool(row, ctx?.state?.host)
199
247
  const { inspectSource } = getDeps()
200
- return inspectSource(row.source)
248
+ return { ...inspectSource(current.source ? JSON.stringify(current.source) : null), draft: current.draft }
201
249
  }
202
250
  },
203
251
  {
@@ -244,7 +292,7 @@ function figureToolSpecs(): ToolSpec[] {
244
292
  const open = typeof args?.figureId === 'string' ? args.figureId : ctx?.state?.host?.figureId
245
293
  if (typeof open === 'string' && open.trim()) {
246
294
  const row = await figureInDomain(open.trim(), domainId)
247
- if (row.source) base = JSON.parse(row.source)
295
+ base = sourceForTool(row, ctx?.state?.host).source ?? undefined
248
296
  }
249
297
  }
250
298
 
@@ -258,11 +306,14 @@ function figureToolSpecs(): ToolSpec[] {
258
306
  }
259
307
 
260
308
  const { proposeFigure } = getDeps()
309
+ const feedback = feedbackForTool(ctx?.state?.host, base ? figureIdFrom(args, ctx?.state?.host) : undefined)
261
310
  const result = await proposeFigure({
262
311
  prompt,
263
312
  palette,
264
313
  base,
265
- type: typeof args?.type === 'string' ? args.type : undefined
314
+ type: typeof args?.type === 'string' ? args.type : undefined,
315
+ feedback,
316
+ refine: !!feedback?.length
266
317
  })
267
318
 
268
319
  /*
@@ -283,6 +334,7 @@ function figureToolSpecs(): ToolSpec[] {
283
334
  source: result.source,
284
335
  score: result.score,
285
336
  cost: result.cost,
337
+ quality: result.quality,
286
338
  violations: result.violations,
287
339
  attempts: result.attempts
288
340
  }
@@ -22,6 +22,9 @@ export class NewFigure {
22
22
  @Field({ nullable: true, description: 'Classification code used to group figures in the palette.' })
23
23
  category?: string
24
24
 
25
+ @Field(type => [String], { nullable: true, description: 'Free-form tags. A figure may carry several.' })
26
+ tags?: string[]
27
+
25
28
  @Field({ description: 'The canonical FigureSource JSON.' })
26
29
  source: string
27
30
 
@@ -52,6 +55,13 @@ export class FigurePatch {
52
55
  @Field({ nullable: true, description: 'The new classification code.' })
53
56
  category?: string
54
57
 
58
+ /*
59
+ 빈 배열과 안 준 것을 가른다. `undefined` 는 「건드리지 않는다」, `[]` 는 「전부 지운다」다.
60
+ 둘을 뭉개면 태그를 다 지우려는 저작자가 지울 방법을 잃는다.
61
+ */
62
+ @Field(type => [String], { nullable: true, description: 'The new tags. An empty list clears them; omitting the field leaves them.' })
63
+ tags?: string[]
64
+
55
65
  @Field({ nullable: true, description: 'The new FigureSource JSON.' })
56
66
  source?: string
57
67
 
@@ -118,6 +128,31 @@ export class FigureSaveResult {
118
128
  * 것들이라 그 칸이 필요 없었다. 발행 판정은 막는 것과 알리는 것을 함께 내므로, 어느 쪽인지를 값이
119
129
  * 스스로 말해야 한다 — 코드를 보고 화면이 다시 분류하면 규칙이 두 벌이 된다.
120
130
  */
131
+ /** A measured, one-anchor repair candidate for a release-gate sizing split. */
132
+ @ObjectType({ description: 'A reversible anchor change measured to reduce a sizing split.' })
133
+ export class FigureGateFix {
134
+ @Field({ description: 'Sizing test that this candidate improves, for example y or x-y.' })
135
+ findingWay: string
136
+
137
+ @Field({ description: 'Stable FigureSource part name to change.' })
138
+ part: string
139
+
140
+ @Field({ description: 'Axis whose anchor will change.' })
141
+ axis: string
142
+
143
+ @Field({ nullable: true, description: 'Current explicit anchor, if one is present.' })
144
+ from?: string
145
+
146
+ @Field({ description: 'Suggested explicit anchor: min, center, max, or span.' })
147
+ to: string
148
+
149
+ @Field(type => Int, { description: 'Number of blocking sizing-split checks removed by this one change.' })
150
+ resolved: number
151
+
152
+ @Field({ description: 'True only when every blocking sizing check is clear after remeasurement.' })
153
+ safe: boolean
154
+ }
155
+
121
156
  @ObjectType({ description: 'One finding from the release gate.' })
122
157
  export class FigureGateFinding {
123
158
  @Field({ description: 'Stable code — figure-model validation codes, or sizing-splits / sizing-loose.' })
@@ -135,6 +170,12 @@ export class FigureGateFinding {
135
170
  @Field({ nullable: true, description: 'Where it points: part names that came apart, or a path in the source.' })
136
171
  at?: string
137
172
 
173
+ @Field(type => [FigureGateFix], {
174
+ nullable: true,
175
+ description: 'Measured repair candidates. They are previews until the author explicitly applies one.'
176
+ })
177
+ fixes?: FigureGateFix[]
178
+
138
179
  @Field({ description: 'True when this finding blocks the release.' })
139
180
  blocking: boolean
140
181
  }
@@ -89,11 +89,31 @@ export class Figure {
89
89
  @Field({ nullable: true, description: 'A detailed description for the figure.' })
90
90
  description?: string
91
91
 
92
- /** 분류 코드. 팔레트에서 묶어 보이는 기준. */
92
+ /**
93
+ * 분류 하나. **팔레트에서 묶어 보이는 기준이다.**
94
+ *
95
+ * 도형 하나에 하나뿐이다 — 팔레트는 목록을 한 벌로 나눠 보여 주므로, 한 도형이 두 묶음에
96
+ * 동시에 있으면 저작자가 같은 것을 두 번 만난다.
97
+ */
93
98
  @Column({ nullable: true })
94
- @Field({ nullable: true, description: 'Classification code used to group figures in the palette.' })
99
+ @Field({ nullable: true, description: 'Classification code used to group figures in the palette. One per figure.' })
95
100
  category?: string
96
101
 
102
+ /**
103
+ * 태그. **여럿 붙는다.**
104
+ *
105
+ * 분류와 다른 일을 한다. 분류는 「이것이 무엇인가」를 한 번 정하는 것이고, 태그는 「어디에
106
+ * 걸리는가」를 여러 갈래로 적는 것이다 — 컨베이어이면서 반출 구역의 것일 수 있다. 그래서
107
+ * 분류로는 못 하는 찾기가 된다.
108
+ *
109
+ * `simple-json` 으로 담는다. 사내 선례가 그것이고(`ops-master` · `organization` ·
110
+ * `personalization`), 드라이버마다 배열 타입이 다른 것을 피한다.
111
+ */
112
+ /* `simple-json` 과 `default: null` 은 사내 선례 그대로다 — board-template · attachment 가 같은 모양이다. */
113
+ @Column('simple-json', { nullable: true, default: null })
114
+ @Field(type => [String], { nullable: true, description: 'Free-form tags. A figure may carry several.' })
115
+ tags?: string[]
116
+
97
117
  @Column({ nullable: true, default: 'draft' })
98
118
  @Field({ nullable: true, description: "Lifecycle state — 'draft' figures do not appear in the board palette." })
99
119
  state?: 'draft' | 'released'