@things-factory/figure-service 10.1.27 → 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-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-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
|
/*
|
|
@@ -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 ' +
|