@things-factory/ai-assistant 10.1.103 → 10.1.106
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 +2 -2
- package/client/utils/entry-proposal.ts +11 -0
- package/dist-client/components/assistant-chat.js +2 -2
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/entry-proposal.d.ts +5 -0
- package/dist-client/utils/entry-proposal.js +10 -0
- package/dist-client/utils/entry-proposal.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.js +2 -1
- package/dist-server/service/data-entry/entry-apply.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +10 -0
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +7 -0
- package/dist-server/service/data-entry/entry-update.js +28 -3
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/service/data-entry/data-entry-update.test.ts +37 -0
- package/server/service/data-entry/entry-apply.ts +3 -2
- package/server/service/data-entry/entry-target.ts +6 -0
- package/server/service/data-entry/entry-update.ts +24 -3
- package/test/entry-proposal.test.ts +19 -0
- package/translations/en.json +4 -1
- package/translations/ja.json +4 -1
- package/translations/ko.json +4 -1
- package/translations/ms.json +4 -1
- package/translations/zh.json +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.106",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"@operato/shell": "^10.0.0",
|
|
35
35
|
"@operato/styles": "^10.0.0",
|
|
36
36
|
"@things-factory/ai-client-base": "^10.1.81",
|
|
37
|
-
"@things-factory/apptool-base": "^10.1.
|
|
38
|
-
"@things-factory/attachment-base": "^10.1.
|
|
39
|
-
"@things-factory/auth-base": "^10.1.
|
|
37
|
+
"@things-factory/apptool-base": "^10.1.106",
|
|
38
|
+
"@things-factory/attachment-base": "^10.1.106",
|
|
39
|
+
"@things-factory/auth-base": "^10.1.106",
|
|
40
40
|
"@things-factory/env": "^10.1.20",
|
|
41
|
-
"@things-factory/shell": "^10.1.
|
|
41
|
+
"@things-factory/shell": "^10.1.106",
|
|
42
42
|
"dompurify": "^3.0.0",
|
|
43
43
|
"graphql-scalars": "^1.22.0",
|
|
44
44
|
"graphql-tag": "^2.12.6",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"copyfiles": "^2.4.1",
|
|
52
52
|
"rimraf": "^5.0.0"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "8d78c14708420773ec32d18b8dd44c300771c44d"
|
|
55
55
|
}
|
|
@@ -110,6 +110,13 @@ describe('the card', () => {
|
|
|
110
110
|
expect(rows[0].issues).toEqual([{ field: 'spentDate', code: 'not-changeable', blocking: false }, { code: 'no-change', blocking: true }])
|
|
111
111
|
expect(rows[1].issues).toEqual([{ field: 'category', code: 'not-in-list', params: { values: ['교통비', '회의비'] }, blocking: true }])
|
|
112
112
|
})
|
|
113
|
+
|
|
114
|
+
it('★ a field restated as it already is is not a change - not shown, not written (the model sent the name back with the note)', async () => {
|
|
115
|
+
const result: any = await proposeChanges(expense, { changes: [{ ref: 'l1', values: { merchant: '카카오T', amount: '12400', category: '회의비' } }] }, { host: {} }, run)
|
|
116
|
+
expect(result.rows[0]).toMatchObject({ before: { category: '교통비' }, values: { category: '회의비' }, issues: [] })
|
|
117
|
+
expect(Object.keys(result.rows[0].values)).toEqual(['category'])
|
|
118
|
+
expect(result.columns.map((one: any) => one.name)).toEqual(['category'])
|
|
119
|
+
})
|
|
113
120
|
})
|
|
114
121
|
|
|
115
122
|
describe('changing what the person pressed', () => {
|
|
@@ -311,3 +318,33 @@ describe('a record with lines', () => {
|
|
|
311
318
|
})
|
|
312
319
|
})
|
|
313
320
|
|
|
321
|
+
describe('checking the changes', () => {
|
|
322
|
+
const named: EntryTarget = {
|
|
323
|
+
...expense,
|
|
324
|
+
update: {
|
|
325
|
+
...expense.update!,
|
|
326
|
+
check: rows => rows.map(row => (row.values.merchant === '스타벅스' ? [{ code: 'name-taken', blocking: true }] : []))
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
it('★ the target checks a change when the card is made - and again when it is pressed', async () => {
|
|
331
|
+
declareEntryTarget(named)
|
|
332
|
+
const proposal: any = { ...(await proposeChanges(named, { changes: [{ ref: 'l1', values: { merchant: '스타벅스' } }, { ref: 'l2', values: { amount: 9000 } }] }, { host: {} }, run)), key: 'c1' }
|
|
333
|
+
expect(proposal.rows[0].issues).toEqual([{ code: 'name-taken', blocking: true }])
|
|
334
|
+
expect(proposal.rows[1].issues).toEqual([])
|
|
335
|
+
/* A card made before the clash: the check at pressing keeps it out. */
|
|
336
|
+
const before: any = { ...proposal, rows: proposal.rows.map((row: any) => ({ ...row, issues: [] })) }
|
|
337
|
+
const outcome = await applyEntryProposal('c1', { messages: [{ id: 'm1', proposals: [before] }], answered: false, passesDoor: async () => true, execute: run, state: {} })
|
|
338
|
+
expect(outcome.rows[0]).toEqual({ index: 0, ok: false, error: 'name-taken' })
|
|
339
|
+
expect(outcome.rows[1]).toMatchObject({ ok: true })
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
describe('a check that cannot run', () => {
|
|
344
|
+
it('★ keeps every change out - nothing is written unchecked', async () => {
|
|
345
|
+
const broken: EntryTarget = { ...expense, update: { ...expense.update!, check: () => { throw new Error('db down') } } }
|
|
346
|
+
const proposal: any = await proposeChanges(broken, { changes: [{ ref: 'l1', values: { amount: 1 } }] }, { host: {} }, run)
|
|
347
|
+
expect(proposal.rows[0].issues).toEqual([{ code: 'check-failed', blocking: true }])
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Dependencies come in as arguments so the rules are tested without a database.
|
|
12
12
|
*/
|
|
13
13
|
import { PROPOSAL_OUTCOME_PREFIX, PROPOSAL_VERDICTS } from '../chat-message/proposal-outcome.js'
|
|
14
|
-
import { readRecords, stillAsProposed, type ChangeRow } from './entry-update.js'
|
|
14
|
+
import { readRecords, stillAsProposed, withChangeChecks, type ChangeRow } from './entry-update.js'
|
|
15
15
|
import { changeToolName, zonedDateTimes, entryGroups, resolveReferences, entryDoorOf, entryTargetOf, entryToolName, rootFieldOf, isBlocked, recordEntryEvent, normalizeEntryRows, withCheckedIssues, type EntryRow, type EntryTarget } from './entry-target.js'
|
|
16
16
|
|
|
17
17
|
export interface StoredProposalMessage {
|
|
@@ -185,7 +185,8 @@ async function applyChanges(key: string, proposal: any, target: EntryTarget, dep
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
const results: EntryRowResult[] = []
|
|
188
|
-
|
|
188
|
+
/* The target's checks of the changes again - another record may have taken the new name since the card was made. */
|
|
189
|
+
for (const row of await withChangeChecks(target, stillAsProposed(target, stored, now), now, { state: deps.state })) {
|
|
189
190
|
if (row.issues.some(one => one.blocking)) {
|
|
190
191
|
const issue = row.issues.find(one => one.blocking)!
|
|
191
192
|
results.push({ index: row.index, ok: false, error: issue.field ? `${issue.field}: ${issue.code}` : issue.code })
|
|
@@ -179,6 +179,12 @@ export interface EntryUpdate {
|
|
|
179
179
|
labelOf?(record: EntryRecord): string
|
|
180
180
|
/** The fields that may change. Default: every declared field. */
|
|
181
181
|
fields?: string[]
|
|
182
|
+
/**
|
|
183
|
+
* Read-only checks of the changes - a new name that another record already has. Asked when the card is made and
|
|
184
|
+
* again when it is pressed, like an entry's check; one issue list per row. Without it a clash is caught only by
|
|
185
|
+
* the database's own refusal.
|
|
186
|
+
*/
|
|
187
|
+
check?(rows: { id: string; record?: EntryRecord; values: Record<string, unknown> }[], context: EntryContext): EntryIssue[][] | Promise<EntryIssue[][]>
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
/**
|
|
@@ -115,8 +115,12 @@ export function changeRows(target: EntryTarget, picked: { id: string; via: Chang
|
|
|
115
115
|
if (record) before[name] = record[name] ?? null
|
|
116
116
|
}
|
|
117
117
|
if (!record) issues.push({ code: 'record-not-found', blocking: true })
|
|
118
|
-
else
|
|
119
|
-
|
|
118
|
+
else {
|
|
119
|
+
/* A field the model restated as it already is does not change - the card shows it as no change, and it is not written. */
|
|
120
|
+
const same = Object.keys(values).filter(name => fields.has(name) && !issues.some(issue => issue.field === name) && sameFieldValue(fields.get(name)!, before[name], values[name]))
|
|
121
|
+
if (same.length && same.length === Object.keys(values).length) issues.push({ code: 'no-change', blocking: true })
|
|
122
|
+
else for (const name of same) delete values[name], delete before[name]
|
|
123
|
+
}
|
|
120
124
|
if (!Object.keys(values).length && !issues.some(issue => issue.blocking)) issues.push({ code: 'nothing-to-change', blocking: true })
|
|
121
125
|
return { index, id: one.id, via: one.via, ...(record && target.update?.labelOf ? { label: target.update.labelOf(record) } : {}), before, values, issues }
|
|
122
126
|
})
|
|
@@ -148,7 +152,8 @@ export async function proposeChanges(target: EntryTarget, args: any, context: an
|
|
|
148
152
|
} catch (error: any) {
|
|
149
153
|
return { rejected: true, error: 'read-failed', message: `could not read the records: ${error?.message ?? error}` }
|
|
150
154
|
}
|
|
151
|
-
const
|
|
155
|
+
const changed = await resolveReferences(changeableFields(target), zonedDateTimes(changeableFields(target), changeRows(target, picked, records), context?.state?.domain?.timezone), run)
|
|
156
|
+
const rows = await withChangeChecks(target, changed, records, { state: context?.state })
|
|
152
157
|
const blocked = rows.filter(blockedChange).length
|
|
153
158
|
return {
|
|
154
159
|
proposed: true,
|
|
@@ -209,3 +214,19 @@ export function stillAsProposed(target: EntryTarget, rows: ChangeRow[], now: Map
|
|
|
209
214
|
return moved ? { ...row, now: record, issues: [...row.issues, { field: moved, code: 'changed-since', blocking: true }] } : { ...row, now: record }
|
|
210
215
|
})
|
|
211
216
|
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The target's own checks of the changes, added to each row. A check that could not run keeps every row out - a
|
|
220
|
+
* change is not written unchecked (an entry's failed check refuses its card the same way).
|
|
221
|
+
*/
|
|
222
|
+
export async function withChangeChecks<T extends ChangeRow>(target: EntryTarget, rows: T[], records: Map<string, EntryRecord>, context: { state: any }): Promise<T[]> {
|
|
223
|
+
if (!target.update?.check) return rows
|
|
224
|
+
let found: EntryIssue[][] | undefined
|
|
225
|
+
try {
|
|
226
|
+
found = await target.update.check(rows.map(row => ({ id: row.id, record: records.get(row.id), values: row.values })), context)
|
|
227
|
+
} catch {
|
|
228
|
+
return rows.map(row => ({ ...row, issues: [...row.issues, { code: 'check-failed', blocking: true }] }))
|
|
229
|
+
}
|
|
230
|
+
return rows.map((row, i) => (found?.[i]?.length ? { ...row, issues: [...row.issues, ...found[i]] } : row))
|
|
231
|
+
}
|
|
232
|
+
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
entryOutcomeNote,
|
|
12
12
|
entryProposalHandler,
|
|
13
13
|
entryReadyCount,
|
|
14
|
+
entryTitle,
|
|
14
15
|
latestEntryKeys,
|
|
15
16
|
type EntryApplyAnswer
|
|
16
17
|
} from '../client/utils/entry-proposal'
|
|
@@ -21,6 +22,24 @@ const t = (key: string, options: any = {}) =>
|
|
|
21
22
|
const card = (key: string, target = 'office.expense-line', rows: any[] = []) => ({ key, kind: 'entry', target, rows })
|
|
22
23
|
|
|
23
24
|
describe('the entry card', () => {
|
|
25
|
+
it('★ is titled by its kind - a 수정안 is never titled as a 입력안', () => {
|
|
26
|
+
const named = (key: string, options: any = {}) => (key === 'label.office-entry-counterparty' ? 'counterparty' : t(key, options))
|
|
27
|
+
expect(entryTitle({ kind: 'entry', target: 'office.counterparty', labelKey: 'label.office-entry-counterparty', rows: [] }, named)).toBe('counterparty to enter')
|
|
28
|
+
expect(entryTitle({ kind: 'entry', op: 'update', target: 'office.counterparty', labelKey: 'label.office-entry-counterparty', rows: [] }, named)).toBe('counterparty to change')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('★ every target label names the record only - the card adds 입력안 or 수정안', () => {
|
|
32
|
+
/* The framework's own declarers; the applications hold the same rule in their translations. */
|
|
33
|
+
const offending: string[] = []
|
|
34
|
+
for (const root of ['../../ops-master', '../../organization', '../../i18n-base'])
|
|
35
|
+
for (const lang of ['ko', 'ja', 'zh', 'ms']) {
|
|
36
|
+
const table: Record<string, string> = JSON.parse(readFileSync(join(__dirname, root, 'translations', `${lang}.json`), 'utf8'))
|
|
37
|
+
for (const [key, value] of Object.entries(table))
|
|
38
|
+
if (/^label\.[a-z0-9-]+-entry-/.test(key) && /입력안|수정안|入力案|录入方案|cadangan/.test(value)) offending.push(`${root}/${lang}: ${key}`)
|
|
39
|
+
}
|
|
40
|
+
expect(offending).toEqual([])
|
|
41
|
+
})
|
|
42
|
+
|
|
24
43
|
it('★ counts only the rows that will go in', () => {
|
|
25
44
|
const p = card('k', undefined, [
|
|
26
45
|
{ index: 0, values: {}, sources: [], issues: [] },
|
package/translations/en.json
CHANGED
|
@@ -254,6 +254,8 @@
|
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "{count} figures",
|
|
255
255
|
"ai-assistant.workspace.contains-steps": "{count} progress",
|
|
256
256
|
"ai-assistant.entry.change": "change {count} records",
|
|
257
|
+
"ai-assistant.entry.title-create": "{name} to enter",
|
|
258
|
+
"ai-assistant.entry.title-update": "{name} to change",
|
|
257
259
|
"ai-assistant.entry.changed": "changed {count} records.",
|
|
258
260
|
"ai-assistant.entry.changed-none": "no record was changed.",
|
|
259
261
|
"ai-assistant.entry.changes": "{count} records",
|
|
@@ -275,5 +277,6 @@
|
|
|
275
277
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" - could not be looked up",
|
|
276
278
|
"ai-assistant.entry.issue.not-a-reference": "not a record it can point to",
|
|
277
279
|
"ai-assistant.entry.groups": "{count} records",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count} records kept out whole"
|
|
280
|
+
"ai-assistant.entry.groups-kept-out": "{count} records kept out whole",
|
|
281
|
+
"ai-assistant.entry.issue.check-failed": "could not be checked - not changed"
|
|
279
282
|
}
|
package/translations/ja.json
CHANGED
|
@@ -254,6 +254,8 @@
|
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "指標 {count}",
|
|
255
255
|
"ai-assistant.workspace.contains-steps": "経過 {count}",
|
|
256
256
|
"ai-assistant.entry.change": "{count}件を修正",
|
|
257
|
+
"ai-assistant.entry.title-create": "{name}の入力案",
|
|
258
|
+
"ai-assistant.entry.title-update": "{name}の修正案",
|
|
257
259
|
"ai-assistant.entry.changed": "{count}件を修正しました。",
|
|
258
260
|
"ai-assistant.entry.changed-none": "修正された記録はありません。",
|
|
259
261
|
"ai-assistant.entry.changes": "{count}件",
|
|
@@ -275,5 +277,6 @@
|
|
|
275
277
|
"ai-assistant.entry.issue.reference-unreadable": "「{text}」— 検索できませんでした",
|
|
276
278
|
"ai-assistant.entry.issue.not-a-reference": "参照先の記録が分かりません",
|
|
277
279
|
"ai-assistant.entry.groups": "{count}件",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count}件は行が止まり全体を入力しません"
|
|
280
|
+
"ai-assistant.entry.groups-kept-out": "{count}件は行が止まり全体を入力しません",
|
|
281
|
+
"ai-assistant.entry.issue.check-failed": "検査できなかったため修正しません"
|
|
279
282
|
}
|
package/translations/ko.json
CHANGED
|
@@ -254,6 +254,8 @@
|
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "수치 {count}",
|
|
255
255
|
"ai-assistant.workspace.contains-steps": "경과 {count}",
|
|
256
256
|
"ai-assistant.entry.change": "{count}건 고치기",
|
|
257
|
+
"ai-assistant.entry.title-create": "{name} 입력안",
|
|
258
|
+
"ai-assistant.entry.title-update": "{name} 수정안",
|
|
257
259
|
"ai-assistant.entry.changed": "{count}건을 고쳤습니다.",
|
|
258
260
|
"ai-assistant.entry.changed-none": "고친 기록이 없습니다.",
|
|
259
261
|
"ai-assistant.entry.changes": "{count}건",
|
|
@@ -275,5 +277,6 @@
|
|
|
275
277
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" — 찾아보지 못했습니다",
|
|
276
278
|
"ai-assistant.entry.issue.not-a-reference": "가리키는 기록을 알 수 없습니다",
|
|
277
279
|
"ai-assistant.entry.groups": "{count}건",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count}건은 줄이 막혀 통째로 입력하지 않습니다"
|
|
280
|
+
"ai-assistant.entry.groups-kept-out": "{count}건은 줄이 막혀 통째로 입력하지 않습니다",
|
|
281
|
+
"ai-assistant.entry.issue.check-failed": "검사를 하지 못해 고치지 않습니다"
|
|
279
282
|
}
|
package/translations/ms.json
CHANGED
|
@@ -254,6 +254,8 @@
|
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "{count} angka",
|
|
255
255
|
"ai-assistant.workspace.contains-steps": "{count} kemajuan",
|
|
256
256
|
"ai-assistant.entry.change": "ubah {count} rekod",
|
|
257
|
+
"ai-assistant.entry.title-create": "cadangan kemasukan {name}",
|
|
258
|
+
"ai-assistant.entry.title-update": "cadangan perubahan {name}",
|
|
257
259
|
"ai-assistant.entry.changed": "{count} rekod diubah.",
|
|
258
260
|
"ai-assistant.entry.changed-none": "tiada rekod diubah.",
|
|
259
261
|
"ai-assistant.entry.changes": "{count} rekod",
|
|
@@ -275,5 +277,6 @@
|
|
|
275
277
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" - tidak dapat dicari",
|
|
276
278
|
"ai-assistant.entry.issue.not-a-reference": "bukan rekod yang boleh dirujuk",
|
|
277
279
|
"ai-assistant.entry.groups": "{count} rekod",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count} rekod ditahan sepenuhnya"
|
|
280
|
+
"ai-assistant.entry.groups-kept-out": "{count} rekod ditahan sepenuhnya",
|
|
281
|
+
"ai-assistant.entry.issue.check-failed": "tidak dapat disemak - tidak diubah"
|
|
279
282
|
}
|
package/translations/zh.json
CHANGED
|
@@ -254,6 +254,8 @@
|
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "指标 {count}",
|
|
255
255
|
"ai-assistant.workspace.contains-steps": "进度 {count}",
|
|
256
256
|
"ai-assistant.entry.change": "修改{count}条",
|
|
257
|
+
"ai-assistant.entry.title-create": "{name}录入方案",
|
|
258
|
+
"ai-assistant.entry.title-update": "{name}修改方案",
|
|
257
259
|
"ai-assistant.entry.changed": "已修改{count}条。",
|
|
258
260
|
"ai-assistant.entry.changed-none": "未修改任何记录。",
|
|
259
261
|
"ai-assistant.entry.changes": "{count}条",
|
|
@@ -275,5 +277,6 @@
|
|
|
275
277
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" — 无法查找",
|
|
276
278
|
"ai-assistant.entry.issue.not-a-reference": "无法确定所指记录",
|
|
277
279
|
"ai-assistant.entry.groups": "{count}条",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count}条因有行被拦而整体不录入"
|
|
280
|
+
"ai-assistant.entry.groups-kept-out": "{count}条因有行被拦而整体不录入",
|
|
281
|
+
"ai-assistant.entry.issue.check-failed": "无法检查,未修改"
|
|
279
282
|
}
|