@things-factory/ai-assistant 10.1.88 → 10.1.91
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 +45 -5
- package/client/components/assistant-dock.ts +30 -2
- package/client/components/entry-review.ts +246 -0
- package/client/components/workspace-view.ts +428 -0
- package/client/pages/assistant-workspace-page.ts +237 -0
- package/client/route.ts +9 -0
- package/client/utils/assistant-proposals.ts +11 -1
- package/client/utils/entry-proposal.ts +103 -0
- package/client/utils/workspace.ts +89 -0
- package/dist-client/components/assistant-chat.d.ts +3 -0
- package/dist-client/components/assistant-chat.js +46 -5
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +1 -0
- package/dist-client/components/assistant-dock.js +25 -2
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/entry-review.d.ts +9 -0
- package/dist-client/components/entry-review.js +247 -0
- package/dist-client/components/entry-review.js.map +1 -0
- package/dist-client/components/workspace-view.d.ts +19 -0
- package/dist-client/components/workspace-view.js +416 -0
- package/dist-client/components/workspace-view.js.map +1 -0
- package/dist-client/pages/assistant-workspace-page.d.ts +27 -0
- package/dist-client/pages/assistant-workspace-page.js +263 -0
- package/dist-client/pages/assistant-workspace-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +10 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-proposals.d.ts +1 -0
- package/dist-client/utils/assistant-proposals.js +9 -1
- package/dist-client/utils/assistant-proposals.js.map +1 -1
- package/dist-client/utils/entry-proposal.d.ts +55 -0
- package/dist-client/utils/entry-proposal.js +52 -0
- package/dist-client/utils/entry-proposal.js.map +1 -0
- package/dist-client/utils/workspace.d.ts +40 -0
- package/dist-client/utils/workspace.js +68 -0
- package/dist-client/utils/workspace.js.map +1 -0
- package/dist-server/service/assistant-mode/registry.js +13 -0
- package/dist-server/service/assistant-mode/registry.js.map +1 -1
- package/dist-server/service/assistant-mode/resolve.d.ts +11 -0
- package/dist-server/service/assistant-mode/resolve.js +15 -3
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +7 -1
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +46 -0
- package/dist-server/service/data-entry/entry-apply.js +102 -0
- package/dist-server/service/data-entry/entry-apply.js.map +1 -0
- package/dist-server/service/data-entry/entry-resolver.d.ts +4 -0
- package/dist-server/service/data-entry/entry-resolver.js +80 -0
- package/dist-server/service/data-entry/entry-resolver.js.map +1 -0
- package/dist-server/service/data-entry/entry-sources.d.ts +1 -0
- package/dist-server/service/data-entry/entry-sources.js +41 -0
- package/dist-server/service/data-entry/entry-sources.js.map +1 -0
- package/dist-server/service/data-entry/entry-target.d.ts +157 -0
- package/dist-server/service/data-entry/entry-target.js +343 -0
- package/dist-server/service/data-entry/entry-target.js.map +1 -0
- package/dist-server/service/data-entry/entry-tools.d.ts +43 -0
- package/dist-server/service/data-entry/entry-tools.js +185 -0
- package/dist-server/service/data-entry/entry-tools.js.map +1 -0
- package/dist-server/service/index.d.ts +6 -1
- package/dist-server/service/index.js +13 -2
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/workspace/workspace-blocks.d.ts +24 -0
- package/dist-server/service/workspace/workspace-blocks.js +118 -0
- package/dist-server/service/workspace/workspace-blocks.js.map +1 -0
- package/dist-server/service/workspace/workspace-tools.d.ts +24 -0
- package/dist-server/service/workspace/workspace-tools.js +65 -0
- package/dist-server/service/workspace/workspace-tools.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/assistant-mode/registry.ts +13 -0
- package/server/service/assistant-mode/resolve.test.ts +2 -1
- package/server/service/assistant-mode/resolve.ts +15 -3
- package/server/service/assistant-mode/types.ts +7 -1
- package/server/service/data-entry/data-entry.test.ts +311 -0
- package/server/service/data-entry/entry-apply.ts +134 -0
- package/server/service/data-entry/entry-resolver.ts +74 -0
- package/server/service/data-entry/entry-sources.ts +35 -0
- package/server/service/data-entry/entry-target.ts +442 -0
- package/server/service/data-entry/entry-tools.ts +174 -0
- package/server/service/index.ts +10 -1
- package/server/service/workspace/workspace-blocks.ts +127 -0
- package/server/service/workspace/workspace-tools.ts +66 -0
- package/server/service/workspace/workspace.test.ts +58 -0
- package/test/entry-proposal.test.ts +67 -0
- package/test/open-doors.test.ts +2 -1
- package/test/workspace.test.ts +54 -0
- package/things-factory.config.js +6 -1
- package/translations/en.json +56 -1
- package/translations/ja.json +56 -1
- package/translations/ko.json +56 -1
- package/translations/ms.json +56 -1
- package/translations/zh.json +56 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findEntryProposal = findEntryProposal;
|
|
4
|
+
exports.applyEntryProposal = applyEntryProposal;
|
|
5
|
+
exports.entryProposalAnswered = entryProposalAnswered;
|
|
6
|
+
/*
|
|
7
|
+
* Writing a proposal the person pressed (operato-application/design/ai/data-entry.md §4).
|
|
8
|
+
*
|
|
9
|
+
* The rows written are the **stored** proposal's, never rows the screen sends: what the person reviewed
|
|
10
|
+
* on the card is what goes in. The server asks everything again - the door, whether a newer card replaced
|
|
11
|
+
* this one, whether it was already answered, the target's checks (records move between proposing and
|
|
12
|
+
* pressing) - and enters each row by running the application's own mutation with the person's request
|
|
13
|
+
* state. Each call is its own request (the door's `@transaction`), so one refused row does not undo the
|
|
14
|
+
* others and each is reported by its place.
|
|
15
|
+
*
|
|
16
|
+
* Dependencies come in as arguments so the rules are tested without a database.
|
|
17
|
+
*/
|
|
18
|
+
const proposal_outcome_js_1 = require("../chat-message/proposal-outcome.js");
|
|
19
|
+
const entry_target_js_1 = require("./entry-target.js");
|
|
20
|
+
const nothing = (verdict, errorCode) => ({ verdict, errorCode, applied: 0, missed: 0, rows: [] });
|
|
21
|
+
function findEntryProposal(messages, key) {
|
|
22
|
+
let at = -1;
|
|
23
|
+
let proposal;
|
|
24
|
+
messages.forEach((message, i) => {
|
|
25
|
+
for (const one of message.proposals ?? [])
|
|
26
|
+
if (one?.key === key)
|
|
27
|
+
(at = i), (proposal = one);
|
|
28
|
+
});
|
|
29
|
+
if (!proposal)
|
|
30
|
+
return { superseded: false };
|
|
31
|
+
/* A later card for the same record replaces this one - entering both would enter the corrected rows twice. */
|
|
32
|
+
const superseded = messages.slice(at + 1).some(message => (message.proposals ?? []).some(one => one?.kind === 'entry' && one?.target === proposal.target));
|
|
33
|
+
return { proposal, superseded };
|
|
34
|
+
}
|
|
35
|
+
async function applyEntryProposal(key, deps, targetOf = entry_target_js_1.entryTargetOf) {
|
|
36
|
+
const { proposal, superseded } = findEntryProposal(deps.messages, key);
|
|
37
|
+
if (!proposal)
|
|
38
|
+
return nothing('refused', 'proposal-not-found');
|
|
39
|
+
if (proposal.kind !== 'entry')
|
|
40
|
+
return nothing('refused', 'not-an-entry-proposal');
|
|
41
|
+
if (superseded)
|
|
42
|
+
return nothing('refused', 'superseded');
|
|
43
|
+
if (deps.answered)
|
|
44
|
+
return nothing('refused', 'already-answered');
|
|
45
|
+
const target = targetOf(proposal.target);
|
|
46
|
+
if (!target)
|
|
47
|
+
return nothing('unavailable', 'unknown-entry-target');
|
|
48
|
+
const door = (0, entry_target_js_1.entryDoorOf)(target.mutation);
|
|
49
|
+
if (!(await deps.passesDoor(door))) {
|
|
50
|
+
await (0, entry_target_js_1.recordEntryEvent)(target, { phase: 'apply', tool: (0, entry_target_js_1.entryToolName)(target.key), rows: 0, ok: false, error: 'door-closed', detail: { proposalKey: key } }, { state: deps.state });
|
|
51
|
+
return nothing('refused', 'door-closed');
|
|
52
|
+
}
|
|
53
|
+
/* Read again from the stored values - the same rules, and today's records for the target's checks. */
|
|
54
|
+
let { rows } = (0, entry_target_js_1.normalizeEntryRows)(target, (proposal.rows ?? []).map((row) => ({ values: row.values, sources: row.sources })));
|
|
55
|
+
if (target.check) {
|
|
56
|
+
try {
|
|
57
|
+
rows = (0, entry_target_js_1.withCheckedIssues)(rows, await target.check(rows, { state: deps.state }));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
deps.report?.(`[ai-assistant] entry check of '${target.key}' failed`, error);
|
|
61
|
+
return nothing('refused', 'check-failed');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const results = [];
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
if ((0, entry_target_js_1.isBlocked)(row)) {
|
|
67
|
+
const issue = row.issues.find(one => one.blocking);
|
|
68
|
+
results.push({ index: row.index, ok: false, error: issue.field ? `${issue.field}: ${issue.code}` : issue.code });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const answer = await deps.execute(target.mutation, target.variables(row));
|
|
73
|
+
if (answer.errors?.length) {
|
|
74
|
+
results.push({ index: row.index, ok: false, error: answer.errors.map(one => one.message).join('; ') || 'refused' });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const applied = target.appliedOf?.(answer.data, row);
|
|
78
|
+
results.push({ index: row.index, ok: true, ...(applied?.id ? { id: applied.id } : {}), ...(applied?.label ? { label: applied.label } : {}) });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
results.push({ index: row.index, ok: false, error: error?.message || 'refused' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const applied = results.filter(one => one.ok).length;
|
|
85
|
+
const missed = results.length - applied;
|
|
86
|
+
await (0, entry_target_js_1.recordEntryEvent)(target, { phase: 'apply', tool: (0, entry_target_js_1.entryToolName)(target.key), rows: results.length, ok: applied > 0, applied, missed, ...(applied ? {} : { error: 'no-row-entered' }), detail: { proposalKey: key, rows: results } }, { state: deps.state });
|
|
87
|
+
return applied
|
|
88
|
+
? { verdict: 'accepted', applied, missed, rows: results }
|
|
89
|
+
: { verdict: 'refused', errorCode: 'no-row-entered', applied, missed, rows: results };
|
|
90
|
+
}
|
|
91
|
+
/** An outcome line already answers this key - `requested` alone does not: the dock writes it just before calling here. */
|
|
92
|
+
function entryProposalAnswered(key, lines) {
|
|
93
|
+
const answered = `${proposal_outcome_js_1.PROPOSAL_OUTCOME_PREFIX}${key}: `;
|
|
94
|
+
return lines.some(line => {
|
|
95
|
+
const text = (line ?? '').trim();
|
|
96
|
+
if (!text.startsWith(answered))
|
|
97
|
+
return false;
|
|
98
|
+
const rest = text.slice(answered.length);
|
|
99
|
+
return proposal_outcome_js_1.PROPOSAL_VERDICTS.some(verdict => rest === verdict || rest.startsWith(`${verdict} `));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=entry-apply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-apply.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-apply.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Writing a proposal the person pressed (operato-application/design/ai/data-entry.md §4).\n *\n * The rows written are the **stored** proposal's, never rows the screen sends: what the person reviewed\n * on the card is what goes in. The server asks everything again - the door, whether a newer card replaced\n * this one, whether it was already answered, the target's checks (records move between proposing and\n * pressing) - and enters each row by running the application's own mutation with the person's request\n * state. Each call is its own request (the door's `@transaction`), so one refused row does not undo the\n * others and each is reported by its place.\n *\n * Dependencies come in as arguments so the rules are tested without a database.\n */\nimport { PROPOSAL_OUTCOME_PREFIX, PROPOSAL_VERDICTS } from '../chat-message/proposal-outcome.js'\nimport { entryDoorOf, entryTargetOf, entryToolName, isBlocked, recordEntryEvent, normalizeEntryRows, withCheckedIssues, type EntryRow, type EntryTarget } from './entry-target.js'\n\nexport interface StoredProposalMessage {\n id: string\n proposals: any[]\n}\n\nexport interface EntryRowResult {\n index: number\n ok: boolean\n id?: string\n label?: string\n /** A blocking issue kept it out, or the target refused it. */\n error?: string\n}\n\nexport interface EntryApplyOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied: number\n missed: number\n rows: EntryRowResult[]\n}\n\nexport interface EntryApplyDeps {\n /** The conversation's messages that carry proposals, oldest first. */\n messages: StoredProposalMessage[]\n /** Whether an outcome line already answers this key. */\n answered: boolean\n passesDoor(door: string): Promise<boolean>\n /**\n * Runs the target's mutation for one row with the person's request state - the application's API, its\n * directives and its own checks. Answers what GraphQL answers.\n */\n execute(mutation: string, variables: Record<string, unknown>): Promise<{ data?: any; errors?: readonly { message: string }[] }>\n state: any\n report?(message: string, error: unknown): void\n}\n\nconst nothing = (verdict: EntryApplyOutcome['verdict'], errorCode: string): EntryApplyOutcome => ({ verdict, errorCode, applied: 0, missed: 0, rows: [] })\n\nexport function findEntryProposal(messages: StoredProposalMessage[], key: string): { proposal?: any; superseded: boolean } {\n let at = -1\n let proposal: any\n messages.forEach((message, i) => {\n for (const one of message.proposals ?? []) if (one?.key === key) (at = i), (proposal = one)\n })\n if (!proposal) return { superseded: false }\n /* A later card for the same record replaces this one - entering both would enter the corrected rows twice. */\n const superseded = messages.slice(at + 1).some(message => (message.proposals ?? []).some(one => one?.kind === 'entry' && one?.target === proposal.target))\n return { proposal, superseded }\n}\n\nexport async function applyEntryProposal(key: string, deps: EntryApplyDeps, targetOf: (key: string) => EntryTarget | undefined = entryTargetOf): Promise<EntryApplyOutcome> {\n const { proposal, superseded } = findEntryProposal(deps.messages, key)\n if (!proposal) return nothing('refused', 'proposal-not-found')\n if (proposal.kind !== 'entry') return nothing('refused', 'not-an-entry-proposal')\n if (superseded) return nothing('refused', 'superseded')\n if (deps.answered) return nothing('refused', 'already-answered')\n\n const target = targetOf(proposal.target)\n if (!target) return nothing('unavailable', 'unknown-entry-target')\n const door = entryDoorOf(target.mutation)\n if (!(await deps.passesDoor(door))) {\n await recordEntryEvent(target, { phase: 'apply', tool: entryToolName(target.key), rows: 0, ok: false, error: 'door-closed', detail: { proposalKey: key } }, { state: deps.state })\n return nothing('refused', 'door-closed')\n }\n\n /* Read again from the stored values - the same rules, and today's records for the target's checks. */\n let { rows } = normalizeEntryRows(target, (proposal.rows ?? []).map((row: EntryRow) => ({ values: row.values, sources: row.sources })))\n if (target.check) {\n try {\n rows = withCheckedIssues(rows, await target.check(rows, { state: deps.state }))\n } catch (error) {\n deps.report?.(`[ai-assistant] entry check of '${target.key}' failed`, error)\n return nothing('refused', 'check-failed')\n }\n }\n\n const results: EntryRowResult[] = []\n for (const row of rows) {\n if (isBlocked(row)) {\n const issue = row.issues.find(one => one.blocking)!\n results.push({ index: row.index, ok: false, error: issue.field ? `${issue.field}: ${issue.code}` : issue.code })\n continue\n }\n try {\n const answer = await deps.execute(target.mutation, target.variables(row))\n if (answer.errors?.length) {\n results.push({ index: row.index, ok: false, error: answer.errors.map(one => one.message).join('; ') || 'refused' })\n continue\n }\n const applied = target.appliedOf?.(answer.data, row)\n results.push({ index: row.index, ok: true, ...(applied?.id ? { id: applied.id } : {}), ...(applied?.label ? { label: applied.label } : {}) })\n } catch (error: any) {\n results.push({ index: row.index, ok: false, error: error?.message || 'refused' })\n }\n }\n\n const applied = results.filter(one => one.ok).length\n const missed = results.length - applied\n await recordEntryEvent(\n target,\n { phase: 'apply', tool: entryToolName(target.key), rows: results.length, ok: applied > 0, applied, missed, ...(applied ? {} : { error: 'no-row-entered' }), detail: { proposalKey: key, rows: results } },\n { state: deps.state }\n )\n return applied\n ? { verdict: 'accepted', applied, missed, rows: results }\n : { verdict: 'refused', errorCode: 'no-row-entered', applied, missed, rows: results }\n}\n\n/** An outcome line already answers this key - `requested` alone does not: the dock writes it just before calling here. */\nexport function entryProposalAnswered(key: string, lines: readonly (string | null | undefined)[]): boolean {\n const answered = `${PROPOSAL_OUTCOME_PREFIX}${key}: `\n return lines.some(line => {\n const text = (line ?? '').trim()\n if (!text.startsWith(answered)) return false\n const rest = text.slice(answered.length)\n return PROPOSAL_VERDICTS.some(verdict => rest === verdict || rest.startsWith(`${verdict} `))\n })\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EntryProposalResolver = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const graphql_scalars_1 = require("graphql-scalars");
|
|
7
|
+
const typeorm_1 = require("typeorm");
|
|
8
|
+
const env_1 = require("@things-factory/env");
|
|
9
|
+
const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
|
|
10
|
+
const shell_1 = require("@things-factory/shell");
|
|
11
|
+
const chat_session_js_1 = require("../chat-session/chat-session.js");
|
|
12
|
+
const chat_message_js_1 = require("../chat-message/chat-message.js");
|
|
13
|
+
const session_write_gate_js_1 = require("../chat-session/session-write-gate.js");
|
|
14
|
+
const participant_lookup_js_1 = require("../chat-session-participant/participant-lookup.js");
|
|
15
|
+
const proposal_outcome_js_1 = require("../chat-message/proposal-outcome.js");
|
|
16
|
+
const tool_access_js_1 = require("../tool-access.js");
|
|
17
|
+
const entry_apply_js_1 = require("./entry-apply.js");
|
|
18
|
+
let EntryProposalResolver = class EntryProposalResolver {
|
|
19
|
+
/*
|
|
20
|
+
* Not `@transaction`: each row is its own call of the application's mutation (entry-apply.ts), so one
|
|
21
|
+
* refused row keeps the others. The dock reports the outcome afterwards through `reportProposalOutcome`.
|
|
22
|
+
*/
|
|
23
|
+
async applyEntryProposal(sessionId, proposalKey, context) {
|
|
24
|
+
const { domain, user } = context.state;
|
|
25
|
+
const session = await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } });
|
|
26
|
+
if (!session)
|
|
27
|
+
throw new Error(`ChatSession ${sessionId} not found`);
|
|
28
|
+
await (0, session_write_gate_js_1.assertMayWriteSession)({
|
|
29
|
+
session,
|
|
30
|
+
userId: user?.id,
|
|
31
|
+
isParticipant: () => (0, participant_lookup_js_1.isSessionParticipant)(domain.id, session.id, user?.id),
|
|
32
|
+
t: (key, options) => (context.t ? context.t(key, options) : String(options?.defaultValue ?? key))
|
|
33
|
+
});
|
|
34
|
+
const key = (proposalKey ?? '').trim();
|
|
35
|
+
const carrying = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage).find({
|
|
36
|
+
select: ['id', 'proposals', 'createdAt'],
|
|
37
|
+
where: { session: { id: session.id }, proposals: (0, typeorm_1.Not)((0, typeorm_1.IsNull)()) },
|
|
38
|
+
order: { createdAt: 'ASC' }
|
|
39
|
+
});
|
|
40
|
+
const lines = await (0, shell_1.getRepository)(chat_message_js_1.ChatMessage).find({
|
|
41
|
+
select: ['content'],
|
|
42
|
+
where: { session: { id: session.id }, role: 'system', content: (0, typeorm_1.Like)(`${proposal_outcome_js_1.PROPOSAL_OUTCOME_PREFIX}${key}:%`) }
|
|
43
|
+
});
|
|
44
|
+
const passes = (0, tool_access_js_1.toolAccessFor)(context);
|
|
45
|
+
return await (0, entry_apply_js_1.applyEntryProposal)(key, {
|
|
46
|
+
messages: carrying.map((message) => ({ id: message.id, proposals: message.proposalsJson ?? [] })),
|
|
47
|
+
answered: (0, entry_apply_js_1.entryProposalAnswered)(key, lines.map((line) => line.content)),
|
|
48
|
+
passesDoor: door => passes(door),
|
|
49
|
+
state: context.state,
|
|
50
|
+
/*
|
|
51
|
+
* The application's API, as the person - the same request state the door would have seen had they typed
|
|
52
|
+
* the row in: domain, user, and what their address and prohibitions say. No `tx` is handed on, so the
|
|
53
|
+
* door opens its own transaction per row.
|
|
54
|
+
*/
|
|
55
|
+
execute: async (mutation, variables) => {
|
|
56
|
+
const { tx: _tx, afterCommit: _after, ...state } = context.state;
|
|
57
|
+
const result = await shell_1.GraphqlLocalClient.client.mutate({ mutation: (0, graphql_tag_1.default)(mutation), variables, context: { state, t: context.t } });
|
|
58
|
+
return { data: result.data, errors: result.errors };
|
|
59
|
+
},
|
|
60
|
+
report: (message, error) => env_1.logger.error(message, error)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
exports.EntryProposalResolver = EntryProposalResolver;
|
|
65
|
+
tslib_1.__decorate([
|
|
66
|
+
(0, type_graphql_1.Mutation)(() => graphql_scalars_1.GraphQLJSON, {
|
|
67
|
+
description: 'Enter the rows of a data entry proposal the person pressed. The rows are the stored proposal\'s; the door, a newer proposal and the target\'s checks are asked again, and each row is written in its own transaction.'
|
|
68
|
+
}),
|
|
69
|
+
(0, type_graphql_1.Directive)('@privilege(category: "ai-assistant", privilege: "mutation", domainOwnerGranted: true)'),
|
|
70
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('sessionId')),
|
|
71
|
+
tslib_1.__param(1, (0, type_graphql_1.Arg)('proposalKey')),
|
|
72
|
+
tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
|
|
73
|
+
tslib_1.__metadata("design:type", Function),
|
|
74
|
+
tslib_1.__metadata("design:paramtypes", [String, String, Object]),
|
|
75
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
76
|
+
], EntryProposalResolver.prototype, "applyEntryProposal", null);
|
|
77
|
+
exports.EntryProposalResolver = EntryProposalResolver = tslib_1.__decorate([
|
|
78
|
+
(0, type_graphql_1.Resolver)()
|
|
79
|
+
], EntryProposalResolver);
|
|
80
|
+
//# sourceMappingURL=entry-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-resolver.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\nimport { Like, Not, IsNull } from 'typeorm'\n\nimport { logger } from '@things-factory/env'\nimport gql from 'graphql-tag'\n\nimport { getRepository, GraphqlLocalClient } from '@things-factory/shell'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\nimport { ChatMessage } from '../chat-message/chat-message.js'\nimport { assertMayWriteSession } from '../chat-session/session-write-gate.js'\nimport { isSessionParticipant } from '../chat-session-participant/participant-lookup.js'\nimport { PROPOSAL_OUTCOME_PREFIX } from '../chat-message/proposal-outcome.js'\nimport { toolAccessFor } from '../tool-access.js'\nimport { applyEntryProposal, entryProposalAnswered, type EntryApplyOutcome } from './entry-apply.js'\n\n@Resolver()\nexport class EntryProposalResolver {\n /*\n * Not `@transaction`: each row is its own call of the application's mutation (entry-apply.ts), so one\n * refused row keeps the others. The dock reports the outcome afterwards through `reportProposalOutcome`.\n */\n @Mutation(() => GraphQLJSON, {\n description:\n 'Enter the rows of a data entry proposal the person pressed. The rows are the stored proposal\\'s; the door, a newer proposal and the target\\'s checks are asked again, and each row is written in its own transaction.'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\", domainOwnerGranted: true)')\n async applyEntryProposal(\n @Arg('sessionId') sessionId: string,\n @Arg('proposalKey') proposalKey: string,\n @Ctx() context: ResolverContext\n ): Promise<EntryApplyOutcome> {\n const { domain, user } = context.state\n const session = await getRepository(ChatSession).findOneBy({ id: sessionId, domain: { id: domain.id } } as any)\n if (!session) throw new Error(`ChatSession ${sessionId} not found`)\n await assertMayWriteSession({\n session,\n userId: user?.id,\n isParticipant: () => isSessionParticipant(domain.id, session.id, user?.id),\n t: (key, options) => (context.t ? context.t(key, options) : String(options?.defaultValue ?? key))\n })\n\n const key = (proposalKey ?? '').trim()\n const carrying = await getRepository(ChatMessage).find({\n select: ['id', 'proposals', 'createdAt'] as any,\n where: { session: { id: session.id } as any, proposals: Not(IsNull()) } as any,\n order: { createdAt: 'ASC' } as any\n })\n const lines = await getRepository(ChatMessage).find({\n select: ['content'] as any,\n where: { session: { id: session.id } as any, role: 'system', content: Like(`${PROPOSAL_OUTCOME_PREFIX}${key}:%`) } as any\n })\n\n const passes = toolAccessFor(context)\n return await applyEntryProposal(key, {\n messages: carrying.map((message: any) => ({ id: message.id, proposals: message.proposalsJson ?? [] })),\n answered: entryProposalAnswered(key, lines.map((line: any) => line.content)),\n passesDoor: door => passes(door),\n state: context.state,\n /*\n * The application's API, as the person - the same request state the door would have seen had they typed\n * the row in: domain, user, and what their address and prohibitions say. No `tx` is handed on, so the\n * door opens its own transaction per row.\n */\n execute: async (mutation, variables) => {\n const { tx: _tx, afterCommit: _after, ...state } = context.state as any\n const result: any = await GraphqlLocalClient.client.mutate({ mutation: gql(mutation), variables, context: { state, t: context.t } })\n return { data: result.data, errors: result.errors }\n },\n report: (message, error) => logger.error(message, error)\n })\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadSourceTexts(ids: string[], context: any): Promise<Map<string, string>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadSourceTexts = loadSourceTexts;
|
|
4
|
+
/*
|
|
5
|
+
* The text of the files a proposal cites - read through the conversation's own gate, as readChatAttachment
|
|
6
|
+
* reads them (chat-session/attachment-tools.ts). A file of another conversation, or of a private one the
|
|
7
|
+
* caller does not sit in, is not read: it is left out, and the rows citing it say they were not checked.
|
|
8
|
+
*/
|
|
9
|
+
const attachment_base_1 = require("@things-factory/attachment-base");
|
|
10
|
+
const shell_1 = require("@things-factory/shell");
|
|
11
|
+
const chat_session_js_1 = require("../chat-session/chat-session.js");
|
|
12
|
+
const session_visibility_js_1 = require("../chat-session/session-visibility.js");
|
|
13
|
+
const participant_lookup_js_1 = require("../chat-session-participant/participant-lookup.js");
|
|
14
|
+
const session_attachments_js_1 = require("../chat-session/session-attachments.js");
|
|
15
|
+
const attachment_policy_js_1 = require("../chat-session/attachment-policy.js");
|
|
16
|
+
/** The whole text is read - the check must see every line, not the part the model was shown. */
|
|
17
|
+
const MAX_CHARS = 2_000_000;
|
|
18
|
+
async function loadSourceTexts(ids, context) {
|
|
19
|
+
const { domain, user } = context?.state ?? {};
|
|
20
|
+
const texts = new Map();
|
|
21
|
+
if (!domain?.id)
|
|
22
|
+
return texts;
|
|
23
|
+
for (const id of [...new Set(ids)].slice(0, 20)) {
|
|
24
|
+
const row = await (0, shell_1.getRepository)(attachment_base_1.Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: session_attachments_js_1.CHAT_ATTACHMENT_REF } });
|
|
25
|
+
const session = row && (await (0, shell_1.getRepository)(chat_session_js_1.ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } }));
|
|
26
|
+
if (!row || !session)
|
|
27
|
+
continue;
|
|
28
|
+
if (!(0, session_visibility_js_1.mayReadSession)(session.visibility, await (0, participant_lookup_js_1.isSessionParticipant)(domain.id, session.id, user?.id)))
|
|
29
|
+
continue;
|
|
30
|
+
if ((0, attachment_policy_js_1.attachmentKindOf)({ type: row.mimetype, name: row.name }) !== 'text')
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
texts.set(id, String(await attachment_base_1.STORAGE.readFile(row.path, 'utf8')).slice(0, MAX_CHARS));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* gone from storage - the rows citing it say they were not checked */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return texts;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=entry-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-sources.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-sources.ts"],"names":[],"mappings":";;AAiBA,0CAiBC;AAlCD;;;;GAIG;AACH,qEAAqE;AACrE,iDAAqD;AAErD,qEAA6D;AAC7D,iFAAsE;AACtE,6FAAwF;AACxF,mFAA4E;AAC5E,+EAAuE;AAEvE,gGAAgG;AAChG,MAAM,SAAS,GAAG,SAAS,CAAA;AAEpB,KAAK,UAAU,eAAe,CAAC,GAAa,EAAE,OAAY;IAC/D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;IAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;IACvC,IAAI,CAAC,MAAM,EAAE,EAAE;QAAE,OAAO,KAAK,CAAA;IAC7B,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAChD,MAAM,GAAG,GAAQ,MAAM,IAAA,qBAAa,EAAC,4BAAU,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,4CAAmB,EAAS,EAAE,CAAC,CAAA;QAC3I,MAAM,OAAO,GAAQ,GAAG,IAAI,CAAC,MAAM,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAS,EAAE,CAAC,CAAC,CAAA;QACtI,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO;YAAE,SAAQ;QAC9B,IAAI,CAAC,IAAA,sCAAc,EAAC,OAAO,CAAC,UAAU,EAAE,MAAM,IAAA,4CAAoB,EAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;YAAE,SAAQ;QAC9G,IAAI,IAAA,uCAAgB,EAAC,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM;YAAE,SAAQ;QACjF,IAAI,CAAC;YACH,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,yBAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAA;QACrF,CAAC;QAAC,MAAM,CAAC;YACP,sEAAsE;QACxE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["/*\n * The text of the files a proposal cites - read through the conversation's own gate, as readChatAttachment\n * reads them (chat-session/attachment-tools.ts). A file of another conversation, or of a private one the\n * caller does not sit in, is not read: it is left out, and the rows citing it say they were not checked.\n */\nimport { Attachment, STORAGE } from '@things-factory/attachment-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from '../chat-session/chat-session.js'\nimport { mayReadSession } from '../chat-session/session-visibility.js'\nimport { isSessionParticipant } from '../chat-session-participant/participant-lookup.js'\nimport { CHAT_ATTACHMENT_REF } from '../chat-session/session-attachments.js'\nimport { attachmentKindOf } from '../chat-session/attachment-policy.js'\n\n/** The whole text is read - the check must see every line, not the part the model was shown. */\nconst MAX_CHARS = 2_000_000\n\nexport async function loadSourceTexts(ids: string[], context: any): Promise<Map<string, string>> {\n const { domain, user } = context?.state ?? {}\n const texts = new Map<string, string>()\n if (!domain?.id) return texts\n for (const id of [...new Set(ids)].slice(0, 20)) {\n const row: any = await getRepository(Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: CHAT_ATTACHMENT_REF } as any })\n const session: any = row && (await getRepository(ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } as any }))\n if (!row || !session) continue\n if (!mayReadSession(session.visibility, await isSessionParticipant(domain.id, session.id, user?.id))) continue\n if (attachmentKindOf({ type: row.mimetype, name: row.name }) !== 'text') continue\n try {\n texts.set(id, String(await STORAGE.readFile(row.path, 'utf8')).slice(0, MAX_CHARS))\n } catch {\n /* gone from storage - the rows citing it say they were not checked */\n }\n }\n return texts\n}\n"]}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export type EntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'boolean' | 'enum';
|
|
2
|
+
export interface EntryField {
|
|
3
|
+
name: string;
|
|
4
|
+
type: EntryFieldType;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
/** The allowed values of an `enum` field. */
|
|
7
|
+
values?: string[];
|
|
8
|
+
/** What the model reads about the field - English, like every tool description. */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** i18n key of the column heading on the card. Absent: the field name. */
|
|
11
|
+
labelKey?: string;
|
|
12
|
+
/** A number field the card sums - and compares with the total the material prints (§ totals). */
|
|
13
|
+
total?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Where a row came from - which file, and where in it. Shown on the card beside the row. */
|
|
16
|
+
export interface EntrySource {
|
|
17
|
+
attachmentId?: string;
|
|
18
|
+
/** "row 3", "the receipt photo", "the person's message" - the model's words. */
|
|
19
|
+
where?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface EntryIssue {
|
|
22
|
+
/** The field it is about. Absent: the row as a whole. */
|
|
23
|
+
field?: string;
|
|
24
|
+
/** A code the screen translates: `ai-assistant.entry.issue.<code>`, or the target's own `messageKey`. */
|
|
25
|
+
code: string;
|
|
26
|
+
messageKey?: string;
|
|
27
|
+
params?: Record<string, unknown>;
|
|
28
|
+
/** A blocking issue keeps the row out when the person presses the button. */
|
|
29
|
+
blocking: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface EntryRowInput {
|
|
32
|
+
values?: Record<string, unknown>;
|
|
33
|
+
sources?: EntrySource[];
|
|
34
|
+
}
|
|
35
|
+
export interface EntryRow {
|
|
36
|
+
index: number;
|
|
37
|
+
values: Record<string, unknown>;
|
|
38
|
+
sources: EntrySource[];
|
|
39
|
+
issues: EntryIssue[];
|
|
40
|
+
/** The line of a text source where this row's numbers and dates were all found - the server's check, not the model's word. */
|
|
41
|
+
evidence?: {
|
|
42
|
+
attachmentId: string;
|
|
43
|
+
line: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export interface EntryContext {
|
|
47
|
+
/** The request state - domain, user, and when writing, `tx` and `afterCommit`. */
|
|
48
|
+
state: any;
|
|
49
|
+
}
|
|
50
|
+
export interface EntryApplied {
|
|
51
|
+
/** The created record's id, when there is one. */
|
|
52
|
+
id?: string;
|
|
53
|
+
/** A short name for the conversation line - "2026-09-12 · 택시 · ₩12,400". */
|
|
54
|
+
label?: string;
|
|
55
|
+
}
|
|
56
|
+
/** What happened, for an application that writes down every assistant call (office's AI audit). */
|
|
57
|
+
export interface EntryEvent {
|
|
58
|
+
phase: 'propose' | 'apply';
|
|
59
|
+
tool: string;
|
|
60
|
+
rows: number;
|
|
61
|
+
ok: boolean;
|
|
62
|
+
applied?: number;
|
|
63
|
+
missed?: number;
|
|
64
|
+
error?: string;
|
|
65
|
+
/** propose: the rows as the model gave them; apply: the proposal key and each row's result. */
|
|
66
|
+
detail?: unknown;
|
|
67
|
+
}
|
|
68
|
+
export interface EntryTarget {
|
|
69
|
+
/** `<app>.<record>` - `office.expense-line`. */
|
|
70
|
+
key: string;
|
|
71
|
+
/** i18n key of the record's name on the card. */
|
|
72
|
+
labelKey: string;
|
|
73
|
+
/** What one row is, for the model. */
|
|
74
|
+
description: string;
|
|
75
|
+
fields: EntryField[];
|
|
76
|
+
/**
|
|
77
|
+
* The application's own mutation that enters one record, as a GraphQL document with one root field -
|
|
78
|
+
* `mutation ($line: NewExpenseLine!) { addExpenseLine(line: $line) { id } }`. Its root field is the door:
|
|
79
|
+
* the propose tool is offered only to someone who passes it, and each row is entered by running it with
|
|
80
|
+
* the person's request state, so `@privilege`, `@transaction` and the door's own checks apply as they do
|
|
81
|
+
* to a person typing it in.
|
|
82
|
+
*/
|
|
83
|
+
mutation: string;
|
|
84
|
+
/** The mutation's variables for one row. */
|
|
85
|
+
variables(row: EntryRow): Record<string, unknown>;
|
|
86
|
+
/** What the conversation says of an entered row - its id and a short name. */
|
|
87
|
+
appliedOf?(data: any, row: EntryRow): EntryApplied | undefined;
|
|
88
|
+
/** At most this many rows in one proposal. Default 200. */
|
|
89
|
+
maxRows?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Read-only checks the application owns - a duplicate of what is already entered, a code that does not
|
|
92
|
+
* exist. Asked when the rows are proposed and again when they are entered: the records may have moved.
|
|
93
|
+
* Returns one list per row, in order.
|
|
94
|
+
*/
|
|
95
|
+
check?(rows: EntryRow[], context: EntryContext): EntryIssue[][] | Promise<EntryIssue[][]>;
|
|
96
|
+
/** Told of every proposal and every entering - an application that audits its assistant writes it down. A throw is ignored. */
|
|
97
|
+
record?(event: EntryEvent, context: EntryContext): void | Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `Mutation.<root field>` of the target's mutation - the one declaration both the tool's gate and the
|
|
101
|
+
* entering read. A document that is not one mutation with one root field is refused at boot.
|
|
102
|
+
*/
|
|
103
|
+
export declare function entryDoorOf(mutation: string): string;
|
|
104
|
+
/** Tells the target, and never lets its record keeping stop the work. */
|
|
105
|
+
export declare function recordEntryEvent(target: EntryTarget, event: EntryEvent, context: EntryContext): Promise<void>;
|
|
106
|
+
export declare const DEFAULT_MAX_ROWS = 200;
|
|
107
|
+
/** Validates and keeps a target - `registerEntryTarget` (entry-tools.ts) also opens its tool. A malformed declaration is refused at boot, not in a conversation. */
|
|
108
|
+
export declare function declareEntryTarget(target: EntryTarget): void;
|
|
109
|
+
export declare function entryTargetOf(key: string | undefined): EntryTarget | undefined;
|
|
110
|
+
export declare function entryTargets(): EntryTarget[];
|
|
111
|
+
/** For tests. */
|
|
112
|
+
export declare function clearEntryTargets(): void;
|
|
113
|
+
/** `office.expense-line` → `proposeOfficeExpenseLineEntries` - one tool per target, so each has its own door. */
|
|
114
|
+
export declare function entryToolName(key: string): string;
|
|
115
|
+
export declare function entryCategoryName(key: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* The rows as the target reads them - each value in its field's type, and what was wrong said as an issue.
|
|
118
|
+
*
|
|
119
|
+
* A value that does not fit is **kept as it was written and marked**, never dropped or guessed: the card
|
|
120
|
+
* shows what the material said, and the person corrects it in the conversation.
|
|
121
|
+
*/
|
|
122
|
+
export declare function normalizeEntryRows(target: EntryTarget, input: unknown): {
|
|
123
|
+
rows: EntryRow[];
|
|
124
|
+
cut: number;
|
|
125
|
+
};
|
|
126
|
+
/** Adds the target's own findings to each row. A check that throws marks nothing and is reported by the caller. */
|
|
127
|
+
export declare function withCheckedIssues(rows: EntryRow[], found: EntryIssue[][] | undefined): EntryRow[];
|
|
128
|
+
export declare function isBlocked(row: EntryRow): boolean;
|
|
129
|
+
/** The ways a number or a date is written in a file. */
|
|
130
|
+
export declare function writtenForms(type: EntryFieldType, value: unknown): string[];
|
|
131
|
+
/**
|
|
132
|
+
* A form found as a whole number - "12,400" is not found in "112,400", "12,4001", "12,400.50" or "12,400,000";
|
|
133
|
+
* "2026-09-12" not in "2026-09-120". A CSV's own commas are not part of the number.
|
|
134
|
+
*/
|
|
135
|
+
export declare function lineHas(line: string, forms: string[]): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Checks each row against the text of the files it cites. `texts` holds what could be read, by attachment id;
|
|
138
|
+
* an id that is not there was an image, or could not be opened.
|
|
139
|
+
*/
|
|
140
|
+
export declare function verifyRowsAgainstSources(rows: EntryRow[], fields: EntryField[], texts: Map<string, string>): EntryRow[];
|
|
141
|
+
export interface EntryTotal {
|
|
142
|
+
field: string;
|
|
143
|
+
/** The sum over every row, and over the rows that will be entered. */
|
|
144
|
+
all: number;
|
|
145
|
+
ready: number;
|
|
146
|
+
/** The total the material prints, as the model read it - and whether the server found it in the file. */
|
|
147
|
+
declared?: number;
|
|
148
|
+
declaredFound?: boolean;
|
|
149
|
+
/** declared - all. */
|
|
150
|
+
difference?: number;
|
|
151
|
+
}
|
|
152
|
+
/** The sums of the fields declared `total`, and how they stand against the total the material prints. */
|
|
153
|
+
export declare function entryTotals(rows: EntryRow[], fields: EntryField[], declared: {
|
|
154
|
+
field?: string;
|
|
155
|
+
value?: unknown;
|
|
156
|
+
attachmentId?: string;
|
|
157
|
+
} | undefined, texts: Map<string, string>): EntryTotal[];
|