@things-factory/figure-service 10.1.167 → 10.1.169
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-context.d.ts +108 -0
- package/dist-server/service/figure/figure-context.js +368 -0
- package/dist-server/service/figure/figure-context.js.map +1 -0
- package/dist-server/service/figure/figure-format.js +10 -16
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/service/figure/figure-mutation.js +2 -2
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-proposal.js +4 -2
- package/dist-server/service/figure/figure-proposal.js.map +1 -1
- package/dist-server/service/figure/figure-propose-steps.d.ts +34 -6
- package/dist-server/service/figure/figure-propose-steps.js +128 -17
- package/dist-server/service/figure/figure-propose-steps.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +197 -49
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/server/service/figure/figure-context.test.ts +288 -0
- package/server/service/figure/figure-context.ts +447 -0
- package/server/service/figure/figure-format.test.ts +18 -1
- package/server/service/figure/figure-format.ts +10 -16
- package/server/service/figure/figure-llm-smoke.test.ts +82 -13
- package/server/service/figure/figure-mutation.ts +3 -3
- package/server/service/figure/figure-proposal.ts +4 -2
- package/server/service/figure/figure-propose-steps.test.ts +146 -6
- package/server/service/figure/figure-propose-steps.ts +145 -26
- package/server/service/figure/figure-tools-door-db.test.ts +227 -0
- package/server/service/figure/figure-tools.test.ts +382 -22
- package/server/service/figure/figure-tools.ts +218 -50
- package/translations/en.json +11 -9
- package/translations/es.json +11 -9
- package/translations/ja.json +11 -9
- package/translations/ko.json +11 -9
- package/translations/ms.json +11 -9
- package/translations/zh.json +11 -9
|
@@ -35,7 +35,22 @@
|
|
|
35
35
|
* 있다. 그래서 호출자 도메인 소속을 먼저 확인한다.
|
|
36
36
|
*/
|
|
37
37
|
import { randomUUID } from 'crypto'
|
|
38
|
-
import {
|
|
38
|
+
import { ILike } from 'typeorm'
|
|
39
|
+
import {
|
|
40
|
+
aiClientFor,
|
|
41
|
+
registerToolCategory,
|
|
42
|
+
type AIImageMediaType,
|
|
43
|
+
type ToolAttachment,
|
|
44
|
+
type ToolCallContext,
|
|
45
|
+
type ToolCategory,
|
|
46
|
+
type ToolImageAttachment,
|
|
47
|
+
type ToolSpec
|
|
48
|
+
} from '@things-factory/ai-client-base'
|
|
49
|
+
|
|
50
|
+
import { FIGURE_CONTEXT_BUDGET, figureViewOf, type FigureReadRequest } from './figure-context.js'
|
|
51
|
+
|
|
52
|
+
/** How many figures a list without search names. */
|
|
53
|
+
const LIST_LIMIT = 200
|
|
39
54
|
|
|
40
55
|
/*
|
|
41
56
|
서버 의존성은 lazy require 다. 등록은 부팅 초반에 평가되고 그때 typeorm DataSource 가 준비되지
|
|
@@ -101,17 +116,29 @@ async function figureInDomain(figureId: string, domainId: string): Promise<any>
|
|
|
101
116
|
return found
|
|
102
117
|
}
|
|
103
118
|
|
|
119
|
+
/** The form of a creation id: the row id a new figure's first save inserts it under (`figure-mutation.ts`). */
|
|
120
|
+
const CREATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
121
|
+
|
|
104
122
|
/**
|
|
105
123
|
* The figure a tool is about: by id, or by its type name, in the caller's domain. None named: none.
|
|
124
|
+
*
|
|
125
|
+
* A figure not saved yet is a subject too (ADR-0061 decision 3): the modeller names it by its creation id, which its first
|
|
126
|
+
* save inserts the row under (ADR-0072 decision 6), so before that save there is no row. It is the figure open on that
|
|
127
|
+
* screen and nothing else: the id the screen has open, with the draft the screen sent. What is read is that draft, put
|
|
128
|
+
* through the save's door like any draft (`sourceForTool`); nothing the server holds is read for it.
|
|
106
129
|
*/
|
|
107
|
-
async function resolveFigureTarget(figureId: string | undefined, domainId: string,
|
|
130
|
+
async function resolveFigureTarget(figureId: string | undefined, domainId: string, host: any): Promise<any> {
|
|
108
131
|
if (!figureId) return undefined
|
|
109
132
|
const { getRepository, Figure } = getDeps()
|
|
110
133
|
const found =
|
|
111
134
|
(await getRepository(Figure).findOne({ where: { id: figureId, domain: { id: domainId } } })) ??
|
|
112
135
|
(await getRepository(Figure).findOne({ where: { type: figureId, domain: { id: domainId } } }))
|
|
113
|
-
if (
|
|
114
|
-
|
|
136
|
+
if (found) return found
|
|
137
|
+
const draft = host?.draftSource
|
|
138
|
+
if (CREATION_ID.test(figureId) && host?.figureId === figureId && draft && typeof draft === 'object') {
|
|
139
|
+
return { id: figureId, name: typeof draft.name === 'string' ? draft.name : undefined, source: null }
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`figure not found in this tenant: ${figureId}`)
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
/**
|
|
@@ -129,7 +156,7 @@ const isFigure = (source: any) => source?.version === 'figure-asset-1'
|
|
|
129
156
|
function figureNotOpen() {
|
|
130
157
|
return {
|
|
131
158
|
error: 'FIGURE_NOT_OPEN',
|
|
132
|
-
message: '
|
|
159
|
+
message: 'A revision can be proposed only for the figure open in the modeller.',
|
|
133
160
|
nextStep:
|
|
134
161
|
'Tell the person to open the figure in the modeller and ask again there. A new figure needs no open one: call proposeFigureCandidate with create: true.'
|
|
135
162
|
}
|
|
@@ -141,7 +168,7 @@ function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
|
|
|
141
168
|
|
|
142
169
|
if (typeof draft !== 'object' || Array.isArray(draft)) throw new Error('the unsaved draft is not a figure asset')
|
|
143
170
|
const serialized = JSON.stringify(draft)
|
|
144
|
-
|
|
171
|
+
/* Validity is the save door's (the kernel's 1 MiB); how much the model is shown is `figure-context`'s. */
|
|
145
172
|
try {
|
|
146
173
|
getDeps().parseSource(serialized)
|
|
147
174
|
} catch (e: any) {
|
|
@@ -151,6 +178,36 @@ function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
|
|
|
151
178
|
return { source: draft, draft: true }
|
|
152
179
|
}
|
|
153
180
|
|
|
181
|
+
/** The finite numbers of an untrusted record, by name; none: undefined. */
|
|
182
|
+
function numbersOf(record: any): Record<string, number> | undefined {
|
|
183
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) return undefined
|
|
184
|
+
const kept = Object.entries(record).filter((entry): entry is [string, number] => typeof entry[1] === 'number' && Number.isFinite(entry[1]))
|
|
185
|
+
return kept.length ? Object.fromEntries(kept) : undefined
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* What the person has in view on the open draft: the part selected and what the preview is trying (ADR-0097 §5.6
|
|
190
|
+
* item 5). The model hears it only here, through a read (ADR-0061 decision 3). Told only when it was seen on the version
|
|
191
|
+
* being read -- a selection from another version may name a part that is gone -- and a part only when the draft has it.
|
|
192
|
+
*/
|
|
193
|
+
function focusOf(host: any, source: any): { selection?: { part: string }; trying?: { clips?: Record<string, number>; controls?: Record<string, number> } } {
|
|
194
|
+
const onThisVersion = (seen: any) => typeof host?.context === 'string' && seen?.context === host.context && seen?.revision === host.revision
|
|
195
|
+
const focus: ReturnType<typeof focusOf> = {}
|
|
196
|
+
|
|
197
|
+
const part = host?.selection?.part
|
|
198
|
+
const nodes = source?.document?.model?.nodes
|
|
199
|
+
if (typeof part === 'string' && onThisVersion(host.selection) && Array.isArray(nodes) && nodes.some((node: any) => node?.id === part)) {
|
|
200
|
+
focus.selection = { part }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (onThisVersion(host?.trying)) {
|
|
204
|
+
const clips = numbersOf(host.trying.clips)
|
|
205
|
+
const controls = numbersOf(host.trying.controls)
|
|
206
|
+
if (clips || controls) focus.trying = { ...(clips ? { clips } : {}), ...(controls ? { controls } : {}) }
|
|
207
|
+
}
|
|
208
|
+
return focus
|
|
209
|
+
}
|
|
210
|
+
|
|
154
211
|
/**
|
|
155
212
|
* 무엇을 고치라는 것인지 대화면 문맥에서 해소한다.
|
|
156
213
|
*
|
|
@@ -170,7 +227,7 @@ function figureIdFrom(args: any, host: any): string | undefined {
|
|
|
170
227
|
function missingFigureTarget() {
|
|
171
228
|
return {
|
|
172
229
|
error: 'FIGURE_TARGET_REQUIRED',
|
|
173
|
-
message: '
|
|
230
|
+
message: 'No figure is open on this screen.',
|
|
174
231
|
nextStep: 'For an existing figure, call listFigures and match the user’s requested name or type; ask which figure only if ambiguous. For a new figure, call proposeFigureCandidate with create: true and the user’s prompt. Do not invent an id or repeat this call without a target.'
|
|
175
232
|
}
|
|
176
233
|
}
|
|
@@ -178,10 +235,63 @@ function missingFigureTarget() {
|
|
|
178
235
|
const FIGURE_ID_ARG = {
|
|
179
236
|
figureId: {
|
|
180
237
|
type: 'string',
|
|
181
|
-
description: 'The figure to read. Omit to read the one the
|
|
238
|
+
description: 'The figure to read. Omit to read the one open in the modeller — with its unsaved changes, and also when it is a new figure not saved yet; the server fills it in, and you cannot see which it is. A figure not saved yet is readable only while it is open in the modeller. On the catalog nothing is open, so resolve the figure the person asked for with listFigures first.'
|
|
182
239
|
}
|
|
183
240
|
} as const
|
|
184
241
|
|
|
242
|
+
const PARTS_ARG = {
|
|
243
|
+
parts: {
|
|
244
|
+
type: 'array',
|
|
245
|
+
items: { type: 'string' },
|
|
246
|
+
description:
|
|
247
|
+
'Only for a figure too large to read whole (the answer then has `outline` and no `source`): the ids of the parts to read in full, from `outline.parts`. Omit to read the part selected.'
|
|
248
|
+
},
|
|
249
|
+
sections: {
|
|
250
|
+
type: 'array',
|
|
251
|
+
items: { type: 'string' },
|
|
252
|
+
description:
|
|
253
|
+
'Only for a figure too large to read whole: JSON Pointers of sections to read, such as one named in `outline.omitted` or its `entries` (for example /document/capabilities/bindings/0).'
|
|
254
|
+
}
|
|
255
|
+
} as const
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A figure too large to show whole, as the model reads it (ADR-0061 2026-10-16 decision 4): its outline and the parts
|
|
259
|
+
* asked for. The full source stays here; what is measured is measured on it.
|
|
260
|
+
*/
|
|
261
|
+
function partialRead(source: any, request: FigureReadRequest) {
|
|
262
|
+
const view = figureViewOf(source, request)
|
|
263
|
+
if (view.whole) return { source }
|
|
264
|
+
return {
|
|
265
|
+
outline: view.outline,
|
|
266
|
+
partSources: view.parts,
|
|
267
|
+
...(Object.keys(view.sections ?? {}).length ? { sectionSources: view.sections } : {}),
|
|
268
|
+
...(view.notRead ? { notRead: view.notRead } : {}),
|
|
269
|
+
note:
|
|
270
|
+
`This figure is too large to read whole (${view.outline!.chars} characters; ${FIGURE_CONTEXT_BUDGET} are read at once), so this is its outline, the parts in partSources and the sections in sectionSources. ` +
|
|
271
|
+
'Read what a change touches with parts and sections, then pass the same to proposeFigureCandidate: only parts read in full may be changed, and nothing in a section in outline.omitted that was not read.'
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The parts the model named, as strings; anything else is dropped. */
|
|
276
|
+
const stringsOf = (list: unknown): string[] =>
|
|
277
|
+
Array.isArray(list) ? list.filter((one: unknown): one is string => typeof one === 'string' && !!one.trim()).map((one: string) => one.trim()) : []
|
|
278
|
+
|
|
279
|
+
/** An inspection whose findings would crowd the model's context, cut to the budget with how many there were. */
|
|
280
|
+
function findingsWithinBudget(inspection: any) {
|
|
281
|
+
const findings = Array.isArray(inspection?.findings) ? inspection.findings : []
|
|
282
|
+
if (JSON.stringify(inspection).length <= FIGURE_CONTEXT_BUDGET) return inspection
|
|
283
|
+
const head = JSON.stringify({ ...inspection, findings: [], truncated: { shown: findings.length, total: findings.length } }).length
|
|
284
|
+
let room = FIGURE_CONTEXT_BUDGET - head
|
|
285
|
+
const shown: any[] = []
|
|
286
|
+
for (const finding of findings) {
|
|
287
|
+
const cost = JSON.stringify(finding).length + 1
|
|
288
|
+
if (cost > room) break
|
|
289
|
+
shown.push(finding)
|
|
290
|
+
room -= cost
|
|
291
|
+
}
|
|
292
|
+
return { ...inspection, findings: shown, truncated: { shown: shown.length, total: findings.length } }
|
|
293
|
+
}
|
|
294
|
+
|
|
185
295
|
/**
|
|
186
296
|
* 도구 넷.
|
|
187
297
|
*
|
|
@@ -197,18 +307,40 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
197
307
|
doors: ['Query.figures'],
|
|
198
308
|
description:
|
|
199
309
|
'Lists the figures in this tenant with their type names, states and part counts. ' +
|
|
200
|
-
'Call it before naming a type — type names are stored identifiers and must not be invented.'
|
|
201
|
-
|
|
202
|
-
|
|
310
|
+
'Call it before naming a type — type names are stored identifiers and must not be invented. ' +
|
|
311
|
+
`Without search it lists the ${LIST_LIMIT} most recently changed; when there are more it says truncated, and a figure the person names may be among the rest - list again with search.`,
|
|
312
|
+
schema: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: {
|
|
315
|
+
search: {
|
|
316
|
+
type: 'string',
|
|
317
|
+
description: 'Part of a figure name or type name, in any case. Omit to list the most recently changed.'
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
builder: async (args: any, ctx: any) => {
|
|
203
322
|
const domainId = requireDomainId(ctx?.state)
|
|
204
323
|
const { getRepository, Figure } = getDeps()
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
324
|
+
const search = typeof args?.search === 'string' ? args.search.trim() : ''
|
|
325
|
+
const domain = { id: domainId }
|
|
326
|
+
/* `_` and `%` in the words are LIKE wildcards: they can only widen what is found, never hide a match. */
|
|
327
|
+
const where = search
|
|
328
|
+
? [
|
|
329
|
+
{ domain, name: ILike(`%${search}%`) },
|
|
330
|
+
{ domain, type: ILike(`%${search}%`) }
|
|
331
|
+
]
|
|
332
|
+
: { domain }
|
|
333
|
+
/* One more than is shown, to know whether there is more without counting the table. */
|
|
334
|
+
const rows = await getRepository(Figure).find({ where, order: { updatedAt: 'DESC' }, take: LIST_LIMIT + 1 })
|
|
335
|
+
const truncated = rows.length > LIST_LIMIT
|
|
210
336
|
return {
|
|
211
|
-
|
|
337
|
+
...(truncated
|
|
338
|
+
? {
|
|
339
|
+
truncated: true,
|
|
340
|
+
note: `Only the ${LIST_LIMIT} most recently changed are listed. A figure not here may still exist: call listFigures again with search.`
|
|
341
|
+
}
|
|
342
|
+
: {}),
|
|
343
|
+
figures: rows.slice(0, LIST_LIMIT).map((row: any) => ({
|
|
212
344
|
id: row.id,
|
|
213
345
|
type: row.type,
|
|
214
346
|
name: row.name,
|
|
@@ -228,8 +360,12 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
228
360
|
description:
|
|
229
361
|
'Returns the source (the figure asset) of one figure, with its measured cost ' +
|
|
230
362
|
'(triangles, part groups, and how many of it hold 60fps on one screen). Call it before proposing a revision — ' +
|
|
231
|
-
'revising without reading it means proposing against a guess.'
|
|
232
|
-
|
|
363
|
+
'revising without reading it means proposing against a guess. On the figure open in the modeller it also says what ' +
|
|
364
|
+
'the person has in view: `selection.part` is the part selected - "this part" or "the selected one" means it - and ' +
|
|
365
|
+
'`trying` the clip and control values the preview is trying, which are not part of the figure. ' +
|
|
366
|
+
'A figure too large to read whole comes as `outline` (every part by name, shape and what it is fastened to) and ' +
|
|
367
|
+
'`partSources` (the parts read in full) instead of `source`.',
|
|
368
|
+
schema: { type: 'object', properties: { ...FIGURE_ID_ARG, ...PARTS_ARG } },
|
|
233
369
|
builder: async (args: any, ctx: any) => {
|
|
234
370
|
const domainId = requireDomainId(ctx?.state)
|
|
235
371
|
const figureId = figureIdFrom(args, ctx?.host)
|
|
@@ -244,15 +380,19 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
244
380
|
|
|
245
381
|
/* Measured as the catalogue measures it: triangles, draw groups, and its grade against one screen. */
|
|
246
382
|
const { figureCostOf, figureScoreOf } = getDeps()
|
|
383
|
+
/* What is in view belongs to the draft on screen; the stored source may be another figure than the one open. */
|
|
384
|
+
const focus = current.draft ? focusOf(ctx?.host, current.source) : {}
|
|
385
|
+
const asked = stringsOf(args?.parts)
|
|
247
386
|
return {
|
|
248
387
|
id: row.id,
|
|
249
388
|
type: row.type,
|
|
250
389
|
name: row.name,
|
|
251
390
|
state: row.state,
|
|
252
|
-
source:
|
|
391
|
+
...partialRead(current.source, { parts: asked.length ? asked : focus.selection ? [focus.selection.part] : [], sections: stringsOf(args?.sections) }),
|
|
253
392
|
draft: current.draft,
|
|
254
393
|
cost: figureCostOf(current.source),
|
|
255
|
-
score: figureScoreOf(current.source) ?? null
|
|
394
|
+
score: figureScoreOf(current.source) ?? null,
|
|
395
|
+
...focus
|
|
256
396
|
}
|
|
257
397
|
}
|
|
258
398
|
},
|
|
@@ -274,7 +414,8 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
274
414
|
|
|
275
415
|
const current = sourceForTool(row, ctx?.host)
|
|
276
416
|
const { inspectSource } = getDeps()
|
|
277
|
-
|
|
417
|
+
/* Inspected whole, here; only what the model is told is held to its budget. */
|
|
418
|
+
return { ...findingsWithinBudget(inspectSource(current.source ? JSON.stringify(current.source) : null, null, undefined, row.type)), draft: current.draft }
|
|
278
419
|
}
|
|
279
420
|
},
|
|
280
421
|
{
|
|
@@ -304,11 +445,23 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
304
445
|
create: {
|
|
305
446
|
type: 'boolean',
|
|
306
447
|
description: 'True to create rather than revise, even with a figure open.'
|
|
448
|
+
},
|
|
449
|
+
parts: {
|
|
450
|
+
type: 'array',
|
|
451
|
+
items: { type: 'string' },
|
|
452
|
+
description:
|
|
453
|
+
'Only for a figure too large to read whole: the parts this change touches, read first with getFigureSource parts. Only these and the part selected may be changed; new parts may be added and fastened to any part.'
|
|
454
|
+
},
|
|
455
|
+
sections: {
|
|
456
|
+
type: 'array',
|
|
457
|
+
items: { type: 'string' },
|
|
458
|
+
description:
|
|
459
|
+
'Only for a figure too large to read whole: JSON Pointers of the sections in outline.omitted this change touches, read first with getFigureSource sections. Nothing else in those sections may be changed.'
|
|
307
460
|
}
|
|
308
461
|
},
|
|
309
462
|
required: ['prompt']
|
|
310
463
|
},
|
|
311
|
-
builder: async (args: any, ctx:
|
|
464
|
+
builder: async (args: any, ctx: ToolCallContext) => {
|
|
312
465
|
const domainId = requireDomainId(ctx?.state)
|
|
313
466
|
const prompt = typeof args?.prompt === 'string' ? args.prompt.trim() : ''
|
|
314
467
|
if (!prompt) throw new Error('prompt is required: there is nothing to ask for')
|
|
@@ -325,6 +478,7 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
325
478
|
const create = args?.create === true || args?.figureId === null
|
|
326
479
|
let base: unknown
|
|
327
480
|
let figureId: string | undefined
|
|
481
|
+
let focus: ReturnType<typeof focusOf> | undefined
|
|
328
482
|
if (!create) {
|
|
329
483
|
const said = typeof args?.figureId === 'string' ? args.figureId.trim() : ''
|
|
330
484
|
const open = typeof host?.figureId === 'string' ? host.figureId.trim() : ''
|
|
@@ -335,19 +489,22 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
335
489
|
if (!open) return missingFigureTarget()
|
|
336
490
|
const row = await resolveFigureTarget(open, domainId, host)
|
|
337
491
|
if (!row) return missingFigureTarget()
|
|
338
|
-
|
|
492
|
+
const current = sourceForTool(row, host)
|
|
493
|
+
base = current.source ?? undefined
|
|
339
494
|
if (!isFigure(base)) return figureNotOpen()
|
|
340
495
|
if (typeof host?.context !== 'string' || !Number.isInteger(host?.revision)) return figureNotOpen()
|
|
341
496
|
figureId = open
|
|
497
|
+
/* "Make this one bigger": the part selected is what the request points at. */
|
|
498
|
+
if (current.draft) focus = focusOf(host, base)
|
|
342
499
|
}
|
|
343
500
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
501
|
+
/*
|
|
502
|
+
The pictures the person sent with this turn, as the server checked them (ADR-0061, 2026-10-16 decision 2) --
|
|
503
|
+
every one, in the order attached. Never from the host context: the client fills it.
|
|
504
|
+
*/
|
|
505
|
+
const images = (ctx?.attachments ?? [])
|
|
506
|
+
.filter((file: ToolAttachment): file is ToolImageAttachment => file.kind === 'image')
|
|
507
|
+
.map(file => ({ data: file.data, mediaType: file.mediaType as AIImageMediaType }))
|
|
351
508
|
|
|
352
509
|
const { proposeFigureSteps } = getDeps()
|
|
353
510
|
let result: any
|
|
@@ -363,7 +520,11 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
363
520
|
prompt,
|
|
364
521
|
base,
|
|
365
522
|
name: typeof args?.name === 'string' && args.name.trim() ? args.name.trim() : undefined,
|
|
366
|
-
|
|
523
|
+
...(images.length ? { images } : {}),
|
|
524
|
+
...(focus && Object.keys(focus).length ? { focus } : {}),
|
|
525
|
+
...(!create && (stringsOf(args?.parts).length || stringsOf(args?.sections).length)
|
|
526
|
+
? { read: { parts: stringsOf(args?.parts), sections: stringsOf(args?.sections) } }
|
|
527
|
+
: {})
|
|
367
528
|
})
|
|
368
529
|
} catch (error) {
|
|
369
530
|
/*
|
|
@@ -382,6 +543,8 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
382
543
|
목록을 한 단계로 적용한다. `args` 는 같은 제안을 두 번 세지 않기 위한 열쇠의 일부다.
|
|
383
544
|
*/
|
|
384
545
|
const name = result.source.name || 'Figure'
|
|
546
|
+
/* The card is read by a person, so the server writes it in the request's language (ADR-0061, 2026-10-16 decision 1). */
|
|
547
|
+
const t = ctx.t
|
|
385
548
|
return {
|
|
386
549
|
proposed: true,
|
|
387
550
|
/*
|
|
@@ -391,10 +554,16 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
391
554
|
*/
|
|
392
555
|
key: randomUUID(),
|
|
393
556
|
icon: 'view_in_ar',
|
|
394
|
-
label: create
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
557
|
+
label: create
|
|
558
|
+
? t('figure.proposal.label-create', { name, defaultValue: 'Make {name}' })
|
|
559
|
+
: t('figure.proposal.label-revise', { name, defaultValue: 'Revise {name}' }),
|
|
560
|
+
reason: t('figure.proposal.reason', {
|
|
561
|
+
steps: result.steps.length,
|
|
562
|
+
grade: result.score?.grade ?? '-',
|
|
563
|
+
triangles: result.cost.triangles,
|
|
564
|
+
attempts: result.attempts,
|
|
565
|
+
defaultValue: '{steps} commands · grade {grade} · {triangles} triangles · made in {attempts} tries'
|
|
566
|
+
}),
|
|
398
567
|
args: { prompt, figureId, create },
|
|
399
568
|
create,
|
|
400
569
|
figureId,
|
|
@@ -420,21 +589,20 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
420
589
|
export function registerFigureToolCategories(): void {
|
|
421
590
|
const authoring: ToolCategory = {
|
|
422
591
|
name: 'figure-authoring',
|
|
423
|
-
description: '
|
|
592
|
+
description: 'Reads the source, cost and release verdict of the figure being authored, and proposes changes (nothing is saved).',
|
|
424
593
|
guidance: [
|
|
425
|
-
'-
|
|
426
|
-
'
|
|
427
|
-
'
|
|
428
|
-
'
|
|
429
|
-
'
|
|
430
|
-
'-
|
|
431
|
-
'-
|
|
432
|
-
'-
|
|
433
|
-
'-
|
|
434
|
-
'-
|
|
435
|
-
'
|
|
436
|
-
'
|
|
437
|
-
'- 무게·삼각형 수를 물으면 `getFigureSource` 의 `cost` 를 쓴다. 눈대중으로 세지 않는다.'
|
|
594
|
+
'- **For a request to make or change a figure, always call `proposeFigureCandidate`.** A proposal exists only as the result of that call.',
|
|
595
|
+
' Writing "I made a proposal" or "press apply" without the call is **false**: no card appears, and the person looks for a button that does not exist.',
|
|
596
|
+
' A proposal is a list of modeller commands. The modeller applies it as one step only when the person runs it from the dock card, and one undo takes it all back. Do not say the figure has changed before that.',
|
|
597
|
+
' A revision can be proposed only for the figure open in the modeller. Say what the result\'s `steps` do as they are; do not invent changes the commands do not make.',
|
|
598
|
+
'- Never save the source yourself. Propose; the author decides on the screen whether to take it.',
|
|
599
|
+
'- Name coordinates and sizes by their X/Y/Z axes. Do not attach width, depth or height meanings that have not been confirmed.',
|
|
600
|
+
'- **For a request to change a figure, call `getFigureSource` first.** Revising without reading proposes against a guess, and a renamed part breaks the bindings of the boards it is already placed on.',
|
|
601
|
+
'- On the catalog nothing is open. To read an existing figure, find the id of the name or type the person said with `listFigures`; ask which figure only when it is ambiguous. To revise an existing figure, tell the person to open it in the modeller and ask there. For a new figure, do not read existing sources: call `proposeFigureCandidate` with create: true.',
|
|
602
|
+
'- Do not invent type names; check them with `listFigures`. **A type name is a stored identifier**, and boards find the figure by it. A new figure\'s type is set by a person at its first save.',
|
|
603
|
+
'- When asked why a figure will not release, call `inspectFigure`. Do not say it can be released without its verdict.',
|
|
604
|
+
' Each finding carries **why it is a problem and what to change**. Pass it on as it is; a summary makes the author look again for which axis of which part to fix.',
|
|
605
|
+
'- For weight or triangle counts, use `cost` from `getFigureSource`. Do not estimate.'
|
|
438
606
|
].join('\n'),
|
|
439
607
|
specs: figureToolSpecs()
|
|
440
608
|
}
|
package/translations/en.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"error.figure-proposal-failed": "The figure could not be made.",
|
|
2
3
|
"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
4
|
"figure.gate.occupancy-empty-axis.message": "The declared space is empty on {axis}.",
|
|
4
5
|
"figure.gate.occupancy-undeclared.how": "Use \"fit to parts\" to declare the space from the parts as they are now.",
|
|
@@ -75,14 +76,12 @@
|
|
|
75
76
|
"figure.gate.contract.public-input-range-narrowed.why": "An instance may have set a value outside the new range. Which values are placed is not recorded.",
|
|
76
77
|
"figure.gate.contract.public-input-added.message": "'{input}' can now be set on a board.",
|
|
77
78
|
"figure.gate.contract.public-input-added.why": "Placed instances that give no value stand at its default.",
|
|
78
|
-
"figure.gate.contract.
|
|
79
|
-
"figure.gate.contract.
|
|
80
|
-
"figure.gate.contract.
|
|
81
|
-
"figure.gate.contract.
|
|
82
|
-
"figure.gate.contract.
|
|
83
|
-
"figure.gate.contract.
|
|
84
|
-
"figure.gate.contract.carrier-added.message": "The figure is now carrier type {type}.",
|
|
85
|
-
"figure.gate.contract.carrier-added.why": "Live data with this code finds this figure once the type map takes this release.",
|
|
79
|
+
"figure.gate.contract.extension-gone.message": "The extension '{key}' is gone.",
|
|
80
|
+
"figure.gate.contract.extension-gone.why": "Another product reads this value by its key. After this release it finds none.",
|
|
81
|
+
"figure.gate.contract.extension-changed.message": "The value of the extension '{key}' changed.",
|
|
82
|
+
"figure.gate.contract.extension-changed.why": "The product that owns this key reads the new value and judges the change.",
|
|
83
|
+
"figure.gate.contract.extension-added.message": "The extension '{key}' is new.",
|
|
84
|
+
"figure.gate.contract.extension-added.why": "The product that owns this key reads this value.",
|
|
86
85
|
"figure.gate.contract.joints-none": "nothing",
|
|
87
86
|
"figure.gate.contract.state-input-joints-changed.message": "The control '{id}' moved {before}. It now moves {after}.",
|
|
88
87
|
"figure.gate.contract.state-input-joints-changed.why": "A controller writes this value to drive those joints. After this release the same value moves other joints, or by other amounts.",
|
|
@@ -150,5 +149,8 @@
|
|
|
150
149
|
"figure.gate.contract.slot-anchor-gone.message": "The anchor '{anchor}' that slots stand on is gone.",
|
|
151
150
|
"figure.gate.contract.slot-anchor-gone.why": "A placement that lays its own slots on this anchor holds nothing after this release.",
|
|
152
151
|
"figure.gate.contract.slot-anchor-moved.message": "The anchor '{anchor}' moved from the {before} face to the {after} face.",
|
|
153
|
-
"figure.gate.contract.slot-anchor-moved.why": "Every slot on it, and what sits there, stands on the new face after this release."
|
|
152
|
+
"figure.gate.contract.slot-anchor-moved.why": "Every slot on it, and what sits there, stands on the new face after this release.",
|
|
153
|
+
"figure.proposal.label-create": "Make {name}",
|
|
154
|
+
"figure.proposal.label-revise": "Revise {name}",
|
|
155
|
+
"figure.proposal.reason": "{steps} commands · grade {grade} · {triangles} triangles · made in {attempts} tries"
|
|
154
156
|
}
|
package/translations/es.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"error.figure-proposal-failed": "No se pudo crear la figura.",
|
|
2
3
|
"error.figure-release-does-not-compile": "el código fuente publicado de {type} (versión {version}) no compila. Ábralo en el modelador, corríjalo y vuelva a publicarlo para que se dibuje en un tablero.",
|
|
3
4
|
"figure.gate.occupancy-empty-axis.message": "El espacio declarado está vacío en {axis}.",
|
|
4
5
|
"figure.gate.occupancy-undeclared.how": "Use «ajustar a las piezas» para declarar el espacio a partir de las piezas tal como están ahora.",
|
|
@@ -75,14 +76,12 @@
|
|
|
75
76
|
"figure.gate.contract.public-input-range-narrowed.why": "Puede que una instancia haya definido un valor fuera del nuevo rango. No se registra qué valores se colocaron.",
|
|
76
77
|
"figure.gate.contract.public-input-added.message": "'{input}' ahora se puede definir en un tablero.",
|
|
77
78
|
"figure.gate.contract.public-input-added.why": "Las instancias colocadas que no dan ningún valor quedan en su valor predeterminado.",
|
|
78
|
-
"figure.gate.contract.
|
|
79
|
-
"figure.gate.contract.
|
|
80
|
-
"figure.gate.contract.
|
|
81
|
-
"figure.gate.contract.
|
|
82
|
-
"figure.gate.contract.
|
|
83
|
-
"figure.gate.contract.
|
|
84
|
-
"figure.gate.contract.carrier-added.message": "La figura es ahora del tipo de carrier {type}.",
|
|
85
|
-
"figure.gate.contract.carrier-added.why": "Los datos en tiempo real con este código encuentran esta figura cuando el mapa de tipos adopte esta publicación.",
|
|
79
|
+
"figure.gate.contract.extension-gone.message": "La extensión '{key}' ya no está.",
|
|
80
|
+
"figure.gate.contract.extension-gone.why": "Otro producto lee este valor por su clave. Tras esta publicación no lo encuentra.",
|
|
81
|
+
"figure.gate.contract.extension-changed.message": "El valor de la extensión '{key}' cambió.",
|
|
82
|
+
"figure.gate.contract.extension-changed.why": "El producto dueño de esta clave lee el nuevo valor y juzga el cambio.",
|
|
83
|
+
"figure.gate.contract.extension-added.message": "La extensión '{key}' es nueva.",
|
|
84
|
+
"figure.gate.contract.extension-added.why": "El producto dueño de esta clave lee este valor.",
|
|
86
85
|
"figure.gate.contract.joints-none": "ninguna",
|
|
87
86
|
"figure.gate.contract.state-input-joints-changed.message": "El control '{id}' movía {before}. Ahora mueve {after}.",
|
|
88
87
|
"figure.gate.contract.state-input-joints-changed.why": "Un controlador escribe este valor para accionar esas articulaciones. Tras esta publicación, el mismo valor mueve otras articulaciones, o en otra medida.",
|
|
@@ -150,5 +149,8 @@
|
|
|
150
149
|
"figure.gate.contract.slot-anchor-gone.message": "El anclaje '{anchor}' sobre el que están las ranuras ya no existe.",
|
|
151
150
|
"figure.gate.contract.slot-anchor-gone.why": "Una colocación que pone sus propias ranuras en este anclaje no contiene nada tras esta publicación.",
|
|
152
151
|
"figure.gate.contract.slot-anchor-moved.message": "El anclaje '{anchor}' pasó de la cara {before} a la cara {after}.",
|
|
153
|
-
"figure.gate.contract.slot-anchor-moved.why": "Cada ranura sobre él, y lo que contiene, queda en la nueva cara tras esta publicación."
|
|
152
|
+
"figure.gate.contract.slot-anchor-moved.why": "Cada ranura sobre él, y lo que contiene, queda en la nueva cara tras esta publicación.",
|
|
153
|
+
"figure.proposal.label-create": "Crear {name}",
|
|
154
|
+
"figure.proposal.label-revise": "Corregir {name}",
|
|
155
|
+
"figure.proposal.reason": "{steps} comandos · grado {grade} · {triangles} triángulos · hecho en {attempts} intentos"
|
|
154
156
|
}
|
package/translations/ja.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"error.figure-proposal-failed": "フィギュアを作成できませんでした。",
|
|
2
3
|
"error.figure-release-does-not-compile": "{type} のリリース版(バージョン {version})のソースがコンパイルできません。モデラーで開いて修正し、もう一度リリースするとボードに表示されます。",
|
|
3
4
|
"figure.gate.occupancy-empty-axis.message": "空間宣言の {axis} が空です。",
|
|
4
5
|
"figure.gate.occupancy-undeclared.how": "「部品に合わせる」を使うと、現在の部品から空間を宣言します。",
|
|
@@ -75,14 +76,12 @@
|
|
|
75
76
|
"figure.gate.contract.public-input-range-narrowed.why": "新しい範囲外の値を設定したインスタンスがある可能性があります。どの値が設定されたかは記録されていません。",
|
|
76
77
|
"figure.gate.contract.public-input-added.message": "'{input}' をボードで設定できるようになりました。",
|
|
77
78
|
"figure.gate.contract.public-input-added.why": "値を渡していない配置済みのインスタンスは既定値になります。",
|
|
78
|
-
"figure.gate.contract.
|
|
79
|
-
"figure.gate.contract.
|
|
80
|
-
"figure.gate.contract.
|
|
81
|
-
"figure.gate.contract.
|
|
82
|
-
"figure.gate.contract.
|
|
83
|
-
"figure.gate.contract.
|
|
84
|
-
"figure.gate.contract.carrier-added.message": "この Figure は Carrier タイプ {type} になりました。",
|
|
85
|
-
"figure.gate.contract.carrier-added.why": "タイプマップがこのリリースを採用すると、このコードのリアルタイムデータがこの Figure を見つけます。",
|
|
79
|
+
"figure.gate.contract.extension-gone.message": "拡張プロパティ '{key}' がなくなりました。",
|
|
80
|
+
"figure.gate.contract.extension-gone.why": "他の製品がこのキーで値を読みます。このリリース以降、値が見つかりません。",
|
|
81
|
+
"figure.gate.contract.extension-changed.message": "拡張プロパティ '{key}' の値が変わりました。",
|
|
82
|
+
"figure.gate.contract.extension-changed.why": "このキーを定めた製品が新しい値を読み、変更を判定します。",
|
|
83
|
+
"figure.gate.contract.extension-added.message": "拡張プロパティ '{key}' が追加されました。",
|
|
84
|
+
"figure.gate.contract.extension-added.why": "このキーを定めた製品がこの値を読みます。",
|
|
86
85
|
"figure.gate.contract.joints-none": "なし",
|
|
87
86
|
"figure.gate.contract.state-input-joints-changed.message": "'{id}' 操作値が動かす関節が {before} から {after} に変わりました。",
|
|
88
87
|
"figure.gate.contract.state-input-joints-changed.why": "コントローラーはこの値を書いてそれらの関節を動かします。このリリース以降、同じ値で別の関節が動くか、別の量だけ動きます。",
|
|
@@ -150,5 +149,8 @@
|
|
|
150
149
|
"figure.gate.contract.slot-anchor-gone.message": "スロットが立つ '{anchor}' 基準面がなくなりました。",
|
|
151
150
|
"figure.gate.contract.slot-anchor-gone.why": "この基準面に独自のスロットを置いた配置は、このリリース以降何も保持できません。",
|
|
152
151
|
"figure.gate.contract.slot-anchor-moved.message": "'{anchor}' 基準面が {before} 面から {after} 面に移りました。",
|
|
153
|
-
"figure.gate.contract.slot-anchor-moved.why": "その上のすべてのスロットと置かれたものは、このリリース以降新しい面に立ちます。"
|
|
152
|
+
"figure.gate.contract.slot-anchor-moved.why": "その上のすべてのスロットと置かれたものは、このリリース以降新しい面に立ちます。",
|
|
153
|
+
"figure.proposal.label-create": "{name} を作る",
|
|
154
|
+
"figure.proposal.label-revise": "{name} を直す",
|
|
155
|
+
"figure.proposal.reason": "コマンド {steps} 件 · 等級 {grade} · 三角形 {triangles} · {attempts} 回で作成"
|
|
154
156
|
}
|
package/translations/ko.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"error.figure-proposal-failed": "도형을 만들지 못했습니다.",
|
|
2
3
|
"error.figure-release-does-not-compile": "{type} 의 발행판({version})이 지금 컴파일되지 않습니다. 저작 화면에서 열어 수정한 후 다시 발행해야 보드에 나옵니다.",
|
|
3
4
|
"figure.gate.occupancy-empty-axis.message": "공간 선언의 {axis} 가 비어 있습니다.",
|
|
4
5
|
"figure.gate.occupancy-undeclared.how": "「부품에 맞추기」를 누르면 지금 부품으로 공간을 선언합니다.",
|
|
@@ -75,14 +76,12 @@
|
|
|
75
76
|
"figure.gate.contract.public-input-range-narrowed.why": "새 범위 밖의 값을 준 인스턴스가 있을 수 있습니다. 어떤 값이 놓였는지는 기록하지 않습니다.",
|
|
76
77
|
"figure.gate.contract.public-input-added.message": "'{input}' 값을 이제 보드에서 정할 수 있습니다.",
|
|
77
78
|
"figure.gate.contract.public-input-added.why": "값이 지정되지 않은 인스턴스에는 기본값이 적용됩니다.",
|
|
78
|
-
"figure.gate.contract.
|
|
79
|
-
"figure.gate.contract.
|
|
80
|
-
"figure.gate.contract.
|
|
81
|
-
"figure.gate.contract.
|
|
82
|
-
"figure.gate.contract.
|
|
83
|
-
"figure.gate.contract.
|
|
84
|
-
"figure.gate.contract.carrier-added.message": "이 Figure는 이제 Carrier 타입 {type}입니다.",
|
|
85
|
-
"figure.gate.contract.carrier-added.why": "타입 맵이 이 발행본을 쓰면 이 코드의 실시간 데이터가 이 Figure를 찾습니다.",
|
|
79
|
+
"figure.gate.contract.extension-gone.message": "'{key}' 확장 속성이 없어졌습니다.",
|
|
80
|
+
"figure.gate.contract.extension-gone.why": "다른 제품이 이 키로 값을 읽습니다. 이 발행 뒤에는 값을 찾지 못합니다.",
|
|
81
|
+
"figure.gate.contract.extension-changed.message": "'{key}' 확장 속성의 값이 바뀌었습니다.",
|
|
82
|
+
"figure.gate.contract.extension-changed.why": "이 키를 정한 제품이 새 값을 읽고 바뀐 점을 판정합니다.",
|
|
83
|
+
"figure.gate.contract.extension-added.message": "'{key}' 확장 속성이 새로 생겼습니다.",
|
|
84
|
+
"figure.gate.contract.extension-added.why": "이 키를 정한 제품이 이 값을 읽습니다.",
|
|
86
85
|
"figure.gate.contract.joints-none": "없음",
|
|
87
86
|
"figure.gate.contract.state-input-joints-changed.message": "'{id}' 조작값이 움직이던 관절이 {before} 에서 {after} 로 바뀌었습니다.",
|
|
88
87
|
"figure.gate.contract.state-input-joints-changed.why": "컨트롤러는 이 값을 써서 그 관절들을 움직입니다. 이 발행 뒤에는 같은 값이 다른 관절을 움직이거나 다른 양만큼 움직입니다.",
|
|
@@ -150,5 +149,8 @@
|
|
|
150
149
|
"figure.gate.contract.slot-anchor-gone.message": "적재 위치의 '{anchor}' 기준면이 없어졌습니다.",
|
|
151
150
|
"figure.gate.contract.slot-anchor-gone.why": "이 기준면을 사용하는 적재 위치는 발행 후 물품을 수용할 수 없습니다.",
|
|
152
151
|
"figure.gate.contract.slot-anchor-moved.message": "'{anchor}' 기준면이 {before} 면에서 {after} 면으로 옮겨졌습니다.",
|
|
153
|
-
"figure.gate.contract.slot-anchor-moved.why": "발행 후 해당 기준면의 모든 슬롯과 배치된 항목이 새 기준면으로 이동합니다."
|
|
152
|
+
"figure.gate.contract.slot-anchor-moved.why": "발행 후 해당 기준면의 모든 슬롯과 배치된 항목이 새 기준면으로 이동합니다.",
|
|
153
|
+
"figure.proposal.label-create": "{name} 만들기",
|
|
154
|
+
"figure.proposal.label-revise": "{name} 고치기",
|
|
155
|
+
"figure.proposal.reason": "명령 {steps}개 · 등급 {grade} · 삼각형 {triangles} · {attempts}번에 만들었다"
|
|
154
156
|
}
|