@things-factory/figure-service 10.1.33 → 10.1.35
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/dist-server/service/figure/figure-gate.d.ts +13 -0
- package/dist-server/service/figure/figure-gate.js +133 -0
- package/dist-server/service/figure/figure-gate.js.map +1 -1
- package/dist-server/service/figure/figure-propose.js +9 -1
- package/dist-server/service/figure/figure-propose.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +24 -9
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/service/figure/figure-type.d.ts +6 -0
- package/dist-server/service/figure/figure-type.js +26 -1
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/figure/figure-gate.test.ts +20 -0
- package/server/service/figure/figure-gate.ts +145 -0
- package/server/service/figure/figure-propose.test.ts +23 -3
- package/server/service/figure/figure-propose.ts +10 -1
- package/server/service/figure/figure-tools.ts +22 -9
- package/server/service/figure/figure-type.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-service",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.35",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
"migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@hatiolab/figure-model": "^0.1.
|
|
26
|
+
"@hatiolab/figure-model": "^0.1.32",
|
|
27
27
|
"@operato/typeorm-history": "^10.0.0",
|
|
28
28
|
"@things-factory/ai-client-base": "^10.1.31",
|
|
29
|
-
"@things-factory/auth-base": "^10.1.
|
|
29
|
+
"@things-factory/auth-base": "^10.1.34",
|
|
30
30
|
"@things-factory/env": "^10.1.20",
|
|
31
|
-
"@things-factory/shell": "^10.1.
|
|
31
|
+
"@things-factory/shell": "^10.1.34",
|
|
32
32
|
"graphql-scalars": "^1.22.4",
|
|
33
33
|
"graphql-upload": "^15.0.2",
|
|
34
34
|
"koa-router": "^7.4.0"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "396f43b648b704f1938a2d890ab2f6efa33fdb86"
|
|
37
37
|
}
|
|
@@ -30,6 +30,26 @@ describe('release-gate repair candidates', () => {
|
|
|
30
30
|
]))
|
|
31
31
|
expect(split?.fixes?.every(fix => fix.resolved > 0)).toBe(true)
|
|
32
32
|
})
|
|
33
|
+
|
|
34
|
+
it('offers one coordinated draft repair only after the full sizing suite clears', () => {
|
|
35
|
+
const source = {
|
|
36
|
+
version: 2,
|
|
37
|
+
type: 'STACK',
|
|
38
|
+
base: { x: 100, y: 200, z: 100 },
|
|
39
|
+
detailLevel: 'M',
|
|
40
|
+
parts: [
|
|
41
|
+
{ name: 'body', primitive: 'cube', transform: { position: { x: 0, y: 50, z: 0 }, size: { x: 100, y: 100, z: 100 } }, material: { token: 'palette.primary' }, anchor: { y: 'min' } },
|
|
42
|
+
{ name: 'cap-a', primitive: 'cube', transform: { position: { x: -25, y: 110, z: 0 }, size: { x: 20, y: 20, z: 20 } }, material: { token: 'palette.accent' }, anchor: { y: 'max' } },
|
|
43
|
+
{ name: 'cap-b', primitive: 'cube', transform: { position: { x: 25, y: 110, z: 0 }, size: { x: 20, y: 20, z: 20 } }, material: { token: 'palette.accent' }, anchor: { y: 'max' } }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const split = inspectSource(JSON.stringify(source)).findings.find(finding => finding.code === 'sizing-splits')
|
|
48
|
+
|
|
49
|
+
expect(split?.fixPlan).toEqual(expect.objectContaining({ safe: true, resolved: expect.any(Number) }))
|
|
50
|
+
expect(split?.fixPlan?.fixes.length).toBeGreaterThan(0)
|
|
51
|
+
expect(split?.fixPlan?.fixes.every(fix => fix.safe)).toBe(true)
|
|
52
|
+
})
|
|
33
53
|
})
|
|
34
54
|
|
|
35
55
|
/*
|
|
@@ -13,6 +13,18 @@ export interface GateFix {
|
|
|
13
13
|
safe: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A coordinated repair, measured from the current source all the way through the sizing suite.
|
|
18
|
+
* A split often involves several independently anchored parts, so no single anchor edit can be
|
|
19
|
+
* called a solution. This remains a draft-only edit; it is never saved or released by the gate.
|
|
20
|
+
*/
|
|
21
|
+
export interface GateFixPlan {
|
|
22
|
+
fixes: GateFix[]
|
|
23
|
+
resolved: number
|
|
24
|
+
/** True only after the complete plan leaves no release-blocking sizing split. */
|
|
25
|
+
safe: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
/**
|
|
17
29
|
* 발행해도 되나 — **서버가 판정한다.**
|
|
18
30
|
*
|
|
@@ -65,6 +77,8 @@ export interface GateFinding {
|
|
|
65
77
|
* 수락하기 전에는 정본을 바꾸지 않는다.
|
|
66
78
|
*/
|
|
67
79
|
fixes?: GateFix[]
|
|
80
|
+
/** The one-click, fully remeasured repair when individual candidates are insufficient. */
|
|
81
|
+
fixPlan?: GateFixPlan
|
|
68
82
|
blocking: boolean
|
|
69
83
|
}
|
|
70
84
|
|
|
@@ -122,6 +136,134 @@ function measuredFixes(source: FigureSource, factor: number): GateFix[] {
|
|
|
122
136
|
return fixes.sort((a, b) => b.resolved - a.resolved || a.part.localeCompare(b.part)).slice(0, 12)
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
function sourceWithFix(source: FigureSource, fix: Pick<GateFix, 'part' | 'axis' | 'to'>): FigureSource {
|
|
140
|
+
return {
|
|
141
|
+
...source,
|
|
142
|
+
parts: source.parts.map(part => part.name === fix.part
|
|
143
|
+
? { ...part, anchor: { ...(part.anchor ?? {}), [fix.axis]: fix.to } }
|
|
144
|
+
: part)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function splitCount(source: FigureSource, factor: number) {
|
|
149
|
+
return sizingReport(compile(source), factor).findings.filter(finding => finding.code === 'sizing-splits').length
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Find a small coordinated repair by repeatedly choosing the measured anchor edit that removes
|
|
154
|
+
* the most remaining splits. Every step is evaluated against the real compiler; prose is never
|
|
155
|
+
* interpreted. If the complete suite is not clear, no "recommended" button is exposed.
|
|
156
|
+
*/
|
|
157
|
+
function measuredFixPlan(source: FigureSource, factor: number): GateFixPlan | undefined {
|
|
158
|
+
const originalCount = splitCount(source, factor)
|
|
159
|
+
if (originalCount === 0) return undefined
|
|
160
|
+
|
|
161
|
+
let working = source
|
|
162
|
+
let remaining = originalCount
|
|
163
|
+
const used = new Set<string>()
|
|
164
|
+
const fixes: GateFix[] = []
|
|
165
|
+
|
|
166
|
+
for (let step = 0; step < source.parts.length * 3 && remaining > 0; step++) {
|
|
167
|
+
const report = sizingReport(compile(working), factor)
|
|
168
|
+
let best: GateFix | undefined
|
|
169
|
+
|
|
170
|
+
for (const finding of report.findings) {
|
|
171
|
+
if (finding.code !== 'sizing-splits' || !finding.way) continue
|
|
172
|
+
const axes = finding.way.split('-').filter((axis): axis is Axis => ['x', 'y', 'z'].includes(axis))
|
|
173
|
+
const names = new Set(finding.clumps.flat())
|
|
174
|
+
|
|
175
|
+
for (const part of working.parts) {
|
|
176
|
+
if (!names.has(part.name)) continue
|
|
177
|
+
for (const axis of axes) {
|
|
178
|
+
const key = `${part.name}:${axis}`
|
|
179
|
+
if (used.has(key)) continue
|
|
180
|
+
for (const to of ['min', 'center', 'max', 'span'] as const) {
|
|
181
|
+
if (part.anchor?.[axis] === to) continue
|
|
182
|
+
const afterCount = splitCount(sourceWithFix(working, { part: part.name, axis, to }), factor)
|
|
183
|
+
const resolved = remaining - afterCount
|
|
184
|
+
if (resolved <= 0) continue
|
|
185
|
+
const candidate: GateFix = {
|
|
186
|
+
findingWay: finding.way,
|
|
187
|
+
part: part.name,
|
|
188
|
+
axis,
|
|
189
|
+
from: source.parts.find(one => one.name === part.name)?.anchor?.[axis],
|
|
190
|
+
to,
|
|
191
|
+
resolved,
|
|
192
|
+
safe: false
|
|
193
|
+
}
|
|
194
|
+
if (!best || candidate.resolved > best.resolved ||
|
|
195
|
+
(candidate.resolved === best.resolved && `${candidate.part}:${candidate.axis}:${candidate.to}` < `${best.part}:${best.axis}:${best.to}`)) {
|
|
196
|
+
best = candidate
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!best) break
|
|
204
|
+
fixes.push(best)
|
|
205
|
+
used.add(`${best.part}:${best.axis}`)
|
|
206
|
+
working = sourceWithFix(working, best)
|
|
207
|
+
remaining = splitCount(working, factor)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (remaining === 0 && fixes.length > 0) {
|
|
211
|
+
return { fixes: fixes.map(fix => ({ ...fix, safe: true })), resolved: originalCount, safe: true }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* Some assemblies need two anchors to move together. Neither first move removes a whole split,
|
|
216
|
+
* so greedy improvement alone cannot see the repair. Search a small, deterministic beam of those
|
|
217
|
+
* coordinated moves, and still return only a plan that the compiler proves clear.
|
|
218
|
+
*/
|
|
219
|
+
const firstReport = sizingReport(compile(source), factor)
|
|
220
|
+
const candidates = new Map<string, { part: string; axis: Axis }>()
|
|
221
|
+
for (const finding of firstReport.findings) {
|
|
222
|
+
if (finding.code !== 'sizing-splits' || !finding.way) continue
|
|
223
|
+
const axes = finding.way.split('-').filter((axis): axis is Axis => ['x', 'y', 'z'].includes(axis))
|
|
224
|
+
const names = new Set(finding.clumps.flat())
|
|
225
|
+
for (const part of source.parts) {
|
|
226
|
+
if (!names.has(part.name)) continue
|
|
227
|
+
for (const axis of axes) candidates.set(`${part.name}:${axis}`, { part: part.name, axis })
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
type SearchState = { source: FigureSource; fixes: GateFix[]; used: Set<string>; remaining: number }
|
|
232
|
+
let frontier: SearchState[] = [{ source, fixes: [], used: new Set(), remaining: originalCount }]
|
|
233
|
+
const visited = new Set<string>()
|
|
234
|
+
for (let depth = 0; depth < 8 && frontier.length; depth++) {
|
|
235
|
+
const next: SearchState[] = []
|
|
236
|
+
for (const state of frontier) {
|
|
237
|
+
for (const [key, target] of candidates) {
|
|
238
|
+
if (state.used.has(key)) continue
|
|
239
|
+
const current = state.source.parts.find(part => part.name === target.part)
|
|
240
|
+
if (!current) continue
|
|
241
|
+
for (const to of ['min', 'center', 'max', 'span'] as const) {
|
|
242
|
+
if (current.anchor?.[target.axis] === to) continue
|
|
243
|
+
const changed = sourceWithFix(state.source, { ...target, to })
|
|
244
|
+
const signature = JSON.stringify(changed.parts.map(part => [part.name, part.anchor]))
|
|
245
|
+
if (visited.has(signature)) continue
|
|
246
|
+
visited.add(signature)
|
|
247
|
+
const after = splitCount(changed, factor)
|
|
248
|
+
const fix: GateFix = {
|
|
249
|
+
findingWay: 'coordinated', part: target.part, axis: target.axis,
|
|
250
|
+
from: source.parts.find(part => part.name === target.part)?.anchor?.[target.axis], to,
|
|
251
|
+
resolved: state.remaining - after, safe: false
|
|
252
|
+
}
|
|
253
|
+
const fixes = [...state.fixes, fix]
|
|
254
|
+
if (after === 0) return { fixes: fixes.map(one => ({ ...one, safe: true })), resolved: originalCount, safe: true }
|
|
255
|
+
next.push({ source: changed, fixes, used: new Set([...state.used, key]), remaining: after })
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/* Keep enough alternatives to discover a paired repair without making release inspection expensive. */
|
|
260
|
+
frontier = next.sort((a, b) => a.remaining - b.remaining || a.fixes.length - b.fixes.length ||
|
|
261
|
+
JSON.stringify(a.fixes).localeCompare(JSON.stringify(b.fixes))).slice(0, 24)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
125
267
|
/**
|
|
126
268
|
* 정본 하나를 판정한다.
|
|
127
269
|
*
|
|
@@ -190,6 +332,7 @@ export function inspectSource(sourceJson?: string | null, releasedJson?: string
|
|
|
190
332
|
const source = parsed as FigureSource
|
|
191
333
|
const report = sizingReport(compile(source))
|
|
192
334
|
const fixes = measuredFixes(source, report.factor)
|
|
335
|
+
const fixPlan = measuredFixPlan(source, report.factor)
|
|
193
336
|
|
|
194
337
|
for (const finding of report.findings) {
|
|
195
338
|
findings.push({
|
|
@@ -201,6 +344,8 @@ export function inspectSource(sourceJson?: string | null, releasedJson?: string
|
|
|
201
344
|
/* 숫자가 아니라 자리를 말한다 — 어느 부품들이 갈라져 나왔나. */
|
|
202
345
|
at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,
|
|
203
346
|
fixes: finding.code === 'sizing-splits' ? fixes.filter(fix => fix.findingWay === finding.way) : undefined,
|
|
347
|
+
/* A plan is attached once: it clears the full suite, not merely this card's symptom. */
|
|
348
|
+
fixPlan: finding.code === 'sizing-splits' && finding === report.findings.find(one => one.code === 'sizing-splits') ? fixPlan : undefined,
|
|
204
349
|
blocking: finding.code === 'sizing-splits'
|
|
205
350
|
})
|
|
206
351
|
}
|
|
@@ -82,17 +82,19 @@ function candidate(over: Record<string, unknown> = {}) {
|
|
|
82
82
|
* 고치는 요청에서 「부품 이름을 바꾸지 말라」가 빠지면 AI 가 이름을 갈아 버리고, 그러면
|
|
83
83
|
* 이미 배치된 도면의 바인딩이 끊긴다. 답만 보는 모형으로는 그것이 나갔는지 알 수 없다.
|
|
84
84
|
*/
|
|
85
|
-
function stubClient(answers: unknown[]): AIClient & { calls: number; sent: any[][]; system?: string } {
|
|
85
|
+
function stubClient(answers: unknown[]): AIClient & { calls: number; sent: any[][]; options: any[]; system?: string } {
|
|
86
86
|
const client = {
|
|
87
87
|
id: 'stub',
|
|
88
88
|
calls: 0,
|
|
89
89
|
sent: [] as any[][],
|
|
90
|
+
options: [] as any[],
|
|
90
91
|
system: undefined as string | undefined,
|
|
91
92
|
async chat() {
|
|
92
93
|
return ''
|
|
93
94
|
},
|
|
94
|
-
async generateJSON(messages: any[], options?: { systemPrompt?: string }) {
|
|
95
|
+
async generateJSON(messages: any[], options?: { systemPrompt?: string; maxTokens?: number }) {
|
|
95
96
|
client.sent.push(JSON.parse(JSON.stringify(messages)))
|
|
97
|
+
client.options.push(options)
|
|
96
98
|
client.system = options?.systemPrompt
|
|
97
99
|
const answer = answers[client.calls]
|
|
98
100
|
client.calls++
|
|
@@ -100,7 +102,7 @@ function stubClient(answers: unknown[]): AIClient & { calls: number; sent: any[]
|
|
|
100
102
|
return answer
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
|
-
return client as unknown as AIClient & { calls: number; sent: any[][]; system?: string }
|
|
105
|
+
return client as unknown as AIClient & { calls: number; sent: any[][]; options: any[]; system?: string }
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/** 첫 요청에 실려 나간 사람 쪽 말. 이어 붙인 것까지 한 덩어리로 본다. */
|
|
@@ -266,6 +268,24 @@ describe('joints (ADR-0066)', () => {
|
|
|
266
268
|
})
|
|
267
269
|
|
|
268
270
|
describe('고치라는 요청일 때', () => {
|
|
271
|
+
it('편집 후보에는 JSON 완결에 충분한 출력 한도를 준다', async () => {
|
|
272
|
+
const client = stubClient([candidate()])
|
|
273
|
+
setDefaultAIClient(client as any)
|
|
274
|
+
|
|
275
|
+
await proposeFigure({ prompt: '색을 바꿔 줘', base: candidate() as any, palette: PALETTE })
|
|
276
|
+
|
|
277
|
+
expect((client as any).options?.[0]?.maxTokens).toBe(16384)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('새 Figure 후보에도 같은 출력 한도를 준다', async () => {
|
|
281
|
+
const client = stubClient([candidate()])
|
|
282
|
+
setDefaultAIClient(client as any)
|
|
283
|
+
|
|
284
|
+
await proposeFigure({ prompt: '교반기', type: 'MIXER', palette: PALETTE })
|
|
285
|
+
|
|
286
|
+
expect((client as any).options?.[0]?.maxTokens).toBe(16384)
|
|
287
|
+
})
|
|
288
|
+
|
|
269
289
|
it('요청이 안 건드린 것을 그대로 실으라고 못 박는다', async () => {
|
|
270
290
|
/*
|
|
271
291
|
이 문장이 없으면 애니메이션이 있는 도형을 「색만 바꿔 줘」로 고쳤을 때 모델이 clip 을
|
|
@@ -75,6 +75,12 @@ import type { FigureQualityReview } from './figure-quality.js'
|
|
|
75
75
|
/** 몇 번까지 다시 시키나. 이보다 늘려도 대개 같은 자리에서 막힌다. */
|
|
76
76
|
const MAX_ATTEMPTS = 3
|
|
77
77
|
|
|
78
|
+
/*
|
|
79
|
+
* Figure 후보는 새로 만들든 기존 것을 고치든 완결된 정본 하나를 JSON으로 낸다. 기본 4,096
|
|
80
|
+
* 토큰으로는 부품 수가 조금만 늘어도 JSON이 중간에서 끊겨 닫는 괄호를 잃는다.
|
|
81
|
+
*/
|
|
82
|
+
const FIGURE_GENERATE_MAX_TOKENS = 16384
|
|
83
|
+
|
|
78
84
|
export interface ProposeInput {
|
|
79
85
|
/** 사람이 시킨 말. */
|
|
80
86
|
prompt: string
|
|
@@ -478,7 +484,10 @@ export async function proposeFigure(input: ProposeInput): Promise<ProposeResult>
|
|
|
478
484
|
// 코드 울타리나 앞뒤 설명을 우리가 걷어내지 않아도 된다.
|
|
479
485
|
let answer: unknown
|
|
480
486
|
try {
|
|
481
|
-
answer = await client.generateJSON(messages, {
|
|
487
|
+
answer = await client.generateJSON(messages, {
|
|
488
|
+
systemPrompt: system,
|
|
489
|
+
maxTokens: FIGURE_GENERATE_MAX_TOKENS
|
|
490
|
+
})
|
|
482
491
|
} catch (e) {
|
|
483
492
|
// 모델이 JSON 을 못 냈다. 그것도 사유이므로 돌려주고 다시 시킨다.
|
|
484
493
|
reasons = [`The answer was not valid JSON: ${(e as Error).message}`]
|
|
@@ -408,15 +408,28 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
408
408
|
hostImage = { data: buf, mediaType: rawImage.mediaType || 'image/png' }
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
411
|
+
let result: any
|
|
412
|
+
try {
|
|
413
|
+
result = await proposeFigure({
|
|
414
|
+
prompt,
|
|
415
|
+
palette,
|
|
416
|
+
base,
|
|
417
|
+
type: typeof args?.type === 'string' ? args.type : undefined,
|
|
418
|
+
feedback,
|
|
419
|
+
refine: !!feedback?.length,
|
|
420
|
+
image: hostImage
|
|
421
|
+
})
|
|
422
|
+
} catch (error) {
|
|
423
|
+
/*
|
|
424
|
+
* 후보 생성은 세 번의 JSON·형식 검사 끝에 실패할 수 있다. 그 마지막 검사 사유를
|
|
425
|
+
* ProposeFailure.reasons 에만 두면 agentic-loop은 "만들지 못했습니다"만 받아
|
|
426
|
+
* 사용자 화면에는 "응답이 비어있습니다"가 된다. 재시도할 수 있는 말로 경계를
|
|
427
|
+
* 넘긴다. 다른 예외(권한·통신)는 원문을 보존한다.
|
|
428
|
+
*/
|
|
429
|
+
const reasons = Array.isArray((error as any)?.reasons) ? (error as any).reasons : []
|
|
430
|
+
if (reasons.length > 0) throw new Error([(error as Error).message, ...reasons].join('\n'))
|
|
431
|
+
throw error
|
|
432
|
+
}
|
|
420
433
|
|
|
421
434
|
/*
|
|
422
435
|
**`proposed: true` 가 카드를 만든다.** 이 표시가 없으면 후보는 도구 추적 안에 묻히고,
|
|
@@ -161,6 +161,18 @@ export class FigureGateFix {
|
|
|
161
161
|
safe: boolean
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
@ObjectType({ description: 'A coordinated, fully remeasured set of anchor changes that clears release-blocking sizing splits.' })
|
|
165
|
+
export class FigureGateFixPlan {
|
|
166
|
+
@Field(type => [FigureGateFix], { description: 'The draft-only anchor changes to apply together.' })
|
|
167
|
+
fixes: FigureGateFix[]
|
|
168
|
+
|
|
169
|
+
@Field(type => Int, { description: 'Number of blocking sizing checks cleared by the complete plan.' })
|
|
170
|
+
resolved: number
|
|
171
|
+
|
|
172
|
+
@Field({ description: 'True only when the full plan was remeasured with no sizing split left.' })
|
|
173
|
+
safe: boolean
|
|
174
|
+
}
|
|
175
|
+
|
|
164
176
|
@ObjectType({ description: 'One finding from the release gate.' })
|
|
165
177
|
export class FigureGateFinding {
|
|
166
178
|
@Field({ description: 'Stable code — figure-model validation codes, or sizing-splits / sizing-loose.' })
|
|
@@ -184,6 +196,12 @@ export class FigureGateFinding {
|
|
|
184
196
|
})
|
|
185
197
|
fixes?: FigureGateFix[]
|
|
186
198
|
|
|
199
|
+
@Field(type => FigureGateFixPlan, {
|
|
200
|
+
nullable: true,
|
|
201
|
+
description: 'A coordinated recommended repair, available only when the complete sizing suite clears.'
|
|
202
|
+
})
|
|
203
|
+
fixPlan?: FigureGateFixPlan
|
|
204
|
+
|
|
187
205
|
@Field({ description: 'True when this finding blocks the release.' })
|
|
188
206
|
blocking: boolean
|
|
189
207
|
}
|