@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.
Files changed (31) hide show
  1. package/dist-server/service/figure/figure-gate.d.ts +4 -1
  2. package/dist-server/service/figure/figure-gate.js +40 -1
  3. package/dist-server/service/figure/figure-gate.js.map +1 -1
  4. package/dist-server/service/figure/figure-mutation.d.ts +7 -0
  5. package/dist-server/service/figure/figure-mutation.js +43 -7
  6. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  7. package/dist-server/service/figure/figure-propose.js +1 -1
  8. package/dist-server/service/figure/figure-propose.js.map +1 -1
  9. package/dist-server/service/figure/figure-query.js +11 -1
  10. package/dist-server/service/figure/figure-query.js.map +1 -1
  11. package/dist-server/service/figure/figure-rename.d.ts +69 -0
  12. package/dist-server/service/figure/figure-rename.js +69 -0
  13. package/dist-server/service/figure/figure-rename.js.map +1 -0
  14. package/dist-server/service/figure/figure-tools.js +15 -2
  15. package/dist-server/service/figure/figure-tools.js.map +1 -1
  16. package/dist-server/service/figure/figure-type.d.ts +8 -2
  17. package/dist-server/service/figure/figure-type.js +12 -3
  18. package/dist-server/service/figure/figure-type.js.map +1 -1
  19. package/dist-server/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +6 -6
  21. package/server/service/figure/figure-gate.ts +43 -3
  22. package/server/service/figure/figure-mutation.ts +58 -8
  23. package/server/service/figure/figure-propose.ts +1 -1
  24. package/server/service/figure/figure-query.ts +12 -1
  25. package/server/service/figure/figure-rename-db.test.ts +138 -0
  26. package/server/service/figure/figure-rename.test.ts +99 -0
  27. package/server/service/figure/figure-rename.ts +98 -0
  28. package/server/service/figure/figure-tool-doors.test.ts +143 -0
  29. package/server/service/figure/figure-tools.test.ts +6 -1
  30. package/server/service/figure/figure-tools.ts +15 -2
  31. 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 when host context has an open figureId or an unsaved new editor draft. On the catalog, first resolve the user’s requested figure with listFigures.'
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, draw calls, how many fit on one board). Call it before proposing a revision — ' +
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
- * 타입을 못 찾아 오류 없이 빈 화면이 된다. 고칠 수 있는 이름은 `name` 뿐이다.
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. The `type` field is absent by design — it is a stored identifier.' })
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