@things-factory/ai-assistant 10.1.91 → 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.
Files changed (40) hide show
  1. package/client/components/assistant-chat.ts +5 -3
  2. package/client/components/entry-review.ts +56 -0
  3. package/client/utils/entry-proposal.ts +20 -5
  4. package/dist-client/components/assistant-chat.js +5 -3
  5. package/dist-client/components/assistant-chat.js.map +1 -1
  6. package/dist-client/components/entry-review.d.ts +5 -0
  7. package/dist-client/components/entry-review.js +55 -0
  8. package/dist-client/components/entry-review.js.map +1 -1
  9. package/dist-client/tsconfig.tsbuildinfo +1 -1
  10. package/dist-client/utils/entry-proposal.d.ts +5 -0
  11. package/dist-client/utils/entry-proposal.js +16 -5
  12. package/dist-client/utils/entry-proposal.js.map +1 -1
  13. package/dist-server/service/data-entry/entry-apply.d.ts +3 -3
  14. package/dist-server/service/data-entry/entry-apply.js +55 -2
  15. package/dist-server/service/data-entry/entry-apply.js.map +1 -1
  16. package/dist-server/service/data-entry/entry-resolver.js +3 -7
  17. package/dist-server/service/data-entry/entry-resolver.js.map +1 -1
  18. package/dist-server/service/data-entry/entry-target.d.ts +51 -0
  19. package/dist-server/service/data-entry/entry-target.js +77 -34
  20. package/dist-server/service/data-entry/entry-target.js.map +1 -1
  21. package/dist-server/service/data-entry/entry-tools.d.ts +7 -1
  22. package/dist-server/service/data-entry/entry-tools.js +73 -3
  23. package/dist-server/service/data-entry/entry-tools.js.map +1 -1
  24. package/dist-server/service/data-entry/entry-update.d.ts +99 -0
  25. package/dist-server/service/data-entry/entry-update.js +196 -0
  26. package/dist-server/service/data-entry/entry-update.js.map +1 -0
  27. package/dist-server/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +2 -2
  29. package/server/service/data-entry/data-entry-update.test.ts +192 -0
  30. package/server/service/data-entry/entry-apply.ts +63 -6
  31. package/server/service/data-entry/entry-resolver.ts +4 -9
  32. package/server/service/data-entry/entry-target.ts +97 -31
  33. package/server/service/data-entry/entry-tools.ts +77 -3
  34. package/server/service/data-entry/entry-update.ts +209 -0
  35. package/test/entry-proposal.test.ts +15 -2
  36. package/translations/en.json +16 -1
  37. package/translations/ja.json +16 -1
  38. package/translations/ko.json +16 -1
  39. package/translations/ms.json +16 -1
  40. package/translations/zh.json +16 -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.applied
19
- ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })
20
- : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' });
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.target, one.key);
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":"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","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}\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.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)\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(one.target, one.key)\n return latest\n}\n"]}
1
+ {"version":3,"file":"entry-proposal.js","sourceRoot":"","sources":["../../client/utils/entry-proposal.ts"],"names":[],"mappings":"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","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"]}
@@ -25,10 +25,10 @@ export interface EntryApplyDeps {
25
25
  answered: boolean;
26
26
  passesDoor(door: string): Promise<boolean>;
27
27
  /**
28
- * Runs the target's mutation for one row with the person's request state - the application's API, its
29
- * directives and its own checks. Answers what GraphQL answers.
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(mutation: string, variables: Record<string, unknown>): Promise<{
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 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));
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":"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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"]}
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: 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
- },
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":"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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"]}
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
  *
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_MAX_ROWS = void 0;
3
+ exports.findToolName = exports.changeToolName = exports.DEFAULT_MAX_ROWS = void 0;
4
4
  exports.entryDoorOf = entryDoorOf;
5
+ exports.rootFieldOf = rootFieldOf;
5
6
  exports.recordEntryEvent = recordEntryEvent;
6
7
  exports.declareEntryTarget = declareEntryTarget;
7
8
  exports.entryTargetOf = entryTargetOf;
@@ -9,6 +10,8 @@ exports.entryTargets = entryTargets;
9
10
  exports.clearEntryTargets = clearEntryTargets;
10
11
  exports.entryToolName = entryToolName;
11
12
  exports.entryCategoryName = entryCategoryName;
13
+ exports.readFieldValue = readFieldValue;
14
+ exports.sameFieldValue = sameFieldValue;
12
15
  exports.normalizeEntryRows = normalizeEntryRows;
13
16
  exports.withCheckedIssues = withCheckedIssues;
14
17
  exports.isBlocked = isBlocked;
@@ -36,14 +39,18 @@ const graphql_1 = require("graphql");
36
39
  * entering read. A document that is not one mutation with one root field is refused at boot.
37
40
  */
38
41
  function entryDoorOf(mutation) {
39
- const doc = (0, graphql_1.parse)(mutation);
42
+ return rootFieldOf(mutation, 'mutation');
43
+ }
44
+ /** `Query.<field>` or `Mutation.<field>` of a document with one operation and one root field. */
45
+ function rootFieldOf(document, operation) {
46
+ const doc = (0, graphql_1.parse)(document);
40
47
  const operations = doc.definitions.filter((one) => one.kind === 'OperationDefinition');
41
- if (operations.length !== 1 || operations[0].operation !== 'mutation')
42
- throw new Error('is not one mutation');
48
+ if (operations.length !== 1 || operations[0].operation !== operation)
49
+ throw new Error(`is not one ${operation}`);
43
50
  const fields = operations[0].selectionSet.selections.filter((one) => one.kind === 'Field');
44
51
  if (fields.length !== 1)
45
52
  throw new Error('must have exactly one root field');
46
- return `Mutation.${fields[0].name.value}`;
53
+ return `${operation === 'query' ? 'Query' : 'Mutation'}.${fields[0].name.value}`;
47
54
  }
48
55
  /** Tells the target, and never lets its record keeping stop the work. */
49
56
  async function recordEntryEvent(target, event, context) {
@@ -71,6 +78,24 @@ function declareEntryTarget(target) {
71
78
  }
72
79
  if (typeof target.variables !== 'function')
73
80
  throw new Error(`[ai-assistant] entry target '${target.key}' has no variables.`);
81
+ if (target.update) {
82
+ for (const [what, document, operation] of [
83
+ ['update.read', target.update.read?.query, 'query'],
84
+ ['update.mutation', target.update.mutation, 'mutation'],
85
+ ...(target.update.find ? [['update.find', target.update.find.query, 'query']] : [])
86
+ ]) {
87
+ try {
88
+ rootFieldOf(document ?? '', operation);
89
+ }
90
+ catch (error) {
91
+ throw new Error(`[ai-assistant] entry target '${target.key}': ${what} ${error?.message ?? 'does not parse'}.`);
92
+ }
93
+ }
94
+ const known = new Set(target.fields.map(field => field.name));
95
+ for (const name of target.update.fields ?? [])
96
+ if (!known.has(name))
97
+ throw new Error(`[ai-assistant] entry target '${target.key}': update field '${name}' is not declared.`);
98
+ }
74
99
  if (!target.fields?.length)
75
100
  throw new Error(`[ai-assistant] entry target '${target.key}' declares no fields.`);
76
101
  const names = new Set();
@@ -93,6 +118,16 @@ function entryTargets() {
93
118
  function clearEntryTargets() {
94
119
  targets.clear();
95
120
  }
121
+ const pascalOf = (key) => key
122
+ .split(/[.-]/)
123
+ .filter(Boolean)
124
+ .map(part => part[0].toUpperCase() + part.slice(1))
125
+ .join('');
126
+ /** `office.expense-line` → `proposeOfficeExpenseLineChanges` / `findOfficeExpenseLine` - the tools of changing. */
127
+ const changeToolName = (key) => `propose${pascalOf(key)}Changes`;
128
+ exports.changeToolName = changeToolName;
129
+ const findToolName = (key) => `find${pascalOf(key)}`;
130
+ exports.findToolName = findToolName;
96
131
  /** `office.expense-line` → `proposeOfficeExpenseLineEntries` - one tool per target, so each has its own door. */
97
132
  function entryToolName(key) {
98
133
  const pascal = key
@@ -142,6 +177,41 @@ function toBoolean(value) {
142
177
  return false;
143
178
  return undefined;
144
179
  }
180
+ /** One value in its field's type - or the code of why it is not. An empty value is the caller's to judge. */
181
+ function readFieldValue(field, value) {
182
+ switch (field.type) {
183
+ case 'number': {
184
+ const read = toNumber(value);
185
+ return read === undefined ? { code: 'not-a-number' } : { read };
186
+ }
187
+ case 'integer': {
188
+ const read = toNumber(value);
189
+ return read === undefined || !Number.isInteger(read) ? { code: 'not-an-integer' } : { read };
190
+ }
191
+ case 'date': {
192
+ const read = toDate(value);
193
+ return read === undefined ? { code: 'not-a-date' } : { read };
194
+ }
195
+ case 'boolean': {
196
+ const read = toBoolean(value);
197
+ return read === undefined ? { code: 'not-a-boolean' } : { read };
198
+ }
199
+ case 'enum':
200
+ return field.values.includes(String(value).trim()) ? { read: String(value).trim() } : { code: 'not-in-list' };
201
+ default:
202
+ return { read: String(value).trim() };
203
+ }
204
+ }
205
+ /** Two values of one field the same - "12400.00" read from the database is 12400. */
206
+ function sameFieldValue(field, a, b) {
207
+ if (empty(a) && empty(b))
208
+ return true;
209
+ if (empty(a) || empty(b))
210
+ return false;
211
+ const x = readFieldValue(field, a);
212
+ const y = readFieldValue(field, b);
213
+ return x.read !== undefined && y.read !== undefined ? x.read === y.read : String(a) === String(b);
214
+ }
145
215
  /**
146
216
  * The rows as the target reads them - each value in its field's type, and what was wrong said as an issue.
147
217
  *
@@ -167,37 +237,10 @@ function normalizeEntryRows(target, input) {
167
237
  issues.push({ field: field.name, code: 'required', blocking: true });
168
238
  continue;
169
239
  }
170
- let read;
171
- let code = '';
172
- switch (field.type) {
173
- case 'number':
174
- read = toNumber(value);
175
- code = 'not-a-number';
176
- break;
177
- case 'integer':
178
- read = toNumber(value);
179
- if (read !== undefined && !Number.isInteger(read))
180
- read = undefined;
181
- code = 'not-an-integer';
182
- break;
183
- case 'date':
184
- read = toDate(value);
185
- code = 'not-a-date';
186
- break;
187
- case 'boolean':
188
- read = toBoolean(value);
189
- code = 'not-a-boolean';
190
- break;
191
- case 'enum':
192
- read = field.values.includes(String(value).trim()) ? String(value).trim() : undefined;
193
- code = 'not-in-list';
194
- break;
195
- default:
196
- read = String(value).trim();
197
- }
240
+ const { read, code } = readFieldValue(field, value);
198
241
  if (read === undefined) {
199
242
  values[field.name] = value;
200
- issues.push({ field: field.name, code, params: code === 'not-in-list' ? { values: field.values } : undefined, blocking: true });
243
+ issues.push({ field: field.name, code: code, params: code === 'not-in-list' ? { values: field.values } : undefined, blocking: true });
201
244
  }
202
245
  else {
203
246
  values[field.name] = read;