@things-factory/ai-assistant 10.1.207 → 10.1.210
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/components/assistant-chat.ts +238 -9
- package/client/components/assistant-dock.ts +9 -4
- package/client/components/assistant-invite.ts +138 -0
- package/client/components/assistant-session-manager.ts +53 -12
- package/client/components/proposal-request.test.ts +10 -0
- package/client/components/roster-identity.test.ts +2 -12
- package/client/components/roster-identity.ts +0 -6
- package/client/setup-assistant-dock.ts +1 -0
- package/client/utils/assistant-session-actions.ts +3 -1
- package/client/utils/assistant-session-admin.ts +9 -2
- package/client/utils/assistant-session-controller.ts +1 -1
- package/client/utils/assistant-typing.test.ts +33 -0
- package/client/utils/assistant-typing.ts +53 -0
- package/dist-client/components/assistant-chat.d.ts +28 -0
- package/dist-client/components/assistant-chat.js +259 -8
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.js +12 -4
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-invite.d.ts +39 -0
- package/dist-client/components/assistant-invite.js +150 -0
- package/dist-client/components/assistant-invite.js.map +1 -0
- package/dist-client/components/assistant-session-manager.d.ts +5 -1
- package/dist-client/components/assistant-session-manager.js +40 -10
- package/dist-client/components/assistant-session-manager.js.map +1 -1
- package/dist-client/components/proposal-request.test.js +8 -0
- package/dist-client/components/proposal-request.test.js.map +1 -1
- package/dist-client/components/roster-identity.d.ts +0 -2
- package/dist-client/components/roster-identity.js +0 -7
- package/dist-client/components/roster-identity.js.map +1 -1
- package/dist-client/components/roster-identity.test.js +2 -13
- package/dist-client/components/roster-identity.test.js.map +1 -1
- package/dist-client/setup-assistant-dock.js +1 -0
- package/dist-client/setup-assistant-dock.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-session-actions.d.ts +1 -0
- package/dist-client/utils/assistant-session-actions.js +3 -1
- package/dist-client/utils/assistant-session-actions.js.map +1 -1
- package/dist-client/utils/assistant-session-admin.d.ts +5 -2
- package/dist-client/utils/assistant-session-admin.js +3 -0
- package/dist-client/utils/assistant-session-admin.js.map +1 -1
- package/dist-client/utils/assistant-session-controller.d.ts +1 -1
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-typing.d.ts +20 -0
- package/dist-client/utils/assistant-typing.js +48 -0
- package/dist-client/utils/assistant-typing.js.map +1 -0
- package/dist-client/utils/assistant-typing.test.d.ts +1 -0
- package/dist-client/utils/assistant-typing.test.js +30 -0
- package/dist-client/utils/assistant-typing.test.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +5 -12
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +1 -1
- package/dist-server/service/assistant-mode/index.js +4 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.js +9 -2
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/assistant-mode/screen-rules.d.ts +19 -0
- package/dist-server/service/assistant-mode/screen-rules.js +40 -6
- package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
- package/dist-server/service/chat-message/chat-message-room.js +3 -1
- package/dist-server/service/chat-message/chat-message-room.js.map +1 -1
- package/dist-server/service/chat-message/chat-typing-subscription.d.ts +10 -0
- package/dist-server/service/chat-message/chat-typing-subscription.js +68 -0
- package/dist-server/service/chat-message/chat-typing-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-typing.d.ts +28 -0
- package/dist-server/service/chat-message/chat-typing.js +40 -0
- package/dist-server/service/chat-message/chat-typing.js.map +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.js +6 -0
- package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
- package/dist-server/service/chat-session/attachment-tools.js +2 -2
- package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
- package/dist-server/service/chat-session/chat-session.d.ts +2 -1
- package/dist-server/service/chat-session/chat-session.js +1 -1
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/chat-session/session-activity-broadcast.d.ts +12 -0
- package/dist-server/service/chat-session/session-activity-broadcast.js +15 -0
- package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -1
- package/dist-server/service/chat-session/session-activity-publish.d.ts +2 -0
- package/dist-server/service/chat-session/session-activity-publish.js +5 -2
- package/dist-server/service/chat-session/session-activity-publish.js.map +1 -1
- package/dist-server/service/chat-session/session-visibility.d.ts +17 -2
- package/dist-server/service/chat-session/session-visibility.js +28 -3
- package/dist-server/service/chat-session/session-visibility.js.map +1 -1
- package/dist-server/service/chat-session/session-write-gate.d.ts +1 -1
- package/dist-server/service/chat-session/session-write-gate.js +1 -1
- package/dist-server/service/chat-session/session-write-gate.js.map +1 -1
- package/dist-server/service/chat-session-participant/index.d.ts +1 -0
- package/dist-server/service/chat-session-participant/index.js +4 -1
- package/dist-server/service/chat-session-participant/index.js.map +1 -1
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +16 -1
- package/dist-server/service/chat-session-participant/invite-notification.js +34 -5
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
- package/dist-server/service/chat-session-participant/mention-invite.d.ts +30 -4
- package/dist-server/service/chat-session-participant/mention-invite.js +78 -31
- package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -1
- package/dist-server/service/chat-session-participant/read-roster.js +5 -2
- package/dist-server/service/chat-session-participant/read-roster.js.map +1 -1
- package/dist-server/service/chat-session-resolver.d.ts +27 -0
- package/dist-server/service/chat-session-resolver.js +226 -25
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +19 -0
- package/dist-server/service/data-entry/entry-target.js +20 -5
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.js +2 -2
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +2 -1
- package/dist-server/service/data-entry/entry-update.js +5 -3
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/service/data-read/list-read.d.ts +10 -0
- package/dist-server/service/data-read/list-read.js +12 -0
- package/dist-server/service/data-read/list-read.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +7 -0
- package/dist-server/service/data-read/read-target.js +2 -2
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/data-read/read-tools.js +1 -1
- package/dist-server/service/data-read/read-tools.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +9 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/testing/conversation-script.d.ts +1 -1
- package/dist-server/service/testing/conversation-script.js.map +1 -1
- package/dist-server/service/testing/mode-conformance.js +4 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/server/explicit-graphql-types.test.ts +1 -1
- package/server/service/assistant-chat-resolver.ts +7 -12
- package/server/service/assistant-mode/index.ts +1 -1
- package/server/service/assistant-mode/mode-categories.ts +11 -3
- package/server/service/assistant-mode/mode-front.test.ts +53 -1
- package/server/service/assistant-mode/resolve.test.ts +2 -1
- package/server/service/assistant-mode/screen-rules.test.ts +41 -3
- package/server/service/assistant-mode/screen-rules.ts +44 -5
- package/server/service/chat-message/chat-message-room.test.ts +7 -2
- package/server/service/chat-message/chat-message-room.ts +3 -1
- package/server/service/chat-message/chat-typing-subscription.ts +52 -0
- package/server/service/chat-message/chat-typing.test.ts +45 -0
- package/server/service/chat-message/chat-typing.ts +48 -0
- package/server/service/chat-message/fold-history-window.test.ts +1 -1
- package/server/service/chat-session/attachment-tool-result.test.ts +9 -1
- package/server/service/chat-session/attachment-tool-result.ts +7 -0
- package/server/service/chat-session/attachment-tools.ts +3 -3
- package/server/service/chat-session/chat-session.ts +3 -2
- package/server/service/chat-session/group-room.test.ts +233 -0
- package/server/service/chat-session/session-activity-broadcast.ts +20 -0
- package/server/service/chat-session/session-activity-publish.ts +6 -1
- package/server/service/chat-session/session-activity-receivers.test.ts +26 -0
- package/server/service/chat-session/session-visibility.test.ts +32 -1
- package/server/service/chat-session/session-visibility.ts +33 -4
- package/server/service/chat-session/session-write-gate.test.ts +7 -1
- package/server/service/chat-session/session-write-gate.ts +2 -2
- package/server/service/chat-session-participant/index.ts +2 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +36 -1
- package/server/service/chat-session-participant/invite-notification.ts +43 -5
- package/server/service/chat-session-participant/mention-invite.test.ts +49 -1
- package/server/service/chat-session-participant/mention-invite.ts +82 -28
- package/server/service/chat-session-participant/read-roster.test.ts +13 -4
- package/server/service/chat-session-participant/read-roster.ts +6 -3
- package/server/service/chat-session-resolver.ts +204 -27
- package/server/service/data-entry/data-entry-update.test.ts +19 -0
- package/server/service/data-entry/data-entry.test.ts +65 -1
- package/server/service/data-entry/entry-target.ts +36 -5
- package/server/service/data-entry/entry-tools.ts +2 -2
- package/server/service/data-entry/entry-update.ts +6 -3
- package/server/service/data-read/list-missing.test.ts +49 -0
- package/server/service/data-read/list-read.ts +18 -0
- package/server/service/data-read/read-target.ts +10 -3
- package/server/service/data-read/read-tools.ts +1 -1
- package/server/service/data-read/record-keys.test.ts +36 -0
- package/server/service/index.ts +6 -1
- package/server/service/testing/conversation-script.ts +1 -1
- package/server/service/testing/mode-conformance.ts +2 -0
- package/test/assistant-dock.test.ts +5 -1
- package/test/assistant-session-manager.test.ts +4 -2
- package/test/open-doors.test.ts +10 -5
- package/translations/en.json +29 -4
- package/translations/es.json +29 -4
- package/translations/ja.json +29 -4
- package/translations/ko.json +30 -5
- package/translations/ms.json +29 -4
- package/translations/zh.json +29 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Data entry from material (design/ai/data-entry.md) - the rules a row passes, what the tool hands back,
|
|
3
3
|
* and what the server asks again before it writes.
|
|
4
4
|
*/
|
|
5
|
-
import { offsetAt, zonedDateTimes, clearEntryTargets, declareEntryTarget, entryDoorOf, entryToolName, entryTotals, lineHas, normalizeEntryRows, printedTotalOf, verifyRowsAgainstSources, writtenForms, type EntryTarget } from './entry-target'
|
|
5
|
+
import { readFieldValue, recordIdOfKey, resolveReferences, offsetAt, zonedDateTimes, clearEntryTargets, declareEntryTarget, entryDoorOf, entryToolName, entryTotals, lineHas, normalizeEntryRows, printedTotalOf, verifyRowsAgainstSources, writtenForms, type EntryTarget } from './entry-target'
|
|
6
6
|
import { entryCategoryOf, proposeEntries } from './entry-tools'
|
|
7
7
|
import { applyEntryProposal, entryProposalAnswered, findEntryProposal, type EntryApplyDeps } from './entry-apply'
|
|
8
8
|
import { modeToolCategories } from '../assistant-mode/resolve'
|
|
@@ -410,3 +410,67 @@ describe("a moment is the domain's wall time", () => {
|
|
|
410
410
|
expect(result.rows[0].values.startTime).toBe('2026-10-01T08:30:00+09:00')
|
|
411
411
|
})
|
|
412
412
|
})
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* 2026-10-23: an office link card took `tax-invoice:<uuid>` - the key the link evidence and the open record show - and
|
|
416
|
+
* looked it up as an approval number. A reference that names a record of an axis reads that axis's key as its id.
|
|
417
|
+
*/
|
|
418
|
+
describe('a reference names the record the model was shown', () => {
|
|
419
|
+
const ID = 'fe0e8503-ec5f-4da4-9c07-2d342c93d498'
|
|
420
|
+
const invoice = {
|
|
421
|
+
name: 'invoice',
|
|
422
|
+
type: 'reference' as const,
|
|
423
|
+
axis: 'tax-invoice',
|
|
424
|
+
reference: {
|
|
425
|
+
query: 'query ($text: String) { invoices(text: $text) { items { id approvalNo } } }',
|
|
426
|
+
variables: (text: string) => ({ text }),
|
|
427
|
+
recordsOf: (data: any, text: string) => (data?.invoices?.items ?? []).filter((one: any) => one.id === text || one.approvalNo === text)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const asked: string[] = []
|
|
431
|
+
const run = async (_query: string, variables: any) => {
|
|
432
|
+
asked.push(variables.text)
|
|
433
|
+
return { data: { invoices: { items: [{ id: ID, approvalNo: '20260120-10260120-17151515' }] } } }
|
|
434
|
+
}
|
|
435
|
+
const resolved = async (text: string, field: any = invoice) => {
|
|
436
|
+
const [row] = await resolveReferences([field], [{ values: { invoice: { text } }, issues: [] as any[] }], run)
|
|
437
|
+
return row.issues.length ? row.issues[0].code : row.values.invoice.id
|
|
438
|
+
}
|
|
439
|
+
beforeEach(() => (asked.length = 0))
|
|
440
|
+
|
|
441
|
+
it('★ the key `<axis>:<uuid>`, the bare uuid and the record\'s own words all find it', async () => {
|
|
442
|
+
expect(await resolved(`tax-invoice:${ID}`)).toBe(ID)
|
|
443
|
+
expect(asked).toEqual([ID])
|
|
444
|
+
expect(await resolved(ID)).toBe(ID)
|
|
445
|
+
expect(await resolved('20260120-10260120-17151515')).toBe(ID)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('★ the key of a record of another axis is not this record, whatever its uuid', async () => {
|
|
449
|
+
expect(await resolved(`cash-transaction:${ID}`)).toBe('reference-not-found')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('a reference with no axis reads what was written, as before', async () => {
|
|
453
|
+
const { axis, ...plain } = invoice
|
|
454
|
+
expect(await resolved(`tax-invoice:${ID}`, plain)).toBe('reference-not-found')
|
|
455
|
+
expect(asked).toEqual([`tax-invoice:${ID}`])
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('★ a field that holds a record\'s id reads its key as the id - one axis or any of several', () => {
|
|
459
|
+
const question = { name: 'questionId', type: 'string' as const, axis: 'strategy-question' }
|
|
460
|
+
expect(readFieldValue(question, ` strategy-question:${ID} `)).toEqual({ read: ID })
|
|
461
|
+
expect(readFieldValue(question, `initiative:${ID}`)).toEqual({ read: `initiative:${ID}` })
|
|
462
|
+
const owner = { name: 'ownerId', type: 'string' as const, axis: ['strategy-question', 'initiative'] }
|
|
463
|
+
expect(readFieldValue(owner, `initiative:${ID}`)).toEqual({ read: ID })
|
|
464
|
+
expect(readFieldValue({ name: 'note', type: 'string' }, `initiative:${ID}`)).toEqual({ read: `initiative:${ID}` })
|
|
465
|
+
/* A model that hands a resolved reference the key, not the id. */
|
|
466
|
+
expect(readFieldValue(invoice, { id: `tax-invoice:${ID}` })).toEqual({ read: { id: ID } })
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('a key names an id only when it is one', () => {
|
|
470
|
+
expect(recordIdOfKey('contract', `contract:${ID}`)).toBe(ID)
|
|
471
|
+
expect(recordIdOfKey('contract', ID.toUpperCase())).toBe(ID.toUpperCase())
|
|
472
|
+
expect(recordIdOfKey('contract', 'contract:H2026012')).toBeUndefined()
|
|
473
|
+
expect(recordIdOfKey('contract', 'H2026012')).toBeUndefined()
|
|
474
|
+
expect(recordIdOfKey('contract', `contract-round:${ID}`)).toBeUndefined()
|
|
475
|
+
})
|
|
476
|
+
})
|
|
@@ -36,6 +36,14 @@ export interface EntryField {
|
|
|
36
36
|
* taken; none or several keep the row out and say so - the server never picks among several.
|
|
37
37
|
*/
|
|
38
38
|
reference?: EntryReference
|
|
39
|
+
/**
|
|
40
|
+
* The axis of the record the field names (`tax-invoice`, `contract`) - several when it may name a record of each - for a
|
|
41
|
+
* record the model is shown by its key: an open record's anchor and an application's link evidence write
|
|
42
|
+
* `<axis>:<id>`. The key is read as the id it names, so what the model was shown is what it may write (2026-10-23: a
|
|
43
|
+
* card took `tax-invoice:<uuid>` from the evidence, looked it up as an approval number and found nothing). A reference
|
|
44
|
+
* reads it before its query runs; a string field holding an id reads it as the id.
|
|
45
|
+
*/
|
|
46
|
+
axis?: string | readonly string[]
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export interface EntryReference {
|
|
@@ -46,6 +54,22 @@ export interface EntryReference {
|
|
|
46
54
|
recordsOf(data: any, text: string): { id: string; label?: string }[]
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The id a record's key names - `<axis>:<uuid>` as an anchor writes it, or the uuid alone. Nothing for a word, or for
|
|
61
|
+
* the key of a record of another axis: that one is not this record, whatever its uuid.
|
|
62
|
+
*/
|
|
63
|
+
export function recordIdOfKey(axis: string | readonly string[], text: string): string | undefined {
|
|
64
|
+
const value = String(text ?? '').trim()
|
|
65
|
+
const prefix = (typeof axis === 'string' ? [axis] : axis).map(one => `${one}:`).find(one => value.startsWith(one))
|
|
66
|
+
const id = prefix ? value.slice(prefix.length) : value
|
|
67
|
+
return UUID.test(id) ? id : undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** What the model wrote for a field that names a record - the id, when it wrote the record's key. */
|
|
71
|
+
const shownId = (field: EntryField, text: string): string => (field.axis && recordIdOfKey(field.axis, text)) || text
|
|
72
|
+
|
|
49
73
|
/** A resolved reference, as a row keeps it. */
|
|
50
74
|
export interface ReferenceValue {
|
|
51
75
|
id: string
|
|
@@ -116,6 +140,12 @@ export interface EntryEvent {
|
|
|
116
140
|
export interface EntryTarget {
|
|
117
141
|
/** `<app>.<record>` - `office.expense-line`. */
|
|
118
142
|
key: string
|
|
143
|
+
/**
|
|
144
|
+
* The records the card is about besides its own (the axes of `subject` anchors) - a link card is about the deposit,
|
|
145
|
+
* the invoice and the round it joins. On those records' screens its propose tool is put in front (ADR-0061 decision
|
|
146
|
+
* 9); asked to link a deposit from its 360, the model found the invoice and never opened the card (2026-10-24).
|
|
147
|
+
*/
|
|
148
|
+
axes?: string[]
|
|
119
149
|
/** i18n key of the record's name on the card. */
|
|
120
150
|
labelKey: string
|
|
121
151
|
/** What one row is, for the model. */
|
|
@@ -381,11 +411,11 @@ export function readFieldValue(field: EntryField, value: unknown): { read?: unkn
|
|
|
381
411
|
return field.values!.includes(String(value).trim()) ? { read: String(value).trim() } : { code: 'not-in-list' }
|
|
382
412
|
case 'reference':
|
|
383
413
|
/* Resolved already ({ id }), or a name to look up ({ text }) - resolveReferences settles the second. */
|
|
384
|
-
if (value && typeof value === 'object' && typeof (value as any).id === 'string') return { read: { id: (value as any).id, ...((value as any).label ? { label: String((value as any).label) } : {}) } }
|
|
414
|
+
if (value && typeof value === 'object' && typeof (value as any).id === 'string') return { read: { id: shownId(field, (value as any).id), ...((value as any).label ? { label: String((value as any).label) } : {}) } }
|
|
385
415
|
if (value && typeof value === 'object' && typeof (value as any).text === 'string') return { read: { text: (value as any).text } }
|
|
386
416
|
return typeof value === 'string' || typeof value === 'number' ? { read: { text: String(value).trim() } } : { code: 'not-a-reference' }
|
|
387
417
|
default:
|
|
388
|
-
return { read: String(value).trim() }
|
|
418
|
+
return { read: shownId(field, String(value).trim()) }
|
|
389
419
|
}
|
|
390
420
|
}
|
|
391
421
|
|
|
@@ -651,11 +681,12 @@ export async function resolveReferences<T extends { values: Record<string, unkno
|
|
|
651
681
|
for (const field of references) {
|
|
652
682
|
const value: any = row.values[field.name]
|
|
653
683
|
if (!value || typeof value !== 'object' || typeof value.text !== 'string') continue
|
|
654
|
-
const
|
|
684
|
+
const text = shownId(field, value.text)
|
|
685
|
+
const key = `${field.name}\u0000${text}`
|
|
655
686
|
if (!asked.has(key)) {
|
|
656
687
|
try {
|
|
657
|
-
const answer = await run(field.reference!.query, field.reference!.variables(
|
|
658
|
-
asked.set(key, answer.errors?.length ? new Error(answer.errors[0].message) : field.reference!.recordsOf(answer.data,
|
|
688
|
+
const answer = await run(field.reference!.query, field.reference!.variables(text))
|
|
689
|
+
asked.set(key, answer.errors?.length ? new Error(answer.errors[0].message) : field.reference!.recordsOf(answer.data, text) ?? [])
|
|
659
690
|
} catch (error: any) {
|
|
660
691
|
asked.set(key, error instanceof Error ? error : new Error(String(error)))
|
|
661
692
|
}
|
|
@@ -153,7 +153,7 @@ function changeSpecs(target: EntryTarget, run: (context: any) => GraphqlRunner):
|
|
|
153
153
|
doors: [...new Set([rootFieldOf(update.mutation, 'mutation'), readDoor, ...referenceDoorsOf(changeableFields(target))])],
|
|
154
154
|
description:
|
|
155
155
|
`Propose changes to existing records: ${target.description} ` +
|
|
156
|
-
'Name each record by ref: "current" (the one on screen), "selected" (every row selected on screen)
|
|
156
|
+
'Name each record by ref: "current" (the one on screen), "selected" (every row selected on screen), the id a list read or a find tool returned, or the record\'s key (`<kind>:<id>`) as you were shown it - nothing need be selected to change rows a read found. ' +
|
|
157
157
|
'The server reads each record, and the card shows what it holds now and what it becomes; nothing changes until the person presses it.',
|
|
158
158
|
schema: {
|
|
159
159
|
type: 'object',
|
|
@@ -163,7 +163,7 @@ function changeSpecs(target: EntryTarget, run: (context: any) => GraphqlRunner):
|
|
|
163
163
|
items: {
|
|
164
164
|
type: 'object',
|
|
165
165
|
properties: {
|
|
166
|
-
ref: { type: 'string', description: '"current", "selected",
|
|
166
|
+
ref: { type: 'string', description: '"current", "selected", a record id from a list read or a find tool, or the record\'s key as shown - never a guessed id.' },
|
|
167
167
|
values: { type: 'object', description: 'Only the fields that change, with their new values.', properties: Object.fromEntries(changeableFields(target).map(field => [field.name, fieldSchema(field)])) }
|
|
168
168
|
},
|
|
169
169
|
required: ['ref', 'values']
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
changeToolName,
|
|
14
14
|
findToolName,
|
|
15
15
|
readFieldValue,
|
|
16
|
+
recordIdOfKey,
|
|
16
17
|
resolveReferences,
|
|
17
18
|
zonedDateTimes,
|
|
18
19
|
sameFieldValue,
|
|
@@ -52,7 +53,8 @@ export function changeableFields(target: EntryTarget): EntryField[] {
|
|
|
52
53
|
/**
|
|
53
54
|
* The records the changes name. `current` is the record on screen, `selected` every row selected there -
|
|
54
55
|
* both from the surface's host context, which is **not trusted**: an id from it is only a candidate, read
|
|
55
|
-
* through the application before anything is shown. Any other ref is an id, from the find tool
|
|
56
|
+
* through the application before anything is shown. Any other ref is an id, from the find tool, or the key the record
|
|
57
|
+
* was shown by.
|
|
56
58
|
*/
|
|
57
59
|
export function resolveChangeRefs(
|
|
58
60
|
targetKey: string,
|
|
@@ -74,7 +76,8 @@ export function resolveChangeRefs(
|
|
|
74
76
|
if (ids.length) for (const id of ids) picked.push({ id, via: 'selected', values })
|
|
75
77
|
else unresolved.push('selected')
|
|
76
78
|
} else if (ref) {
|
|
77
|
-
|
|
79
|
+
/* An open record's key - `contract-round:<id>` for `office.contract-round` - names the record of this target's axis. */
|
|
80
|
+
picked.push({ id: recordIdOfKey(targetKey.split('.').pop()!, ref) ?? ref, via: 'id', values })
|
|
78
81
|
} else {
|
|
79
82
|
unresolved.push('no-ref')
|
|
80
83
|
}
|
|
@@ -143,7 +146,7 @@ export async function proposeChanges(target: EntryTarget, args: any, context: an
|
|
|
143
146
|
message:
|
|
144
147
|
unresolved[0] === 'current' || unresolved[0] === 'selected'
|
|
145
148
|
? `the screen names no ${unresolved[0] === 'current' ? 'record' : 'selected rows'} of this kind. Ask the person which record, or call ${findToolName(target.key)}.`
|
|
146
|
-
: `name each record: ref "current", "selected", or an id
|
|
149
|
+
: `name each record: ref "current", "selected", or an id a list read or ${findToolName(target.key)} returned. Never guess an id.`
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
let records: Map<string, EntryRecord>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A list read takes what a row is missing, as its screen's filters say it (2026-10-24) - asked to fill the bank lines'
|
|
3
|
+
* unregistered parties, the model had no condition for "unregistered" and searched the asker's own name.
|
|
4
|
+
*/
|
|
5
|
+
import { listReadTarget } from './list-read'
|
|
6
|
+
import { filtersOf, readCriteria } from './read-target'
|
|
7
|
+
|
|
8
|
+
const lines = listReadTarget({
|
|
9
|
+
key: 'office.cash-transaction',
|
|
10
|
+
title: 'menu.cash-transaction-list',
|
|
11
|
+
name: 'bank lines',
|
|
12
|
+
listQuery: 'transactions',
|
|
13
|
+
selection: 'id amount counterpartyName',
|
|
14
|
+
columns: [{ name: 'amount', type: 'number' }],
|
|
15
|
+
missing: [
|
|
16
|
+
{
|
|
17
|
+
name: 'counterparty',
|
|
18
|
+
description: 'a party name and no registered counterparty',
|
|
19
|
+
filters: [
|
|
20
|
+
{ name: 'counterpartyId', operator: 'is_null', value: true },
|
|
21
|
+
{ name: 'counterpartyName', operator: 'is_not_null', value: true }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{ name: 'account', description: 'no account subject', filters: [{ name: 'accountId', operator: 'is_null', value: true }] }
|
|
25
|
+
]
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('what a row is missing', () => {
|
|
29
|
+
it('★ the condition asks with the screen\'s own filters', () => {
|
|
30
|
+
const { criteria } = readCriteria(lines, { missing: 'counterparty' })
|
|
31
|
+
expect(filtersOf(lines, criteria)).toEqual([
|
|
32
|
+
{ name: 'counterpartyId', operator: 'is_null', value: true },
|
|
33
|
+
{ name: 'counterpartyName', operator: 'is_not_null', value: true }
|
|
34
|
+
])
|
|
35
|
+
expect(filtersOf(lines, readCriteria(lines, { missing: 'account' }).criteria)).toEqual([{ name: 'accountId', operator: 'is_null', value: true }])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('★ the model is told each thing a row may miss, and nothing else is taken', () => {
|
|
39
|
+
const missing = lines.criteria.find(one => one.name === 'missing')!
|
|
40
|
+
expect(missing.values).toEqual(['counterparty', 'account'])
|
|
41
|
+
expect(missing.description).toMatch(/counterparty - a party name and no registered counterparty/)
|
|
42
|
+
expect(readCriteria(lines, { missing: 'phone' }).refused).toEqual([{ name: 'missing', code: 'not-in-list' }])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('a list that declares nothing missing has no such condition', () => {
|
|
46
|
+
const { missing, ...plain } = { ...(lines as any), missing: undefined }
|
|
47
|
+
expect(listReadTarget({ key: 'office.x', title: 't', name: 'x', listQuery: 'xs', selection: 'id', columns: [] }).criteria.some(one => one.name === 'missing')).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -33,6 +33,12 @@ export interface ListRead {
|
|
|
33
33
|
route?: string
|
|
34
34
|
/** The route has a URL-bound grid. Otherwise conditions are disclosed as not applied on its screen card. */
|
|
35
35
|
screenQuery?: boolean
|
|
36
|
+
/**
|
|
37
|
+
* What a row may be missing - the screen's own "no counterparty", "no account subject" filters - the `missing`
|
|
38
|
+
* condition. Asked to fill the bank lines' unregistered parties, the model had no way to say "unregistered" and
|
|
39
|
+
* searched the asker's own name (2026-10-24).
|
|
40
|
+
*/
|
|
41
|
+
missing?: { name: string; description: string; filters: ReadFilter[] }[]
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/** The money column a minimum or maximum amount means - `amount` when there is one, else the first summed column. */
|
|
@@ -62,6 +68,18 @@ export function listReadTarget(list: ListRead): ReadTarget {
|
|
|
62
68
|
...(list.status
|
|
63
69
|
? [{ name: 'status', type: 'enum', values: list.status.values, labelKey: 'ai-assistant.read.status', filter: (v: any) => ({ name: list.status!.field, operator: 'eq', value: v }) }]
|
|
64
70
|
: []),
|
|
71
|
+
...(list.missing?.length
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
name: 'missing',
|
|
75
|
+
type: 'enum',
|
|
76
|
+
values: list.missing.map(one => one.name),
|
|
77
|
+
labelKey: 'ai-assistant.read.missing',
|
|
78
|
+
description: `the rows missing one thing: ${list.missing.map(one => `${one.name} - ${one.description}`).join('; ')}`,
|
|
79
|
+
filter: (v: any) => list.missing!.find(one => one.name === v)!.filters
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
65
83
|
...(amount
|
|
66
84
|
? [
|
|
67
85
|
{ name: 'minAmount', type: 'number', labelKey: 'ai-assistant.read.min-amount', description: `at least this ${amount}${inCurrency}`, filter: (v: any) => ({ name: amount, operator: 'gte', value: v }) },
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
|
|
11
11
|
*/
|
|
12
|
-
import { readFieldValue, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'
|
|
12
|
+
import { readFieldValue, recordIdOfKey, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'
|
|
13
13
|
import { encodeListQuery } from '@operato/utils/list-query.js'
|
|
14
14
|
import type { GraphqlRunner } from '../data-entry/entry-update.js'
|
|
15
15
|
import { parse } from 'graphql'
|
|
@@ -143,6 +143,8 @@ export interface DetailArg {
|
|
|
143
143
|
values?: string[]
|
|
144
144
|
description?: string
|
|
145
145
|
required?: boolean
|
|
146
|
+
/** A record's id: the axis (or axes) of the record, whose key `<axis>:<id>` the model may have been shown (EntryField.axis). */
|
|
147
|
+
axis?: string | readonly string[]
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
/**
|
|
@@ -160,6 +162,11 @@ export interface DetailRead {
|
|
|
160
162
|
/** One query with one root field. Its variables are the arguments - each declared in `args`. */
|
|
161
163
|
query: string
|
|
162
164
|
args?: DetailArg[]
|
|
165
|
+
/**
|
|
166
|
+
* The records the answer is about, whose screens put it in front (EntryTarget.axes) - the management check answers
|
|
167
|
+
* for any ledger record; asked on a deposit's 360 why it was in the check, the model never opened it (2026-10-24).
|
|
168
|
+
*/
|
|
169
|
+
axes?: string[]
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
/** Characters of an answer handed to the model - a longer one is refused, and the model narrows it. */
|
|
@@ -200,8 +207,8 @@ export function detailReadOf(key: string | undefined): DetailRead | undefined {
|
|
|
200
207
|
export function readCriteria(target: ReadTarget, args: any): { criteria: Record<string, unknown>; refused: { name: string; code: string }[] } {
|
|
201
208
|
const criteria: Record<string, unknown> = {}
|
|
202
209
|
const refused: { name: string; code: string }[] = []
|
|
203
|
-
/* One record by its id - every target takes it (the id a list answer or a find tool gave). */
|
|
204
|
-
if (typeof args?.id === 'string' && args.id.trim()) criteria.id = args.id.trim()
|
|
210
|
+
/* One record by its id - every target takes it (the id a list answer or a find tool gave, or the key `<axis>:<id>` an open record is shown by, the axis being the key's last part). */
|
|
211
|
+
if (typeof args?.id === 'string' && args.id.trim()) criteria.id = recordIdOfKey(target.key.split('.').pop()!, args.id) ?? args.id.trim()
|
|
205
212
|
if (target.search && typeof args?.text === 'string' && args.text.trim()) criteria.text = args.text.trim()
|
|
206
213
|
for (const criterion of target.criteria) {
|
|
207
214
|
const value = args?.[criterion.name]
|
|
@@ -246,7 +246,7 @@ export async function queryDetail(target: DetailRead, args: any, run: GraphqlRun
|
|
|
246
246
|
if (arg.required) refused.push({ name: arg.name, code: 'required' })
|
|
247
247
|
continue
|
|
248
248
|
}
|
|
249
|
-
const { read, code } = readFieldValue({ name: arg.name, type: arg.type === 'string' ? 'text' : arg.type, values: arg.values } as any, value)
|
|
249
|
+
const { read, code } = readFieldValue({ name: arg.name, type: arg.type === 'string' ? 'text' : arg.type, values: arg.values, axis: arg.axis } as any, value)
|
|
250
250
|
if (read === undefined) refused.push({ name: arg.name, code: code ?? 'unreadable' })
|
|
251
251
|
else variables[arg.name] = read
|
|
252
252
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A read takes a record by the key it was shown by (2026-10-23) - `<axis>:<uuid>`, as an open record's anchor and an
|
|
3
|
+
* application's evidence write it. A 360 read and a list read asked by id took the key as the id and found nothing.
|
|
4
|
+
*/
|
|
5
|
+
import { readCriteria, type DetailRead, type ReadTarget } from './read-target'
|
|
6
|
+
import { queryDetail } from './read-tools'
|
|
7
|
+
|
|
8
|
+
const ID = 'f92c04ec-40c4-4be8-8160-080cff20e750'
|
|
9
|
+
|
|
10
|
+
describe('a read names the record the model was shown', () => {
|
|
11
|
+
const lines = { key: 'office.cash-transaction', criteria: [] } as unknown as ReadTarget
|
|
12
|
+
|
|
13
|
+
it('★ a list read by id takes the key of its own axis - the target key\'s last part', () => {
|
|
14
|
+
expect(readCriteria(lines, { id: `cash-transaction:${ID}` }).criteria).toEqual({ id: ID })
|
|
15
|
+
expect(readCriteria(lines, { id: ID }).criteria).toEqual({ id: ID })
|
|
16
|
+
/* Another record's key is not this record - asked as written, it finds nothing. */
|
|
17
|
+
expect(readCriteria(lines, { id: `contract:${ID}` }).criteria).toEqual({ id: `contract:${ID}` })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('★ a whole answer takes the key of the axis its id argument declares', async () => {
|
|
21
|
+
const summary: DetailRead = {
|
|
22
|
+
key: 'office.contract-360',
|
|
23
|
+
description: "one contract's 360.",
|
|
24
|
+
query: 'query ($id: String!) { contract360(id: $id) { code } }',
|
|
25
|
+
args: [{ name: 'id', type: 'string', required: true, axis: 'contract' }]
|
|
26
|
+
}
|
|
27
|
+
const asked: any[] = []
|
|
28
|
+
const run = async (_document: string, variables: any) => {
|
|
29
|
+
asked.push(variables)
|
|
30
|
+
return { data: { contract360: { code: 'H2026012' } } }
|
|
31
|
+
}
|
|
32
|
+
await queryDetail(summary, { id: `contract:${ID}` }, run)
|
|
33
|
+
await queryDetail(summary, { id: `cash-transaction:${ID}` }, run)
|
|
34
|
+
expect(asked).toEqual([{ id: ID }, { id: `cash-transaction:${ID}` }])
|
|
35
|
+
})
|
|
36
|
+
})
|
package/server/service/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { AssistantCapabilityResolver } from './assistant-capability-resolver.js'
|
|
|
5
5
|
import { AiAgentResolver } from './ai-agent-resolver.js'
|
|
6
6
|
import { MySessionActivitySubscription } from './chat-session/session-activity-subscription.js'
|
|
7
7
|
import { ChatMessageSubscription } from './chat-message/chat-message-subscription.js'
|
|
8
|
+
import { ChatTypingSubscription } from './chat-message/chat-typing-subscription.js'
|
|
8
9
|
import { entities as ChatSessionEntities } from './chat-session/index.js'
|
|
9
10
|
import { entities as ChatMessageEntities } from './chat-message/index.js'
|
|
10
11
|
import { entities as ChatSessionParticipantEntities } from './chat-session-participant/index.js'
|
|
@@ -29,7 +30,9 @@ export * from './assistant-capability-resolver.js'
|
|
|
29
30
|
export * from './tool-access.js'
|
|
30
31
|
/* Data entry from material - an app registers what it can enter; a mode names it (design/ai/data-entry.md). */
|
|
31
32
|
export * from './data-entry/entry-target.js'
|
|
32
|
-
export { registerEntryTarget, entryCategoryOf } from './data-entry/entry-tools.js'
|
|
33
|
+
export { registerEntryTarget, entryCategoryOf, runAsCaller } from './data-entry/entry-tools.js'
|
|
34
|
+
/* An application's tool that prepares many changes at once makes the same change card the model's would (2026-10-24). */
|
|
35
|
+
export { proposeChanges, MAX_CHANGES } from './data-entry/entry-update.js'
|
|
33
36
|
/* Reading an application's lists - an app registers what it lets read; a mode names it (design/ai/read-targets.md). */
|
|
34
37
|
export * from './data-read/read-target.js'
|
|
35
38
|
export { registerReadTarget, readCategoryOf, registerDetailRead, detailCategoryOf } from './data-read/read-tools.js'
|
|
@@ -79,6 +82,8 @@ export const schema = {
|
|
|
79
82
|
/* A domain's AI agents - the settings card and the dock's picker (design/ai/agent-settings.md). */
|
|
80
83
|
AiAgentResolver,
|
|
81
84
|
ChatMessageSubscription,
|
|
85
|
+
/* 「입력 중」 — 저장하지 않는 방의 한 줄. 듣는 사람은 메시지와 같은 방 판정을 지난다. */
|
|
86
|
+
ChatTypingSubscription,
|
|
82
87
|
/* 내가 참여한 모든 세션의 활동 — 창을 닫아 둔 표면이 배지·목록을 갱신할 수 있게. */
|
|
83
88
|
MySessionActivitySubscription,
|
|
84
89
|
/* 사람이 누른 입력 제안을 쓰는 자리 — 행마다 앱의 코드로, 그 사람의 권한으로. */
|
|
@@ -126,7 +126,7 @@ export interface ConversationOptions {
|
|
|
126
126
|
*/
|
|
127
127
|
participants?: ScriptParticipant[]
|
|
128
128
|
/** 이 대화가 공개인가 — 기본은 공개(ADR-0073 판정 2). 'private' 이면 멘션이 사람을 앉히지 않는다. */
|
|
129
|
-
visibility?: 'open' | 'private'
|
|
129
|
+
visibility?: 'open' | 'group' | 'private'
|
|
130
130
|
/** Who the assistant is on this surface — in real use it comes from the registered mode. */
|
|
131
131
|
systemPrompt?: string
|
|
132
132
|
/** Which registered categories this conversation may use. Omitted allows none. */
|
|
@@ -145,6 +145,8 @@ export function modeCarriesSharedRules(kind: string): string[] {
|
|
|
145
145
|
const found: string[] = []
|
|
146
146
|
if (/name our plumbing|do not describe our machinery/i.test(prompt)) found.push('absence-rule-copy')
|
|
147
147
|
if (/^\s*Next:/im.test(prompt) || /end the reply with one last line/i.test(prompt)) found.push('next-request-rule-copy')
|
|
148
|
+
if (/finish what was asked in this turn|never ask whether to propose/i.test(prompt)) found.push('finish-rule-copy')
|
|
149
|
+
if (/\[writer → addressee\]/.test(prompt)) found.push('speaker-rule-copy')
|
|
148
150
|
return found
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -168,7 +168,11 @@ describe('what twin needed of the dock', () => {
|
|
|
168
168
|
})
|
|
169
169
|
|
|
170
170
|
it('a carried conversation that could not be made is said, not swallowed', () => {
|
|
171
|
-
assert.match(dock, /this\.startCarried\(ask\)\.catch\(
|
|
171
|
+
assert.match(dock, /this\.startCarried\(ask\)\.catch\(\(?\w*\)? => \{\s*notify\(\{ level: 'error'/)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('★ and nothing is sent into the previous conversation after it - the request depended on the context that failed', () => {
|
|
175
|
+
assert.match(dock, /this\.startCarried\(ask\)\.catch\([\s\S]*?return undefined\s*\}\)[\s\S]{0,200}?if \(!id\) return\n/)
|
|
172
176
|
})
|
|
173
177
|
})
|
|
174
178
|
|
|
@@ -58,11 +58,13 @@ describe('the doors are the framework ones, as they are', () => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
describe('nothing irreversible without asking', () => {
|
|
61
|
-
it('delete
|
|
61
|
+
it('delete, making public, making private (its people leave) and leaving ask first with the platform confirm; making it a group, hiding and renaming do not', () => {
|
|
62
62
|
assert.match(manager, /import \{ OxPrompt \} from '@operato\/popup'/)
|
|
63
63
|
assert.match(manager, /this\.askRemove\(entry, name\)/)
|
|
64
64
|
assert.match(manager, /this\.askOpen\(entry, name\)/)
|
|
65
|
-
assert.match(manager,
|
|
65
|
+
assert.match(manager, /this\.askPrivate\(entry, name\)/)
|
|
66
|
+
assert.match(manager, /this\.askLeave\(entry, name\)/)
|
|
67
|
+
assert.match(manager, /@select=\$\{\(\) => this\.setVisibility\(entry, 'group'\)\}/)
|
|
66
68
|
})
|
|
67
69
|
|
|
68
70
|
it('no browser dialog - they sit outside the theme', () => {
|
package/test/open-doors.test.ts
CHANGED
|
@@ -55,12 +55,15 @@ const OPEN_BY_DECISION: Record<Root, Record<string, string>> = {
|
|
|
55
55
|
Mutation: {},
|
|
56
56
|
Subscription: {
|
|
57
57
|
chatMessageActivity:
|
|
58
|
-
"subscribe: a member of the domain, and the room open to the caller — open to the domain, private to its
|
|
58
|
+
"subscribe: a member of the domain, and the room open to the caller — open to the domain, group or private to its seats " +
|
|
59
59
|
'(ADR-0073 decision 2), refused with the words of a missing session otherwise. Every message: same domain + session, ' +
|
|
60
60
|
'and the room asked again. The body is redacted by doors for each receiver (ADR-0073 ruling 1).',
|
|
61
61
|
mySessionActivity:
|
|
62
62
|
"subscribe: a member of the domain. Every message: same domain, and the caller among the payload's participants. " +
|
|
63
|
-
'Badge fields and a one-line preview only, no body.'
|
|
63
|
+
'Badge fields and a one-line preview only, no body.',
|
|
64
|
+
chatTyping:
|
|
65
|
+
'subscribe: a member of the domain, and the room open to the caller, asked as chatMessageActivity asks it. Every notice: ' +
|
|
66
|
+
"same domain + session, not the caller's own, and the room asked again. A user id, a name and a time - nothing stored, no body."
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -129,7 +132,7 @@ describe('what the walk itself has to be true for', () => {
|
|
|
129
132
|
/* If the resolver list shrank to nothing, the lists above would pass by having nothing to look at. */
|
|
130
133
|
expect(fieldsOf('Query').length).toBeGreaterThanOrEqual(10)
|
|
131
134
|
expect(fieldsOf('Mutation').length).toBeGreaterThanOrEqual(13)
|
|
132
|
-
expect(fieldsOf('Subscription')).toHaveLength(
|
|
135
|
+
expect(fieldsOf('Subscription')).toHaveLength(3)
|
|
133
136
|
})
|
|
134
137
|
|
|
135
138
|
it('tells an undeclared field from one the schema does not have', () => {
|
|
@@ -145,11 +148,13 @@ describe('what the walk itself has to be true for', () => {
|
|
|
145
148
|
* 2026-09-30: 30 — applyEntryProposal (enter the rows the person pressed, design/ai/data-entry.md), declared.
|
|
146
149
|
* 2026-10-12: 35 — aiAgentSettings · aiAgentChoices · aiProviderModels · saveAiAgents · aiUsageSummary (a domain's AI
|
|
147
150
|
* agents and their usage, design/ai/agent-settings.md), declared.
|
|
151
|
+
* 2026-10-24: 41 — mayInviteToAISession · inviteToAISession · removeFromAISession · leaveAISession · sayTyping declared, chatTyping open (a
|
|
152
|
+
* group conversation and its typing line, ADR-0073 decision 3).
|
|
148
153
|
*/
|
|
149
154
|
const all = fieldsOf('Query').length + fieldsOf('Mutation').length + fieldsOf('Subscription').length
|
|
150
155
|
const open = openFields('Query').length + openFields('Mutation').length + openFields('Subscription').length
|
|
151
156
|
|
|
152
|
-
expect(all).toBe(
|
|
153
|
-
expect(open).toBe(
|
|
157
|
+
expect(all).toBe(41)
|
|
158
|
+
expect(open).toBe(3)
|
|
154
159
|
})
|
|
155
160
|
})
|
package/translations/en.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"ai-assistant.dock.carry-failed": "The selected context could not be attached. Nothing was sent. Try again from the original screen.",
|
|
2
3
|
"ai-assistant.dock.wait-before-switch": "Wait for the current response before changing conversations.",
|
|
3
4
|
"ai-assistant.dock.switch-title": "Change conversation?",
|
|
4
5
|
"ai-assistant.dock.switch-draft": "Your unsent text and attachments will not move to the other conversation.",
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
"ai-assistant.session.reset-filters": "Clear search and filters",
|
|
15
16
|
"ai-assistant.session.filtered-empty": "No conversations match these filters here.",
|
|
16
17
|
"ai-assistant.session.scope-empty": "No conversations yet.",
|
|
17
|
-
"ai-assistant.session.audience-private": "
|
|
18
|
+
"ai-assistant.session.audience-private": "Only me",
|
|
18
19
|
"ai-assistant.session.audience-open": "Everyone in this domain",
|
|
19
20
|
"ai-assistant.session.manage": "Manage conversation",
|
|
20
21
|
"ai-assistant.session.list": "Conversations",
|
|
@@ -121,7 +122,6 @@
|
|
|
121
122
|
"ai-assistant.text.say-to-room-tooltip": "say this to the people here — the assistant does not answer",
|
|
122
123
|
"ai-assistant.text.no-room-yet": "this conversation is not saved yet, so there is nobody else in it to talk to",
|
|
123
124
|
"ai-assistant.text.will-be-invited": "{names} will join this conversation when you send",
|
|
124
|
-
"ai-assistant.text.cannot-invite": "{names} will not join — only the people already in this conversation can bring someone in",
|
|
125
125
|
"ai-assistant.text.answer-needs-permission": "you do not have permission to see this answer. ask an administrator for access.",
|
|
126
126
|
"ai-assistant.label.conversation": "conversation",
|
|
127
127
|
"ai-assistant.text.switch-conversation": "switch to {name}",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
"ai-assistant.session.filter-whose": "Whose",
|
|
183
183
|
"ai-assistant.session.filter-participant": "I'm in",
|
|
184
184
|
"ai-assistant.session.filter-visibility": "Visibility",
|
|
185
|
-
"ai-assistant.session.filter-any-visibility": "
|
|
185
|
+
"ai-assistant.session.filter-any-visibility": "Any audience",
|
|
186
186
|
"ai-assistant.session.filter-open": "Public",
|
|
187
187
|
"ai-assistant.session.filter-private": "Private",
|
|
188
188
|
"ai-assistant.session.filter-unread": "Unread",
|
|
@@ -247,6 +247,7 @@
|
|
|
247
247
|
"ai-assistant.read.year": "year",
|
|
248
248
|
"ai-assistant.read.month": "month",
|
|
249
249
|
"ai-assistant.read.flow": "in or out",
|
|
250
|
+
"ai-assistant.read.missing": "missing",
|
|
250
251
|
"ai-assistant.read.period": "month",
|
|
251
252
|
"ai-assistant.read.status": "status",
|
|
252
253
|
"ai-assistant.read.min-amount": "minimum amount",
|
|
@@ -399,5 +400,29 @@
|
|
|
399
400
|
"ai-assistant.run.assistant-run-model-error": "the AI model failed during the run.",
|
|
400
401
|
"ai-assistant.run.assistant-run-abandoned": "the run stopped partway and was started again.",
|
|
401
402
|
"ai-assistant.run.ai-not-configured": "AI is not set up for this domain.",
|
|
402
|
-
"ai-assistant.run.ai-agent-not-available": "the AI agent picked for this run is not available here."
|
|
403
|
+
"ai-assistant.run.ai-agent-not-available": "the AI agent picked for this run is not available here.",
|
|
404
|
+
"ai-assistant.text.cannot-invite-private": "{names} will not join — a private conversation is yours alone. Make it a group to bring people in",
|
|
405
|
+
"ai-assistant.text.cannot-invite-group": "{names} will not join — only the person who started this group can bring someone in",
|
|
406
|
+
"ai-assistant.button.invite": "Invite",
|
|
407
|
+
"ai-assistant.label.participants": "People in this conversation",
|
|
408
|
+
"ai-assistant.button.remove-participant": "Send {name} out",
|
|
409
|
+
"ai-assistant.text.invited": "{name} can now read and write here, and has been told.",
|
|
410
|
+
"ai-assistant.text.removed-participant": "{name} is no longer in this conversation.",
|
|
411
|
+
"ai-assistant.text.typing-many": "{count} people are typing",
|
|
412
|
+
"ai-assistant.text.typing": "Typing: {names}",
|
|
413
|
+
"ai-assistant.text.invite-search": "Name or email",
|
|
414
|
+
"ai-assistant.text.invite-none-found": "No one to invite by that name.",
|
|
415
|
+
"ai-assistant.session.filter-group": "Group",
|
|
416
|
+
"ai-assistant.session.make-group": "Make it a group",
|
|
417
|
+
"ai-assistant.session.leave": "Leave",
|
|
418
|
+
"ai-assistant.session.confirm-private": "Make \"{name}\" private? Only you will read it, and everyone else in it leaves.",
|
|
419
|
+
"ai-assistant.session.confirm-leave-open": "Leave \"{name}\"? Its new messages stop reaching you.",
|
|
420
|
+
"ai-assistant.session.confirm-leave": "Leave \"{name}\"? You will not read it again unless its owner brings you back.",
|
|
421
|
+
"ai-assistant.session.leave-refused": "you could not leave this conversation.",
|
|
422
|
+
"ai-assistant.session.audience-group": "Invited people",
|
|
423
|
+
"ai-assistant.text.invite-title": "Conversation invite",
|
|
424
|
+
"ai-assistant.text.invite-body-named": "{who} brought you into the conversation \"{name}\".",
|
|
425
|
+
"ai-assistant.text.invite-body": "{who} brought you into a conversation.",
|
|
426
|
+
"ai-assistant.text.board-invite-title": "Board collaboration invite",
|
|
427
|
+
"ai-assistant.text.board-invite-body": "{who} invited you to collaborate on a board."
|
|
403
428
|
}
|