@things-factory/figure-service 10.1.27 → 10.1.29
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.d.ts +7 -0
- package/dist-server/service/figure/figure-mutation.js +43 -7
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-propose.js +1 -1
- 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-rename.d.ts +69 -0
- package/dist-server/service/figure/figure-rename.js +69 -0
- package/dist-server/service/figure/figure-rename.js.map +1 -0
- package/dist-server/service/figure/figure-tools.js +15 -2
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/service/figure/figure-type.d.ts +8 -2
- package/dist-server/service/figure/figure-type.js +12 -3
- package/dist-server/service/figure/figure-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/service/figure/figure-gate.ts +43 -3
- package/server/service/figure/figure-mutation.ts +58 -8
- package/server/service/figure/figure-propose.ts +1 -1
- package/server/service/figure/figure-query.ts +12 -1
- package/server/service/figure/figure-rename-db.test.ts +138 -0
- package/server/service/figure/figure-rename.test.ts +99 -0
- package/server/service/figure/figure-rename.ts +98 -0
- package/server/service/figure/figure-tool-doors.test.ts +143 -0
- package/server/service/figure/figure-tools.test.ts +6 -1
- package/server/service/figure/figure-tools.ts +15 -2
- package/server/service/figure/figure-type.ts +11 -3
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* figure 도구가 지나는 문 — ADR-0072 결정 2.
|
|
3
|
+
*
|
|
4
|
+
* 선언만 해 두고 아무도 읽지 않으면 그 선언은 다음 사람이 지우거나 바꿔도 아무 일이 없다. 여기서는
|
|
5
|
+
* 실제로 등록된 명세를 등록기에 넣고, 등록기가 무엇도 문제 삼지 않는지 묻는다. 도구 목록이 늘면
|
|
6
|
+
* 이 시험이 먼저 문다.
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync, readFileSync } from 'fs'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
|
|
11
|
+
import { clearToolRegistry, getAllToolSpecs, getToolRegistryWarnings } from '@things-factory/ai-client-base'
|
|
12
|
+
|
|
13
|
+
import { registerFigureToolCategories } from './figure-tools'
|
|
14
|
+
|
|
15
|
+
describe('figure tools — declared doors', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
clearToolRegistry()
|
|
18
|
+
registerFigureToolCategories()
|
|
19
|
+
})
|
|
20
|
+
afterAll(() => clearToolRegistry())
|
|
21
|
+
|
|
22
|
+
it('★ every figure tool says which doors it reaches, as a list', () => {
|
|
23
|
+
const undeclared = getAllToolSpecs()
|
|
24
|
+
.filter(spec => !Array.isArray((spec as any).doors))
|
|
25
|
+
.map(spec => spec.name)
|
|
26
|
+
|
|
27
|
+
expect(undeclared).toEqual([])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('★ a door a tool reaches comes with the privilege that guards it', () => {
|
|
31
|
+
const unguarded = getAllToolSpecs()
|
|
32
|
+
.filter(spec => ((spec as any).doors ?? []).length > 0 && !(spec as any).privilege)
|
|
33
|
+
.map(spec => spec.name)
|
|
34
|
+
|
|
35
|
+
expect(unguarded).toEqual([])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('the registry itself finds nothing to report about these specs', () => {
|
|
39
|
+
expect(getToolRegistryWarnings().map(w => `${w.code}:${w.params.tool ?? ''}`)).toEqual([])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* 베낀 권한이 문의 지시자와 같은가 — ADR-0072 결정 5 (b).
|
|
45
|
+
*
|
|
46
|
+
* 권한은 문의 지시자에서 나와야 하는데(ADR-0071 결정 2) 지금은 읽을 자리가 없다: 지시자가 남기는
|
|
47
|
+
* 것은 (category, privilege) 쌍 목록뿐이고 필드별 지도가 없다. 그래서 도구 명세가 권한을 베껴
|
|
48
|
+
* 적는다. 베낀 값은 **문이 바뀌어도 안 따라간다** — 문에 권한을 붙이거나 떼거나 이름을 고치면
|
|
49
|
+
* 도구 명세만 옛 값을 들고 남고, 그때 게이트는 문이 안 묻는 것을 묻거나 문이 묻는 것을 안 묻는다.
|
|
50
|
+
*
|
|
51
|
+
* 여기서는 리졸버 원문의 지시자를 읽어 도구가 적은 것과 견준다. AUTH 에 청한 필드별 지도가 서면
|
|
52
|
+
* 이 시험과 베끼기가 함께 없어진다.
|
|
53
|
+
*/
|
|
54
|
+
describe('figure tools — the copied privilege matches the door', () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
clearToolRegistry()
|
|
57
|
+
registerFigureToolCategories()
|
|
58
|
+
})
|
|
59
|
+
afterAll(() => clearToolRegistry())
|
|
60
|
+
|
|
61
|
+
/** 리졸버 원문에서 `Query.figures` 같은 문 이름 → 그 문의 `@privilege` 지시자. */
|
|
62
|
+
const doorPrivileges = (): Map<string, { category: string; privilege: string; owner: boolean }> => {
|
|
63
|
+
const found = new Map<string, { category: string; privilege: string; owner: boolean }>()
|
|
64
|
+
|
|
65
|
+
for (const file of readdirSync(__dirname).filter(one => one.endsWith('.ts') && !one.endsWith('.test.ts'))) {
|
|
66
|
+
const lines = readFileSync(join(__dirname, file), 'utf8').split('\n')
|
|
67
|
+
let privilege: { category: string; privilege: string; owner: boolean } | undefined
|
|
68
|
+
let kind: 'Query' | 'Mutation' | undefined
|
|
69
|
+
let decorator = ''
|
|
70
|
+
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
const declared = /@privilege\(category: "([^"]+)", privilege: "([^"]+)"(, domainOwnerGranted: (true|false))?\)/.exec(line)
|
|
73
|
+
if (declared) {
|
|
74
|
+
privilege = { category: declared[1]!, privilege: declared[2]!, owner: declared[4] === 'true' }
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const opens = /@(Query|Mutation)\(/.exec(line)
|
|
79
|
+
if (opens) {
|
|
80
|
+
kind = opens[1] as 'Query' | 'Mutation'
|
|
81
|
+
decorator = line
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!kind) {
|
|
86
|
+
/* 지시자와 문 사이에 딴 것이 오면 그 지시자는 이 문의 것이 아니다. */
|
|
87
|
+
if (line.trim() && !line.trim().startsWith('@') && !line.trim().startsWith('*') && !line.trim().startsWith('/*')) {
|
|
88
|
+
privilege = undefined
|
|
89
|
+
}
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
decorator += '\n' + line
|
|
94
|
+
const method = /^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/.exec(line)
|
|
95
|
+
if (!method) continue
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
필드 이름이 메서드 이름과 다를 수 있는 길은 하나뿐이다 — 데코레이터의 `name:`. 그것이
|
|
99
|
+
나타나면 조용히 메서드 이름으로 읽지 않고 멈춘다. 잘못 읽은 짝은 초록으로 보인다.
|
|
100
|
+
*/
|
|
101
|
+
if (/\bname:\s*'/.test(decorator)) {
|
|
102
|
+
throw new Error(`${file} 의 ${kind} 가 데코레이터에서 이름을 정한다 — 이 시험이 그 길을 아직 안 읽는다`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (privilege) found.set(`${kind}.${method[1]}`, privilege)
|
|
106
|
+
kind = undefined
|
|
107
|
+
privilege = undefined
|
|
108
|
+
decorator = ''
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return found
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
it('★ 도구가 적은 권한이 그 문의 지시자와 한 글자도 다르지 않다', () => {
|
|
116
|
+
const doors = doorPrivileges()
|
|
117
|
+
|
|
118
|
+
const mismatched = getAllToolSpecs().flatMap(spec =>
|
|
119
|
+
((spec as any).doors ?? []).map((door: string) => {
|
|
120
|
+
const declared = doors.get(door)
|
|
121
|
+
const copied = (spec as any).privilege
|
|
122
|
+
if (!declared) return `${spec.name} → ${door}: 그런 문이 없거나 그 문이 권한을 선언하지 않는다`
|
|
123
|
+
if (
|
|
124
|
+
declared.category !== copied?.category ||
|
|
125
|
+
declared.privilege !== copied?.privilege ||
|
|
126
|
+
declared.owner !== (copied?.owner ?? false)
|
|
127
|
+
) {
|
|
128
|
+
return `${spec.name} → ${door}: 문은 ${JSON.stringify(declared)} 인데 도구는 ${JSON.stringify(copied)}`
|
|
129
|
+
}
|
|
130
|
+
return undefined
|
|
131
|
+
})
|
|
132
|
+
).filter(Boolean)
|
|
133
|
+
|
|
134
|
+
expect(mismatched).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('견줄 문을 실제로 찾아 읽었다 — 0 건이면 위 시험은 언제나 초록이다', () => {
|
|
138
|
+
const doors = doorPrivileges()
|
|
139
|
+
expect(doors.get('Query.figures')).toEqual({ category: 'figure', privilege: 'query', owner: true })
|
|
140
|
+
expect(doors.get('Mutation.proposeFigure')).toEqual({ category: 'figure', privilege: 'mutation', owner: true })
|
|
141
|
+
expect(doors.size).toBeGreaterThanOrEqual(4)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -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 ' +
|
|
@@ -41,11 +41,19 @@ export class NewFigure {
|
|
|
41
41
|
/**
|
|
42
42
|
* 수정.
|
|
43
43
|
*
|
|
44
|
-
* `type`
|
|
45
|
-
* 타입을 못 찾아 오류 없이 빈 화면이 된다.
|
|
44
|
+
* `type` 은 **한 번도 발행되지 않은 초안에서만** 바뀐다(ADR-0063 결정 4). 저장되는 식별자라
|
|
45
|
+
* 발행된 뒤에 바꾸면 이미 배치된 보드가 타입을 못 찾아 오류 없이 빈 화면이 된다. 발행 전에는
|
|
46
|
+
* 그 도형을 가리키는 도면이 있을 수 없다 — 도면은 최신 발행판을 그리므로 발행된 적 없는
|
|
47
|
+
* 도형은 그려질 자리가 없다.
|
|
48
|
+
*
|
|
49
|
+
* 닫아 두었더니 저작자가 이름을 한 번도 정하지 못했다. 새 Figure 에는 기계가 지은 이름이
|
|
50
|
+
* 붙고(`FIGURE_MU6RJH47`) 칸에 자물쇠가 걸려서, 그 이름이 그대로 도면에 적히는 이름이 됐다.
|
|
46
51
|
*/
|
|
47
|
-
@InputType({ description: 'Input for updating an existing figure.
|
|
52
|
+
@InputType({ description: 'Input for updating an existing figure. `type` is accepted only while the figure has never been released.' })
|
|
48
53
|
export class FigurePatch {
|
|
54
|
+
@Field({ nullable: true, description: 'The stored identifier. Accepted only before the first release; rejected once a version exists.' })
|
|
55
|
+
type?: string
|
|
56
|
+
|
|
49
57
|
@Field({ nullable: true, description: 'The new display name.' })
|
|
50
58
|
name?: string
|
|
51
59
|
|