@things-factory/ai-assistant 10.1.98 → 10.1.100

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 (36) hide show
  1. package/client/components/entry-review.ts +27 -8
  2. package/client/components/workspace-view.ts +6 -1
  3. package/client/utils/entry-proposal.ts +2 -1
  4. package/dist-client/components/entry-review.d.ts +1 -1
  5. package/dist-client/components/entry-review.js +27 -7
  6. package/dist-client/components/entry-review.js.map +1 -1
  7. package/dist-client/components/workspace-view.d.ts +1 -1
  8. package/dist-client/components/workspace-view.js +7 -2
  9. package/dist-client/components/workspace-view.js.map +1 -1
  10. package/dist-client/tsconfig.tsbuildinfo +1 -1
  11. package/dist-client/utils/entry-proposal.d.ts +2 -0
  12. package/dist-client/utils/entry-proposal.js.map +1 -1
  13. package/dist-server/service/data-entry/entry-apply.js +2 -0
  14. package/dist-server/service/data-entry/entry-apply.js.map +1 -1
  15. package/dist-server/service/data-entry/entry-target.d.ts +54 -4
  16. package/dist-server/service/data-entry/entry-target.js +115 -10
  17. package/dist-server/service/data-entry/entry-target.js.map +1 -1
  18. package/dist-server/service/data-entry/entry-tools.d.ts +2 -1
  19. package/dist-server/service/data-entry/entry-tools.js +27 -10
  20. package/dist-server/service/data-entry/entry-tools.js.map +1 -1
  21. package/dist-server/service/data-entry/entry-update.d.ts +1 -0
  22. package/dist-server/service/data-entry/entry-update.js +4 -4
  23. package/dist-server/service/data-entry/entry-update.js.map +1 -1
  24. package/dist-server/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +2 -2
  26. package/server/service/data-entry/data-entry-update.test.ts +61 -1
  27. package/server/service/data-entry/data-entry.test.ts +36 -4
  28. package/server/service/data-entry/entry-apply.ts +3 -1
  29. package/server/service/data-entry/entry-target.ts +139 -14
  30. package/server/service/data-entry/entry-tools.ts +28 -10
  31. package/server/service/data-entry/entry-update.ts +5 -4
  32. package/translations/en.json +7 -1
  33. package/translations/ja.json +7 -1
  34. package/translations/ko.json +7 -1
  35. package/translations/ms.json +7 -1
  36. package/translations/zh.json +7 -1
@@ -12,6 +12,7 @@ export interface EntryRowView {
12
12
  sources: {
13
13
  attachmentId?: string;
14
14
  where?: string;
15
+ person?: boolean;
15
16
  }[];
16
17
  issues: EntryIssueView[];
17
18
  /** The server found this row's numbers and dates together on this line of the cited file. */
@@ -27,6 +28,7 @@ export interface EntryTotalView {
27
28
  declared?: number;
28
29
  declaredFound?: boolean;
29
30
  difference?: number;
31
+ declaredBy?: 'model' | 'file';
30
32
  }
31
33
  export interface EntryRowResult {
32
34
  index: number;
@@ -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 /** 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"]}
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; person?: boolean }[]\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 declaredBy?: 'model' | 'file'\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"]}
@@ -57,6 +57,8 @@ async function applyEntryProposal(key, deps, targetOf = entry_target_js_1.entryT
57
57
  }
58
58
  /* Read again from the stored values - the same rules, and today's records for the target's checks. */
59
59
  let { rows } = (0, entry_target_js_1.normalizeEntryRows)(target, (proposal.rows ?? []).map((row) => ({ values: row.values, sources: row.sources })));
60
+ /* A reference that was not settled on the card is asked again - never entered as a name. */
61
+ rows = await (0, entry_target_js_1.resolveReferences)(target.fields, rows, deps.execute);
60
62
  if (target.check) {
61
63
  try {
62
64
  rows = (0, entry_target_js_1.withCheckedIssues)(rows, await target.check(rows, { state: deps.state }));
@@ -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 { 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"]}
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, resolveReferences, 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 /* A reference that was not settled on the card is asked again - never entered as a name. */\n rows = await resolveReferences(target.fields, rows, deps.execute)\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"]}
@@ -1,4 +1,4 @@
1
- export type EntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'boolean' | 'enum';
1
+ export type EntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'boolean' | 'enum' | 'reference';
2
2
  export interface EntryField {
3
3
  name: string;
4
4
  type: EntryFieldType;
@@ -11,12 +11,37 @@ export interface EntryField {
11
11
  labelKey?: string;
12
12
  /** A number field the card sums - and compares with the total the material prints (§ totals). */
13
13
  total?: boolean;
14
+ /** An enum's values on the card: `<prefix><value in lowercase>` - `text.expense-payment-` shows PERSONAL_CARD as 개인카드. */
15
+ valueKeyPrefix?: string;
16
+ /**
17
+ * A `reference` field points at another record - a loan, an item, a location. The model knows it by a name or
18
+ * a code; the server finds it through the application's query, as the person, and keeps its id. One match is
19
+ * taken; none or several keep the row out and say so - the server never picks among several.
20
+ */
21
+ reference?: EntryReference;
22
+ }
23
+ export interface EntryReference {
24
+ query: string;
25
+ /** The query's variables for what the model wrote - "기업은행", "A-100". */
26
+ variables(text: string): Record<string, unknown>;
27
+ /** The records the answer holds that the text names - the application filters, the server does not guess. */
28
+ recordsOf(data: any, text: string): {
29
+ id: string;
30
+ label?: string;
31
+ }[];
32
+ }
33
+ /** A resolved reference, as a row keeps it. */
34
+ export interface ReferenceValue {
35
+ id: string;
36
+ label?: string;
14
37
  }
15
38
  /** Where a row came from - which file, and where in it. Shown on the card beside the row. */
16
39
  export interface EntrySource {
17
40
  attachmentId?: string;
18
- /** "row 3", "the receipt photo", "the person's message" - the model's words. */
41
+ /** "row 3", "the receipt photo" - the model's words, in the person's language. */
19
42
  where?: string;
43
+ /** The value came from what the person said in the conversation - shown in the screen's own words, never the model's. */
44
+ person?: boolean;
20
45
  }
21
46
  export interface EntryIssue {
22
47
  /** The field it is about. Absent: the row as a whole. */
@@ -114,13 +139,13 @@ export interface EntryUpdate {
114
139
  read: {
115
140
  query: string;
116
141
  variables(ids: string[]): Record<string, unknown>;
117
- recordsOf(data: any): EntryRecord[];
142
+ recordsOf(data: any, ids: string[]): EntryRecord[];
118
143
  };
119
144
  /** Finds candidates from what the person said - "last week's 12,400 taxi". */
120
145
  find?: {
121
146
  query: string;
122
147
  variables(criteria: Record<string, unknown>): Record<string, unknown>;
123
- recordsOf(data: any): EntryRecord[];
148
+ recordsOf(data: any, criteria: Record<string, unknown>): EntryRecord[];
124
149
  criteria: EntryField[];
125
150
  };
126
151
  /** The application's own mutation that changes one record. */
@@ -189,6 +214,14 @@ export declare function lineHas(line: string, forms: string[]): boolean;
189
214
  * an id that is not there was an image, or could not be opened.
190
215
  */
191
216
  export declare function verifyRowsAgainstSources(rows: EntryRow[], fields: EntryField[], texts: Map<string, string>): EntryRow[];
217
+ /**
218
+ * The total a file prints - a line named 합계 · 총계 · 총액 · total · sum, and the last amount on it. The server
219
+ * reads it itself, so the card can compare even when the model did not say it (2026-10-01: once it did, once not).
220
+ */
221
+ export declare function printedTotalOf(text: string): {
222
+ value: number;
223
+ line: number;
224
+ } | undefined;
192
225
  export interface EntryTotal {
193
226
  field: string;
194
227
  /** The sum over every row, and over the rows that will be entered. */
@@ -199,6 +232,8 @@ export interface EntryTotal {
199
232
  declaredFound?: boolean;
200
233
  /** declared - all. */
201
234
  difference?: number;
235
+ /** Where the declared total came from - the model's reading, or the server's own from a file's total line. */
236
+ declaredBy?: 'model' | 'file';
202
237
  }
203
238
  /** The sums of the fields declared `total`, and how they stand against the total the material prints. */
204
239
  export declare function entryTotals(rows: EntryRow[], fields: EntryField[], declared: {
@@ -206,3 +241,18 @@ export declare function entryTotals(rows: EntryRow[], fields: EntryField[], decl
206
241
  value?: unknown;
207
242
  attachmentId?: string;
208
243
  } | undefined, texts: Map<string, string>): EntryTotal[];
244
+ /** The doors of the queries a target's reference fields reach - the person must pass them too. */
245
+ export declare function referenceDoorsOf(fields: EntryField[]): string[];
246
+ /**
247
+ * Settles each reference a row names by text, through the application's query as the person. One match keeps its
248
+ * id and name; none or several keep the row out - several with their names, so the person can say which.
249
+ */
250
+ export declare function resolveReferences<T extends {
251
+ values: Record<string, unknown>;
252
+ issues: EntryIssue[];
253
+ }>(fields: EntryField[], rows: T[], run: (document: string, variables: Record<string, unknown>) => Promise<{
254
+ data?: any;
255
+ errors?: readonly {
256
+ message: string;
257
+ }[];
258
+ }>): Promise<T[]>;
@@ -18,7 +18,10 @@ exports.isBlocked = isBlocked;
18
18
  exports.writtenForms = writtenForms;
19
19
  exports.lineHas = lineHas;
20
20
  exports.verifyRowsAgainstSources = verifyRowsAgainstSources;
21
+ exports.printedTotalOf = printedTotalOf;
21
22
  exports.entryTotals = entryTotals;
23
+ exports.referenceDoorsOf = referenceDoorsOf;
24
+ exports.resolveReferences = resolveReferences;
22
25
  /*
23
26
  * Data entry from material - what an application declares so the assistant can enter its records
24
27
  * (operato-application/design/ai/data-entry.md).
@@ -64,6 +67,8 @@ async function recordEntryEvent(target, event, context) {
64
67
  }
65
68
  }
66
69
  exports.DEFAULT_MAX_ROWS = 200;
70
+ /* The model's way of saying a value came from the person - kept only to read older rows and loose words. */
71
+ const PERSON = /person|message|user|사용자|말/i;
67
72
  const targets = new Map();
68
73
  const KEY = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/;
69
74
  /** Validates and keeps a target - `registerEntryTarget` (entry-tools.ts) also opens its tool. A malformed declaration is refused at boot, not in a conversation. */
@@ -100,6 +105,14 @@ function declareEntryTarget(target) {
100
105
  throw new Error(`[ai-assistant] entry target '${target.key}' declares no fields.`);
101
106
  const names = new Set();
102
107
  for (const field of target.fields) {
108
+ if (field.type === 'reference') {
109
+ try {
110
+ rootFieldOf(field.reference?.query ?? '', 'query');
111
+ }
112
+ catch (error) {
113
+ throw new Error(`[ai-assistant] entry target '${target.key}': reference field '${field.name}' ${error?.message ?? 'has no query'}.`);
114
+ }
115
+ }
103
116
  if (names.has(field.name))
104
117
  throw new Error(`[ai-assistant] entry target '${target.key}' declares '${field.name}' twice.`);
105
118
  names.add(field.name);
@@ -198,6 +211,13 @@ function readFieldValue(field, value) {
198
211
  }
199
212
  case 'enum':
200
213
  return field.values.includes(String(value).trim()) ? { read: String(value).trim() } : { code: 'not-in-list' };
214
+ case 'reference':
215
+ /* Resolved already ({ id }), or a name to look up ({ text }) - resolveReferences settles the second. */
216
+ if (value && typeof value === 'object' && typeof value.id === 'string')
217
+ return { read: { id: value.id, ...(value.label ? { label: String(value.label) } : {}) } };
218
+ if (value && typeof value === 'object' && typeof value.text === 'string')
219
+ return { read: { text: value.text } };
220
+ return typeof value === 'string' || typeof value === 'number' ? { read: { text: String(value).trim() } } : { code: 'not-a-reference' };
201
221
  default:
202
222
  return { read: String(value).trim() };
203
223
  }
@@ -208,6 +228,10 @@ function sameFieldValue(field, a, b) {
208
228
  return true;
209
229
  if (empty(a) || empty(b))
210
230
  return false;
231
+ if (field.type === 'reference') {
232
+ const idOf = (v) => (v && typeof v === 'object' ? v.id ?? v.text : String(v));
233
+ return idOf(a) === idOf(b);
234
+ }
211
235
  const x = readFieldValue(field, a);
212
236
  const y = readFieldValue(field, b);
213
237
  return x.read !== undefined && y.read !== undefined ? x.read === y.read : String(a) === String(b);
@@ -248,11 +272,16 @@ function normalizeEntryRows(target, input) {
248
272
  }
249
273
  const sources = Array.isArray(raw?.sources)
250
274
  ? raw.sources
251
- .filter((one) => one && (one.attachmentId || one.where))
252
- .map((one) => ({
253
- ...(typeof one.attachmentId === 'string' ? { attachmentId: one.attachmentId } : {}),
254
- ...(typeof one.where === 'string' ? { where: one.where.slice(0, 120) } : {})
255
- }))
275
+ .filter((one) => one && (one.attachmentId || one.where || one.person === true))
276
+ .map((one) => {
277
+ /* "the person's message" written by the model is the person flag - the screen says it in its own words. */
278
+ if (one.person === true || (!one.attachmentId && typeof one.where === 'string' && PERSON.test(one.where)))
279
+ return { person: true };
280
+ return {
281
+ ...(typeof one.attachmentId === 'string' ? { attachmentId: one.attachmentId } : {}),
282
+ ...(typeof one.where === 'string' ? { where: one.where.slice(0, 120) } : {})
283
+ };
284
+ })
256
285
  : [];
257
286
  if (!sources.length)
258
287
  issues.push({ code: 'no-source', blocking: false });
@@ -277,7 +306,6 @@ function isBlocked(row) {
277
306
  * message (a correction said in the conversation). A photo cannot be read here - the row says so.
278
307
  * Names and descriptions are not looked for: the model tidies them, and a tidied name is not a wrong one.
279
308
  */
280
- const PERSON = /person|message|user|사용자|말/i;
281
309
  /** The ways a number or a date is written in a file. */
282
310
  function writtenForms(type, value) {
283
311
  if (type === 'number' || type === 'integer') {
@@ -353,7 +381,7 @@ function verifyRowsAgainstSources(rows, fields, texts) {
353
381
  if (at >= 0)
354
382
  return { ...row, evidence: { attachmentId: id, line: at + 1 } };
355
383
  }
356
- const saidByPerson = row.sources.some(source => !source.attachmentId && PERSON.test(source.where ?? ''));
384
+ const saidByPerson = row.sources.some(source => source.person);
357
385
  const issues = [...row.issues];
358
386
  for (const one of forms) {
359
387
  const anywhere = readable.some(id => texts.get(id).split(/\r?\n/).some(line => lineHas(line, one.forms)));
@@ -365,15 +393,45 @@ function verifyRowsAgainstSources(rows, fields, texts) {
365
393
  return { ...row, issues };
366
394
  });
367
395
  }
396
+ /**
397
+ * The total a file prints - a line named 합계 · 총계 · 총액 · total · sum, and the last amount on it. The server
398
+ * reads it itself, so the card can compare even when the model did not say it (2026-10-01: once it did, once not).
399
+ */
400
+ function printedTotalOf(text) {
401
+ const lines = text.split(/\r?\n/);
402
+ for (let i = lines.length - 1; i >= 0; i--) {
403
+ if (!/(합\s*계|총\s*계|총\s*액|total|sum)/i.test(lines[i]))
404
+ continue;
405
+ const amounts = [...lines[i].matchAll(/-?\d{1,3}(?:,\d{3})+(?:\.\d+)?|-?\d+(?:\.\d+)?/g)].map(match => Number(match[0].replace(/,/g, '')));
406
+ if (amounts.length)
407
+ return { value: amounts[amounts.length - 1], line: i + 1 };
408
+ }
409
+ return undefined;
410
+ }
368
411
  /** The sums of the fields declared `total`, and how they stand against the total the material prints. */
369
412
  function entryTotals(rows, fields, declared, texts) {
370
- return fields
371
- .filter(field => field.total && (field.type === 'number' || field.type === 'integer'))
413
+ const totalled = fields.filter(field => field.total && (field.type === 'number' || field.type === 'integer'));
414
+ /* With one totalled field and no total from the model, the server looks for the file's own total line. */
415
+ const printed = !declared && totalled.length === 1 ? [...texts.values()].map(printedTotalOf).find(Boolean) : undefined;
416
+ return totalled
372
417
  .map(field => {
373
418
  const sum = (list) => Math.round(list.reduce((acc, row) => acc + (typeof row.values[field.name] === 'number' ? row.values[field.name] : 0), 0) * 100) / 100;
374
419
  const total = { field: field.name, all: sum(rows), ready: sum(rows.filter(row => !isBlocked(row))) };
375
- const value = declared && (declared.field ?? field.name) === field.name ? toNumber(declared.value) : undefined;
420
+ if (printed) {
421
+ total.declared = printed.value;
422
+ total.declaredFound = true;
423
+ total.declaredBy = 'file';
424
+ total.difference = Math.round((printed.value - total.all) * 100) / 100;
425
+ return total;
426
+ }
427
+ /*
428
+ * With one totalled field the model's total is that field's, whatever it named it - on office's second card it
429
+ * wrote the file's column name (이용금액) for amount, and the total was dropped (2026-10-01).
430
+ */
431
+ const mine = declared && (totalled.length === 1 || (declared.field ?? field.name) === field.name);
432
+ const value = mine ? toNumber(declared.value) : undefined;
376
433
  if (value !== undefined) {
434
+ total.declaredBy = 'model';
377
435
  total.declared = value;
378
436
  total.difference = Math.round((value - total.all) * 100) / 100;
379
437
  const text = declared.attachmentId ? texts.get(declared.attachmentId) : undefined;
@@ -383,4 +441,51 @@ function entryTotals(rows, fields, declared, texts) {
383
441
  return total;
384
442
  });
385
443
  }
444
+ /** The doors of the queries a target's reference fields reach - the person must pass them too. */
445
+ function referenceDoorsOf(fields) {
446
+ return [...new Set(fields.filter(field => field.type === 'reference' && field.reference).map(field => rootFieldOf(field.reference.query, 'query')))];
447
+ }
448
+ /**
449
+ * Settles each reference a row names by text, through the application's query as the person. One match keeps its
450
+ * id and name; none or several keep the row out - several with their names, so the person can say which.
451
+ */
452
+ async function resolveReferences(fields, rows, run) {
453
+ const references = fields.filter(field => field.type === 'reference' && field.reference);
454
+ if (!references.length)
455
+ return rows;
456
+ const asked = new Map();
457
+ for (const row of rows) {
458
+ for (const field of references) {
459
+ const value = row.values[field.name];
460
+ if (!value || typeof value !== 'object' || typeof value.text !== 'string')
461
+ continue;
462
+ const key = `${field.name}\u0000${value.text}`;
463
+ if (!asked.has(key)) {
464
+ try {
465
+ const answer = await run(field.reference.query, field.reference.variables(value.text));
466
+ asked.set(key, answer.errors?.length ? new Error(answer.errors[0].message) : field.reference.recordsOf(answer.data, value.text) ?? []);
467
+ }
468
+ catch (error) {
469
+ asked.set(key, error instanceof Error ? error : new Error(String(error)));
470
+ }
471
+ }
472
+ const found = asked.get(key);
473
+ if (found instanceof Error) {
474
+ row.issues.push({ field: field.name, code: 'reference-unreadable', params: { text: value.text }, blocking: true });
475
+ }
476
+ else if (found.length === 1) {
477
+ row.values[field.name] = { id: found[0].id, ...(found[0].label ? { label: found[0].label } : {}) };
478
+ }
479
+ else {
480
+ row.issues.push({
481
+ field: field.name,
482
+ code: found.length ? 'reference-ambiguous' : 'reference-not-found',
483
+ params: { text: value.text, ...(found.length ? { candidates: found.slice(0, 5).map(one => one.label || one.id).join(', ') } : {}) },
484
+ blocking: true
485
+ });
486
+ }
487
+ }
488
+ }
489
+ return rows;
490
+ }
386
491
  //# sourceMappingURL=entry-target.js.map