@things-factory/ai-assistant 10.1.102 → 10.1.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/service/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-tools.js +1 -0
- package/dist-server/service/data-entry/entry-tools.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 +19 -1
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/data-entry/data-entry-update.test.ts +30 -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-tools.ts +1 -0
- package/server/service/data-entry/entry-update.ts +18 -1
- package/translations/en.json +2 -1
- package/translations/ja.json +2 -1
- package/translations/ko.json +2 -1
- package/translations/ms.json +2 -1
- package/translations/zh.json +2 -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.104",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"copyfiles": "^2.4.1",
|
|
52
52
|
"rimraf": "^5.0.0"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "0d05c7573220c6da2dec2654a4b86d7bd8e3c4fb"
|
|
55
55
|
}
|
|
@@ -311,3 +311,33 @@ describe('a record with lines', () => {
|
|
|
311
311
|
})
|
|
312
312
|
})
|
|
313
313
|
|
|
314
|
+
describe('checking the changes', () => {
|
|
315
|
+
const named: EntryTarget = {
|
|
316
|
+
...expense,
|
|
317
|
+
update: {
|
|
318
|
+
...expense.update!,
|
|
319
|
+
check: rows => rows.map(row => (row.values.merchant === '스타벅스' ? [{ code: 'name-taken', blocking: true }] : []))
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
it('★ the target checks a change when the card is made - and again when it is pressed', async () => {
|
|
324
|
+
declareEntryTarget(named)
|
|
325
|
+
const proposal: any = { ...(await proposeChanges(named, { changes: [{ ref: 'l1', values: { merchant: '스타벅스' } }, { ref: 'l2', values: { amount: 9000 } }] }, { host: {} }, run)), key: 'c1' }
|
|
326
|
+
expect(proposal.rows[0].issues).toEqual([{ code: 'name-taken', blocking: true }])
|
|
327
|
+
expect(proposal.rows[1].issues).toEqual([])
|
|
328
|
+
/* A card made before the clash: the check at pressing keeps it out. */
|
|
329
|
+
const before: any = { ...proposal, rows: proposal.rows.map((row: any) => ({ ...row, issues: [] })) }
|
|
330
|
+
const outcome = await applyEntryProposal('c1', { messages: [{ id: 'm1', proposals: [before] }], answered: false, passesDoor: async () => true, execute: run, state: {} })
|
|
331
|
+
expect(outcome.rows[0]).toEqual({ index: 0, ok: false, error: 'name-taken' })
|
|
332
|
+
expect(outcome.rows[1]).toMatchObject({ ok: true })
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('a check that cannot run', () => {
|
|
337
|
+
it('★ keeps every change out - nothing is written unchecked', async () => {
|
|
338
|
+
const broken: EntryTarget = { ...expense, update: { ...expense.update!, check: () => { throw new Error('db down') } } }
|
|
339
|
+
const proposal: any = await proposeChanges(broken, { changes: [{ ref: 'l1', values: { amount: 1 } }] }, { host: {} }, run)
|
|
340
|
+
expect(proposal.rows[0].issues).toEqual([{ code: 'check-failed', blocking: true }])
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
@@ -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
|
/**
|
|
@@ -252,6 +252,7 @@ export function entryCategoryOf(target: EntryTarget, run: (context: any) => Grap
|
|
|
252
252
|
guidance: [
|
|
253
253
|
`- When the person gives material to enter as records (files, photos, pasted text), read all of it first - call readChatAttachment for a file sent in an earlier turn - then call ${name} once with every record as a row.`,
|
|
254
254
|
'- Never invent a value. Leave a field out when the material does not say it; the card marks it, and you ask the person for it.',
|
|
255
|
+
'- A row kept out because a name was not found, or matches several records, is settled in the conversation: say which name, list the candidates the card names, and ask; then propose again with the name the person chose. The card has no picker - never tell the person to choose on it.',
|
|
255
256
|
'- Give each row its sources: which attachment and where in it, written in the person\'s language. A value the person gave or corrected in the conversation adds the source { person: true }. The server looks for each row\'s numbers and dates in the cited file and marks what it does not find.',
|
|
256
257
|
'- When the material prints a total, always pass it as declaredTotal exactly as printed - do not add it up yourself. The server also reads a total line of a text file itself.',
|
|
257
258
|
`- ${name} only proposes. Nothing is entered until the person presses the button on the card - never say the records were entered.`,
|
|
@@ -148,7 +148,8 @@ export async function proposeChanges(target: EntryTarget, args: any, context: an
|
|
|
148
148
|
} catch (error: any) {
|
|
149
149
|
return { rejected: true, error: 'read-failed', message: `could not read the records: ${error?.message ?? error}` }
|
|
150
150
|
}
|
|
151
|
-
const
|
|
151
|
+
const changed = await resolveReferences(changeableFields(target), zonedDateTimes(changeableFields(target), changeRows(target, picked, records), context?.state?.domain?.timezone), run)
|
|
152
|
+
const rows = await withChangeChecks(target, changed, records, { state: context?.state })
|
|
152
153
|
const blocked = rows.filter(blockedChange).length
|
|
153
154
|
return {
|
|
154
155
|
proposed: true,
|
|
@@ -209,3 +210,19 @@ export function stillAsProposed(target: EntryTarget, rows: ChangeRow[], now: Map
|
|
|
209
210
|
return moved ? { ...row, now: record, issues: [...row.issues, { field: moved, code: 'changed-since', blocking: true }] } : { ...row, now: record }
|
|
210
211
|
})
|
|
211
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The target's own checks of the changes, added to each row. A check that could not run keeps every row out - a
|
|
216
|
+
* change is not written unchecked (an entry's failed check refuses its card the same way).
|
|
217
|
+
*/
|
|
218
|
+
export async function withChangeChecks<T extends ChangeRow>(target: EntryTarget, rows: T[], records: Map<string, EntryRecord>, context: { state: any }): Promise<T[]> {
|
|
219
|
+
if (!target.update?.check) return rows
|
|
220
|
+
let found: EntryIssue[][] | undefined
|
|
221
|
+
try {
|
|
222
|
+
found = await target.update.check(rows.map(row => ({ id: row.id, record: records.get(row.id), values: row.values })), context)
|
|
223
|
+
} catch {
|
|
224
|
+
return rows.map(row => ({ ...row, issues: [...row.issues, { code: 'check-failed', blocking: true }] }))
|
|
225
|
+
}
|
|
226
|
+
return rows.map((row, i) => (found?.[i]?.length ? { ...row, issues: [...row.issues, ...found[i]] } : row))
|
|
227
|
+
}
|
|
228
|
+
|
package/translations/en.json
CHANGED
|
@@ -275,5 +275,6 @@
|
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" - could not be looked up",
|
|
276
276
|
"ai-assistant.entry.issue.not-a-reference": "not a record it can point to",
|
|
277
277
|
"ai-assistant.entry.groups": "{count} records",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count} records kept out whole"
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count} records kept out whole",
|
|
279
|
+
"ai-assistant.entry.issue.check-failed": "could not be checked - not changed"
|
|
279
280
|
}
|
package/translations/ja.json
CHANGED
|
@@ -275,5 +275,6 @@
|
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "「{text}」— 検索できませんでした",
|
|
276
276
|
"ai-assistant.entry.issue.not-a-reference": "参照先の記録が分かりません",
|
|
277
277
|
"ai-assistant.entry.groups": "{count}件",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count}件は行が止まり全体を入力しません"
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count}件は行が止まり全体を入力しません",
|
|
279
|
+
"ai-assistant.entry.issue.check-failed": "検査できなかったため修正しません"
|
|
279
280
|
}
|
package/translations/ko.json
CHANGED
|
@@ -275,5 +275,6 @@
|
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" — 찾아보지 못했습니다",
|
|
276
276
|
"ai-assistant.entry.issue.not-a-reference": "가리키는 기록을 알 수 없습니다",
|
|
277
277
|
"ai-assistant.entry.groups": "{count}건",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count}건은 줄이 막혀 통째로 입력하지 않습니다"
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count}건은 줄이 막혀 통째로 입력하지 않습니다",
|
|
279
|
+
"ai-assistant.entry.issue.check-failed": "검사를 하지 못해 고치지 않습니다"
|
|
279
280
|
}
|
package/translations/ms.json
CHANGED
|
@@ -275,5 +275,6 @@
|
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" - tidak dapat dicari",
|
|
276
276
|
"ai-assistant.entry.issue.not-a-reference": "bukan rekod yang boleh dirujuk",
|
|
277
277
|
"ai-assistant.entry.groups": "{count} rekod",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count} rekod ditahan sepenuhnya"
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count} rekod ditahan sepenuhnya",
|
|
279
|
+
"ai-assistant.entry.issue.check-failed": "tidak dapat disemak - tidak diubah"
|
|
279
280
|
}
|
package/translations/zh.json
CHANGED
|
@@ -275,5 +275,6 @@
|
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" — 无法查找",
|
|
276
276
|
"ai-assistant.entry.issue.not-a-reference": "无法确定所指记录",
|
|
277
277
|
"ai-assistant.entry.groups": "{count}条",
|
|
278
|
-
"ai-assistant.entry.groups-kept-out": "{count}条因有行被拦而整体不录入"
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count}条因有行被拦而整体不录入",
|
|
279
|
+
"ai-assistant.entry.issue.check-failed": "无法检查,未修改"
|
|
279
280
|
}
|