@things-factory/figure-ui 10.1.166 → 10.1.168
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/client/figure-assistant.ts +5 -0
- package/client/modeller/ai-proposal.ts +20 -2
- package/client/modeller/figure-properties.ts +1 -3
- package/client/modeller/product-session.ts +9 -4
- package/client/pages/figure-modeller-page.ts +7 -6
- package/dist-client/figure-assistant.js +5 -0
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/modeller/ai-proposal.d.ts +8 -0
- package/dist-client/modeller/ai-proposal.js +16 -3
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +1 -2
- package/dist-client/modeller/figure-properties.js +1 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +14 -3
- package/dist-client/modeller/product-session.js +9 -4
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +7 -6
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/ai-proposal.test.ts +73 -2
- package/test/browser/acceptance-page.js +3 -1
- package/test/browser/product-page.check.mjs +63 -3
- package/test/browser/product-page.js +7 -2
- package/test/figure-assistant-surface.test.ts +27 -1
- package/test/links.test.ts +1 -1
- package/test/proposal-record.test.ts +18 -6
- package/test/refusals.test.ts +1 -1
- package/translations/en.json +0 -12
- package/translations/es.json +0 -12
- package/translations/ja.json +0 -12
- package/translations/ko.json +35 -47
- package/translations/ms.json +0 -12
- package/translations/zh.json +0 -12
- package/client/modeller/carrier-declaration.ts +0 -63
- package/client/modeller/figure-carrier.ts +0 -117
- package/dist-client/modeller/carrier-declaration.d.ts +0 -31
- package/dist-client/modeller/carrier-declaration.js +0 -53
- package/dist-client/modeller/carrier-declaration.js.map +0 -1
- package/dist-client/modeller/figure-carrier.d.ts +0 -26
- package/dist-client/modeller/figure-carrier.js +0 -123
- package/dist-client/modeller/figure-carrier.js.map +0 -1
- package/test/carrier-declaration.test.ts +0 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.168",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "npm run clean:server && npm run clean:client"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@hatiolab/figure-model": "^0.
|
|
31
|
-
"@hatiolab/things-scene": "^10.1.
|
|
30
|
+
"@hatiolab/figure-model": "^0.7.0",
|
|
31
|
+
"@hatiolab/things-scene": "^10.1.90",
|
|
32
32
|
"@material/web": "^2.0.0",
|
|
33
33
|
"@operato/data-grist": "^10.0.0",
|
|
34
34
|
"@operato/graphql": "^10.0.0",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"@operato/slot-layout-editor": "^10.29.25",
|
|
42
42
|
"@operato/styles": "^10.0.0",
|
|
43
43
|
"@operato/utils": "^10.0.0",
|
|
44
|
-
"@things-factory/ai-assistant": "^10.1.
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
46
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
47
|
-
"@things-factory/setting-base": "^10.1.
|
|
44
|
+
"@things-factory/ai-assistant": "^10.1.168",
|
|
45
|
+
"@things-factory/figure-service": "^10.1.168",
|
|
46
|
+
"@things-factory/figure-viewer": "^10.1.168",
|
|
47
|
+
"@things-factory/setting-base": "^10.1.167",
|
|
48
48
|
"three": "^0.185.1"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "fe067ff11d90effffc2523374453d9b8313b489f"
|
|
51
51
|
}
|
package/test/ai-proposal.test.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { fileURLToPath } from 'node:url'
|
|
|
9
9
|
import { createFigureAsset, addFigureBox, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
|
|
11
11
|
import { createFigureProductSession } from '../client/modeller/product-session.js'
|
|
12
|
-
import {
|
|
12
|
+
import type { FigureProposalStep } from '@hatiolab/figure-model'
|
|
13
|
+
import { pendNewFigure, runFigureProposal, takeFigureOffer, takePendingNewFigure, type FigureApplyDetail } from '../client/modeller/ai-proposal.js'
|
|
13
14
|
|
|
14
15
|
const ko = JSON.parse(readFileSync(fileURLToPath(new URL('../translations/ko.json', import.meta.url)), 'utf8'))
|
|
15
16
|
const say = (key: string, params: Record<string, string> = {}) => {
|
|
@@ -51,9 +52,18 @@ test('a person\'s edit after the proposal was asked makes it stale -- refused, n
|
|
|
51
52
|
assert.equal(s.read().name, 'A')
|
|
52
53
|
})
|
|
53
54
|
|
|
54
|
-
const taker = (result: Partial<FigureApplyDetail>) => ({
|
|
55
|
+
const taker = (result: Partial<FigureApplyDetail>, saved: Array<{ figureId: string; type?: string; name?: string }> = []) => ({
|
|
55
56
|
offered: [] as FigureApplyDetail[],
|
|
56
57
|
opened: [] as unknown[],
|
|
58
|
+
asked: [] as string[],
|
|
59
|
+
shown: [] as string[],
|
|
60
|
+
async savedInto(key: string) {
|
|
61
|
+
this.asked.push(key)
|
|
62
|
+
return saved
|
|
63
|
+
},
|
|
64
|
+
openSaved(figureId: string) {
|
|
65
|
+
this.shown.push(figureId)
|
|
66
|
+
},
|
|
57
67
|
offer(detail: FigureApplyDetail) {
|
|
58
68
|
this.offered.push(detail)
|
|
59
69
|
return Object.assign(detail, result)
|
|
@@ -94,6 +104,67 @@ test('a new figure with no new modeller on screen opens one', async () => {
|
|
|
94
104
|
assert.equal(target.opened.length, 1)
|
|
95
105
|
})
|
|
96
106
|
|
|
107
|
+
/*
|
|
108
|
+
The same proposal is one figure (ADR-0061 decision 3, ruled 2026-10-16): a new figure taken from it in the modeller keeps
|
|
109
|
+
the figure's own creation id, so what says the proposal was already made is the save's record of it, read before the
|
|
110
|
+
card runs again.
|
|
111
|
+
*/
|
|
112
|
+
test('a new figure\'s card run again after it was saved opens that figure, and makes none', async () => {
|
|
113
|
+
const K = '11111111-1111-4111-8111-111111111111'
|
|
114
|
+
const target = taker({ applied: true }, [{ figureId: 'R', type: 'box', name: 'Box with lid' }])
|
|
115
|
+
const out = await runFigureProposal({ key: K, steps, create: true }, target, say)
|
|
116
|
+
assert.deepEqual(target.asked, [K])
|
|
117
|
+
assert.deepEqual(target.shown, ['R'])
|
|
118
|
+
assert.equal(target.offered.length + target.opened.length, 0, 'not offered to a modeller, no new one opened')
|
|
119
|
+
assert.deepEqual(out, { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names: 'Box with lid' }) })
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('a new figure\'s card not saved anywhere yet runs as before; a revision is not looked up', async () => {
|
|
123
|
+
const K = '11111111-1111-4111-8111-111111111111'
|
|
124
|
+
const fresh = taker({})
|
|
125
|
+
assert.equal((await runFigureProposal({ key: K, steps, create: true }, fresh, say)).verdict, 'accepted')
|
|
126
|
+
assert.deepEqual({ asked: fresh.asked, opened: fresh.opened.length }, { asked: [K], opened: 1 })
|
|
127
|
+
const revision = taker({ applied: true }, [{ figureId: 'R' }])
|
|
128
|
+
await runFigureProposal({ key: K, steps, context: 'c', revision: 3 }, revision, say)
|
|
129
|
+
assert.deepEqual({ asked: revision.asked, shown: revision.shown }, { asked: [], shown: [] })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
Open (ADR-0061 decision 3, ruled 2026-10-16, not prevented): the same new figure's card run in another tab before the
|
|
134
|
+
first figure is saved finds no record yet, and opens a second figure under the proposal's key.
|
|
135
|
+
*/
|
|
136
|
+
test('the same new figure\'s card in another tab before the first save makes a second figure under its key', async () => {
|
|
137
|
+
const K = '11111111-1111-4111-8111-111111111111'
|
|
138
|
+
const ids: string[] = []
|
|
139
|
+
const tab = () =>
|
|
140
|
+
createFigureProductSession({
|
|
141
|
+
load: store.load,
|
|
142
|
+
save: async (q: { id?: string; source: string; creationId?: string }) => (ids.push(q.creationId!), { id: q.id ?? q.creationId!, source: q.source })
|
|
143
|
+
})
|
|
144
|
+
const made = [{ kind: 'add-part', part: { id: 'lid', kind: 'box', dimensions: { width: 100, height: 10, depth: 100 } } }] as any[]
|
|
145
|
+
const first = tab()
|
|
146
|
+
first.startNew()
|
|
147
|
+
const detail: FigureApplyDetail = { key: K, steps: made, create: true }
|
|
148
|
+
const here = { ...taker({}), offer: (d: FigureApplyDetail) => (takeFigureOffer(first, d, { onSavedFigure: false, say }), d) }
|
|
149
|
+
assert.equal((await runFigureProposal(detail, here, say)).verdict, 'accepted')
|
|
150
|
+
|
|
151
|
+
const second = tab()
|
|
152
|
+
const there = {
|
|
153
|
+
...taker({}),
|
|
154
|
+
async openNew(s: FigureProposalStep[], key?: string) {
|
|
155
|
+
second.startNew()
|
|
156
|
+
second.createUnder(key!)
|
|
157
|
+
second.applyProposal(s, second.read().revision!, second.read().context!, key)
|
|
158
|
+
return { applied: true }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
assert.equal((await runFigureProposal({ key: K, steps: made, create: true }, there, say)).verdict, 'accepted')
|
|
162
|
+
await first.save()
|
|
163
|
+
await second.save()
|
|
164
|
+
assert.deepEqual(ids, [first.read().creationId, K])
|
|
165
|
+
assert.notEqual(ids[0], ids[1], 'two figures')
|
|
166
|
+
})
|
|
167
|
+
|
|
97
168
|
test('an empty card is not applied', async () => {
|
|
98
169
|
assert.equal((await runFigureProposal({ steps: [] }, taker({ applied: true }), say)).verdict, 'unavailable')
|
|
99
170
|
})
|
|
@@ -202,7 +202,9 @@ async function start() {
|
|
|
202
202
|
})
|
|
203
203
|
const dock = {
|
|
204
204
|
offer: detail => (window.dispatchEvent(new CustomEvent('figure-apply', { detail })), detail),
|
|
205
|
-
openNew: async () => ({ applied: false, code: 'not-opened' })
|
|
205
|
+
openNew: async () => ({ applied: false, code: 'not-opened' }),
|
|
206
|
+
savedInto: async () => [],
|
|
207
|
+
openSaved: () => {}
|
|
206
208
|
}
|
|
207
209
|
window.acceptance = {
|
|
208
210
|
open: async id => (await session.open(id), editor.requestUpdate(), editor.dispatchEvent(new CustomEvent('changed'))),
|
|
@@ -541,14 +541,30 @@ describe('the page around the figure: the shell, the dock, the properties', () =
|
|
|
541
541
|
assert.equal(await beforeUnload(), true)
|
|
542
542
|
})
|
|
543
543
|
|
|
544
|
-
|
|
544
|
+
/*
|
|
545
|
+
A figure not saved yet is a subject (ADR-0061 decision 3): its conversation is anchored on the creation id its first
|
|
546
|
+
save inserts the row under, so the first save changes nothing the dock knows -- the same conversation goes on.
|
|
547
|
+
*/
|
|
548
|
+
test('a new figure: the dock talks about it by its creation id, and its first save keeps the conversation', async () => {
|
|
545
549
|
await press('button[save]', 'save')
|
|
546
550
|
await until(() => !product.view().dirty, 'saved')
|
|
547
551
|
await read(() => assistant.forget())
|
|
548
552
|
await open(null)
|
|
549
|
-
|
|
550
|
-
assert.
|
|
553
|
+
const creationId = await read(() => product.page().session.read().creationId)
|
|
554
|
+
assert.match(creationId, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
555
|
+
assert.deepEqual((await said()).filter(s => s.call !== 'notify'), [{ call: 'provide', key: `figure:${creationId}` }])
|
|
556
|
+
assert.deepEqual(await read(() => assistant.surface.anchor()), { anchorType: 'figure', anchorId: creationId })
|
|
557
|
+
assert.deepEqual(await read(() => { const { figureId, editorMode } = assistant.surface.hostContext(); return { figureId, editorMode } }), { figureId: creationId, editorMode: 'new' })
|
|
551
558
|
assert.equal((await read(() => product.context())).menu, undefined)
|
|
559
|
+
await rename('Gate')
|
|
560
|
+
await read(() => { assistant.forget(); product.forget() })
|
|
561
|
+
await press('button[save]', 'save')
|
|
562
|
+
await press('figure-first-save >>> md-filled-button[slot=confirm]', 'the first save')
|
|
563
|
+
await until(() => product.view().id && !product.view().dirty, 'saved')
|
|
564
|
+
assert.equal((await callsTo('saveFigure'))[0].creationId, creationId)
|
|
565
|
+
assert.equal(await read(() => product.view().id), creationId)
|
|
566
|
+
assert.deepEqual((await said()).filter(s => s.call !== 'notify'), [], 'no new conversation, none taken back')
|
|
567
|
+
assert.deepEqual(await read(() => { const { figureId, editorMode } = assistant.surface.hostContext(); return { figureId, editorMode } }), { figureId: creationId, editorMode: 'existing' })
|
|
552
568
|
})
|
|
553
569
|
|
|
554
570
|
test('inactive: the surface is taken back', async () => {
|
|
@@ -643,6 +659,7 @@ describe('AI proposals reaching the page', () => {
|
|
|
643
659
|
const outcome = await read(key => assistant.surface.proposalOutcome(key), K1)
|
|
644
660
|
assert.deepEqual(outcome, { verdict: 'accepted', note: 'figure.text.proposal-saved-into {"names":"Door"}' })
|
|
645
661
|
assert.deepEqual(await callsTo('figureProposalOutcome'), [{ proposalKey: K1 }])
|
|
662
|
+
await read(() => product.answer('figureProposalOutcome'))
|
|
646
663
|
})
|
|
647
664
|
|
|
648
665
|
test("a new figure proposed: the modeller opens a new one, takes the steps under the proposal's key, and the dock is told", async () => {
|
|
@@ -661,6 +678,7 @@ describe('AI proposals reaching the page', () => {
|
|
|
661
678
|
outcome: { verdict: 'accepted', applied: 1, note: 'figure.text.proposal-applied {"n":"1"}' },
|
|
662
679
|
told: [{ level: 'info', note: 'figure.text.proposal-applied {"n":"1"}' }]
|
|
663
680
|
})
|
|
681
|
+
assert.deepEqual(await callsTo('figureProposalOutcome'), [{ proposalKey: K2 }], 'asked whether it was saved before')
|
|
664
682
|
assert.deepEqual((await read(() => product.navigations())).map(n => n.path), ['/figure-modeller'])
|
|
665
683
|
const made = await read(() => product.view())
|
|
666
684
|
assert.equal(made.id, undefined)
|
|
@@ -671,6 +689,48 @@ describe('AI proposals reaching the page', () => {
|
|
|
671
689
|
const [saved] = await callsTo('saveFigure')
|
|
672
690
|
assert.deepEqual({ creationId: saved.creationId, proposalKeys: saved.proposalKeys }, { creationId: K2, proposalKeys: [K2] })
|
|
673
691
|
})
|
|
692
|
+
|
|
693
|
+
/*
|
|
694
|
+
The draft the dock made on a new figure stays that figure's conversation (ADR-0061 decision 3, ruled 2026-10-16): the
|
|
695
|
+
figure keeps its creation id, and the save records the proposal. Run again once saved, the card opens that figure --
|
|
696
|
+
the record, not the row id, is what makes one proposal one figure.
|
|
697
|
+
*/
|
|
698
|
+
test('a new figure\'s proposal taken on a new figure keeps its conversation; run again after the save, it opens that figure', async () => {
|
|
699
|
+
/* After the one above: the figure on the page is saved, so a new one opens without a question. */
|
|
700
|
+
await until(() => product.view().id && !product.view().dirty, 'the one above saved')
|
|
701
|
+
await open(null)
|
|
702
|
+
const creationId = await read(() => product.page().session.read().creationId)
|
|
703
|
+
await read(() => { assistant.forget(); product.forget() })
|
|
704
|
+
const run = () =>
|
|
705
|
+
read(
|
|
706
|
+
async ({ key, steps }) => {
|
|
707
|
+
const told = []
|
|
708
|
+
const outcome = await assistant.handlers.proposeFigureCandidate({ key, steps, create: true }, { notify: (level, note) => told.push({ level, note }) })
|
|
709
|
+
return { outcome, told }
|
|
710
|
+
},
|
|
711
|
+
{ key: K1, steps: LID }
|
|
712
|
+
)
|
|
713
|
+
assert.deepEqual((await run()).outcome, { verdict: 'accepted', applied: 1, note: 'figure.text.proposal-applied {"n":"1"}' })
|
|
714
|
+
assert.equal(await read(() => product.page().session.read().creationId), creationId, 'the proposal\'s key is not the figure\'s id')
|
|
715
|
+
assert.deepEqual((await said()).filter(s => s.call !== 'notify'), [], 'the same conversation: no surface given or taken back')
|
|
716
|
+
assert.deepEqual(await read(() => product.navigations()), [], 'taken where it was asked')
|
|
717
|
+
await press('button[save]', 'save')
|
|
718
|
+
await press('figure-first-save >>> md-filled-button[slot=confirm]', 'the first save')
|
|
719
|
+
await until(() => product.view().id && !product.view().dirty, 'saved')
|
|
720
|
+
const [saved] = await callsTo('saveFigure')
|
|
721
|
+
assert.deepEqual({ creationId: saved.creationId, proposalKeys: saved.proposalKeys }, { creationId, proposalKeys: [K1] })
|
|
722
|
+
|
|
723
|
+
await open(null)
|
|
724
|
+
await read(() => product.forget())
|
|
725
|
+
const before = await read(() => product.rows().length)
|
|
726
|
+
const names = await read(id => product.rows().find(r => r.id === id).name, creationId)
|
|
727
|
+
const note = `figure.text.proposal-saved-into ${JSON.stringify({ names })}`
|
|
728
|
+
assert.deepEqual(await run(), { outcome: { verdict: 'accepted', note }, told: [{ level: 'info', note }] })
|
|
729
|
+
assert.deepEqual(await callsTo('figureProposalOutcome'), [{ proposalKey: K1 }])
|
|
730
|
+
assert.deepEqual((await read(() => product.navigations())).map(n => n.path), [`/figure-modeller/${creationId}`])
|
|
731
|
+
assert.equal(await read(() => product.rows().length), before, 'no second figure')
|
|
732
|
+
assert.deepEqual(await callsTo('saveFigure'), [])
|
|
733
|
+
})
|
|
674
734
|
})
|
|
675
735
|
|
|
676
736
|
describe('the catalogue preview', () => {
|
|
@@ -61,6 +61,8 @@ const calls = []
|
|
|
61
61
|
/* Answers a check wants instead of the server's: field -> function(variables) returning data or throwing. */
|
|
62
62
|
const overrides = new Map()
|
|
63
63
|
let made = 0
|
|
64
|
+
/* What the saves recorded of AI proposals: { proposalKey, figureId }. */
|
|
65
|
+
const records = []
|
|
64
66
|
|
|
65
67
|
function row(id) {
|
|
66
68
|
const found = rows.get(id)
|
|
@@ -71,8 +73,10 @@ function row(id) {
|
|
|
71
73
|
const answers = {
|
|
72
74
|
figure: ({ id }) => ({ __typename: 'Figure', properties: null, tags: null, category: null, description: null, score: null, triangles: null, groups: null, thumbnailUpdatedAt: null, updatedAt: null, updater: null, thumbnailHash: null, ...row(id) }),
|
|
73
75
|
figureTypeNames: () => [...rows.values()].map(r => r.type),
|
|
74
|
-
saveFigure: async ({ id, source, type, creationId, thumbnail }) => {
|
|
76
|
+
saveFigure: async ({ id, source, type, creationId, thumbnail, proposalKeys }) => {
|
|
75
77
|
const saved = id ? row(id) : { id: creationId ?? `made-${++made}`, namespace: null, type: type ?? `figure-${made}`, state: 'draft', version: 0 }
|
|
78
|
+
/* The save's record of the proposals its source holds, as the server keeps it (figure-proposal.ts). */
|
|
79
|
+
for (const key of proposalKeys ?? []) if (!records.some(r => r.proposalKey === key && r.figureId === saved.id)) records.push({ proposalKey: key, figureId: saved.id })
|
|
76
80
|
/* The picture is kept by the hash of its bytes, as the server keeps it (ADR-0104 decision 6). */
|
|
77
81
|
Object.assign(saved, { source, name: JSON.parse(source).name, state: 'draft' }, thumbnail === undefined ? {} : { thumbnailHash: await thumbnailHashOf(thumbnail) })
|
|
78
82
|
rows.set(saved.id, saved)
|
|
@@ -87,7 +91,8 @@ const answers = {
|
|
|
87
91
|
return { __typename: 'Figure', id, version: released.version, state: 'released', updatedAt: null }
|
|
88
92
|
},
|
|
89
93
|
figureRecipeTemplates: () => [],
|
|
90
|
-
figureProposalOutcome: () =>
|
|
94
|
+
figureProposalOutcome: ({ proposalKey }) =>
|
|
95
|
+
records.filter(r => r.proposalKey === proposalKey).map(r => ({ figureId: r.figureId, type: rows.get(r.figureId)?.type, name: rows.get(r.figureId)?.name, recordedAt: null })),
|
|
91
96
|
figureReplacedDrafts: () => [],
|
|
92
97
|
figureVersions: () => [],
|
|
93
98
|
figureReleased: () => null,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The modeller's AI - the framework dock talks about the figure on screen (design/ai/shared-dock.md §3).
|
|
3
3
|
*
|
|
4
4
|
* What the page does with the dock -- ① a saved figure provides a surface anchored on its row ② the draft is read at the
|
|
5
|
-
* moment of sending ③ an unsaved figure provides
|
|
5
|
+
* moment of sending ③ an unsaved figure provides one anchored on its creation id ④ the surface is taken back when the page goes inactive or
|
|
6
6
|
* leaves ⑤ a proposal is taken through the page, and a new figure's opens a new figure -- is checked on the page itself
|
|
7
7
|
* in test/browser/product-page.check.mjs. Here: the offer the page takes it by, and the words every outcome is said in.
|
|
8
8
|
*/
|
|
@@ -43,6 +43,32 @@ test('the dock applies a proposal through the page, on the version the model saw
|
|
|
43
43
|
assert.equal(create.applied, undefined)
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
+
/*
|
|
47
|
+
A figure not saved yet is a subject too (ADR-0061 decision 3): its conversation is anchored on the creation id the
|
|
48
|
+
first save inserts the row under, so the anchor is the same before and after that save. Taking a new figure's
|
|
49
|
+
proposal on it must not change that id -- the conversation that asked for the draft would be left behind.
|
|
50
|
+
*/
|
|
51
|
+
test('a new figure keeps its creation id through a new figure\'s proposal and the first save', async () => {
|
|
52
|
+
const sent: Array<{ id?: string; creationId?: string; proposalKeys?: string[] }> = []
|
|
53
|
+
const session = createFigureProductSession({
|
|
54
|
+
load: async (id: string) => ({ id, source: '' }),
|
|
55
|
+
save: async (q: { id?: string; source: string; creationId?: string; proposalKeys?: string[] }) => {
|
|
56
|
+
sent.push({ id: q.id, creationId: q.creationId, proposalKeys: q.proposalKeys })
|
|
57
|
+
return { id: q.id ?? q.creationId!, source: q.source }
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
session.startNew()
|
|
61
|
+
const creationId = session.read().creationId
|
|
62
|
+
assert.match(creationId ?? '', /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
63
|
+
const create: FigureApplyDetail = { key: K1, steps: lid, create: true }
|
|
64
|
+
assert.equal(takeFigureOffer(session, create, { onSavedFigure: false, say }), true)
|
|
65
|
+
assert.equal(session.read().creationId, creationId, 'the proposal\'s key is not the figure\'s id')
|
|
66
|
+
assert.deepEqual(session.proposalsInEffect(), [K1], 'the save still records the proposal')
|
|
67
|
+
await session.save()
|
|
68
|
+
assert.deepEqual(sent, [{ id: undefined, creationId, proposalKeys: [K1] }])
|
|
69
|
+
assert.deepEqual({ id: session.read().id, creationId: session.read().creationId }, { id: creationId, creationId })
|
|
70
|
+
})
|
|
71
|
+
|
|
46
72
|
test('a figure proposal is answered by its handler, in words, never left without one', () => {
|
|
47
73
|
for (const lang of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
|
|
48
74
|
const words = JSON.parse(readFileSync(fileURLToPath(new URL(`../translations/${lang}.json`, import.meta.url)), 'utf8'))
|
package/test/links.test.ts
CHANGED
|
@@ -75,7 +75,7 @@ import { moving } from './joints.ts'
|
|
|
75
75
|
|
|
76
76
|
const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
|
|
77
77
|
|
|
78
|
-
test('
|
|
78
|
+
test('선택기의 값은 지금 따르는 것을 가리킨다', () => {
|
|
79
79
|
assert.equal(choiceOf({ of: 'own' }), 'own')
|
|
80
80
|
assert.equal(choiceOf({ of: 'instance', axis: 'y', times: 1 }), 'instance:y')
|
|
81
81
|
assert.equal(choiceOf({ of: 'instance', axis: 'y', times: 0.25 }), 'share:y', '몫은 비례로')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* What became of an AI proposal (ADR-0097 §5.6 item 5; AI lane, 2026-09-27). A save names the proposals whose steps its
|
|
3
|
-
* text holds -- undone, a proposal is not in it; a new figure
|
|
3
|
+
* text holds -- undone, a proposal is not in it; a new figure opened for a proposal is created under the proposal's key.
|
|
4
4
|
* The server side (recorded in the save's transaction, one figure per key) is tested on sqlite in figure-service
|
|
5
5
|
* (figure-proposal-db).
|
|
6
6
|
*/
|
|
@@ -59,16 +59,28 @@ test('a save names the proposals its text holds: undone, not; redone, again; und
|
|
|
59
59
|
assert.deepEqual(s.proposalsInEffect(), [K1])
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
/*
|
|
63
|
+
Two ways a new figure comes from a proposal (ADR-0061 decision 3, ruled 2026-10-16). Opened for a proposal from another
|
|
64
|
+
screen, it is created under the proposal's key. Taken on the new figure the modeller has open, its conversation is
|
|
65
|
+
anchored on that figure's creation id, which stays; the save records the proposal either way.
|
|
66
|
+
*/
|
|
67
|
+
test('a new figure opened for a proposal is created under its key; one taken on the open new figure keeps its id', async () => {
|
|
63
68
|
const { store, saves } = server()
|
|
64
69
|
const s = createFigureProductSession(store)
|
|
65
70
|
s.startNew()
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
s.createUnder(K1)
|
|
72
|
+
s.applyProposal(lid, s.read().revision!, s.read().context!, K1)
|
|
68
73
|
await s.save({ type: 'LIDDED' })
|
|
69
74
|
assert.equal(saves.at(-1)!.creationId, K1, 'created under the proposal key: taken twice, it meets the first figure')
|
|
70
75
|
assert.deepEqual(saves.at(-1)!.proposalKeys, [K1])
|
|
71
76
|
|
|
77
|
+
s.startNew({ discard: true })
|
|
78
|
+
const own = s.read().creationId
|
|
79
|
+
const detail: FigureApplyDetail = { key: K2, steps: lid, create: true }
|
|
80
|
+
assert.equal(takeFigureOffer(s, detail, { onSavedFigure: false, say }), true)
|
|
81
|
+
await s.save({ type: 'LIDDED_2' })
|
|
82
|
+
assert.deepEqual({ creationId: saves.at(-1)!.creationId, proposalKeys: saves.at(-1)!.proposalKeys }, { creationId: own, proposalKeys: [K2] })
|
|
83
|
+
|
|
72
84
|
/* A new figure the person has touched is theirs: the proposal is not put on it (NEW_FIGURE_UNSAVED), its id stays. */
|
|
73
85
|
s.startNew({ discard: true })
|
|
74
86
|
s.command({ kind: 'rename', name: 'Mine' } as never)
|
|
@@ -84,13 +96,13 @@ test('the key travels from the card to the modeller, on a revision and on a new
|
|
|
84
96
|
const opened: Array<string | undefined> = []
|
|
85
97
|
await runFigureProposal(
|
|
86
98
|
{ key: K1, steps: lid, create: false, context: 'c', revision: 1 },
|
|
87
|
-
{ offer: d => (offered.push(d), { ...d, applied: true }), openNew: async () => ({ applied: false }) },
|
|
99
|
+
{ offer: d => (offered.push(d), { ...d, applied: true }), openNew: async () => ({ applied: false }), savedInto: async () => [], openSaved: () => {} },
|
|
88
100
|
say
|
|
89
101
|
)
|
|
90
102
|
assert.equal(offered[0]!.key, K1)
|
|
91
103
|
await runFigureProposal(
|
|
92
104
|
{ key: K2, steps: lid, create: true },
|
|
93
|
-
{ offer: d => d, openNew: async (_steps, key) => (opened.push(key), { applied: true }) },
|
|
105
|
+
{ offer: d => d, openNew: async (_steps, key) => (opened.push(key), { applied: true }), savedInto: async () => [], openSaved: () => {} },
|
|
94
106
|
say
|
|
95
107
|
)
|
|
96
108
|
assert.deepEqual(opened, [K2])
|
package/test/refusals.test.ts
CHANGED
|
@@ -228,7 +228,7 @@ test('일상에서 코드로만 보이던 거절 넷 — 줄이 안 들어감 ·
|
|
|
228
228
|
layout: { tiers: [{ tierNo: 1, anchorId: 'bin', mount: 'ON', position: { x: 0, y: 0, z: 0 }, rotation: 0, layout: { pattern: 'SINGLE', slot: { allowed: { width: 100, depth: 100, height: 100 }, accepts: ['TOTE'] } } }] }
|
|
229
229
|
})
|
|
230
230
|
const held = editRefusalOf(refusalOf(() => act(slotted, { kind: 'make-repeat', part: 'bin', axis: 'x', pitch: 150 })), say)
|
|
231
|
-
assert.ok(held.includes('bin') && held.includes('
|
|
231
|
+
assert.ok(held.includes('bin') && held.includes('적재 위치 배치') && !held.includes('SURFACE_PROVENANCE'), held)
|
|
232
232
|
/* A new part named as the row's part is (its copies drawn under ids of their own): the same words as a rename. */
|
|
233
233
|
assert.match(editRefusalOf(refusalOf(() => fm.addFigureBox(conveyor(), { id: 'roller', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } })), keyed), /part-name-taken .*"name":"roller"/)
|
|
234
234
|
})
|
package/translations/en.json
CHANGED
|
@@ -973,18 +973,6 @@
|
|
|
973
973
|
"figure.text.detail-unreleased": "Not released yet. Showing the saved draft.",
|
|
974
974
|
"figure.button.detail-edit": "Open editor",
|
|
975
975
|
"figure.button.download-package": "download package",
|
|
976
|
-
"figure.label.group-carrier": "Carrier",
|
|
977
|
-
"figure.label.carrier-type": "Carrier type",
|
|
978
|
-
"figure.text.carrier-type-placeholder": "CARRIERTYPE",
|
|
979
|
-
"figure.label.carrier-size": "Physical size",
|
|
980
|
-
"figure.label.carrier-width": "Width",
|
|
981
|
-
"figure.label.carrier-length": "Length",
|
|
982
|
-
"figure.label.carrier-height": "Height",
|
|
983
|
-
"figure.button.declare-carrier": "Declare carrier",
|
|
984
|
-
"figure.button.clear-carrier": "Clear",
|
|
985
|
-
"figure.text.carrier-type-required": "Enter the carrier type code.",
|
|
986
|
-
"figure.text.carrier-type-too-long": "A carrier type code is at most {max} characters.",
|
|
987
|
-
"figure.text.carrier-length-positive": "{axis} must be greater than 0.",
|
|
988
976
|
"figure.button.import-mesh": "import GLB",
|
|
989
977
|
"figure.text.import-mesh-hint": "brings a GLB file in as a part: the whole file, or one part of it.",
|
|
990
978
|
"figure.text.mesh-whole-file": "whole file",
|
package/translations/es.json
CHANGED
|
@@ -973,18 +973,6 @@
|
|
|
973
973
|
"figure.text.detail-unreleased": "Aún no publicada. Se muestra el borrador guardado.",
|
|
974
974
|
"figure.button.detail-edit": "Abrir editor",
|
|
975
975
|
"figure.button.download-package": "descargar paquete",
|
|
976
|
-
"figure.label.group-carrier": "Portador",
|
|
977
|
-
"figure.label.carrier-type": "Tipo de portador",
|
|
978
|
-
"figure.text.carrier-type-placeholder": "CARRIERTYPE",
|
|
979
|
-
"figure.label.carrier-size": "Tamaño físico",
|
|
980
|
-
"figure.label.carrier-width": "Ancho",
|
|
981
|
-
"figure.label.carrier-length": "Largo",
|
|
982
|
-
"figure.label.carrier-height": "Alto",
|
|
983
|
-
"figure.button.declare-carrier": "Declarar portador",
|
|
984
|
-
"figure.button.clear-carrier": "Quitar",
|
|
985
|
-
"figure.text.carrier-type-required": "Introduzca el código del tipo de portador.",
|
|
986
|
-
"figure.text.carrier-type-too-long": "El código del tipo de portador tiene como máximo {max} caracteres.",
|
|
987
|
-
"figure.text.carrier-length-positive": "{axis} debe ser mayor que 0.",
|
|
988
976
|
"figure.button.import-mesh": "importar GLB",
|
|
989
977
|
"figure.text.import-mesh-hint": "trae un archivo GLB como pieza: el archivo entero o una parte de él.",
|
|
990
978
|
"figure.text.mesh-whole-file": "archivo entero",
|
package/translations/ja.json
CHANGED
|
@@ -973,18 +973,6 @@
|
|
|
973
973
|
"figure.text.detail-unreleased": "まだ公開されていません。保存された下書きを表示します。",
|
|
974
974
|
"figure.button.detail-edit": "編集画面を開く",
|
|
975
975
|
"figure.button.download-package": "パッケージをダウンロード",
|
|
976
|
-
"figure.label.group-carrier": "キャリア",
|
|
977
|
-
"figure.label.carrier-type": "キャリアタイプ",
|
|
978
|
-
"figure.text.carrier-type-placeholder": "CARRIERTYPE",
|
|
979
|
-
"figure.label.carrier-size": "実寸",
|
|
980
|
-
"figure.label.carrier-width": "幅",
|
|
981
|
-
"figure.label.carrier-length": "長さ",
|
|
982
|
-
"figure.label.carrier-height": "高さ",
|
|
983
|
-
"figure.button.declare-carrier": "キャリアとして宣言",
|
|
984
|
-
"figure.button.clear-carrier": "宣言を解除",
|
|
985
|
-
"figure.text.carrier-type-required": "キャリアタイプのコードを入力してください。",
|
|
986
|
-
"figure.text.carrier-type-too-long": "キャリアタイプのコードは {max} 文字までです。",
|
|
987
|
-
"figure.text.carrier-length-positive": "{axis} は 0 より大きい値にしてください。",
|
|
988
976
|
"figure.button.import-mesh": "GLB を取り込む",
|
|
989
977
|
"figure.text.import-mesh-hint": "GLB ファイルを部品として取り込みます。ファイル全体か、その中の 1 つを選びます。",
|
|
990
978
|
"figure.text.mesh-whole-file": "ファイル全体",
|