@things-factory/figure-service 10.1.28 → 10.1.31

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-mutation.d.ts +7 -0
  2. package/dist-server/service/figure/figure-mutation.js +30 -0
  3. package/dist-server/service/figure/figure-mutation.js.map +1 -1
  4. package/dist-server/service/figure/figure-propose.js +1 -1
  5. package/dist-server/service/figure/figure-propose.js.map +1 -1
  6. package/dist-server/service/figure/figure-query.js +19 -5
  7. package/dist-server/service/figure/figure-query.js.map +1 -1
  8. package/dist-server/service/figure/figure-rename.d.ts +69 -0
  9. package/dist-server/service/figure/figure-rename.js +69 -0
  10. package/dist-server/service/figure/figure-rename.js.map +1 -0
  11. package/dist-server/service/figure/figure-tools.js +6 -4
  12. package/dist-server/service/figure/figure-tools.js.map +1 -1
  13. package/dist-server/service/figure/figure-type.d.ts +8 -2
  14. package/dist-server/service/figure/figure-type.js +12 -3
  15. package/dist-server/service/figure/figure-type.js.map +1 -1
  16. package/dist-server/tsconfig.tsbuildinfo +1 -1
  17. package/package.json +6 -6
  18. package/server/service/figure/figure-blueprint-query.test.ts +269 -1
  19. package/server/service/figure/figure-e2e-smoke.test.ts +1 -1
  20. package/server/service/figure/figure-mutation.ts +44 -0
  21. package/server/service/figure/figure-propose.ts +1 -1
  22. package/server/service/figure/figure-query.ts +36 -7
  23. package/server/service/figure/figure-rename-db.test.ts +138 -0
  24. package/server/service/figure/figure-rename.test.ts +99 -0
  25. package/server/service/figure/figure-rename.ts +98 -0
  26. package/server/service/figure/figure-tool-doors.test.ts +98 -9
  27. package/server/service/figure/figure-tools.test.ts +66 -0
  28. package/server/service/figure/figure-tools.ts +6 -4
  29. package/server/service/figure/figure-type.ts +11 -3
  30. package/translations/en.json +3 -0
  31. package/translations/ko.json +3 -0
@@ -0,0 +1,99 @@
1
+ /*
2
+ * 초안의 타입을 고치는 규칙 — ADR-0063 결정 4.
3
+ *
4
+ * 이 규칙이 지키는 것은 하나다: **발행된 이름은 안 바뀐다.** 도면은 그 이름 하나로 도형을
5
+ * 찾으므로, 바뀌면 배치된 자리가 아무 오류 없이 빈다.
6
+ */
7
+ import { readFileSync } from 'fs'
8
+ import { join } from 'path'
9
+
10
+ import { FigureRenameRefused, renameDraftType } from './figure-rename'
11
+
12
+ const draft = (over: Record<string, unknown> = {}) => ({
13
+ type: 'FIGURE_MU6RJH47',
14
+ source: JSON.stringify({ version: 2, type: 'FIGURE_MU6RJH47', base: { x: 1, y: 1, z: 1 }, parts: [] }),
15
+ ...over
16
+ })
17
+
18
+ const never = { version: 0, hasHistory: false, takenByAnother: false }
19
+
20
+ describe('초안의 type 고치기', () => {
21
+ it('★ 행과 정본이 함께 바뀐다 — 한쪽만 바뀌면 발행 때 어느 이름이 나갈지 갈린다', () => {
22
+ const result = renameDraftType(draft(), 'ROBOT_ARM', never)
23
+
24
+ expect(result.type).toBe('ROBOT_ARM')
25
+ expect(JSON.parse(result.source).type).toBe('ROBOT_ARM')
26
+ })
27
+
28
+ it('정본의 다른 값은 건드리지 않는다', () => {
29
+ const before = JSON.parse(draft().source)
30
+ const after = JSON.parse(renameDraftType(draft(), 'ROBOT_ARM', never).source)
31
+
32
+ expect({ ...after, type: before.type }).toEqual(before)
33
+ })
34
+
35
+ it('앞뒤 공백을 뗀다. 대소문자는 저작자가 적은 대로 둔다', () => {
36
+ expect(renameDraftType(draft(), ' ROBOT_ARM ', never).type).toBe('ROBOT_ARM')
37
+ expect(renameDraftType(draft(), 'Robot_Arm', never).type).toBe('Robot_Arm')
38
+ })
39
+
40
+ it('★ 한 번이라도 발행됐으면 거절한다 — 판 번호로', () => {
41
+ expect(() => renameDraftType(draft(), 'ROBOT_ARM', { ...never, version: 1 })).toThrow(FigureRenameRefused)
42
+ })
43
+
44
+ it('★ 판 번호가 도로 0 이어도 이력이 있으면 거절한다 — 되돌리기가 그 자리를 만든다', () => {
45
+ try {
46
+ renameDraftType(draft(), 'ROBOT_ARM', { ...never, hasHistory: true })
47
+ throw new Error('거절하지 않았다')
48
+ } catch (e) {
49
+ expect((e as FigureRenameRefused).code).toBe('already-released')
50
+ }
51
+ })
52
+
53
+ it('같은 도메인에서 쓰는 이름이면 거절하고, 어느 이름인지 말한다', () => {
54
+ try {
55
+ renameDraftType(draft(), 'CONVEYOR', { ...never, takenByAnother: true })
56
+ throw new Error('거절하지 않았다')
57
+ } catch (e) {
58
+ expect((e as FigureRenameRefused).code).toBe('type-taken')
59
+ expect((e as Error).message).toContain('CONVEYOR')
60
+ }
61
+ })
62
+
63
+ it('빈 이름은 거절한다 — 도면에 적히는 이름이다', () => {
64
+ for (const empty of ['', ' ']) {
65
+ try {
66
+ renameDraftType(draft(), empty, never)
67
+ throw new Error('거절하지 않았다')
68
+ } catch (e) {
69
+ expect((e as FigureRenameRefused).code).toBe('empty-type')
70
+ }
71
+ }
72
+ })
73
+ })
74
+
75
+ /*
76
+ * 규칙이 문에 붙어 있는가. 셈이 맞아도 문이 안 부르면 저작자는 그대로 막혀 있다.
77
+ */
78
+ describe('문이 이 규칙을 지난다', () => {
79
+ const source = (name: string) => readFileSync(join(__dirname, name), 'utf8')
80
+
81
+ it('updateFigure 가 type 을 받고 규칙을 부른다', () => {
82
+ const mutation = source('figure-mutation.ts')
83
+
84
+ expect(/renameDraftType/.test(mutation)).toBe(true)
85
+ expect(/patch\.type !== undefined && patch\.type !== figure\.type/.test(mutation)).toBe(true)
86
+ })
87
+
88
+ it('다듬은 이름이 저장된다 — patch 의 날값이 아니라', () => {
89
+ const mutation = source('figure-mutation.ts')
90
+
91
+ expect(/renamed !== undefined \? \{ type: renamed \}/.test(mutation)).toBe(true)
92
+ })
93
+
94
+ it('문이 type 을 받는 칸을 갖는다', () => {
95
+ const type = source('figure-type.ts')
96
+
97
+ expect(/class FigurePatch \{[\s\S]{0,400}type\?: string/.test(type)).toBe(true)
98
+ })
99
+ })
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 초안의 타입을 고치는 규칙 — ADR-0063 결정 4.
3
+ *
4
+ * ## 왜 발행 전에만인가
5
+ *
6
+ * `type` 은 도면에 적히는 이름이다. 도면은 `{ type, left, top, width, height }` 만 갖고 늘 최신
7
+ * 발행판을 그리므로, 발행된 도형의 이름을 바꾸면 이미 배치된 도면이 아무 오류 없이 빈 자리가
8
+ * 된다. 한 번도 발행되지 않은 도형은 그려질 자리가 없으니 가리키는 도면도 있을 수 없다.
9
+ *
10
+ * ## 왜 열어야 했나
11
+ *
12
+ * 닫아 두었더니 저작자가 이름을 **한 번도** 정하지 못했다. 새 Figure 에는 기계가 지은 이름이
13
+ * 붙고(`FIGURE_MU6RJH47`) 칸에 자물쇠가 걸려서, 그 이름이 그대로 도면에 적히는 이름이 됐다.
14
+ * 그렇게 남은 초안이 14 개다.
15
+ *
16
+ * ## 셈은 여기, 질의는 부르는 쪽
17
+ *
18
+ * 이 파일은 저장소를 모른다 — 판 번호 · 이력 유무 · 같은 이름의 주인을 **사실로 받는다.**
19
+ * 그래야 규칙을 DB 없이 시험한다. 문(`figure-mutation.ts`)이 그 셋을 읽어 넘긴다.
20
+ */
21
+
22
+ /** 이름을 바꿀 때 알아야 하는 사실. 부르는 쪽이 저장소에서 읽어 온다. */
23
+ export interface RenameFacts {
24
+ /** 발행 판 번호. 0 이면 한 번도 발행되지 않았다. */
25
+ version?: number
26
+ /** 판본이 하나라도 있나. 되돌리기로 판 번호가 도로 낮아져도 이력은 남는다. */
27
+ hasHistory: boolean
28
+ /** 같은 도메인에서 그 이름을 이미 쓰는 **다른** 도형이 있나. */
29
+ takenByAnother: boolean
30
+ }
31
+
32
+ export class FigureRenameRefused extends Error {
33
+ constructor(
34
+ readonly code: 'empty-type' | 'already-released' | 'type-taken',
35
+ message: string
36
+ ) {
37
+ super(message)
38
+ this.name = 'FigureRenameRefused'
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 고친 이름과 정본. 못 고치면 **까닭과 함께 던진다** — 조용히 옛 이름으로 두지 않는다.
44
+ *
45
+ * 정본 안에도 `type` 이 있어서 함께 고친다. 한쪽만 고치면 저장된 JSON 과 행이 다른 이름을
46
+ * 말하고, 발행할 때 어느 쪽이 도면에 적히는지가 갈린다.
47
+ */
48
+ export function renameDraftType(
49
+ current: { type: string; source: string },
50
+ wanted: string,
51
+ facts: RenameFacts
52
+ ): { type: string; source: string } {
53
+ const next = wanted.trim()
54
+
55
+ if (!next) {
56
+ throw new FigureRenameRefused('empty-type', 'type 이 비어 있다 — 도면에 적히는 이름이라 비울 수 없다')
57
+ }
58
+
59
+ if ((facts.version ?? 0) > 0 || facts.hasHistory) {
60
+ throw new FigureRenameRefused(
61
+ 'already-released',
62
+ '발행된 도형의 type 은 바꿀 수 없다 — 이미 배치된 도면이 이 이름을 가리킨다'
63
+ )
64
+ }
65
+
66
+ if (facts.takenByAnother) {
67
+ throw new FigureRenameRefused('type-taken', `type 「${next}」 는 이 도메인에서 이미 쓰고 있다`)
68
+ }
69
+
70
+ const parsed = JSON.parse(current.source) as { type?: string }
71
+ parsed.type = next
72
+
73
+ return { type: next, source: JSON.stringify(parsed) }
74
+ }
75
+
76
+ /**
77
+ * 사실 셋을 저장소에서 읽는다.
78
+ *
79
+ * 규칙에서 떼어 둔 자리이지만 **질의의 모양이 틀리면 저작자에게는 저장 실패로만 보인다.**
80
+ * 실제로 그랬다 — 판본을 `figure: { id }` 로 물었고, `FigureHistory` 는 관계 칸이 아니라
81
+ * `originalId` 로 가리킨다. TypeORM 이 「그런 속성이 없다」로 던졌고 화면에는 영문 오류 한 줄이
82
+ * 떴다. 그래서 이 함수를 내보내 sqlite 를 들고 왕복까지 본다.
83
+ */
84
+ export async function renameFacts(
85
+ repositories: {
86
+ figures: { findOne(options: any): Promise<{ id: string } | null> }
87
+ histories: { count(options: any): Promise<number> }
88
+ },
89
+ figure: { id?: string; version?: number },
90
+ domain: { id: string },
91
+ wanted: string
92
+ ): Promise<RenameFacts> {
93
+ const next = wanted.trim()
94
+ const hasHistory = (await repositories.histories.count({ where: { originalId: figure.id } })) > 0
95
+ const taken = next ? await repositories.figures.findOne({ where: { domain: { id: domain.id }, type: next } }) : null
96
+
97
+ return { version: figure.version, hasHistory, takenByAnother: !!taken && taken.id !== figure.id }
98
+ }
@@ -1,10 +1,13 @@
1
1
  /*
2
- * figure 도구가 지나는 문 — ADR-0072 결정 2.
2
+ * figure 도구가 지나는 문 — ADR-0072 결정 2, ADR-0071 결정 2 보탬(2026-09-18).
3
3
  *
4
- * 선언만 해 두고 아무도 읽지 않으면 그 선언은 다음 사람이 지우거나 바꿔도 아무 일이 없다. 여기서는
5
- * 실제로 등록된 명세를 등록기에 넣고, 등록기가 무엇도 문제 삼지 않는지 묻는다. 도구 목록이 늘면
6
- * 이 시험이 먼저 문다.
4
+ * 선언만 해 두고 아무도 읽지 않으면 그 선언은 다음 사람이 지우거나 바꿔도 아무 일이 없다. 이제는
5
+ * 읽는 쪽이 생겼다 — 관문이 이 목록으로 도구를 내보낼지 정한다. 그래서 여기서 묻는 것이 둘이다:
6
+ * 도구가 문을 적었는가, 그리고 **그 문이 무엇인가를 지키는가.**
7
7
  */
8
+ import { readdirSync, readFileSync } from 'fs'
9
+ import { join } from 'path'
10
+
8
11
  import { clearToolRegistry, getAllToolSpecs, getToolRegistryWarnings } from '@things-factory/ai-client-base'
9
12
 
10
13
  import { registerFigureToolCategories } from './figure-tools'
@@ -24,15 +27,101 @@ describe('figure tools — declared doors', () => {
24
27
  expect(undeclared).toEqual([])
25
28
  })
26
29
 
27
- it('★ a door a tool reaches comes with the privilege that guards it', () => {
30
+ it('the registry itself finds nothing to report about these specs', () => {
31
+ expect(getToolRegistryWarnings().map(w => `${w.code}:${w.params.tool ?? ''}`)).toEqual([])
32
+ })
33
+ })
34
+
35
+ /*
36
+ * 도구가 가리키는 문이 무엇인가를 지키는가 — 관문이 딛고 선 사실.
37
+ *
38
+ * 2026-09-18 까지 도구 명세가 권한을 베껴 적었고 이 자리가 그 사본을 문과 견줬다. 사본은 없어졌다
39
+ * (ADR-0071 결정 2 보탬) — 관문이 AUTH 의 필드 표에서 문의 선언을 직접 읽는다. 그래서 견줄 두 값도
40
+ * 없다.
41
+ *
42
+ * 대신 관문이 **딛고 선 사실**이 남았다: 이 네 도구가 가리키는 문에 지시자가 붙어 있다는 것. 누가
43
+ * `Query.figures` 에서 `@privilege` 를 떼면 그 문은 테넌트 안에서 열리고, 도구도 같이 열린다 —
44
+ * 그 자리가 이제 아무 데서도 안 드러난다. 여기서 문다.
45
+ */
46
+ describe('figure tools — the doors they reach are guarded', () => {
47
+ beforeEach(() => {
48
+ clearToolRegistry()
49
+ registerFigureToolCategories()
50
+ })
51
+ afterAll(() => clearToolRegistry())
52
+
53
+ /** 리졸버 원문에서 `Query.figures` 같은 문 이름 → 그 문의 `@privilege` 지시자. */
54
+ const doorPrivileges = (): Map<string, { category: string; privilege: string; owner: boolean }> => {
55
+ const found = new Map<string, { category: string; privilege: string; owner: boolean }>()
56
+
57
+ for (const file of readdirSync(__dirname).filter(one => one.endsWith('.ts') && !one.endsWith('.test.ts'))) {
58
+ const lines = readFileSync(join(__dirname, file), 'utf8').split('\n')
59
+ let privilege: { category: string; privilege: string; owner: boolean } | undefined
60
+ let kind: 'Query' | 'Mutation' | undefined
61
+ let decorator = ''
62
+
63
+ for (const line of lines) {
64
+ const declared = /@privilege\(category: "([^"]+)", privilege: "([^"]+)"(, domainOwnerGranted: (true|false))?\)/.exec(line)
65
+ if (declared) {
66
+ privilege = { category: declared[1]!, privilege: declared[2]!, owner: declared[4] === 'true' }
67
+ continue
68
+ }
69
+
70
+ const opens = /@(Query|Mutation)\(/.exec(line)
71
+ if (opens) {
72
+ kind = opens[1] as 'Query' | 'Mutation'
73
+ decorator = line
74
+ continue
75
+ }
76
+
77
+ if (!kind) {
78
+ /* 지시자와 문 사이에 딴 것이 오면 그 지시자는 이 문의 것이 아니다. */
79
+ if (line.trim() && !line.trim().startsWith('@') && !line.trim().startsWith('*') && !line.trim().startsWith('/*')) {
80
+ privilege = undefined
81
+ }
82
+ continue
83
+ }
84
+
85
+ decorator += '\n' + line
86
+ const method = /^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/.exec(line)
87
+ if (!method) continue
88
+
89
+ /*
90
+ 필드 이름이 메서드 이름과 다를 수 있는 길은 하나뿐이다 — 데코레이터의 `name:`. 그것이
91
+ 나타나면 조용히 메서드 이름으로 읽지 않고 멈춘다. 잘못 읽은 짝은 초록으로 보인다.
92
+ */
93
+ if (/\bname:\s*'/.test(decorator)) {
94
+ throw new Error(`${file} 의 ${kind} 가 데코레이터에서 이름을 정한다 — 이 시험이 그 길을 아직 안 읽는다`)
95
+ }
96
+
97
+ if (privilege) found.set(`${kind}.${method[1]}`, privilege)
98
+ kind = undefined
99
+ privilege = undefined
100
+ decorator = ''
101
+ }
102
+ }
103
+
104
+ return found
105
+ }
106
+
107
+ it('★ 도구가 가리키는 문마다 그 문을 지키는 지시자가 있다', () => {
108
+ const doors = doorPrivileges()
109
+
28
110
  const unguarded = getAllToolSpecs()
29
- .filter(spec => ((spec as any).doors ?? []).length > 0 && !(spec as any).privilege)
30
- .map(spec => spec.name)
111
+ .flatMap(spec =>
112
+ ((spec as any).doors ?? []).map((door: string) =>
113
+ doors.has(door) ? undefined : `${spec.name} → ${door}: 그런 문이 없거나 그 문이 권한을 선언하지 않는다`
114
+ )
115
+ )
116
+ .filter(Boolean)
31
117
 
32
118
  expect(unguarded).toEqual([])
33
119
  })
34
120
 
35
- it('the registry itself finds nothing to report about these specs', () => {
36
- expect(getToolRegistryWarnings().map(w => `${w.code}:${w.params.tool ?? ''}`)).toEqual([])
121
+ it('견줄 문을 실제로 찾아 읽었다 — 0 건이면 위 시험은 언제나 초록이다', () => {
122
+ const doors = doorPrivileges()
123
+ expect(doors.get('Query.figures')).toEqual({ category: 'figure', privilege: 'query', owner: true })
124
+ expect(doors.get('Mutation.proposeFigure')).toEqual({ category: 'figure', privilege: 'mutation', owner: true })
125
+ expect(doors.size).toBeGreaterThanOrEqual(4)
37
126
  })
38
127
  })
@@ -129,6 +129,72 @@ test('실제 registryAssistantChat 경계가 열린 모델 문맥을 Figure 도
129
129
  expect(JSON.stringify(seen[1])).not.toContain('FIGURE_TARGET_REQUIRED')
130
130
  })
131
131
 
132
+ /*
133
+ 등록된 범주에 도구 넷이 있다는 것과, 넷이 **모델에게 건네졌다**는 것은 다른 사실이다. 그 사이에
134
+ 관문이 하나 있다 — `resolveToolScope` 가 도구마다 `doors` 를 부르는 사람의 권한에 물어 통과한 것만
135
+ 싣는다(ADR-0071 결정 2). 그래서 등록은 넷인데 대화에 실리는 것은 셋일 수 있고, 그때 화면은 아무
136
+ 오류도 내지 않는다 — 모델이 그 도구를 안 부르니 「후보를 만들었습니다」로 끝난다.
137
+
138
+ 위의 배선 시험은 도구 하나(`getFigureSource`)가 오간 것으로 그 줄을 확인한다. 나머지 셋이 실렸는지는
139
+ 보지 않는다. 그래서 건네진 목록 자체를 여기서 읽는다.
140
+ */
141
+ describe('모델에게 건네지는 목록', () => {
142
+ /** `chatWithTools(messages, toolDefs, options)` 의 둘째 인자를 그대로 잡는다. */
143
+ async function offeredWith(access: (door: string) => Promise<boolean>) {
144
+ const offered: string[][] = []
145
+
146
+ await registryAssistantChat({
147
+ client: {
148
+ chatWithTools: async (_messages: any[], toolDefs: any[]) => {
149
+ offered.push((toolDefs ?? []).map(one => one.name ?? one.function?.name))
150
+ return { text: '네.', toolCalls: [], stopReason: 'end_turn' }
151
+ }
152
+ } as any,
153
+ messages: [{ role: 'user', content: '무엇을 할 수 있나' }],
154
+ systemPrompt: 'Author figures.',
155
+ toolCategories: ['figure-authoring'],
156
+ state: { domain: MINE },
157
+ hostContext: { figureId: 'fig-1', draftSource: SOURCE },
158
+ toolAccess: access
159
+ } as any)
160
+
161
+ expect(offered).toHaveLength(1)
162
+ return offered[0]!.sort()
163
+ }
164
+
165
+ it('★ 도구 넷이 다 건네진다 — 문을 다 가진 사람에게', async () => {
166
+ expect(await offeredWith(async () => true)).toEqual([
167
+ 'getFigureSource',
168
+ 'inspectFigure',
169
+ 'listFigures',
170
+ 'proposeFigureCandidate'
171
+ ])
172
+ })
173
+
174
+ /*
175
+ 넷이 문 넷을 하나씩 나눠 가지므로, 문 하나를 닫으면 도구 하나만 빠져야 한다. 이 갈래가 없으면
176
+ 위 시험은 「관문이 아무것도 안 걸러도」 초록이다.
177
+
178
+ 후보를 내는 도구를 닫힌 쪽으로 골랐다. 읽기 권한만 가진 사람이 저작 대화를 열면 목록과 정본과
179
+ 검사는 되고 후보 생성은 안 되는 것이 그 사람에게 맞는 모양이다.
180
+ */
181
+ it('★ 문이 닫힌 도구 하나만 빠진다 — 나머지 셋은 그대로다', async () => {
182
+ const offered = await offeredWith(async door => door !== 'Mutation.proposeFigure')
183
+
184
+ expect(offered).toEqual(['getFigureSource', 'inspectFigure', 'listFigures'])
185
+ })
186
+
187
+ /*
188
+ 권한을 물어볼 길이 없으면 문에 닿는 도구는 전부 닫힌다(`resolveToolScope`: `if (!access) return false`).
189
+ figure 의 넷은 전부 문에 닿으므로 하나도 남지 않는다 — 부분만 열리는 일이 없다는 뜻이다.
190
+ */
191
+ it('물어볼 길이 없으면 하나도 건네지지 않는다', async () => {
192
+ const offered = await offeredWith(undefined as any)
193
+
194
+ expect(offered).toEqual([])
195
+ })
196
+ })
197
+
132
198
  describe('범주가 등록된다', () => {
133
199
  it('figure-authoring 이 도구 넷과 함께 올라온다', () => {
134
200
  const category = getToolCategories().find((one: any) => one.name === 'figure-authoring')!
@@ -18,6 +18,12 @@
18
18
  * `ai-assistant:mutation` 이 지킨다 — 서로 다른 질문이라 서로 다른 권한이 지킨다. figure 가 board
19
19
  * 의 권한을 빌리지 않는다.
20
20
  *
21
+ * ── 권한은 적지 않는다. 문에 적힌 것을 관문이 읽는다 ────────────────────────
22
+ * 2026-09-18 까지 도구마다 `privilege: { category: 'figure', ... }` 를 적어 두었다. 문의 지시자와
23
+ * 같은 값이라 그때는 맞았고, 문이 선언을 바꾸는 날 어긋난다 — 어긋나면 느슨한 쪽이 이긴다. 이제
24
+ * 관문이 `doors` 에 적힌 문의 선언을 AUTH 의 표에서 읽어 묻는다(ADR-0071 결정 2 보탬). 여기 남길
25
+ * 것은 **닿는 문**뿐이다.
26
+ *
21
27
  * ── 규율을 도구와 함께 보낸다 ───────────────────────────────────────────────
22
28
  * 도구 목록만으로는 「언제 반드시 불러야 하는지」를 모른다. twin 이 그 공백에서 사고를 냈다 —
23
29
  * 조치 요청에 도구를 부르지 않고 「제안해 두었습니다」라고만 답해, 사용자에게 **없는 버튼을 누르라고
@@ -249,7 +255,6 @@ function figureToolSpecs(): ToolSpec[] {
249
255
  name: 'listFigures',
250
256
  /* 이 도메인의 도형 목록 — 저장소를 직접 읽지만 같은 일을 하는 문은 이것이다. */
251
257
  doors: ['Query.figures'],
252
- privilege: { category: 'figure', privilege: 'query', owner: true },
253
258
  description:
254
259
  'Lists the figures in this tenant with their type names, states and part counts. ' +
255
260
  'Call it before naming a type — type names are stored identifiers and must not be invented.',
@@ -280,7 +285,6 @@ function figureToolSpecs(): ToolSpec[] {
280
285
  /* 저장된 도형의 원본. **저장 전 초안**을 물으면 저장소에 갈 것이 없어 화면이 보낸 것을 그대로 돌려준다 —
281
286
  * 그 갈래에는 닿는 문이 없지만 `doors: []` 로 적지 않는다. 그러면 저장된 도형을 읽는 권한 검사가 선언에서 사라진다. */
282
287
  doors: ['Query.figure'],
283
- privilege: { category: 'figure', privilege: 'query', owner: true },
284
288
  description:
285
289
  'Returns the canonical FigureSource of one figure, with its measured cost ' +
286
290
  '(triangles, part groups, and how many of it hold 60fps on one screen). Call it before proposing a revision — ' +
@@ -318,7 +322,6 @@ function figureToolSpecs(): ToolSpec[] {
318
322
  name: 'inspectFigure',
319
323
  /* 검사 결과 — 같은 일을 하는 문. */
320
324
  doors: ['Query.inspectFigure'],
321
- privilege: { category: 'figure', privilege: 'query', owner: true },
322
325
  description:
323
326
  'Says whether a figure may be released and, when it may not, what is wrong — each finding ' +
324
327
  'carries why it is a problem and what to change. Call it when asked about releasing, or ' +
@@ -340,7 +343,6 @@ function figureToolSpecs(): ToolSpec[] {
340
343
  name: 'proposeFigureCandidate',
341
344
  /* 후보 생성 — 저장은 하지 않지만 같은 문의 권한을 쓴다. */
342
345
  doors: ['Mutation.proposeFigure'],
343
- privilege: { category: 'figure', privilege: 'mutation', owner: true },
344
346
  description:
345
347
  'Asks for a figure candidate from a description, optionally revising an existing one. ' +
346
348
  '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
 
@@ -0,0 +1,3 @@
1
+ {
2
+ "error.figure-release-does-not-compile": "the released source of {type} (version {version}) does not compile. open it in the modeller, fix it and release it again before it draws on a board."
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "error.figure-release-does-not-compile": "{type} 의 발행판({version})이 지금 컴파일되지 않습니다. 저작 화면에서 열어 고친 뒤 다시 발행해야 보드에 나옵니다."
3
+ }