@things-factory/ai-assistant 10.1.90 → 10.1.97
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 +31 -3
- package/client/components/assistant-dock.ts +9 -1
- package/client/components/entry-review.ts +56 -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/entry-proposal.ts +20 -5
- package/client/utils/workspace.ts +89 -0
- package/dist-client/components/assistant-chat.d.ts +2 -0
- package/dist-client/components/assistant-chat.js +32 -3
- 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 +9 -1
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/entry-review.d.ts +5 -0
- package/dist-client/components/entry-review.js +55 -0
- package/dist-client/components/entry-review.js.map +1 -1
- 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/entry-proposal.d.ts +5 -0
- package/dist-client/utils/entry-proposal.js +16 -5
- package/dist-client/utils/entry-proposal.js.map +1 -1
- 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/resolve.d.ts +2 -2
- package/dist-server/service/assistant-mode/resolve.js +5 -3
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +3 -3
- package/dist-server/service/data-entry/entry-apply.js +55 -2
- package/dist-server/service/data-entry/entry-apply.js.map +1 -1
- package/dist-server/service/data-entry/entry-resolver.js +3 -7
- package/dist-server/service/data-entry/entry-resolver.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +51 -0
- package/dist-server/service/data-entry/entry-target.js +77 -34
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.d.ts +7 -1
- package/dist-server/service/data-entry/entry-tools.js +73 -3
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +99 -0
- package/dist-server/service/data-entry/entry-update.js +196 -0
- package/dist-server/service/data-entry/entry-update.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -0
- package/dist-server/service/index.js +3 -0
- 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/resolve.test.ts +2 -1
- package/server/service/assistant-mode/resolve.ts +5 -3
- package/server/service/data-entry/data-entry-update.test.ts +192 -0
- package/server/service/data-entry/data-entry.test.ts +2 -2
- package/server/service/data-entry/entry-apply.ts +63 -6
- package/server/service/data-entry/entry-resolver.ts +4 -9
- package/server/service/data-entry/entry-target.ts +97 -31
- package/server/service/data-entry/entry-tools.ts +77 -3
- package/server/service/data-entry/entry-update.ts +209 -0
- package/server/service/index.ts +3 -0
- 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 +15 -2
- package/test/workspace.test.ts +54 -0
- package/things-factory.config.js +6 -1
- package/translations/en.json +36 -1
- package/translations/ja.json +36 -1
- package/translations/ko.json +36 -1
- package/translations/ms.json +36 -1
- package/translations/zh.json +36 -1
|
@@ -37,6 +37,8 @@ export interface EntryRowResult {
|
|
|
37
37
|
}
|
|
38
38
|
export interface EntryApplyAnswer extends ProposalOutcome {
|
|
39
39
|
rows?: EntryRowResult[];
|
|
40
|
+
/** Set by the handler from the card - which words the conversation line uses. */
|
|
41
|
+
op?: string;
|
|
40
42
|
}
|
|
41
43
|
export declare function isEntryProposal(proposal: any): boolean;
|
|
42
44
|
export declare function entryReadyCount(proposal: any): number;
|
|
@@ -53,3 +55,6 @@ export declare function entryProposalHandler(apply: (sessionId: string, proposal
|
|
|
53
55
|
* cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).
|
|
54
56
|
*/
|
|
55
57
|
export declare function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string>;
|
|
58
|
+
/** A 입력안 and a 수정안 of one record are counted apart - a newer one of the same kind replaces the older. */
|
|
59
|
+
export declare function entryGroupOf(proposal: any): string;
|
|
60
|
+
export declare function isChangeProposal(proposal: any): boolean;
|
|
@@ -15,9 +15,13 @@ export function entryCellIssues(row, field) {
|
|
|
15
15
|
/** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */
|
|
16
16
|
export function entryOutcomeNote(answer, t) {
|
|
17
17
|
const refused = (answer.rows ?? []).filter(row => !row.ok);
|
|
18
|
-
const head = answer.
|
|
19
|
-
?
|
|
20
|
-
|
|
18
|
+
const head = answer.op === 'update'
|
|
19
|
+
? answer.applied
|
|
20
|
+
? t('ai-assistant.entry.changed', { defaultValue: 'changed {count} records.', count: answer.applied })
|
|
21
|
+
: t('ai-assistant.entry.changed-none', { defaultValue: 'no record was changed.' })
|
|
22
|
+
: answer.applied
|
|
23
|
+
? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })
|
|
24
|
+
: t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' });
|
|
21
25
|
const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }));
|
|
22
26
|
const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : [];
|
|
23
27
|
return [head, ...lines, ...more].join('\n');
|
|
@@ -27,7 +31,7 @@ export function entryProposalHandler(apply, t) {
|
|
|
27
31
|
return async (proposal, context) => {
|
|
28
32
|
if (!context.sessionId || !proposal?.key)
|
|
29
33
|
return { verdict: 'unavailable', errorCode: 'no-conversation' };
|
|
30
|
-
const answer = await apply(context.sessionId, proposal.key);
|
|
34
|
+
const answer = { ...(await apply(context.sessionId, proposal.key)), op: proposal.op };
|
|
31
35
|
return {
|
|
32
36
|
verdict: answer.verdict,
|
|
33
37
|
...(answer.errorCode ? { errorCode: answer.errorCode } : {}),
|
|
@@ -46,7 +50,14 @@ export function latestEntryKeys(proposalsInOrder) {
|
|
|
46
50
|
for (const list of proposalsInOrder)
|
|
47
51
|
for (const one of list ?? [])
|
|
48
52
|
if (isEntryProposal(one) && one.key)
|
|
49
|
-
latest.set(one
|
|
53
|
+
latest.set(entryGroupOf(one), one.key);
|
|
50
54
|
return latest;
|
|
51
55
|
}
|
|
56
|
+
/** A 입력안 and a 수정안 of one record are counted apart - a newer one of the same kind replaces the older. */
|
|
57
|
+
export function entryGroupOf(proposal) {
|
|
58
|
+
return `${proposal?.target}:${proposal?.op ?? 'create'}`;
|
|
59
|
+
}
|
|
60
|
+
export function isChangeProposal(proposal) {
|
|
61
|
+
return isEntryProposal(proposal) && proposal.op === 'update';
|
|
62
|
+
}
|
|
52
63
|
//# sourceMappingURL=entry-proposal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-proposal.js","sourceRoot":"","sources":["../../client/utils/entry-proposal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entry-proposal.js","sourceRoot":"","sources":["../../client/utils/entry-proposal.ts"],"names":[],"mappings":"AAiDA,MAAM,UAAU,eAAe,CAAC,QAAa;IAC3C,OAAO,QAAQ,EAAE,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;AACpE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAa;IAC3C,OAAO,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAiB,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAA;AAChH,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,KAAqB;IACjD,OAAO,KAAK,CAAC,UAAU,IAAI,4BAA4B,KAAK,CAAC,IAAI,EAAE,CAAA;AACrE,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,eAAe,CAAC,GAAiB,EAAE,KAAc;IAC/D,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;AAC3F,CAAC;AAED,8GAA8G;AAC9G,MAAM,UAAU,gBAAgB,CAAC,MAAwB,EAAE,CAAyC;IAClG,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,KAAK,QAAQ;QACjC,CAAC,CAAC,MAAM,CAAC,OAAO;YACd,CAAC,CAAC,CAAC,CAAC,4BAA4B,EAAE,EAAE,YAAY,EAAE,0BAA0B,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;YACtG,CAAC,CAAC,CAAC,CAAC,iCAAiC,EAAE,EAAE,YAAY,EAAE,wBAAwB,EAAE,CAAC;QACpF,CAAC,CAAC,MAAM,CAAC,OAAO;YACd,CAAC,CAAC,CAAC,CAAC,4BAA4B,EAAE,EAAE,YAAY,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;YACnG,CAAC,CAAC,CAAC,CAAC,iCAAiC,EAAE,EAAE,YAAY,EAAE,qBAAqB,EAAE,CAAC,CAAA;IACnF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,gCAAgC,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC,CAAC,CAAA;IACpK,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iCAAiC,EAAE,EAAE,YAAY,EAAE,qCAAqC,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACnK,OAAO,CAAC,IAAI,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAC7C,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,oBAAoB,CAClC,KAA4E,EAC5E,CAAyC;IAEzC,OAAO,KAAK,EAAE,QAAa,EAAE,OAAwB,EAA4B,EAAE;QACjF,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,QAAQ,EAAE,GAAG;YAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAA;QACzG,MAAM,MAAM,GAAG,EAAE,GAAG,CAAC,MAAM,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAA;QACrF,OAAO;YACL,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,CAAC;YAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC;YAC1B,IAAI,EAAE,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC;SAClC,CAAA;IACH,CAAC,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,gBAAyB;IACvD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,IAAI,IAAI,gBAAgB;QAAE,KAAK,MAAM,GAAG,IAAI,IAAI,IAAI,EAAE;YAAE,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9I,OAAO,MAAM,CAAA;AACf,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,YAAY,CAAC,QAAa;IACxC,OAAO,GAAG,QAAQ,EAAE,MAAM,IAAI,QAAQ,EAAE,EAAE,IAAI,QAAQ,EAAE,CAAA;AAC1D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAa;IAC5C,OAAO,eAAe,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,EAAE,KAAK,QAAQ,CAAA;AAC9D,CAAC","sourcesContent":["/*\n * The data entry card's rules and its handler (operato-application/design/ai/data-entry.md §5).\n *\n * The card is a review, not an editor: a correction is said in the conversation and the model proposes\n * again - the newer card replaces the older one. Pressing asks the server to enter the stored rows; the\n * screen sends no rows of its own.\n */\nimport type { ProposalContext, ProposalOutcome } from './assistant-proposals.js'\n\nexport interface EntryIssueView {\n field?: string\n code: string\n messageKey?: string\n params?: Record<string, unknown>\n blocking: boolean\n}\n\nexport interface EntryRowView {\n index: number\n values: Record<string, unknown>\n sources: { attachmentId?: string; where?: string }[]\n issues: EntryIssueView[]\n /** The server found this row's numbers and dates together on this line of the cited file. */\n evidence?: { attachmentId: string; line: number }\n}\n\nexport interface EntryTotalView {\n field: string\n all: number\n ready: number\n declared?: number\n declaredFound?: boolean\n difference?: number\n}\n\nexport interface EntryRowResult {\n index: number\n ok: boolean\n id?: string\n label?: string\n error?: string\n}\n\nexport interface EntryApplyAnswer extends ProposalOutcome {\n rows?: EntryRowResult[]\n /** Set by the handler from the card - which words the conversation line uses. */\n op?: string\n}\n\nexport function isEntryProposal(proposal: any): boolean {\n return proposal?.kind === 'entry' && Array.isArray(proposal?.rows)\n}\n\nexport function entryReadyCount(proposal: any): number {\n return (proposal?.rows ?? []).filter((row: EntryRowView) => !row.issues?.some(issue => issue.blocking)).length\n}\n\n/** The i18n key of an issue - the target's own, or the framework's by code. */\nexport function entryIssueKey(issue: EntryIssueView): string {\n return issue.messageKey || `ai-assistant.entry.issue.${issue.code}`\n}\n\n/** The issues of one cell, and of the row as a whole (no field). */\nexport function entryCellIssues(row: EntryRowView, field?: string): EntryIssueView[] {\n return (row.issues ?? []).filter(issue => (field ? issue.field === field : !issue.field))\n}\n\n/** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */\nexport function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string {\n const refused = (answer.rows ?? []).filter(row => !row.ok)\n const head = answer.op === 'update'\n ? answer.applied\n ? t('ai-assistant.entry.changed', { defaultValue: 'changed {count} records.', count: answer.applied })\n : t('ai-assistant.entry.changed-none', { defaultValue: 'no record was changed.' })\n : answer.applied\n ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })\n : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' })\n const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }))\n const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : []\n return [head, ...lines, ...more].join('\\n')\n}\n\n/** The handler every entry card runs - built with the mutation so the rules are tested without a server. */\nexport function entryProposalHandler(\n apply: (sessionId: string, proposalKey: string) => Promise<EntryApplyAnswer>,\n t: (key: string, options?: any) => string\n) {\n return async (proposal: any, context: ProposalContext): Promise<ProposalOutcome> => {\n if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }\n const answer = { ...(await apply(context.sessionId, proposal.key)), op: proposal.op }\n return {\n verdict: answer.verdict,\n ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),\n applied: answer.applied ?? 0,\n missed: answer.missed ?? 0,\n note: entryOutcomeNote(answer, t)\n }\n }\n}\n\n/**\n * The newest card per record, over the conversation's proposals in order - an older one is replaced and\n * cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).\n */\nexport function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string> {\n const latest = new Map<string, string>()\n for (const list of proposalsInOrder) for (const one of list ?? []) if (isEntryProposal(one) && one.key) latest.set(entryGroupOf(one), one.key)\n return latest\n}\n\n/** A 입력안 and a 수정안 of one record are counted apart - a newer one of the same kind replaces the older. */\nexport function entryGroupOf(proposal: any): string {\n return `${proposal?.target}:${proposal?.op ?? 'create'}`\n}\n\nexport function isChangeProposal(proposal: any): boolean {\n return isEntryProposal(proposal) && proposal.op === 'update'\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface WorkspaceBlockView {
|
|
2
|
+
kind: string;
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
}
|
|
5
|
+
export interface WorkspaceView {
|
|
6
|
+
name: string;
|
|
7
|
+
title: string;
|
|
8
|
+
summary?: string;
|
|
9
|
+
blocks: WorkspaceBlockView[];
|
|
10
|
+
}
|
|
11
|
+
export interface WorkspaceVersion {
|
|
12
|
+
/** 1-based, in the order the conversation made them. */
|
|
13
|
+
version: number;
|
|
14
|
+
messageId: string;
|
|
15
|
+
createdAt?: string;
|
|
16
|
+
workspace: WorkspaceView;
|
|
17
|
+
}
|
|
18
|
+
export declare function isWorkspaceProposal(proposal: any): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Every version of the named workspace in a conversation, oldest first. A message the reader may not see
|
|
21
|
+
* comes back redacted without its proposals, so a version behind a door is simply not among them.
|
|
22
|
+
*/
|
|
23
|
+
export declare function workspaceVersions(messages: {
|
|
24
|
+
id: string;
|
|
25
|
+
createdAt?: string;
|
|
26
|
+
proposalsJson?: any[] | null;
|
|
27
|
+
}[], name: string): WorkspaceVersion[];
|
|
28
|
+
/** The version of a card within its conversation - "2 of 3" on the card and on the page. */
|
|
29
|
+
export declare function versionOfCard(proposalsInOrder: any[][], name: string, key: string): {
|
|
30
|
+
version: number;
|
|
31
|
+
of: number;
|
|
32
|
+
};
|
|
33
|
+
export declare function workspaceAddress(sessionId: string, name: string, version?: number): string;
|
|
34
|
+
/** Sorts by one column - numbers as numbers, empty last, the rest by the reader's collation. */
|
|
35
|
+
export declare function sortRows(rows: Record<string, any>[], column: string, direction: 'asc' | 'desc', locale?: string): Record<string, any>[];
|
|
36
|
+
/** The table as CSV - the heading row in the column labels, a BOM so a spreadsheet reads Korean. */
|
|
37
|
+
export declare function tableCsv(columns: {
|
|
38
|
+
name: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
}[], rows: Record<string, any>[]): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The AI 작업판 on the screen - which versions a workspace has, and the table's sort and CSV
|
|
3
|
+
* (operato-application/design/ai/workspace.md). Pure - tested without a browser.
|
|
4
|
+
*/
|
|
5
|
+
export function isWorkspaceProposal(proposal) {
|
|
6
|
+
return proposal?.kind === 'workspace' && !!proposal?.workspace?.name;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every version of the named workspace in a conversation, oldest first. A message the reader may not see
|
|
10
|
+
* comes back redacted without its proposals, so a version behind a door is simply not among them.
|
|
11
|
+
*/
|
|
12
|
+
export function workspaceVersions(messages, name) {
|
|
13
|
+
const versions = [];
|
|
14
|
+
for (const message of messages) {
|
|
15
|
+
for (const proposal of message.proposalsJson ?? []) {
|
|
16
|
+
if (isWorkspaceProposal(proposal) && proposal.workspace.name === name) {
|
|
17
|
+
versions.push({ version: versions.length + 1, messageId: message.id, createdAt: message.createdAt, workspace: proposal.workspace });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return versions;
|
|
22
|
+
}
|
|
23
|
+
/** The version of a card within its conversation - "2 of 3" on the card and on the page. */
|
|
24
|
+
export function versionOfCard(proposalsInOrder, name, key) {
|
|
25
|
+
let version = 0;
|
|
26
|
+
let of = 0;
|
|
27
|
+
for (const list of proposalsInOrder)
|
|
28
|
+
for (const one of list ?? []) {
|
|
29
|
+
if (!isWorkspaceProposal(one) || one.workspace.name !== name)
|
|
30
|
+
continue;
|
|
31
|
+
of += 1;
|
|
32
|
+
if (one.key === key)
|
|
33
|
+
version = of;
|
|
34
|
+
}
|
|
35
|
+
return { version, of };
|
|
36
|
+
}
|
|
37
|
+
export function workspaceAddress(sessionId, name, version) {
|
|
38
|
+
const query = new URLSearchParams({ session: sessionId, name });
|
|
39
|
+
if (version)
|
|
40
|
+
query.set('version', String(version));
|
|
41
|
+
return `assistant-workspace?${query.toString()}`;
|
|
42
|
+
}
|
|
43
|
+
/** Sorts by one column - numbers as numbers, empty last, the rest by the reader's collation. */
|
|
44
|
+
export function sortRows(rows, column, direction, locale) {
|
|
45
|
+
const factor = direction === 'asc' ? 1 : -1;
|
|
46
|
+
const collator = new Intl.Collator(locale, { numeric: true });
|
|
47
|
+
return [...rows].sort((a, b) => {
|
|
48
|
+
const x = a[column];
|
|
49
|
+
const y = b[column];
|
|
50
|
+
const emptyX = x === null || x === undefined || x === '';
|
|
51
|
+
const emptyY = y === null || y === undefined || y === '';
|
|
52
|
+
if (emptyX || emptyY)
|
|
53
|
+
return emptyX === emptyY ? 0 : emptyX ? 1 : -1;
|
|
54
|
+
if (typeof x === 'number' && typeof y === 'number')
|
|
55
|
+
return (x - y) * factor;
|
|
56
|
+
return collator.compare(String(x), String(y)) * factor;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const csvCell = (value) => {
|
|
60
|
+
const text = value === null || value === undefined ? '' : String(value);
|
|
61
|
+
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
62
|
+
};
|
|
63
|
+
/** The table as CSV - the heading row in the column labels, a BOM so a spreadsheet reads Korean. */
|
|
64
|
+
export function tableCsv(columns, rows) {
|
|
65
|
+
const lines = [columns.map(column => csvCell(column.label || column.name)).join(','), ...rows.map(row => columns.map(column => csvCell(row[column.name])).join(','))];
|
|
66
|
+
return '' + lines.join('\r\n');
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../client/utils/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The AI 작업판 on the screen - which versions a workspace has, and the table's sort and CSV\n * (operato-application/design/ai/workspace.md). Pure - tested without a browser.\n */\n\nexport interface WorkspaceBlockView {\n kind: string\n [key: string]: any\n}\n\nexport interface WorkspaceView {\n name: string\n title: string\n summary?: string\n blocks: WorkspaceBlockView[]\n}\n\nexport interface WorkspaceVersion {\n /** 1-based, in the order the conversation made them. */\n version: number\n messageId: string\n createdAt?: string\n workspace: WorkspaceView\n}\n\nexport function isWorkspaceProposal(proposal: any): boolean {\n return proposal?.kind === 'workspace' && !!proposal?.workspace?.name\n}\n\n/**\n * Every version of the named workspace in a conversation, oldest first. A message the reader may not see\n * comes back redacted without its proposals, so a version behind a door is simply not among them.\n */\nexport function workspaceVersions(messages: { id: string; createdAt?: string; proposalsJson?: any[] | null }[], name: string): WorkspaceVersion[] {\n const versions: WorkspaceVersion[] = []\n for (const message of messages) {\n for (const proposal of message.proposalsJson ?? []) {\n if (isWorkspaceProposal(proposal) && proposal.workspace.name === name) {\n versions.push({ version: versions.length + 1, messageId: message.id, createdAt: message.createdAt, workspace: proposal.workspace })\n }\n }\n }\n return versions\n}\n\n/** The version of a card within its conversation - \"2 of 3\" on the card and on the page. */\nexport function versionOfCard(proposalsInOrder: any[][], name: string, key: string): { version: number; of: number } {\n let version = 0\n let of = 0\n for (const list of proposalsInOrder)\n for (const one of list ?? []) {\n if (!isWorkspaceProposal(one) || one.workspace.name !== name) continue\n of += 1\n if (one.key === key) version = of\n }\n return { version, of }\n}\n\nexport function workspaceAddress(sessionId: string, name: string, version?: number): string {\n const query = new URLSearchParams({ session: sessionId, name })\n if (version) query.set('version', String(version))\n return `assistant-workspace?${query.toString()}`\n}\n\n/** Sorts by one column - numbers as numbers, empty last, the rest by the reader's collation. */\nexport function sortRows(rows: Record<string, any>[], column: string, direction: 'asc' | 'desc', locale?: string): Record<string, any>[] {\n const factor = direction === 'asc' ? 1 : -1\n const collator = new Intl.Collator(locale, { numeric: true })\n return [...rows].sort((a, b) => {\n const x = a[column]\n const y = b[column]\n const emptyX = x === null || x === undefined || x === ''\n const emptyY = y === null || y === undefined || y === ''\n if (emptyX || emptyY) return emptyX === emptyY ? 0 : emptyX ? 1 : -1\n if (typeof x === 'number' && typeof y === 'number') return (x - y) * factor\n return collator.compare(String(x), String(y)) * factor\n })\n}\n\nconst csvCell = (value: unknown) => {\n const text = value === null || value === undefined ? '' : String(value)\n return /[\",\\n\\r]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text\n}\n\n/** The table as CSV - the heading row in the column labels, a BOM so a spreadsheet reads Korean. */\nexport function tableCsv(columns: { name: string; label?: string }[], rows: Record<string, any>[]): string {\n const lines = [columns.map(column => csvCell(column.label || column.name)).join(','), ...rows.map(row => columns.map(column => csvCell(row[column.name])).join(','))]\n return '' + lines.join('\\r\\n')\n}\n"]}
|
|
@@ -15,8 +15,8 @@ export interface ModeSettings {
|
|
|
15
15
|
maxTokens?: number;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* The categories a mode opens: its own, the
|
|
19
|
-
* §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).
|
|
18
|
+
* The categories a mode opens: its own, the AI 작업판 (design/ai/workspace.md), the attachment reader when it
|
|
19
|
+
* takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).
|
|
20
20
|
*/
|
|
21
21
|
export declare function modeToolCategories(mode: {
|
|
22
22
|
toolCategories?: string[];
|
|
@@ -25,15 +25,17 @@ function unknownModeError(kind, t) {
|
|
|
25
25
|
return error;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* The categories a mode opens: its own, the
|
|
29
|
-
* §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).
|
|
28
|
+
* The categories a mode opens: its own, the AI 작업판 (design/ai/workspace.md), the attachment reader when it
|
|
29
|
+
* takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).
|
|
30
30
|
*/
|
|
31
31
|
function modeToolCategories(mode) {
|
|
32
32
|
const extra = [
|
|
33
|
+
/* Every conversation may put a large result on the AI 작업판 (design/ai/workspace.md). */
|
|
34
|
+
'assistant-workspace',
|
|
33
35
|
...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),
|
|
34
36
|
...(mode.entryTargets ?? []).map(key => `data-entry:${key}`)
|
|
35
37
|
];
|
|
36
|
-
return
|
|
38
|
+
return [...new Set([...(mode.toolCategories ?? []), ...extra])];
|
|
37
39
|
}
|
|
38
40
|
/** The conversation's settings, from the server's declaration. */
|
|
39
41
|
function resolveModeSettings(ref, t) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":";;AAiDA,
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What the door and the screen each take from a mode.\n *\n * Both start from the same declaration, so a screen cannot show `/` sentences for a conversation\n * the server would answer differently.\n */\nimport { getAssistantMode } from './registry'\nimport type { AssistantModeSlash } from './types'\n\nexport type Translate = (key: string, params?: Record<string, any>) => string\n\nexport interface AssistantModeRef {\n /** Which conversation this is — a registered `<app>.<surface>`. */\n kind: string\n /** What it is about. Part of the conversation's lifetime; the tools read it, not the settings. */\n target?: string\n}\n\n/**\n * A kind nobody registered. The sentence is the person's (ADR-0054); the code and the kind are for\n * whoever reads the log — a screen naming a conversation the server does not have is a mismatched\n * deployment, not something the person did.\n */\nfunction unknownModeError(kind: string, t: Translate): Error {\n const error: any = new Error(\n t('ai-assistant.text.unknown-mode', {\n defaultValue: 'this screen asked for a conversation this server does not have ({kind}). reload the page.',\n kind: kind || '—'\n })\n )\n error.code = 'unknown-assistant-mode'\n error.params = { kind }\n return error\n}\n\nexport interface ModeSettings {\n systemPrompt: string\n toolCategories?: string[]\n requireToolOnFirstTurn?: boolean\n model?: string\n temperature?: number\n maxTokens?: number\n}\n\n/**\n * The categories a mode opens: its own, the AI 작업판 (design/ai/workspace.md), the attachment reader when it\n * takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).\n */\nexport function modeToolCategories(mode: { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[] }): string[] | undefined {\n const extra = [\n /* Every conversation may put a large result on the AI 작업판 (design/ai/workspace.md). */\n 'assistant-workspace',\n ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),\n ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`)\n ]\n return [...new Set([...(mode.toolCategories ?? []), ...extra])]\n}\n\n/** The conversation's settings, from the server's declaration. */\nexport function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings {\n const kind = String(ref?.kind ?? '')\n const mode = kind ? getAssistantMode(kind) : undefined\n\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n systemPrompt: mode.systemPrompt,\n /* A mode that takes attachments can open the files sent in earlier turns (design/ai/shared-dock.md §9-4). */\n toolCategories: modeToolCategories(mode),\n requireToolOnFirstTurn: mode.requireToolOnFirstTurn,\n model: mode.model,\n temperature: mode.temperature,\n maxTokens: mode.maxTokens\n }\n}\n\nexport interface AssistantModeSlashView {\n name: string\n description: string\n template: string\n}\n\nexport interface AssistantModeView {\n kind: string\n slashTemplates: AssistantModeSlashView[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n /** 빈 대화의 제목과 한 줄 — 이 대화가 무엇을 하는 자리인지. */\n intro?: { title: string; subtitle: string }\n /** 빈 대화의 예시 — 이 대화에서 실제로 할 수 있는 말. */\n examples?: Array<{ label: string; items: string[] }>\n footerNotice?: string\n stagedNotice?: string\n /** What the screen may attach - the same policy the server enforces. */\n attachments?: AttachmentPolicy\n}\n\n/** What the screen shows, in the words of the request's locale. */\nexport function assistantModeView(kind: string, t: Translate): AssistantModeView {\n const mode = getAssistantMode(kind)\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n kind: mode.kind,\n slashTemplates: (mode.slashTemplates ?? []).map((slash: AssistantModeSlash) => ({\n name: slash.name,\n description: t(slash.descriptionKey),\n template: t(slash.templateKey)\n })),\n catalogEntries: mode.catalogEntries ?? [],\n proposalChoices: mode.proposalChoices ?? [],\n placeholder: mode.placeholderKey ? t(mode.placeholderKey) : undefined,\n intro: mode.introKeys ? { title: t(mode.introKeys.title), subtitle: t(mode.introKeys.subtitle) } : undefined,\n examples: mode.exampleKeys?.map(group => ({\n label: t(group.labelKey),\n items: group.itemKeys.map(key => t(key))\n })),\n footerNotice: mode.footerNoticeKey ? t(mode.footerNoticeKey) : undefined,\n stagedNotice: mode.stagedNoticeKey ? t(mode.stagedNoticeKey) : undefined,\n attachments: mode.attachments\n }\n}\n"]}
|
|
@@ -25,10 +25,10 @@ export interface EntryApplyDeps {
|
|
|
25
25
|
answered: boolean;
|
|
26
26
|
passesDoor(door: string): Promise<boolean>;
|
|
27
27
|
/**
|
|
28
|
-
* Runs the
|
|
29
|
-
*
|
|
28
|
+
* Runs one of the application's queries or mutations with the person's request state - its API, its directives
|
|
29
|
+
* and its own checks. Answers what GraphQL answers.
|
|
30
30
|
*/
|
|
31
|
-
execute(
|
|
31
|
+
execute(document: string, variables: Record<string, unknown>): Promise<{
|
|
32
32
|
data?: any;
|
|
33
33
|
errors?: readonly {
|
|
34
34
|
message: string;
|
|
@@ -16,6 +16,7 @@ exports.entryProposalAnswered = entryProposalAnswered;
|
|
|
16
16
|
* Dependencies come in as arguments so the rules are tested without a database.
|
|
17
17
|
*/
|
|
18
18
|
const proposal_outcome_js_1 = require("../chat-message/proposal-outcome.js");
|
|
19
|
+
const entry_update_js_1 = require("./entry-update.js");
|
|
19
20
|
const entry_target_js_1 = require("./entry-target.js");
|
|
20
21
|
const nothing = (verdict, errorCode) => ({ verdict, errorCode, applied: 0, missed: 0, rows: [] });
|
|
21
22
|
function findEntryProposal(messages, key) {
|
|
@@ -28,8 +29,10 @@ function findEntryProposal(messages, key) {
|
|
|
28
29
|
});
|
|
29
30
|
if (!proposal)
|
|
30
31
|
return { superseded: false };
|
|
31
|
-
/* A later card
|
|
32
|
-
const superseded = messages
|
|
32
|
+
/* A later card of the same record and the same work replaces this one - pressing both would do the corrected work twice. */
|
|
33
|
+
const superseded = messages
|
|
34
|
+
.slice(at + 1)
|
|
35
|
+
.some(message => (message.proposals ?? []).some(one => one?.kind === 'entry' && one?.target === proposal.target && (one?.op ?? 'create') === (proposal.op ?? 'create')));
|
|
33
36
|
return { proposal, superseded };
|
|
34
37
|
}
|
|
35
38
|
async function applyEntryProposal(key, deps, targetOf = entry_target_js_1.entryTargetOf) {
|
|
@@ -45,6 +48,8 @@ async function applyEntryProposal(key, deps, targetOf = entry_target_js_1.entryT
|
|
|
45
48
|
const target = targetOf(proposal.target);
|
|
46
49
|
if (!target)
|
|
47
50
|
return nothing('unavailable', 'unknown-entry-target');
|
|
51
|
+
if (proposal.op === 'update')
|
|
52
|
+
return applyChanges(key, proposal, target, deps);
|
|
48
53
|
const door = (0, entry_target_js_1.entryDoorOf)(target.mutation);
|
|
49
54
|
if (!(await deps.passesDoor(door))) {
|
|
50
55
|
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 });
|
|
@@ -99,4 +104,52 @@ function entryProposalAnswered(key, lines) {
|
|
|
99
104
|
return proposal_outcome_js_1.PROPOSAL_VERDICTS.some(verdict => rest === verdict || rest.startsWith(`${verdict} `));
|
|
100
105
|
});
|
|
101
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* A pressed 수정안: the doors again, each record read again as the person, a field that moved since the card was
|
|
109
|
+
* made keeps its row out (`changed-since`), and each record is changed by the application's own mutation.
|
|
110
|
+
*/
|
|
111
|
+
async function applyChanges(key, proposal, target, deps) {
|
|
112
|
+
const tool = (0, entry_target_js_1.changeToolName)(target.key);
|
|
113
|
+
const update = target.update;
|
|
114
|
+
if (!update)
|
|
115
|
+
return nothing('unavailable', 'target-does-not-change');
|
|
116
|
+
for (const door of [(0, entry_target_js_1.rootFieldOf)(update.mutation, 'mutation'), (0, entry_target_js_1.rootFieldOf)(update.read.query, 'query')]) {
|
|
117
|
+
if (!(await deps.passesDoor(door))) {
|
|
118
|
+
await (0, entry_target_js_1.recordEntryEvent)(target, { phase: 'apply', tool, rows: 0, ok: false, error: 'door-closed', detail: { proposalKey: key } }, { state: deps.state });
|
|
119
|
+
return nothing('refused', 'door-closed');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const stored = proposal.rows ?? [];
|
|
123
|
+
let now;
|
|
124
|
+
try {
|
|
125
|
+
now = await (0, entry_update_js_1.readRecords)(target, stored.map(row => row.id), deps.execute);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
deps.report?.(`[ai-assistant] reading '${target.key}' again failed`, error);
|
|
129
|
+
return nothing('refused', 'read-failed');
|
|
130
|
+
}
|
|
131
|
+
const results = [];
|
|
132
|
+
for (const row of (0, entry_update_js_1.stillAsProposed)(target, stored, now)) {
|
|
133
|
+
if (row.issues.some(one => one.blocking)) {
|
|
134
|
+
const issue = row.issues.find(one => one.blocking);
|
|
135
|
+
results.push({ index: row.index, ok: false, error: issue.field ? `${issue.field}: ${issue.code}` : issue.code });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const answer = await deps.execute(update.mutation, update.variables({ id: row.id, record: row.now, values: row.values }));
|
|
140
|
+
if (answer.errors?.length) {
|
|
141
|
+
results.push({ index: row.index, ok: false, error: answer.errors.map(one => one.message).join('; ') || 'refused' });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
results.push({ index: row.index, ok: true, id: row.id, ...(row.label ? { label: row.label } : {}) });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
results.push({ index: row.index, ok: false, error: error?.message || 'refused' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const applied = results.filter(one => one.ok).length;
|
|
151
|
+
const missed = results.length - applied;
|
|
152
|
+
await (0, entry_target_js_1.recordEntryEvent)(target, { phase: 'apply', tool, rows: results.length, ok: applied > 0, applied, missed, ...(applied ? {} : { error: 'no-row-changed' }), detail: { proposalKey: key, op: 'update', rows: results } }, { state: deps.state });
|
|
153
|
+
return applied ? { verdict: 'accepted', applied, missed, rows: results } : { verdict: 'refused', errorCode: 'no-row-changed', applied, missed, rows: results };
|
|
154
|
+
}
|
|
102
155
|
//# sourceMappingURL=entry-apply.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-apply.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-apply.ts"],"names":[],"mappings":";;AAsDA,8CAUC;AAED,gDAwDC;AAGD,sDAQC;AArID;;;;;;;;;;;GAWG;AACH,6EAAgG;AAChG,uDAAkL;AAuClL,MAAM,OAAO,GAAG,CAAC,OAAqC,EAAE,SAAiB,EAAqB,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;AAE1J,SAAgB,iBAAiB,CAAC,QAAiC,EAAE,GAAW;IAC9E,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;IACX,IAAI,QAAa,CAAA;IACjB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE;QAC9B,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,SAAS,IAAI,EAAE;YAAE,IAAI,GAAG,EAAE,GAAG,KAAK,GAAG;gBAAE,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAA;IAC7F,CAAC,CAAC,CAAA;IACF,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;IAC3C,8GAA8G;IAC9G,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,IAAI,GAAG,EAAE,MAAM,KAAK,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IAC1J,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;AACjC,CAAC;AAEM,KAAK,UAAU,kBAAkB,CAAC,GAAW,EAAE,IAAoB,EAAE,WAAqD,+BAAa;IAC5I,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;IACtE,IAAI,CAAC,QAAQ;QAAE,OAAO,OAAO,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAA;IAC9D,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAA;IACjF,IAAI,UAAU;QAAE,OAAO,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;IACvD,IAAI,IAAI,CAAC,QAAQ;QAAE,OAAO,OAAO,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAA;IAEhE,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IACxC,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC,aAAa,EAAE,sBAAsB,CAAC,CAAA;IAClE,MAAM,IAAI,GAAG,IAAA,6BAAW,EAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACzC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACnC,MAAM,IAAA,kCAAgB,EAAC,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAA,+BAAa,EAAC,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;QAClL,OAAO,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;IAC1C,CAAC;IAED,sGAAsG;IACtG,IAAI,EAAE,IAAI,EAAE,GAAG,IAAA,oCAAkB,EAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAa,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAA;IACvI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,IAAA,mCAAiB,EAAC,IAAI,EAAE,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QACjF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,EAAE,CAAC,kCAAkC,MAAM,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,CAAA;YAC5E,OAAO,OAAO,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;QAC3C,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAqB,EAAE,CAAA;IACpC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,IAAA,2BAAS,EAAC,GAAG,CAAC,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAA;YACnD,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;YAChH,SAAQ;QACV,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAA;YACzE,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,EAAE,CAAC,CAAA;gBACnH,SAAQ;YACV,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;YACpD,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;QAC/I,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,IAAI,SAAS,EAAE,CAAC,CAAA;QACnF,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,CAAA;IACpD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,OAAO,CAAA;IACvC,MAAM,IAAA,kCAAgB,EACpB,MAAM,EACN,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAA,+BAAa,EAAC,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EACzM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CACtB,CAAA;IACD,OAAO,OAAO;QACZ,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;QACzD,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AACzF,CAAC;AAED,0HAA0H;AAC1H,SAAgB,qBAAqB,CAAC,GAAW,EAAE,KAA6C;IAC9F,MAAM,QAAQ,GAAG,GAAG,6CAAuB,GAAG,GAAG,IAAI,CAAA;IACrD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACvB,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAChC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAA;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;QACxC,OAAO,uCAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,CAAA;IAC9F,CAAC,CAAC,CAAA;AACJ,CAAC","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"]}
|
|
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 { readRecords, stillAsProposed, type ChangeRow } from './entry-update.js'\nimport { changeToolName, entryDoorOf, entryTargetOf, entryToolName, rootFieldOf, 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 one of the application's queries or mutations with the person's request state - its API, its directives\n * and its own checks. Answers what GraphQL answers.\n */\n execute(document: 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 of the same record and the same work replaces this one - pressing both would do the corrected work twice. */\n const superseded = messages\n .slice(at + 1)\n .some(message => (message.proposals ?? []).some(one => one?.kind === 'entry' && one?.target === proposal.target && (one?.op ?? 'create') === (proposal.op ?? 'create')))\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 if (proposal.op === 'update') return applyChanges(key, proposal, target, deps)\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\n/**\n * A pressed 수정안: the doors again, each record read again as the person, a field that moved since the card was\n * made keeps its row out (`changed-since`), and each record is changed by the application's own mutation.\n */\nasync function applyChanges(key: string, proposal: any, target: EntryTarget, deps: EntryApplyDeps): Promise<EntryApplyOutcome> {\n const tool = changeToolName(target.key)\n const update = target.update\n if (!update) return nothing('unavailable', 'target-does-not-change')\n for (const door of [rootFieldOf(update.mutation, 'mutation'), rootFieldOf(update.read.query, 'query')]) {\n if (!(await deps.passesDoor(door))) {\n await recordEntryEvent(target, { phase: 'apply', tool, rows: 0, ok: false, error: 'door-closed', detail: { proposalKey: key } }, { state: deps.state })\n return nothing('refused', 'door-closed')\n }\n }\n\n const stored: ChangeRow[] = proposal.rows ?? []\n let now\n try {\n now = await readRecords(target, stored.map(row => row.id), deps.execute)\n } catch (error) {\n deps.report?.(`[ai-assistant] reading '${target.key}' again failed`, error)\n return nothing('refused', 'read-failed')\n }\n\n const results: EntryRowResult[] = []\n for (const row of stillAsProposed(target, stored, now)) {\n if (row.issues.some(one => one.blocking)) {\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(update.mutation, update.variables({ id: row.id, record: row.now!, values: row.values }))\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 results.push({ index: row.index, ok: true, id: row.id, ...(row.label ? { label: row.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, rows: results.length, ok: applied > 0, applied, missed, ...(applied ? {} : { error: 'no-row-changed' }), detail: { proposalKey: key, op: 'update', rows: results } },\n { state: deps.state }\n )\n return applied ? { verdict: 'accepted', applied, missed, rows: results } : { verdict: 'refused', errorCode: 'no-row-changed', applied, missed, rows: results }\n}\n"]}
|
|
@@ -6,7 +6,6 @@ const type_graphql_1 = require("type-graphql");
|
|
|
6
6
|
const graphql_scalars_1 = require("graphql-scalars");
|
|
7
7
|
const typeorm_1 = require("typeorm");
|
|
8
8
|
const env_1 = require("@things-factory/env");
|
|
9
|
-
const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
|
|
10
9
|
const shell_1 = require("@things-factory/shell");
|
|
11
10
|
const chat_session_js_1 = require("../chat-session/chat-session.js");
|
|
12
11
|
const chat_message_js_1 = require("../chat-message/chat-message.js");
|
|
@@ -15,6 +14,7 @@ const participant_lookup_js_1 = require("../chat-session-participant/participant
|
|
|
15
14
|
const proposal_outcome_js_1 = require("../chat-message/proposal-outcome.js");
|
|
16
15
|
const tool_access_js_1 = require("../tool-access.js");
|
|
17
16
|
const entry_apply_js_1 = require("./entry-apply.js");
|
|
17
|
+
const entry_tools_js_1 = require("./entry-tools.js");
|
|
18
18
|
let EntryProposalResolver = class EntryProposalResolver {
|
|
19
19
|
/*
|
|
20
20
|
* Not `@transaction`: each row is its own call of the application's mutation (entry-apply.ts), so one
|
|
@@ -50,13 +50,9 @@ let EntryProposalResolver = class EntryProposalResolver {
|
|
|
50
50
|
/*
|
|
51
51
|
* The application's API, as the person - the same request state the door would have seen had they typed
|
|
52
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.
|
|
53
|
+
* door opens its own transaction per row (entry-tools.ts `runAsCaller`).
|
|
54
54
|
*/
|
|
55
|
-
execute:
|
|
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
|
-
},
|
|
55
|
+
execute: (0, entry_tools_js_1.runAsCaller)(context),
|
|
60
56
|
report: (message, error) => env_1.logger.error(message, error)
|
|
61
57
|
});
|
|
62
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-resolver.js","sourceRoot":"","sources":["../../../server/service/data-entry/entry-resolver.ts"],"names":[],"mappings":";;;;AAAA,+CAAsE;AACtE,qDAA6C;AAC7C,qCAA2C;AAE3C,6CAA4C;AAC5C,
|
|
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 { getRepository } 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'\nimport { runAsCaller } from './entry-tools.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 (entry-tools.ts `runAsCaller`).\n */\n execute: runAsCaller(context),\n report: (message, error) => logger.error(message, error)\n })\n }\n}\n"]}
|
|
@@ -95,12 +95,53 @@ export interface EntryTarget {
|
|
|
95
95
|
check?(rows: EntryRow[], context: EntryContext): EntryIssue[][] | Promise<EntryIssue[][]>;
|
|
96
96
|
/** Told of every proposal and every entering - an application that audits its assistant writes it down. A throw is ignored. */
|
|
97
97
|
record?(event: EntryEvent, context: EntryContext): void | Promise<void>;
|
|
98
|
+
/** Changing records that exist (design/ai/data-entry.md §5-1). Absent: this target only enters new ones. */
|
|
99
|
+
update?: EntryUpdate;
|
|
100
|
+
}
|
|
101
|
+
/** One record as the application's read query answers it - an `id` and the fields. */
|
|
102
|
+
export type EntryRecord = {
|
|
103
|
+
id: string;
|
|
104
|
+
} & Record<string, unknown>;
|
|
105
|
+
/**
|
|
106
|
+
* How records of a target are changed - every step through the application's API, as the person.
|
|
107
|
+
*
|
|
108
|
+
* Which record is meant is the server's to settle, never the model's memory: whichever way it was picked
|
|
109
|
+
* (on screen, selected, described), the server reads it with `read` before a card is made and again before
|
|
110
|
+
* it is written.
|
|
111
|
+
*/
|
|
112
|
+
export interface EntryUpdate {
|
|
113
|
+
/** Reads the records by id, as the person - one they may not see is not found. */
|
|
114
|
+
read: {
|
|
115
|
+
query: string;
|
|
116
|
+
variables(ids: string[]): Record<string, unknown>;
|
|
117
|
+
recordsOf(data: any): EntryRecord[];
|
|
118
|
+
};
|
|
119
|
+
/** Finds candidates from what the person said - "last week's 12,400 taxi". */
|
|
120
|
+
find?: {
|
|
121
|
+
query: string;
|
|
122
|
+
variables(criteria: Record<string, unknown>): Record<string, unknown>;
|
|
123
|
+
recordsOf(data: any): EntryRecord[];
|
|
124
|
+
criteria: EntryField[];
|
|
125
|
+
};
|
|
126
|
+
/** The application's own mutation that changes one record. */
|
|
127
|
+
mutation: string;
|
|
128
|
+
variables(change: {
|
|
129
|
+
id: string;
|
|
130
|
+
record: EntryRecord;
|
|
131
|
+
values: Record<string, unknown>;
|
|
132
|
+
}): Record<string, unknown>;
|
|
133
|
+
/** How a record is named on the card - "2026-09-12 · 카카오T". */
|
|
134
|
+
labelOf?(record: EntryRecord): string;
|
|
135
|
+
/** The fields that may change. Default: every declared field. */
|
|
136
|
+
fields?: string[];
|
|
98
137
|
}
|
|
99
138
|
/**
|
|
100
139
|
* `Mutation.<root field>` of the target's mutation - the one declaration both the tool's gate and the
|
|
101
140
|
* entering read. A document that is not one mutation with one root field is refused at boot.
|
|
102
141
|
*/
|
|
103
142
|
export declare function entryDoorOf(mutation: string): string;
|
|
143
|
+
/** `Query.<field>` or `Mutation.<field>` of a document with one operation and one root field. */
|
|
144
|
+
export declare function rootFieldOf(document: string, operation: 'query' | 'mutation'): string;
|
|
104
145
|
/** Tells the target, and never lets its record keeping stop the work. */
|
|
105
146
|
export declare function recordEntryEvent(target: EntryTarget, event: EntryEvent, context: EntryContext): Promise<void>;
|
|
106
147
|
export declare const DEFAULT_MAX_ROWS = 200;
|
|
@@ -110,9 +151,19 @@ export declare function entryTargetOf(key: string | undefined): EntryTarget | un
|
|
|
110
151
|
export declare function entryTargets(): EntryTarget[];
|
|
111
152
|
/** For tests. */
|
|
112
153
|
export declare function clearEntryTargets(): void;
|
|
154
|
+
/** `office.expense-line` → `proposeOfficeExpenseLineChanges` / `findOfficeExpenseLine` - the tools of changing. */
|
|
155
|
+
export declare const changeToolName: (key: string) => string;
|
|
156
|
+
export declare const findToolName: (key: string) => string;
|
|
113
157
|
/** `office.expense-line` → `proposeOfficeExpenseLineEntries` - one tool per target, so each has its own door. */
|
|
114
158
|
export declare function entryToolName(key: string): string;
|
|
115
159
|
export declare function entryCategoryName(key: string): string;
|
|
160
|
+
/** One value in its field's type - or the code of why it is not. An empty value is the caller's to judge. */
|
|
161
|
+
export declare function readFieldValue(field: EntryField, value: unknown): {
|
|
162
|
+
read?: unknown;
|
|
163
|
+
code?: string;
|
|
164
|
+
};
|
|
165
|
+
/** Two values of one field the same - "12400.00" read from the database is 12400. */
|
|
166
|
+
export declare function sameFieldValue(field: EntryField, a: unknown, b: unknown): boolean;
|
|
116
167
|
/**
|
|
117
168
|
* The rows as the target reads them - each value in its field's type, and what was wrong said as an issue.
|
|
118
169
|
*
|