@things-factory/figure-service 10.1.26 → 10.1.28
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 +4 -1
- package/dist-server/service/figure/figure-gate.js +40 -1
- package/dist-server/service/figure/figure-gate.js.map +1 -1
- package/dist-server/service/figure/figure-mutation.js +13 -7
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-propose.js +20 -3
- package/dist-server/service/figure/figure-propose.js.map +1 -1
- package/dist-server/service/figure/figure-query.js +11 -1
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +15 -2
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/figure/figure-gate.ts +43 -3
- package/server/service/figure/figure-mutation.ts +14 -8
- package/server/service/figure/figure-propose.test.ts +11 -1
- package/server/service/figure/figure-propose.ts +22 -4
- package/server/service/figure/figure-query.ts +12 -1
- package/server/service/figure/figure-tool-doors.test.ts +38 -0
- package/server/service/figure/figure-tools.test.ts +6 -1
- package/server/service/figure/figure-tools.ts +15 -2
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.28",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@hatiolab/figure-model": "^0.1.21",
|
|
27
27
|
"@operato/typeorm-history": "^10.0.0",
|
|
28
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
29
|
-
"@things-factory/auth-base": "^10.1.
|
|
28
|
+
"@things-factory/ai-client-base": "^10.1.28",
|
|
29
|
+
"@things-factory/auth-base": "^10.1.28",
|
|
30
30
|
"@things-factory/env": "^10.1.20",
|
|
31
|
-
"@things-factory/shell": "^10.1.
|
|
31
|
+
"@things-factory/shell": "^10.1.28",
|
|
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": "3a260a0f79bc6b41a692a103955bfcc541aff60e"
|
|
37
37
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { blocksRelease, compile, sizingReport, validate } from '@hatiolab/figure-model'
|
|
1
|
+
import { blocksRelease, compile, contractChanges, sizingReport, validate } from '@hatiolab/figure-model'
|
|
2
2
|
import type { Axis, FigureSource } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
/** Service transport shape. It deliberately contains only an explicit, reversible FigureSource edit. */
|
|
@@ -127,8 +127,11 @@ function measuredFixes(source: FigureSource, factor: number): GateFix[] {
|
|
|
127
127
|
*
|
|
128
128
|
* 정본이 없거나(아직 저장 전) JSON 이 아니면 **막는다** — 발행할 대상이 없는 것이다. 그 경우를
|
|
129
129
|
* 조용히 통과시키면 빈 것이 발행된다.
|
|
130
|
+
*
|
|
131
|
+
* `releasedJson` 은 **직전 발행판의 정본**이다. 주면 그것과 견주어 「이미 놓인 인스턴스를 깨는
|
|
132
|
+
* 발행인가」를 함께 낸다(ADR-0063 결정 3). 한 번도 발행된 적 없으면 견줄 것이 없으므로 안 준다.
|
|
130
133
|
*/
|
|
131
|
-
export function inspectSource(sourceJson?: string | null): Inspection {
|
|
134
|
+
export function inspectSource(sourceJson?: string | null, releasedJson?: string | null): Inspection {
|
|
132
135
|
if (!sourceJson) {
|
|
133
136
|
return {
|
|
134
137
|
blocked: true,
|
|
@@ -187,11 +190,48 @@ export function inspectSource(sourceJson?: string | null): Inspection {
|
|
|
187
190
|
how: finding.how,
|
|
188
191
|
/* 숫자가 아니라 자리를 말한다 — 어느 부품들이 갈라져 나왔나. */
|
|
189
192
|
at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,
|
|
190
|
-
|
|
193
|
+
fixes: finding.code === 'sizing-splits' ? fixes.filter(fix => fix.findingWay === finding.way) : undefined,
|
|
191
194
|
blocking: finding.code === 'sizing-splits'
|
|
192
195
|
})
|
|
193
196
|
}
|
|
197
|
+
|
|
198
|
+
findings.push(...contractFindings(source, releasedJson))
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
return { findings, blocked: findings.some(finding => finding.blocking) }
|
|
197
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 직전 발행판과 견준 것 — **약속을 깨는 발행인가**(ADR-0063 결정 3).
|
|
206
|
+
*
|
|
207
|
+
* 무엇이 달라졌고 그것이 막는 것인지는 형식이 정한다(`contractChanges`). 여기서는 화면과 발행 문이
|
|
208
|
+
* 읽는 모양으로 옮기기만 한다.
|
|
209
|
+
*
|
|
210
|
+
* **앞 판을 오늘 형식으로 못 읽으면 조용히 넘어가지 않는다.** 판 1 은 오늘 형식이 거절하는 판으로
|
|
211
|
+
* 적혀 있을 수 있고, 그때 견주기를 건너뛰면 아무 말 없이 관문이 없는 것이 된다. 막지는 않는다 —
|
|
212
|
+
* 그러면 그 도형은 영영 다시 발행할 수 없다.
|
|
213
|
+
*/
|
|
214
|
+
function contractFindings(source: FigureSource, releasedJson?: string | null): GateFinding[] {
|
|
215
|
+
if (!releasedJson) return []
|
|
216
|
+
|
|
217
|
+
let released: FigureSource
|
|
218
|
+
try {
|
|
219
|
+
released = JSON.parse(releasedJson) as FigureSource
|
|
220
|
+
return contractChanges(released, source).map(change => ({
|
|
221
|
+
code: change.code,
|
|
222
|
+
message: change.message,
|
|
223
|
+
why: change.why,
|
|
224
|
+
at: change.subject,
|
|
225
|
+
blocking: change.blocking
|
|
226
|
+
}))
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
code: 'released-unreadable',
|
|
231
|
+
message: `직전 발행판을 오늘 형식으로 읽을 수 없어 견주지 못했다: ${(e as Error).message}`,
|
|
232
|
+
why: '이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았다.',
|
|
233
|
+
blocking: false
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -168,7 +168,19 @@ export class FigureMutation {
|
|
|
168
168
|
|
|
169
169
|
막는 이유는 사유와 함께 낸다. 「발행할 수 없습니다」로 뭉치면 무엇을 고쳐야 하는지 모른다.
|
|
170
170
|
*/
|
|
171
|
-
|
|
171
|
+
/*
|
|
172
|
+
직전 발행판을 먼저 꺼낸다. 판 번호를 세는 데도 쓰고, **약속을 깨는 발행인가**를 견주는 데도
|
|
173
|
+
쓴다(ADR-0063 결정 3) — 도면은 type 만 적고 늘 최신 발행판을 그리므로, 다시 발행하는 것은
|
|
174
|
+
그 type 을 놓아 둔 모든 도면에게 같은 약속을 다시 하는 일이다.
|
|
175
|
+
*/
|
|
176
|
+
const released = await historyRepository
|
|
177
|
+
.createQueryBuilder('history')
|
|
178
|
+
.where('history.originalId = :id', { id: figure.id })
|
|
179
|
+
.orderBy('history.version', 'DESC')
|
|
180
|
+
.limit(1)
|
|
181
|
+
.getOne()
|
|
182
|
+
|
|
183
|
+
const inspection = inspectSource(figure.source, released?.source)
|
|
172
184
|
if (inspection.blocked) {
|
|
173
185
|
const reasons = inspection.findings
|
|
174
186
|
.filter(finding => finding.blocking)
|
|
@@ -177,13 +189,7 @@ export class FigureMutation {
|
|
|
177
189
|
throw new Error(`Figure '${figure.type}' 은 아직 발행할 수 없다 — ${reasons.join(' · ')}`)
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
const
|
|
181
|
-
.createQueryBuilder('history')
|
|
182
|
-
.select('MAX(history.version)', 'max')
|
|
183
|
-
.where('history.originalId = :id', { id: figure.id })
|
|
184
|
-
.getRawOne()
|
|
185
|
-
|
|
186
|
-
const nextVersion = highest?.max ? Number(highest.max) + 1 : 1
|
|
192
|
+
const nextVersion = released?.version ? Number(released.version) + 1 : 1
|
|
187
193
|
|
|
188
194
|
return await repository.save({
|
|
189
195
|
/*
|
|
@@ -255,6 +255,16 @@ describe('프롬프트는 형식을 따라간다 — 손으로 적은 목록이
|
|
|
255
255
|
}
|
|
256
256
|
})
|
|
257
257
|
|
|
258
|
+
describe('joints (ADR-0066)', () => {
|
|
259
|
+
it('teaches the joint shape with the type vocabulary, and a joint channel with no path and a number per key', () => {
|
|
260
|
+
const prompt = rulesPrompt(PALETTE)
|
|
261
|
+
expect(prompt).toContain(`type: ${(model as any).JOINT_TYPES.join(' | ')}`)
|
|
262
|
+
expect(prompt).toMatch(/NO path, NO pivot/)
|
|
263
|
+
expect(prompt).toMatch(/limits must include 0/)
|
|
264
|
+
expect(prompt).toMatch(/ONLY place a parent is written/)
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
258
268
|
describe('고치라는 요청일 때', () => {
|
|
259
269
|
it('요청이 안 건드린 것을 그대로 실으라고 못 박는다', async () => {
|
|
260
270
|
/*
|
|
@@ -270,7 +280,7 @@ describe('고치라는 요청일 때', () => {
|
|
|
270
280
|
expect(sent).toMatch(/dropping one is silent/)
|
|
271
281
|
|
|
272
282
|
/* 잃기 쉬운 칸은 이름으로 짚어 준다 — 「전부 지켜라」만으로는 모델이 빠뜨린다. */
|
|
273
|
-
for (const field of ['animations', 'capabilities', 'anchor', 'repeat', 'label', 'materialSlot']) {
|
|
283
|
+
for (const field of ['animations', 'parameters', 'joints', 'capabilities', 'parent', 'anchor', 'repeat', 'label', 'materialSlot']) {
|
|
274
284
|
expect(sent).toContain(field)
|
|
275
285
|
}
|
|
276
286
|
})
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SEGMENT_PRESETS,
|
|
18
18
|
SIZING_RULES,
|
|
19
19
|
FIGURE_PLACEMENTS,
|
|
20
|
+
JOINT_TYPES,
|
|
20
21
|
FIGURE_SOURCE_VERSION,
|
|
21
22
|
compile,
|
|
22
23
|
costOf,
|
|
@@ -148,12 +149,12 @@ export function rulesPrompt(palette: string[]): string {
|
|
|
148
149
|
'',
|
|
149
150
|
'The JSON is a FigureSource:',
|
|
150
151
|
` { version: ${FIGURE_SOURCE_VERSION}, type, base: {x,y,z}, placement?, detailLevel, parts: [...],`,
|
|
151
|
-
' animations?: [...], parameters?: [...], capabilities?: [...] }',
|
|
152
|
+
' animations?: [...], parameters?: [...], capabilities?: [...], joints?: [...] }',
|
|
152
153
|
'',
|
|
153
154
|
'A part is:',
|
|
154
155
|
' { name, primitive, transform: { position: {x,y,z}, size: {x,y,z}, rotation?: {x,y,z} },',
|
|
155
156
|
' material: { token, preset?, flatShading?, transparent?, emissive? },',
|
|
156
|
-
' segments?, shape?, materialSlot?, keepRound?, sizing?, anchor?, repeat?, label?, capability? }',
|
|
157
|
+
' segments?, shape?, materialSlot?, keepRound?, sizing?, anchor?, repeat?, label?, capability?, parent? }',
|
|
157
158
|
'',
|
|
158
159
|
'Hard vocabulary — anything outside these is rejected:',
|
|
159
160
|
` placement ${FIGURE_PLACEMENTS.join(' | ')} (floor: default, ground equipment; ceiling: overhead hoist/OHT; center: airborne/drone/sensor)`,
|
|
@@ -274,6 +275,23 @@ export function rulesPrompt(palette: string[]): string {
|
|
|
274
275
|
' Do NOT answer it with a capability. Capabilities say what a figure does in the FLOW;',
|
|
275
276
|
' how a pose is driven is a different axis entirely.',
|
|
276
277
|
'',
|
|
278
|
+
'Joints — a part that moves RELATIVE TO ANOTHER PART, carrying what is attached to it:',
|
|
279
|
+
' An arm whose shoulder turns and carries the elbow, wrist and gripper; a crane whose bridge',
|
|
280
|
+
' slides and carries the trolley and hook. Without joints every channel moves one part alone,',
|
|
281
|
+
' so two turning links do not compose.',
|
|
282
|
+
' parent on a part names the part it is attached to. It is the ONLY place a parent is written.',
|
|
283
|
+
' A part without parent hangs from the figure. Following parents must end; no loops.',
|
|
284
|
+
` joints: [{ name, child, type: ${JOINT_TYPES.join(' | ')}, origin: {x,y,z}, axis: {x,y,z}, limits?: { min, max } }]`,
|
|
285
|
+
' child is the part that moves; its parent is what it moves relative to. One joint per part.',
|
|
286
|
+
' origin is the point it turns about or slides from, in the same coordinates as part',
|
|
287
|
+
' positions (y from the bottom of the base). axis is the direction: turning follows the',
|
|
288
|
+
' right-hand rule. revolute and continuous are in degrees, prismatic in mm.',
|
|
289
|
+
' Parts are drawn in the pose where every joint is 0, so limits must include 0.',
|
|
290
|
+
' continuous has no limits. Joint names and part names must all be different.',
|
|
291
|
+
' A joint moves only through a parameter (or an animation for endless turning): a channel',
|
|
292
|
+
' { target: <joint name>, keys: [{ at, value: <number> }] } — NO path, NO pivot, and each',
|
|
293
|
+
' value is the joint coordinate, within limits. A 6-axis arm is six joints and six parameters.',
|
|
294
|
+
'',
|
|
277
295
|
' Names are shared between the two lists: an instance addresses a curve by name, so an',
|
|
278
296
|
' animation and a parameter may not be called the same thing.',
|
|
279
297
|
' Doors that open together belong in one curve with two channels, not two curves.',
|
|
@@ -295,8 +313,8 @@ function taskPrompt(input: ProposeInput): string {
|
|
|
295
313
|
'',
|
|
296
314
|
'Revise this existing figure. Keep the part names that still mean the same thing —',
|
|
297
315
|
'renaming a part breaks bindings on instances already placed.',
|
|
298
|
-
'Carry over everything the request did not ask you to change. animations,
|
|
299
|
-
'capability, anchor, repeat, label and materialSlot are easy to drop by writing them out of',
|
|
316
|
+
'Carry over everything the request did not ask you to change. animations, parameters, joints,',
|
|
317
|
+
'capabilities, capability, parent, anchor, repeat, label and materialSlot are easy to drop by writing them out of',
|
|
300
318
|
'the JSON, and dropping one is silent — the figure still validates, it just stops doing what',
|
|
301
319
|
'it did. If the request says nothing about movement, the clips come back unchanged.',
|
|
302
320
|
JSON.stringify(input.base)
|
|
@@ -113,7 +113,18 @@ export class FigureQuery {
|
|
|
113
113
|
throw new Error(`Figure ${id} 를 찾을 수 없다`)
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
/*
|
|
117
|
+
**화면이 발행 문과 같은 것을 본다.** 직전 발행판을 여기서도 함께 넘기지 않으면, 저작 화면은
|
|
118
|
+
조용한데 발행을 누른 순간 계약 위반으로 막힌다 — 그때는 이미 열 군데를 고친 뒤다.
|
|
119
|
+
*/
|
|
120
|
+
const released = await getRepository(FigureHistory)
|
|
121
|
+
.createQueryBuilder('history')
|
|
122
|
+
.where('history.originalId = :id', { id })
|
|
123
|
+
.orderBy('history.version', 'DESC')
|
|
124
|
+
.limit(1)
|
|
125
|
+
.getOne()
|
|
126
|
+
|
|
127
|
+
return inspectSource(figure.source, released?.source)
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
@Directive('@privilege(category: "figure", privilege: "query", domainOwnerGranted: true)')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* figure 도구가 지나는 문 — ADR-0072 결정 2.
|
|
3
|
+
*
|
|
4
|
+
* 선언만 해 두고 아무도 읽지 않으면 그 선언은 다음 사람이 지우거나 바꿔도 아무 일이 없다. 여기서는
|
|
5
|
+
* 실제로 등록된 명세를 등록기에 넣고, 등록기가 무엇도 문제 삼지 않는지 묻는다. 도구 목록이 늘면
|
|
6
|
+
* 이 시험이 먼저 문다.
|
|
7
|
+
*/
|
|
8
|
+
import { clearToolRegistry, getAllToolSpecs, getToolRegistryWarnings } from '@things-factory/ai-client-base'
|
|
9
|
+
|
|
10
|
+
import { registerFigureToolCategories } from './figure-tools'
|
|
11
|
+
|
|
12
|
+
describe('figure tools — declared doors', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
clearToolRegistry()
|
|
15
|
+
registerFigureToolCategories()
|
|
16
|
+
})
|
|
17
|
+
afterAll(() => clearToolRegistry())
|
|
18
|
+
|
|
19
|
+
it('★ every figure tool says which doors it reaches, as a list', () => {
|
|
20
|
+
const undeclared = getAllToolSpecs()
|
|
21
|
+
.filter(spec => !Array.isArray((spec as any).doors))
|
|
22
|
+
.map(spec => spec.name)
|
|
23
|
+
|
|
24
|
+
expect(undeclared).toEqual([])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('★ a door a tool reaches comes with the privilege that guards it', () => {
|
|
28
|
+
const unguarded = getAllToolSpecs()
|
|
29
|
+
.filter(spec => ((spec as any).doors ?? []).length > 0 && !(spec as any).privilege)
|
|
30
|
+
.map(spec => spec.name)
|
|
31
|
+
|
|
32
|
+
expect(unguarded).toEqual([])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('the registry itself finds nothing to report about these specs', () => {
|
|
36
|
+
expect(getToolRegistryWarnings().map(w => `${w.code}:${w.params.tool ?? ''}`)).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -117,7 +117,12 @@ test('실제 registryAssistantChat 경계가 열린 모델 문맥을 Figure 도
|
|
|
117
117
|
} } as any,
|
|
118
118
|
messages: [{ role: 'user', content: '현재 모델을 조회해줘' }],
|
|
119
119
|
systemPrompt: 'Read the current figure.', toolCategories: ['figure-authoring'],
|
|
120
|
-
state: { domain: MINE }, hostContext: { figureId: 'fig-1', draftSource: SOURCE }
|
|
120
|
+
state: { domain: MINE }, hostContext: { figureId: 'fig-1', draftSource: SOURCE },
|
|
121
|
+
/*
|
|
122
|
+
* 도구가 닿는 문의 권한을 선언한 뒤로(ADR-0071 결정 2), 권한을 물어볼 길을 주지 않으면 그 도구는
|
|
123
|
+
* 닫힌다 — 그것이 맞는 기본값이다. 여기서 보는 것은 권한이 아니라 배선이므로 「가진 사람」으로 둔다.
|
|
124
|
+
*/
|
|
125
|
+
toolAccess: async () => true
|
|
121
126
|
})
|
|
122
127
|
expect(seen).toHaveLength(2)
|
|
123
128
|
expect(JSON.stringify(seen[1])).toContain('MIXER')
|
|
@@ -232,7 +232,7 @@ function unsavedFigure(host: any): any | undefined {
|
|
|
232
232
|
const FIGURE_ID_ARG = {
|
|
233
233
|
figureId: {
|
|
234
234
|
type: 'string',
|
|
235
|
-
description: 'The figure to read. Omit
|
|
235
|
+
description: 'The figure to read. Omit to read the one the person has open, saved or not — the server fills it in, and you cannot see which it is. On the catalog nothing is open, so resolve the figure the person asked for with listFigures first.'
|
|
236
236
|
}
|
|
237
237
|
} as const
|
|
238
238
|
|
|
@@ -247,6 +247,9 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
247
247
|
{
|
|
248
248
|
kind: 'read',
|
|
249
249
|
name: 'listFigures',
|
|
250
|
+
/* 이 도메인의 도형 목록 — 저장소를 직접 읽지만 같은 일을 하는 문은 이것이다. */
|
|
251
|
+
doors: ['Query.figures'],
|
|
252
|
+
privilege: { category: 'figure', privilege: 'query', owner: true },
|
|
250
253
|
description:
|
|
251
254
|
'Lists the figures in this tenant with their type names, states and part counts. ' +
|
|
252
255
|
'Call it before naming a type — type names are stored identifiers and must not be invented.',
|
|
@@ -274,9 +277,13 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
274
277
|
{
|
|
275
278
|
kind: 'read',
|
|
276
279
|
name: 'getFigureSource',
|
|
280
|
+
/* 저장된 도형의 원본. **저장 전 초안**을 물으면 저장소에 갈 것이 없어 화면이 보낸 것을 그대로 돌려준다 —
|
|
281
|
+
* 그 갈래에는 닿는 문이 없지만 `doors: []` 로 적지 않는다. 그러면 저장된 도형을 읽는 권한 검사가 선언에서 사라진다. */
|
|
282
|
+
doors: ['Query.figure'],
|
|
283
|
+
privilege: { category: 'figure', privilege: 'query', owner: true },
|
|
277
284
|
description:
|
|
278
285
|
'Returns the canonical FigureSource of one figure, with its measured cost ' +
|
|
279
|
-
'(triangles,
|
|
286
|
+
'(triangles, part groups, and how many of it hold 60fps on one screen). Call it before proposing a revision — ' +
|
|
280
287
|
'revising without reading it means proposing against a guess.',
|
|
281
288
|
schema: { type: 'object', properties: { ...FIGURE_ID_ARG } },
|
|
282
289
|
builder: async (args: any, ctx: any) => {
|
|
@@ -309,6 +316,9 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
309
316
|
{
|
|
310
317
|
kind: 'read',
|
|
311
318
|
name: 'inspectFigure',
|
|
319
|
+
/* 검사 결과 — 같은 일을 하는 문. */
|
|
320
|
+
doors: ['Query.inspectFigure'],
|
|
321
|
+
privilege: { category: 'figure', privilege: 'query', owner: true },
|
|
312
322
|
description:
|
|
313
323
|
'Says whether a figure may be released and, when it may not, what is wrong — each finding ' +
|
|
314
324
|
'carries why it is a problem and what to change. Call it when asked about releasing, or ' +
|
|
@@ -328,6 +338,9 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
328
338
|
{
|
|
329
339
|
kind: 'read',
|
|
330
340
|
name: 'proposeFigureCandidate',
|
|
341
|
+
/* 후보 생성 — 저장은 하지 않지만 같은 문의 권한을 쓴다. */
|
|
342
|
+
doors: ['Mutation.proposeFigure'],
|
|
343
|
+
privilege: { category: 'figure', privilege: 'mutation', owner: true },
|
|
331
344
|
description:
|
|
332
345
|
'Asks for a figure candidate from a description, optionally revising an existing one. ' +
|
|
333
346
|
'Returns the candidate with its score and cost. NOTHING IS SAVED — the author accepts or ' +
|