@things-factory/ai-assistant 10.1.178 → 10.1.181

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 (27) hide show
  1. package/client/components/assistant-chat.ts +19 -12
  2. package/client/utils/attachment-policy.ts +41 -32
  3. package/dist-client/components/assistant-chat.d.ts +1 -0
  4. package/dist-client/components/assistant-chat.js +14 -11
  5. package/dist-client/components/assistant-chat.js.map +1 -1
  6. package/dist-client/tsconfig.tsbuildinfo +1 -1
  7. package/dist-client/utils/attachment-policy.d.ts +10 -6
  8. package/dist-client/utils/attachment-policy.js +40 -39
  9. package/dist-client/utils/attachment-policy.js.map +1 -1
  10. package/dist-server/service/chat-session/attachment-policy.js +8 -2
  11. package/dist-server/service/chat-session/attachment-policy.js.map +1 -1
  12. package/dist-server/service/data-read/list-read.d.ts +2 -2
  13. package/dist-server/service/data-read/list-read.js +5 -7
  14. package/dist-server/service/data-read/list-read.js.map +1 -1
  15. package/dist-server/service/data-read/read-target.d.ts +2 -0
  16. package/dist-server/service/data-read/read-target.js +11 -3
  17. package/dist-server/service/data-read/read-target.js.map +1 -1
  18. package/dist-server/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +14 -13
  20. package/server/service/chat-session/attachment-policy.test.ts +1 -1
  21. package/server/service/chat-session/attachment-policy.ts +7 -2
  22. package/server/service/data-read/data-read.test.ts +41 -14
  23. package/server/service/data-read/list-read.ts +7 -9
  24. package/server/service/data-read/read-target.ts +15 -3
  25. package/test/assistant-dock.test.ts +1 -1
  26. package/test/attachment-one-rule.test.ts +46 -0
  27. package/test/attachment-policy-agree.test.ts +4 -1
@@ -1,3 +1,4 @@
1
+ import { type FileUploadLimits } from '@operato/utils/file-acceptance.js';
1
2
  export type AttachmentKind = 'image' | 'text' | 'spreadsheet';
2
3
  export interface AttachmentPolicy {
3
4
  kinds: AttachmentKind[];
@@ -10,10 +11,6 @@ export declare const DEFAULT_MAX_BYTES: number;
10
11
  export declare const DEFAULT_MAX_FILES = 5;
11
12
  /** How much of a text file reaches the model - the server's TEXT_CHARS_FOR_MODEL. */
12
13
  export declare const TEXT_CHARS_FOR_MODEL = 20000;
13
- export declare function attachmentKindOf(file: {
14
- type?: string;
15
- name?: string;
16
- }): AttachmentKind | null;
17
14
  export type AttachmentRefusal = {
18
15
  code: 'attachments-not-accepted';
19
16
  } | {
@@ -30,12 +27,19 @@ export type AttachmentRefusal = {
30
27
  };
31
28
  /** The characters of a text file the model reads under this policy. */
32
29
  export declare function textCharsOf(policy: AttachmentPolicy | null | undefined): number;
30
+ /** The mode's size and count as picker limits. */
31
+ export declare function limitsOf(policy: AttachmentPolicy | null | undefined): FileUploadLimits;
32
+ /**
33
+ * Whether these files may go, by the one picking rule. `server` is the server's upload limit (shell
34
+ * `fileUploadLimits`); the tighter of it and the mode's policy applies. The first reason, by name: file by file, its
35
+ * kind, then its size, then whether it is one too many - the door says them in the same order.
36
+ */
33
37
  export declare function refuseAttachments(policy: AttachmentPolicy | null | undefined, files: {
34
38
  name?: string;
35
39
  type?: string;
36
40
  size?: number;
37
- }[]): AttachmentRefusal | null;
38
- /** The file picker's `accept` for a policy. */
41
+ }[], server?: FileUploadLimits): AttachmentRefusal | null;
42
+ /** The file picker's `accept` for a policy - a spreadsheet as the person picks it (.xlsx). */
39
43
  export declare function acceptOf(policy: AttachmentPolicy | null | undefined): string;
40
44
  /** Where a conversation's file opens - the gated route, under the page's domain path. */
41
45
  export declare function chatAttachmentUrl(sessionId: string, attachmentId: string): string;
@@ -1,64 +1,65 @@
1
1
  /*
2
- * What a conversation accepts as attachments - the screen's copy of the server rule
2
+ * What a conversation accepts as attachments - the screen's side of the server rule
3
3
  * (server/service/chat-session/attachment-policy.ts, design/ai/shared-dock.md §9-1).
4
4
  *
5
5
  * The screen checks first so the person hears "no" before anything is sent; the door checks again and is
6
- * the authority. `test/attachment-policy-agree.test.ts` runs both copies over one table so they cannot drift.
6
+ * the authority. `test/attachment-policy-agree.test.ts` runs both over one table so they cannot drift.
7
+ *
8
+ * The screen's check is the one file-picking rule (@operato/utils `sortPickedFiles`, ADR-0061 2026-10-17): the mode's
9
+ * kinds become an `accept` (the table below, the one place), its size and count become limits, narrowed by the
10
+ * server's upload limits. What stays here is the mode's words: kinds, and a spreadsheet read into text.
7
11
  */
12
+ import { narrowLimits, sortPickedFiles } from '@operato/utils/file-acceptance.js';
8
13
  export const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
9
14
  export const DEFAULT_MAX_FILES = 5;
10
15
  /** How much of a text file reaches the model - the server's TEXT_CHARS_FOR_MODEL. */
11
16
  export const TEXT_CHARS_FOR_MODEL = 20_000;
12
17
  const TEXT_TYPES = ['text/plain', 'text/csv', 'text/markdown', 'application/json'];
13
18
  const TEXT_EXTENSIONS = ['.txt', '.csv', '.md', '.json'];
14
- export function attachmentKindOf(file) {
15
- const type = (file.type || '').toLowerCase();
16
- if (type.startsWith('image/'))
17
- return 'image';
18
- if (TEXT_TYPES.includes(type))
19
- return 'text';
20
- const name = (file.name || '').toLowerCase();
21
- if (TEXT_EXTENSIONS.some(ext => name.endsWith(ext)))
22
- return 'text';
23
- return null;
24
- }
19
+ const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
20
+ /** Each kind as `accept` patterns - the one table from the mode's kinds to what a picker takes. */
21
+ const KIND_ACCEPT = {
22
+ image: ['image/*'],
23
+ text: [...TEXT_EXTENSIONS, ...TEXT_TYPES],
24
+ spreadsheet: ['.xlsx', XLSX_TYPE]
25
+ };
25
26
  /** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */
26
- function takes(policy, kind) {
27
- return policy.kinds.includes(kind) || (kind === 'text' && policy.kinds.includes('spreadsheet'));
28
- }
27
+ const acceptFor = (kinds) => [...new Set(kinds.flatMap(kind => KIND_ACCEPT[kind]))].join(',');
29
28
  /** The characters of a text file the model reads under this policy. */
30
29
  export function textCharsOf(policy) {
31
30
  return policy?.maxChars ?? TEXT_CHARS_FOR_MODEL;
32
31
  }
33
- export function refuseAttachments(policy, files) {
32
+ /** The mode's size and count as picker limits. */
33
+ export function limitsOf(policy) {
34
+ return { maxFileSize: policy?.maxBytes ?? DEFAULT_MAX_BYTES, maxFiles: policy?.maxFiles ?? DEFAULT_MAX_FILES };
35
+ }
36
+ /**
37
+ * Whether these files may go, by the one picking rule. `server` is the server's upload limit (shell
38
+ * `fileUploadLimits`); the tighter of it and the mode's policy applies. The first reason, by name: file by file, its
39
+ * kind, then its size, then whether it is one too many - the door says them in the same order.
40
+ */
41
+ export function refuseAttachments(policy, files, server) {
34
42
  if (!files.length)
35
43
  return null;
36
44
  if (!policy || !policy.kinds?.length)
37
45
  return { code: 'attachments-not-accepted' };
38
- const maxFiles = policy.maxFiles ?? DEFAULT_MAX_FILES;
39
- if (files.length > maxFiles)
40
- return { code: 'too-many-attachments', maxFiles };
41
- const maxBytes = policy.maxBytes ?? DEFAULT_MAX_BYTES;
42
- for (const file of files) {
43
- const kind = attachmentKindOf(file);
44
- if (!kind || !takes(policy, kind)) {
45
- return { code: 'attachment-kind-not-accepted', name: file.name || '', kinds: policy.kinds };
46
- }
47
- if ((file.size ?? 0) > maxBytes)
48
- return { code: 'attachment-too-large', name: file.name || '', maxBytes };
49
- }
50
- return null;
46
+ /* What is sent: a spreadsheet arrives as the text of its sheets. */
47
+ const sent = policy.kinds.map(kind => (kind === 'spreadsheet' ? 'text' : kind));
48
+ const limits = narrowLimits(limitsOf(policy), server);
49
+ const { refused } = sortPickedFiles(files, { accept: acceptFor(sent), limits });
50
+ if (!refused.length)
51
+ return null;
52
+ const [first] = refused;
53
+ if (first.reason === 'count')
54
+ return { code: 'too-many-attachments', maxFiles: limits.maxFiles };
55
+ return first.reason === 'type'
56
+ ? { code: 'attachment-kind-not-accepted', name: first.file.name || '', kinds: policy.kinds }
57
+ : { code: 'attachment-too-large', name: first.file.name || '', maxBytes: limits.maxFileSize };
51
58
  }
52
- /** The file picker's `accept` for a policy. */
59
+ /** The file picker's `accept` for a policy - a spreadsheet as the person picks it (.xlsx). */
53
60
  export function acceptOf(policy) {
54
- const accept = [];
55
- if (policy?.kinds?.includes('image'))
56
- accept.push('image/*');
57
- if (policy?.kinds?.includes('text') || policy?.kinds?.includes('spreadsheet'))
58
- accept.push(...TEXT_EXTENSIONS, ...TEXT_TYPES);
59
- if (policy?.kinds?.includes('spreadsheet'))
60
- accept.push('.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
61
- return accept.join(',');
61
+ const kinds = policy?.kinds ?? [];
62
+ return acceptFor(kinds.includes('spreadsheet') ? [...kinds, 'text'] : kinds);
62
63
  }
63
64
  /** Where a conversation's file opens - the gated route, under the page's domain path. */
64
65
  export function chatAttachmentUrl(sessionId, attachmentId) {
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../client/utils/attachment-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What a conversation accepts as attachments - the screen's copy of the server rule\n * (server/service/chat-session/attachment-policy.ts, design/ai/shared-dock.md §9-1).\n *\n * The screen checks first so the person hears \"no\" before anything is sent; the door checks again and is\n * the authority. `test/attachment-policy-agree.test.ts` runs both copies over one table so they cannot drift.\n */\n\n/*\n * 'spreadsheet' is a kind a mode opens, never a file the door receives: the screen reads each .xlsx sheet into CSV\n * text before sending (client/utils/spreadsheet-text.ts), so a mode that takes spreadsheets takes that text.\n */\nexport type AttachmentKind = 'image' | 'text' | 'spreadsheet'\n\nexport interface AttachmentPolicy {\n kinds: AttachmentKind[]\n maxBytes?: number\n maxFiles?: number\n /** Per text file, the characters the model reads - a sheet read into text longer than this is refused, not cut. */\n maxChars?: number\n}\n\nexport const DEFAULT_MAX_BYTES = 5 * 1024 * 1024\nexport const DEFAULT_MAX_FILES = 5\n/** How much of a text file reaches the model - the server's TEXT_CHARS_FOR_MODEL. */\nexport const TEXT_CHARS_FOR_MODEL = 20_000\n\nconst TEXT_TYPES = ['text/plain', 'text/csv', 'text/markdown', 'application/json']\nconst TEXT_EXTENSIONS = ['.txt', '.csv', '.md', '.json']\n\nexport function attachmentKindOf(file: { type?: string; name?: string }): AttachmentKind | null {\n const type = (file.type || '').toLowerCase()\n if (type.startsWith('image/')) return 'image'\n if (TEXT_TYPES.includes(type)) return 'text'\n const name = (file.name || '').toLowerCase()\n if (TEXT_EXTENSIONS.some(ext => name.endsWith(ext))) return 'text'\n return null\n}\n\nexport type AttachmentRefusal =\n | { code: 'attachments-not-accepted' }\n | { code: 'attachment-kind-not-accepted'; name: string; kinds: AttachmentKind[] }\n | { code: 'attachment-too-large'; name: string; maxBytes: number }\n | { code: 'too-many-attachments'; maxFiles: number }\n\n/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */\nfunction takes(policy: AttachmentPolicy, kind: AttachmentKind): boolean {\n return policy.kinds.includes(kind) || (kind === 'text' && policy.kinds.includes('spreadsheet'))\n}\n\n/** The characters of a text file the model reads under this policy. */\nexport function textCharsOf(policy: AttachmentPolicy | null | undefined): number {\n return policy?.maxChars ?? TEXT_CHARS_FOR_MODEL\n}\n\nexport function refuseAttachments(\n policy: AttachmentPolicy | null | undefined,\n files: { name?: string; type?: string; size?: number }[]\n): AttachmentRefusal | null {\n if (!files.length) return null\n if (!policy || !policy.kinds?.length) return { code: 'attachments-not-accepted' }\n\n const maxFiles = policy.maxFiles ?? DEFAULT_MAX_FILES\n if (files.length > maxFiles) return { code: 'too-many-attachments', maxFiles }\n\n const maxBytes = policy.maxBytes ?? DEFAULT_MAX_BYTES\n for (const file of files) {\n const kind = attachmentKindOf(file)\n if (!kind || !takes(policy, kind)) {\n return { code: 'attachment-kind-not-accepted', name: file.name || '', kinds: policy.kinds }\n }\n if ((file.size ?? 0) > maxBytes) return { code: 'attachment-too-large', name: file.name || '', maxBytes }\n }\n return null\n}\n\n/** The file picker's `accept` for a policy. */\nexport function acceptOf(policy: AttachmentPolicy | null | undefined): string {\n const accept: string[] = []\n if (policy?.kinds?.includes('image')) accept.push('image/*')\n if (policy?.kinds?.includes('text') || policy?.kinds?.includes('spreadsheet')) accept.push(...TEXT_EXTENSIONS, ...TEXT_TYPES)\n if (policy?.kinds?.includes('spreadsheet')) accept.push('.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')\n return accept.join(',')\n}\n\n/** Where a conversation's file opens - the gated route, under the page's domain path. */\nexport function chatAttachmentUrl(sessionId: string, attachmentId: string): string {\n const domainPath = /^\\/domain\\/[^/]+/.exec(location.pathname)?.[0] ?? ''\n return `${domainPath}/chat-attachments/${encodeURIComponent(sessionId)}/${encodeURIComponent(attachmentId)}`\n}\n"]}
1
+ {"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../client/utils/attachment-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What a conversation accepts as attachments - the screen's side of the server rule\n * (server/service/chat-session/attachment-policy.ts, design/ai/shared-dock.md §9-1).\n *\n * The screen checks first so the person hears \"no\" before anything is sent; the door checks again and is\n * the authority. `test/attachment-policy-agree.test.ts` runs both over one table so they cannot drift.\n *\n * The screen's check is the one file-picking rule (@operato/utils `sortPickedFiles`, ADR-0061 2026-10-17): the mode's\n * kinds become an `accept` (the table below, the one place), its size and count become limits, narrowed by the\n * server's upload limits. What stays here is the mode's words: kinds, and a spreadsheet read into text.\n */\nimport { narrowLimits, sortPickedFiles, type FileUploadLimits } from '@operato/utils/file-acceptance.js'\n\n/*\n * 'spreadsheet' is a kind a mode opens, never a file the door receives: the screen reads each .xlsx sheet into CSV\n * text before sending (client/utils/spreadsheet-text.ts), so a mode that takes spreadsheets takes that text.\n */\nexport type AttachmentKind = 'image' | 'text' | 'spreadsheet'\n\nexport interface AttachmentPolicy {\n kinds: AttachmentKind[]\n maxBytes?: number\n maxFiles?: number\n /** Per text file, the characters the model reads - a sheet read into text longer than this is refused, not cut. */\n maxChars?: number\n}\n\nexport const DEFAULT_MAX_BYTES = 5 * 1024 * 1024\nexport const DEFAULT_MAX_FILES = 5\n/** How much of a text file reaches the model - the server's TEXT_CHARS_FOR_MODEL. */\nexport const TEXT_CHARS_FOR_MODEL = 20_000\n\nconst TEXT_TYPES = ['text/plain', 'text/csv', 'text/markdown', 'application/json']\nconst TEXT_EXTENSIONS = ['.txt', '.csv', '.md', '.json']\nconst XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n\n/** Each kind as `accept` patterns - the one table from the mode's kinds to what a picker takes. */\nconst KIND_ACCEPT: Record<AttachmentKind, string[]> = {\n image: ['image/*'],\n text: [...TEXT_EXTENSIONS, ...TEXT_TYPES],\n spreadsheet: ['.xlsx', XLSX_TYPE]\n}\n\n/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */\nconst acceptFor = (kinds: AttachmentKind[]) => [...new Set(kinds.flatMap(kind => KIND_ACCEPT[kind]))].join(',')\n\nexport type AttachmentRefusal =\n | { code: 'attachments-not-accepted' }\n | { code: 'attachment-kind-not-accepted'; name: string; kinds: AttachmentKind[] }\n | { code: 'attachment-too-large'; name: string; maxBytes: number }\n | { code: 'too-many-attachments'; maxFiles: number }\n\n/** The characters of a text file the model reads under this policy. */\nexport function textCharsOf(policy: AttachmentPolicy | null | undefined): number {\n return policy?.maxChars ?? TEXT_CHARS_FOR_MODEL\n}\n\n/** The mode's size and count as picker limits. */\nexport function limitsOf(policy: AttachmentPolicy | null | undefined): FileUploadLimits {\n return { maxFileSize: policy?.maxBytes ?? DEFAULT_MAX_BYTES, maxFiles: policy?.maxFiles ?? DEFAULT_MAX_FILES }\n}\n\n/**\n * Whether these files may go, by the one picking rule. `server` is the server's upload limit (shell\n * `fileUploadLimits`); the tighter of it and the mode's policy applies. The first reason, by name: file by file, its\n * kind, then its size, then whether it is one too many - the door says them in the same order.\n */\nexport function refuseAttachments(\n policy: AttachmentPolicy | null | undefined,\n files: { name?: string; type?: string; size?: number }[],\n server?: FileUploadLimits\n): AttachmentRefusal | null {\n if (!files.length) return null\n if (!policy || !policy.kinds?.length) return { code: 'attachments-not-accepted' }\n\n /* What is sent: a spreadsheet arrives as the text of its sheets. */\n const sent = policy.kinds.map(kind => (kind === 'spreadsheet' ? 'text' : kind))\n const limits = narrowLimits(limitsOf(policy), server)\n const { refused } = sortPickedFiles(files, { accept: acceptFor(sent), limits })\n if (!refused.length) return null\n\n const [first] = refused\n if (first.reason === 'count') return { code: 'too-many-attachments', maxFiles: limits.maxFiles! }\n return first.reason === 'type'\n ? { code: 'attachment-kind-not-accepted', name: first.file.name || '', kinds: policy.kinds }\n : { code: 'attachment-too-large', name: first.file.name || '', maxBytes: limits.maxFileSize! }\n}\n\n/** The file picker's `accept` for a policy - a spreadsheet as the person picks it (.xlsx). */\nexport function acceptOf(policy: AttachmentPolicy | null | undefined): string {\n const kinds = policy?.kinds ?? []\n return acceptFor(kinds.includes('spreadsheet') ? [...kinds, 'text'] : kinds)\n}\n\n/** Where a conversation's file opens - the gated route, under the page's domain path. */\nexport function chatAttachmentUrl(sessionId: string, attachmentId: string): string {\n const domainPath = /^\\/domain\\/[^/]+/.exec(location.pathname)?.[0] ?? ''\n return `${domainPath}/chat-attachments/${encodeURIComponent(sessionId)}/${encodeURIComponent(attachmentId)}`\n}\n"]}
@@ -44,10 +44,13 @@ function refuseAttachments(policy, files) {
44
44
  return null;
45
45
  if (!policy || !policy.kinds?.length)
46
46
  return { code: 'attachments-not-accepted' };
47
+ /*
48
+ * File by file, in the order sent: its kind, then its size, then whether it is one too many. The screen's picking
49
+ * rule (@operato/utils sortPickedFiles) says them in this order, so the same batch hears the same "no" from both.
50
+ */
47
51
  const maxFiles = policy.maxFiles ?? exports.DEFAULT_MAX_FILES;
48
- if (files.length > maxFiles)
49
- return { code: 'too-many-attachments', maxFiles };
50
52
  const maxBytes = policy.maxBytes ?? exports.DEFAULT_MAX_BYTES;
53
+ let taken = 0;
51
54
  for (const file of files) {
52
55
  const kind = attachmentKindOf(file);
53
56
  if (!kind || !takes(policy, kind)) {
@@ -55,6 +58,9 @@ function refuseAttachments(policy, files) {
55
58
  }
56
59
  if ((file.size ?? 0) > maxBytes)
57
60
  return { code: 'attachment-too-large', name: file.name || '', maxBytes };
61
+ if (taken >= maxFiles)
62
+ return { code: 'too-many-attachments', maxFiles };
63
+ taken++;
58
64
  }
59
65
  return null;
60
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What a conversation accepts as attachments - declared by its mode (design/ai/shared-dock.md §9-1).\n *\n * Pure, so the rule is tested alone and the screen can say \"no\" before anything is sent. A mode that\n * declares nothing accepts nothing: the chat once took .csv and .json, carried them, and no code ever\n * showed them to the model - the person got an answer about a file it had never seen (AI standard §7).\n */\n\n/*\n * 'spreadsheet' is a kind a mode opens, never a file the door receives: the screen reads each .xlsx sheet into CSV\n * text before sending (client/utils/spreadsheet-text.ts), so a mode that takes spreadsheets takes that text.\n */\nexport type AttachmentKind = 'image' | 'text' | 'spreadsheet'\n\nexport interface AttachmentPolicy {\n kinds: AttachmentKind[]\n /** Per file. */\n maxBytes?: number\n /** Per message. */\n maxFiles?: number\n /** Per text file, the characters the model reads - a sheet read into text longer than this is refused, not cut. */\n maxChars?: number\n}\n\nexport const DEFAULT_MAX_BYTES = 5 * 1024 * 1024\nexport const DEFAULT_MAX_FILES = 5\n/** How much of a text file reaches the model in the turn it was sent. */\nexport const TEXT_CHARS_FOR_MODEL = 20_000\n\nconst TEXT_TYPES = ['text/plain', 'text/csv', 'text/markdown', 'application/json']\nconst TEXT_EXTENSIONS = ['.txt', '.csv', '.md', '.json']\n\n/** Which kind a file is, from its type - and its name when the browser did not know the type. */\nexport function attachmentKindOf(file: { type?: string; name?: string }): AttachmentKind | null {\n const type = (file.type || '').toLowerCase()\n if (type.startsWith('image/')) return 'image'\n if (TEXT_TYPES.includes(type)) return 'text'\n const name = (file.name || '').toLowerCase()\n if (TEXT_EXTENSIONS.some(ext => name.endsWith(ext))) return 'text'\n return null\n}\n\nexport type AttachmentRefusal =\n | { code: 'attachments-not-accepted' }\n | { code: 'attachment-kind-not-accepted'; name: string; kinds: AttachmentKind[] }\n | { code: 'attachment-too-large'; name: string; maxBytes: number }\n | { code: 'too-many-attachments'; maxFiles: number }\n\n/** `null` when every file may go; otherwise the first reason, by name - said before sending, never cut silently. */\n/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */\nfunction takes(policy: AttachmentPolicy, kind: AttachmentKind): boolean {\n return policy.kinds.includes(kind) || (kind === 'text' && policy.kinds.includes('spreadsheet'))\n}\n\n/** The characters of a text file the model reads under this policy. */\nexport function textCharsOf(policy: AttachmentPolicy | null | undefined): number {\n return policy?.maxChars ?? TEXT_CHARS_FOR_MODEL\n}\n\nexport function refuseAttachments(\n policy: AttachmentPolicy | null | undefined,\n files: { name?: string; type?: string; size?: number }[]\n): AttachmentRefusal | null {\n if (!files.length) return null\n if (!policy || !policy.kinds?.length) return { code: 'attachments-not-accepted' }\n\n const maxFiles = policy.maxFiles ?? DEFAULT_MAX_FILES\n if (files.length > maxFiles) return { code: 'too-many-attachments', maxFiles }\n\n const maxBytes = policy.maxBytes ?? DEFAULT_MAX_BYTES\n for (const file of files) {\n const kind = attachmentKindOf(file)\n if (!kind || !takes(policy, kind)) {\n return { code: 'attachment-kind-not-accepted', name: file.name || '', kinds: policy.kinds }\n }\n if ((file.size ?? 0) > maxBytes) return { code: 'attachment-too-large', name: file.name || '', maxBytes }\n }\n return null\n}\n\n/** The line history keeps for a file once its turn is past - the model reads the file again only by asking. */\nexport function attachmentHistoryLine(file: { id: string; name?: string; mimetype?: string; size?: number | string }): string {\n return `[attachment ${file.id}: ${file.name || 'unnamed'} (${file.mimetype || 'unknown type'}, ${file.size ?? '?'} bytes) - readChatAttachment to open it]`\n}\n"]}
1
+ {"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What a conversation accepts as attachments - declared by its mode (design/ai/shared-dock.md §9-1).\n *\n * Pure, so the rule is tested alone and the screen can say \"no\" before anything is sent. A mode that\n * declares nothing accepts nothing: the chat once took .csv and .json, carried them, and no code ever\n * showed them to the model - the person got an answer about a file it had never seen (AI standard §7).\n */\n\n/*\n * 'spreadsheet' is a kind a mode opens, never a file the door receives: the screen reads each .xlsx sheet into CSV\n * text before sending (client/utils/spreadsheet-text.ts), so a mode that takes spreadsheets takes that text.\n */\nexport type AttachmentKind = 'image' | 'text' | 'spreadsheet'\n\nexport interface AttachmentPolicy {\n kinds: AttachmentKind[]\n /** Per file. */\n maxBytes?: number\n /** Per message. */\n maxFiles?: number\n /** Per text file, the characters the model reads - a sheet read into text longer than this is refused, not cut. */\n maxChars?: number\n}\n\nexport const DEFAULT_MAX_BYTES = 5 * 1024 * 1024\nexport const DEFAULT_MAX_FILES = 5\n/** How much of a text file reaches the model in the turn it was sent. */\nexport const TEXT_CHARS_FOR_MODEL = 20_000\n\nconst TEXT_TYPES = ['text/plain', 'text/csv', 'text/markdown', 'application/json']\nconst TEXT_EXTENSIONS = ['.txt', '.csv', '.md', '.json']\n\n/** Which kind a file is, from its type - and its name when the browser did not know the type. */\nexport function attachmentKindOf(file: { type?: string; name?: string }): AttachmentKind | null {\n const type = (file.type || '').toLowerCase()\n if (type.startsWith('image/')) return 'image'\n if (TEXT_TYPES.includes(type)) return 'text'\n const name = (file.name || '').toLowerCase()\n if (TEXT_EXTENSIONS.some(ext => name.endsWith(ext))) return 'text'\n return null\n}\n\nexport type AttachmentRefusal =\n | { code: 'attachments-not-accepted' }\n | { code: 'attachment-kind-not-accepted'; name: string; kinds: AttachmentKind[] }\n | { code: 'attachment-too-large'; name: string; maxBytes: number }\n | { code: 'too-many-attachments'; maxFiles: number }\n\n/** `null` when every file may go; otherwise the first reason, by name - said before sending, never cut silently. */\n/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */\nfunction takes(policy: AttachmentPolicy, kind: AttachmentKind): boolean {\n return policy.kinds.includes(kind) || (kind === 'text' && policy.kinds.includes('spreadsheet'))\n}\n\n/** The characters of a text file the model reads under this policy. */\nexport function textCharsOf(policy: AttachmentPolicy | null | undefined): number {\n return policy?.maxChars ?? TEXT_CHARS_FOR_MODEL\n}\n\nexport function refuseAttachments(\n policy: AttachmentPolicy | null | undefined,\n files: { name?: string; type?: string; size?: number }[]\n): AttachmentRefusal | null {\n if (!files.length) return null\n if (!policy || !policy.kinds?.length) return { code: 'attachments-not-accepted' }\n\n /*\n * File by file, in the order sent: its kind, then its size, then whether it is one too many. The screen's picking\n * rule (@operato/utils sortPickedFiles) says them in this order, so the same batch hears the same \"no\" from both.\n */\n const maxFiles = policy.maxFiles ?? DEFAULT_MAX_FILES\n const maxBytes = policy.maxBytes ?? DEFAULT_MAX_BYTES\n let taken = 0\n for (const file of files) {\n const kind = attachmentKindOf(file)\n if (!kind || !takes(policy, kind)) {\n return { code: 'attachment-kind-not-accepted', name: file.name || '', kinds: policy.kinds }\n }\n if ((file.size ?? 0) > maxBytes) return { code: 'attachment-too-large', name: file.name || '', maxBytes }\n if (taken >= maxFiles) return { code: 'too-many-attachments', maxFiles }\n taken++\n }\n return null\n}\n\n/** The line history keeps for a file once its turn is past - the model reads the file again only by asking. */\nexport function attachmentHistoryLine(file: { id: string; name?: string; mimetype?: string; size?: number | string }): string {\n return `[attachment ${file.id}: ${file.name || 'unnamed'} (${file.mimetype || 'unknown type'}, ${file.size ?? '?'} bytes) - readChatAttachment to open it]`\n}\n"]}
@@ -32,7 +32,7 @@ export interface ListRead {
32
32
  search?: string;
33
33
  /** The screen's route. Left out, the list has no screen card. */
34
34
  route?: string;
35
- /** The conditions the screen reads from its address - `year`, `month`, `flow` and `text` (as `search`). Empty: it opens as it is. */
36
- screenTakes?: ('year' | 'month' | 'flow' | 'text')[];
35
+ /** The route has a URL-bound grid. Otherwise conditions are disclosed as not applied on its screen card. */
36
+ screenQuery?: boolean;
37
37
  }
38
38
  export declare function listReadTarget(list: ListRead): ReadTarget;
@@ -34,7 +34,9 @@ function listReadTarget(list) {
34
34
  ]
35
35
  : [])
36
36
  ];
37
- const takes = (list.screenTakes ?? []).filter(name => (name === 'text' ? search : !!narrowing[name]));
37
+ const takes = list.screenQuery
38
+ ? ['id', ...criteria.map(criterion => criterion.name), ...(search ? ['text'] : [])]
39
+ : [];
38
40
  return {
39
41
  key: list.key,
40
42
  labelKey: list.title,
@@ -53,12 +55,8 @@ function listReadTarget(list) {
53
55
  route: list.route,
54
56
  labelKey: list.title,
55
57
  takes,
56
- params: (c) => ({
57
- ...(takes.includes('year') ? { year: c.year } : {}),
58
- ...(takes.includes('month') && c.year ? { month: c.month } : {}),
59
- ...(takes.includes('flow') ? { flow: c.flow } : {}),
60
- ...(takes.includes('text') ? { search: c.text } : {})
61
- })
58
+ queryState: !!list.screenQuery,
59
+ params: () => ({})
62
60
  }
63
61
  }
64
62
  : {})
@@ -1 +1 @@
1
- {"version":3,"file":"list-read.js","sourceRoot":"","sources":["../../../server/service/data-read/list-read.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A list declared in one line (operato-application/design/ai/read-targets.md §3) - the shape most list screens share:\n * a ListParam query, the fields it reads, a date to range over, a status, an amount, search words, and a screen.\n * Each application writes its lists as these lines; this turns one into a read target with the conditions it takes.\n */\nimport type { ReadColumn, ReadCriterion, ReadFilter, ReadTarget } from './read-target.js'\n\nexport interface ListRead {\n /** `<app>.<record>`. */\n key: string\n /** i18n key of the screen's title - the card's name. */\n title: string\n /** The screen's name as the person knows it - the model picks the list by it. */\n name: string\n /** The ListParam query (`transactions`). */\n listQuery: string\n /** Filters every read applies, out of the model's reach - the records the list is of (`category` CONFIRMED). */\n fixedFilters?: ReadFilter[]\n /** The fields of each item it reads - the screen's own selection where there is one. */\n selection: string\n columns: ReadColumn[]\n /** A date (or date-time) field the `from` / `to` conditions range over. */\n dateField?: string\n /** Integer year and month fields, and an in/out field (INCOME | EXPENSE) - the `year`, `month`, `flow` conditions. */\n narrowing?: { year?: string; month?: string; flow?: string }\n /** A field holding the period as YYYY-MM - the `period` condition. */\n monthField?: string\n /** A status field and its values - the `status` condition. */\n status?: { field: string; values: string[] }\n /** The resolver's searchables, said for the model - the list takes ListParam search (the `text` condition). */\n search?: string\n /** The screen's route. Left out, the list has no screen card. */\n route?: string\n /** The conditions the screen reads from its address - `year`, `month`, `flow` and `text` (as `search`). Empty: it opens as it is. */\n screenTakes?: ('year' | 'month' | 'flow' | 'text')[]\n}\n\n/** The money column a minimum or maximum amount means - `amount` when there is one, else the first summed column. */\nconst amountOf = (list: ListRead) => list.columns.find(one => one.name === 'amount' && one.type === 'number')?.name ?? list.columns.find(one => one.total)?.name\n\nexport function listReadTarget(list: ListRead): ReadTarget {\n const search = !!list.search\n const amount = amountOf(list)\n /* A bound compares the stored number - in each row's own currency when the column has one. */\n const inCurrency = list.columns.find(one => one.name === amount)?.currency ? `, compared in each row's own ${list.columns.find(one => one.name === amount)!.currency} - a row in another currency is held to the same number` : ''\n const narrowing = list.narrowing ?? {}\n const criteria: ReadCriterion[] = [\n ...(list.dateField\n ? [\n { name: 'from', type: 'date', labelKey: 'ai-assistant.read.from', description: `the first ${list.dateField}, inclusive`, filter: (v: any) => ({ name: list.dateField!, operator: 'gte', value: v }) },\n { name: 'to', type: 'date', labelKey: 'ai-assistant.read.to', description: `the last ${list.dateField}, inclusive`, filter: (v: any) => ({ name: list.dateField!, operator: 'lte', value: `${v}${/T/.test(String(v)) ? '' : 'T23:59:59.999'}` }) }\n ]\n : []),\n ...(narrowing.year ? [{ name: 'year', type: 'integer', labelKey: 'ai-assistant.read.year', description: 'a year (2026)', filter: (v: any) => ({ name: narrowing.year!, operator: 'eq', value: v }) }] : []),\n ...(narrowing.month ? [{ name: 'month', type: 'integer', labelKey: 'ai-assistant.read.month', description: '1-12, with a year', filter: (v: any) => ({ name: narrowing.month!, operator: 'eq', value: v }) }] : []),\n ...(narrowing.flow\n ? [{ name: 'flow', type: 'enum', values: ['INCOME', 'EXPENSE'], labelKey: 'ai-assistant.read.flow', description: 'money in or out', filter: (v: any) => ({ name: narrowing.flow!, operator: 'eq', value: v }) }]\n : []),\n ...(list.monthField\n ? [{ name: 'period', type: 'string', labelKey: 'ai-assistant.read.period', description: 'a month as YYYY-MM', filter: (v: any) => ({ name: list.monthField!, operator: 'eq', value: v }) }]\n : []),\n ...(list.status\n ? [{ name: 'status', type: 'enum', values: list.status.values, labelKey: 'ai-assistant.read.status', filter: (v: any) => ({ name: list.status!.field, operator: 'eq', value: v }) }]\n : []),\n ...(amount\n ? [\n { name: 'minAmount', type: 'number', labelKey: 'ai-assistant.read.min-amount', description: `at least this ${amount}${inCurrency}`, filter: (v: any) => ({ name: amount, operator: 'gte', value: v }) },\n { name: 'maxAmount', type: 'number', labelKey: 'ai-assistant.read.max-amount', description: `at most this ${amount}${inCurrency}`, filter: (v: any) => ({ name: amount, operator: 'lte', value: v }) }\n ]\n : [])\n ] as ReadCriterion[]\n\n const takes = (list.screenTakes ?? []).filter(name => (name === 'text' ? search : !!narrowing[name]))\n return {\n key: list.key,\n labelKey: list.title,\n description: `the rows of the list 「${list.name}」 (${list.columns.map(one => one.name).join(', ')}).`,\n query:\n `query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]${search ? ', $search: String' : ''}) ` +\n `{ ${list.listQuery}(filters: $filters, pagination: $pagination, sortings: $sortings${search ? ', search: $search' : ''}) { items { ${list.selection} } total } }`,\n criteria,\n ...(list.fixedFilters?.length ? { fixedFilters: list.fixedFilters } : {}),\n columns: list.columns,\n rowsOf: data => ({ rows: data?.[list.listQuery]?.items ?? [], total: data?.[list.listQuery]?.total ?? 0 }),\n ...(search ? { search: { description: `the ${list.search}` } } : {}),\n ...(list.dateField ? { sortings: [{ name: list.dateField, desc: true }] } : {}),\n ...(list.route\n ? {\n screen: {\n route: list.route,\n labelKey: list.title,\n takes,\n params: (c: Record<string, any>) => ({\n ...(takes.includes('year') ? { year: c.year } : {}),\n ...(takes.includes('month') && c.year ? { month: c.month } : {}),\n ...(takes.includes('flow') ? { flow: c.flow } : {}),\n ...(takes.includes('text') ? { search: c.text } : {})\n })\n }\n }\n : {})\n }\n}\n"]}
1
+ {"version":3,"file":"list-read.js","sourceRoot":"","sources":["../../../server/service/data-read/list-read.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A list declared in one line (operato-application/design/ai/read-targets.md §3) - the shape most list screens share:\n * a ListParam query, the fields it reads, a date to range over, a status, an amount, search words, and a screen.\n * Each application writes its lists as these lines; this turns one into a read target with the conditions it takes.\n */\nimport type { ReadColumn, ReadCriterion, ReadFilter, ReadTarget } from './read-target.js'\n\nexport interface ListRead {\n /** `<app>.<record>`. */\n key: string\n /** i18n key of the screen's title - the card's name. */\n title: string\n /** The screen's name as the person knows it - the model picks the list by it. */\n name: string\n /** The ListParam query (`transactions`). */\n listQuery: string\n /** Filters every read applies, out of the model's reach - the records the list is of (`category` CONFIRMED). */\n fixedFilters?: ReadFilter[]\n /** The fields of each item it reads - the screen's own selection where there is one. */\n selection: string\n columns: ReadColumn[]\n /** A date (or date-time) field the `from` / `to` conditions range over. */\n dateField?: string\n /** Integer year and month fields, and an in/out field (INCOME | EXPENSE) - the `year`, `month`, `flow` conditions. */\n narrowing?: { year?: string; month?: string; flow?: string }\n /** A field holding the period as YYYY-MM - the `period` condition. */\n monthField?: string\n /** A status field and its values - the `status` condition. */\n status?: { field: string; values: string[] }\n /** The resolver's searchables, said for the model - the list takes ListParam search (the `text` condition). */\n search?: string\n /** The screen's route. Left out, the list has no screen card. */\n route?: string\n /** The route has a URL-bound grid. Otherwise conditions are disclosed as not applied on its screen card. */\n screenQuery?: boolean\n}\n\n/** The money column a minimum or maximum amount means - `amount` when there is one, else the first summed column. */\nconst amountOf = (list: ListRead) => list.columns.find(one => one.name === 'amount' && one.type === 'number')?.name ?? list.columns.find(one => one.total)?.name\n\nexport function listReadTarget(list: ListRead): ReadTarget {\n const search = !!list.search\n const amount = amountOf(list)\n /* A bound compares the stored number - in each row's own currency when the column has one. */\n const inCurrency = list.columns.find(one => one.name === amount)?.currency ? `, compared in each row's own ${list.columns.find(one => one.name === amount)!.currency} - a row in another currency is held to the same number` : ''\n const narrowing = list.narrowing ?? {}\n const criteria: ReadCriterion[] = [\n ...(list.dateField\n ? [\n { name: 'from', type: 'date', labelKey: 'ai-assistant.read.from', description: `the first ${list.dateField}, inclusive`, filter: (v: any) => ({ name: list.dateField!, operator: 'gte', value: v }) },\n { name: 'to', type: 'date', labelKey: 'ai-assistant.read.to', description: `the last ${list.dateField}, inclusive`, filter: (v: any) => ({ name: list.dateField!, operator: 'lte', value: `${v}${/T/.test(String(v)) ? '' : 'T23:59:59.999'}` }) }\n ]\n : []),\n ...(narrowing.year ? [{ name: 'year', type: 'integer', labelKey: 'ai-assistant.read.year', description: 'a year (2026)', filter: (v: any) => ({ name: narrowing.year!, operator: 'eq', value: v }) }] : []),\n ...(narrowing.month ? [{ name: 'month', type: 'integer', labelKey: 'ai-assistant.read.month', description: '1-12, with a year', filter: (v: any) => ({ name: narrowing.month!, operator: 'eq', value: v }) }] : []),\n ...(narrowing.flow\n ? [{ name: 'flow', type: 'enum', values: ['INCOME', 'EXPENSE'], labelKey: 'ai-assistant.read.flow', description: 'money in or out', filter: (v: any) => ({ name: narrowing.flow!, operator: 'eq', value: v }) }]\n : []),\n ...(list.monthField\n ? [{ name: 'period', type: 'string', labelKey: 'ai-assistant.read.period', description: 'a month as YYYY-MM', filter: (v: any) => ({ name: list.monthField!, operator: 'eq', value: v }) }]\n : []),\n ...(list.status\n ? [{ name: 'status', type: 'enum', values: list.status.values, labelKey: 'ai-assistant.read.status', filter: (v: any) => ({ name: list.status!.field, operator: 'eq', value: v }) }]\n : []),\n ...(amount\n ? [\n { name: 'minAmount', type: 'number', labelKey: 'ai-assistant.read.min-amount', description: `at least this ${amount}${inCurrency}`, filter: (v: any) => ({ name: amount, operator: 'gte', value: v }) },\n { name: 'maxAmount', type: 'number', labelKey: 'ai-assistant.read.max-amount', description: `at most this ${amount}${inCurrency}`, filter: (v: any) => ({ name: amount, operator: 'lte', value: v }) }\n ]\n : [])\n ] as ReadCriterion[]\n\n const takes = list.screenQuery\n ? ['id', ...criteria.map(criterion => criterion.name), ...(search ? ['text'] : [])]\n : []\n return {\n key: list.key,\n labelKey: list.title,\n description: `the rows of the list 「${list.name}」 (${list.columns.map(one => one.name).join(', ')}).`,\n query:\n `query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]${search ? ', $search: String' : ''}) ` +\n `{ ${list.listQuery}(filters: $filters, pagination: $pagination, sortings: $sortings${search ? ', search: $search' : ''}) { items { ${list.selection} } total } }`,\n criteria,\n ...(list.fixedFilters?.length ? { fixedFilters: list.fixedFilters } : {}),\n columns: list.columns,\n rowsOf: data => ({ rows: data?.[list.listQuery]?.items ?? [], total: data?.[list.listQuery]?.total ?? 0 }),\n ...(search ? { search: { description: `the ${list.search}` } } : {}),\n ...(list.dateField ? { sortings: [{ name: list.dateField, desc: true }] } : {}),\n ...(list.route\n ? {\n screen: {\n route: list.route,\n labelKey: list.title,\n takes,\n queryState: !!list.screenQuery,\n params: () => ({})\n }\n }\n : {})\n }\n}\n"]}
@@ -25,6 +25,8 @@ export interface ReadColumn {
25
25
  currency?: string;
26
26
  }
27
27
  export interface ReadScreen {
28
+ /** A URL-bound grid accepts the same predicates used by readRows, including conditions outside its form. */
29
+ queryState?: boolean;
28
30
  /** The application's route of the list screen (`cash-transaction-list`). */
29
31
  route: string;
30
32
  /** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */
@@ -26,6 +26,7 @@ exports.screenAddress = screenAddress;
26
26
  * Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
27
27
  */
28
28
  const entry_target_js_1 = require("../data-entry/entry-target.js");
29
+ const list_query_js_1 = require("@operato/utils/list-query.js");
29
30
  const graphql_1 = require("graphql");
30
31
  /** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */
31
32
  exports.MAX_READ_ROWS = 1000;
@@ -59,7 +60,7 @@ function declareReadTarget(target) {
59
60
  if (target.columns.find(one => one.name === column.currency)?.type !== 'string')
60
61
  throw new Error(`column '${column.name}' of '${target.key}' takes its currency from '${column.currency}', which is no string column`);
61
62
  }
62
- const names = new Set();
63
+ const names = new Set(['id']);
63
64
  for (const criterion of target.criteria) {
64
65
  if (criterion.name === 'id')
65
66
  throw new Error(`read target '${target.key}' declares 'id' - every target takes it`);
@@ -144,7 +145,7 @@ function readCriteria(target, args) {
144
145
  function filtersOf(target, criteria) {
145
146
  return [
146
147
  ...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),
147
- ...target.criteria.flatMap(criterion => (criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []))
148
+ ...target.criteria.flatMap(criterion => criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : [])
148
149
  ];
149
150
  }
150
151
  const valueAt = (row, path) => path.split('.').reduce((at, part) => (at === null || at === undefined ? undefined : at[part]), row);
@@ -255,7 +256,14 @@ function screenAddress(target, criteria) {
255
256
  if (value !== undefined && value !== null && value !== '')
256
257
  params.set(name, String(value));
257
258
  const given = Object.keys(criteria);
258
- const query = params.toString();
259
+ const applicable = Object.fromEntries(Object.entries(criteria).filter(([name]) => screen.takes.includes(name)));
260
+ const query = screen.queryState
261
+ ? (0, list_query_js_1.encodeListQuery)({
262
+ filters: filtersOf(target, applicable),
263
+ search: typeof applicable.text === 'string' ? applicable.text : undefined,
264
+ sorters: target.sortings
265
+ })
266
+ : params.toString();
259
267
  return {
260
268
  href: query ? `${screen.route}?${query}` : screen.route,
261
269
  applied: given.filter(name => screen.takes.includes(name)),
@@ -1 +1 @@
1
- {"version":3,"file":"read-target.js","sourceRoot":"","sources":["../../../server/service/data-read/read-target.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * AI 조회 - an application's list, read through its own query as the person (operato-application/design/ai/read-targets.md).\n *\n * The application declares what it lets the assistant read: its list query, the conditions the model may set and\n * how each becomes one of the query's filters, the columns to show, and the screen that lists the same records.\n * The framework reads the rows through that query with the person's request state - the same @privilege and\n * domain as the screen - counts and sums them itself, and puts a long result on the AI 작업판 as the query returned\n * it. The model is told the count, the sums and a few rows; it never copies a table.\n *\n * Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.\n */\nimport { readFieldValue, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'\nimport type { GraphqlRunner } from '../data-entry/entry-update.js'\nimport { parse } from 'graphql'\n\nexport interface ReadFilter {\n name: string\n operator: string\n value: unknown\n}\n\n/** A condition the model may set - read with the entry fields' rules, turned into the query's filters by the application. */\nexport interface ReadCriterion extends Omit<EntryField, 'required' | 'reference' | 'total'> {\n filter: (value: any) => ReadFilter | ReadFilter[]\n}\n\nexport interface ReadColumn {\n /** The key in the workspace table. */\n name: string\n /** Where the value is in a row - a dotted path into the query's item (`account.name`). Defaults to `name`. */\n path?: string\n type: 'string' | 'number' | 'date'\n labelKey?: string\n /** A number column the server sums. */\n total?: boolean\n /**\n * The column that holds this amount's currency (ISO 4217, `KRW`). Its sums are kept per currency - amounts of two\n * currencies are never added into one number.\n */\n currency?: string\n}\n\nexport interface ReadScreen {\n /** The application's route of the list screen (`cash-transaction-list`). */\n route: string\n /** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */\n params: (criteria: Record<string, any>) => Record<string, string | number | undefined>\n /** The conditions `params` carries - the card names the others as not applied on the screen. */\n takes: string[]\n labelKey?: string\n}\n\nexport interface ReadTarget {\n /** `<app>.<record>` - the tools are named after it. */\n key: string\n /** i18n key naming the records (「은행 입출금」). */\n labelKey?: string\n /** What the records are, for the model. */\n description: string\n /**\n * The list query - takes `$filters: [Filter!]`, `$pagination: Pagination` and `$sortings: [Sorting!]`, and\n * `$search: String` when `search` is declared.\n */\n query: string\n /**\n * The list takes ListParam's free-text term, matched by the resolver across its own searchable fields - the\n * condition `text`. Say what it finds in, for the model.\n */\n search?: { description: string }\n criteria: ReadCriterion[]\n /**\n * Filters every read of this list applies - the records it is of (a book's confirmed lines, not its forecasts). Not\n * a condition: the model neither sees nor sets them, so a read can narrow the list but never widen it.\n */\n fixedFilters?: ReadFilter[]\n columns: ReadColumn[]\n rowsOf: (data: any) => { rows: any[]; total: number }\n /** Orders the rows; the query's own order when left out. */\n sortings?: { name: string; desc?: boolean }[]\n screen?: ReadScreen\n}\n\n/** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */\nexport const MAX_READ_ROWS = 1000\n/** Rows the model is shown; the rest stay on the card. */\nexport const ROWS_FOR_MODEL = 20\n/** A page of the list query. */\nexport const READ_PAGE = 200\n\nconst registry = new Map<string, ReadTarget>()\n\nconst pascalOf = (key: string) =>\n key\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map(part => part[0].toUpperCase() + part.slice(1))\n .join('')\n\nexport const queryToolName = (key: string) => `query${pascalOf(key)}`\nexport const screenToolName = (key: string) => `open${pascalOf(key)}Screen`\nexport const readCategoryName = (key: string) => `data-read:${key}`\n\nexport function declareReadTarget(target: ReadTarget): void {\n if (!/^[a-z0-9-]+\\.[a-z0-9-]+$/.test(target.key)) throw new Error(`read target key '${target.key}' is not <app>.<record>`)\n if (registry.has(target.key) || details.has(target.key)) throw new Error(`read target '${target.key}' is declared twice`)\n rootFieldOf(target.query, 'query')\n if (!target.columns.length) throw new Error(`read target '${target.key}' shows no column`)\n for (const column of target.columns.filter(one => one.currency)) {\n if (!column.total || column.type !== 'number') throw new Error(`column '${column.name}' of '${target.key}' names a currency but is not a summed number`)\n if (target.columns.find(one => one.name === column.currency)?.type !== 'string') throw new Error(`column '${column.name}' of '${target.key}' takes its currency from '${column.currency}', which is no string column`)\n }\n const names = new Set<string>()\n for (const criterion of target.criteria) {\n if (criterion.name === 'id') throw new Error(`read target '${target.key}' declares 'id' - every target takes it`)\n if (criterion.name === 'text' && target.search) throw new Error(`read target '${target.key}' declares 'text' and search - search is the text condition`)\n if (names.has(criterion.name)) throw new Error(`read target '${target.key}' names the condition '${criterion.name}' twice`)\n names.add(criterion.name)\n if (criterion.type === 'enum' && !criterion.values?.length) throw new Error(`condition '${criterion.name}' of '${target.key}' lists no values`)\n }\n if (target.search) names.add('text')\n for (const taken of target.screen?.takes ?? []) if (!names.has(taken)) throw new Error(`the screen of '${target.key}' takes '${taken}', which is no condition`)\n registry.set(target.key, target)\n}\n\nexport function readTargetOf(key: string | undefined): ReadTarget | undefined {\n return key ? registry.get(key) : undefined\n}\n\nexport function clearReadTargets(): void {\n registry.clear()\n details.clear()\n}\n\n/* ── One answer of a query ──────────────────────────────────────────────────────────────────────────────────── */\n\n/** An argument of a detail read - one variable of its query, read with the entry fields' rules. */\nexport interface DetailArg {\n name: string\n type: 'string' | 'integer' | 'number' | 'boolean' | 'date' | 'enum'\n values?: string[]\n description?: string\n required?: boolean\n}\n\n/**\n * A query whose answer is one thing, not a list - a 360's summary, the receivables, a statement (design/ai/read-targets.md\n * §9). The application declares the query with the fields its screen reads; the framework runs it as the person, with\n * the arguments the model set, and hands the answer over as the query returned it. Nothing is summed or cut here: the\n * selection is the projection, and an answer too long to hand over is refused, never cut.\n */\nexport interface DetailRead {\n /** `<app>.<thing>` - the tool is named after it. */\n key: string\n labelKey?: string\n /** What it answers, for the model. */\n description: string\n /** One query with one root field. Its variables are the arguments - each declared in `args`. */\n query: string\n args?: DetailArg[]\n}\n\n/** Characters of an answer handed to the model - a longer one is refused, and the model narrows it. */\nexport const MAX_DETAIL_CHARS = 60_000\n\nconst details = new Map<string, DetailRead>()\n\nexport const detailToolName = (key: string) => `read${pascalOf(key)}`\n\n/** The variables a query declares, and whether each is required (`!`). */\nfunction variablesOf(document: string): { name: string; required: boolean }[] {\n const operation: any = parse(document).definitions.find((one: any) => one.kind === 'OperationDefinition')\n return (operation?.variableDefinitions ?? []).map((one: any) => ({ name: one.variable.name.value, required: one.type.kind === 'NonNullType' && !one.defaultValue }))\n}\n\nexport function declareDetailRead(target: DetailRead): void {\n if (!/^[a-z0-9-]+\\.[a-z0-9-]+$/.test(target.key)) throw new Error(`detail read key '${target.key}' is not <app>.<thing>`)\n if (details.has(target.key) || registry.has(target.key)) throw new Error(`read target '${target.key}' is declared twice`)\n rootFieldOf(target.query, 'query')\n const args = target.args ?? []\n for (const variable of variablesOf(target.query)) {\n const arg = args.find(one => one.name === variable.name)\n if (!arg) throw new Error(`detail read '${target.key}' takes $${variable.name}, which is no argument`)\n if (variable.required && !arg.required) throw new Error(`detail read '${target.key}': $${variable.name} is required by the query - declare it required`)\n }\n for (const arg of args) {\n if (!variablesOf(target.query).some(one => one.name === arg.name)) throw new Error(`detail read '${target.key}' declares '${arg.name}', which its query does not take`)\n if (arg.type === 'enum' && !arg.values?.length) throw new Error(`argument '${arg.name}' of '${target.key}' lists no values`)\n }\n details.set(target.key, target)\n}\n\nexport function detailReadOf(key: string | undefined): DetailRead | undefined {\n return key ? details.get(key) : undefined\n}\n\n/** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */\nexport function readCriteria(target: ReadTarget, args: any): { criteria: Record<string, unknown>; refused: { name: string; code: string }[] } {\n const criteria: Record<string, unknown> = {}\n const refused: { name: string; code: string }[] = []\n /* One record by its id - every target takes it (the id a list answer or a find tool gave). */\n if (typeof args?.id === 'string' && args.id.trim()) criteria.id = args.id.trim()\n if (target.search && typeof args?.text === 'string' && args.text.trim()) criteria.text = args.text.trim()\n for (const criterion of target.criteria) {\n const value = args?.[criterion.name]\n if (value === undefined || value === null || value === '') continue\n const { read, code } = readFieldValue(criterion as EntryField, value)\n if (read === undefined) refused.push({ name: criterion.name, code: code ?? 'unreadable' })\n else criteria[criterion.name] = read\n }\n return { criteria, refused }\n}\n\nexport function filtersOf(target: ReadTarget, criteria: Record<string, unknown>): ReadFilter[] {\n return [\n ...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),\n ...target.criteria.flatMap(criterion => (criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []))\n ]\n}\n\nconst valueAt = (row: any, path: string) => path.split('.').reduce((at, part) => (at === null || at === undefined ? undefined : at[part]), row)\n\n/** One row as the columns say - a number column as a number, a missing value as null. */\nexport function rowOf(target: ReadTarget, item: any): Record<string, string | number | null> {\n return Object.fromEntries([\n /* The id names the record to the model - for one record's detail, a change, or its screen. */\n ...(item?.id ? [['id', String(item.id)] as [string, string]] : []),\n ...target.columns.map(column => {\n const raw = valueAt(item, column.path ?? column.name)\n if (raw === undefined || raw === null || raw === '') return [column.name, null]\n if (column.type === 'number') {\n const number = Number(raw)\n return [column.name, Number.isFinite(number) ? number : null]\n }\n return [column.name, String(raw)]\n })\n ])\n}\n\n/**\n * Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the\n * list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.\n */\nexport async function readRows(target: ReadTarget, filters: ReadFilter[], run: GraphqlRunner, search?: string): Promise<{ rows: Record<string, any>[]; total: number; complete: boolean }> {\n const rows: Record<string, any>[] = []\n let total = 0\n for (let page = 1; rows.length < MAX_READ_ROWS; page++) {\n const answer = await run(target.query, {\n filters: [...filters, ...(target.fixedFilters ?? [])],\n ...(search ? { search } : {}),\n pagination: { page, limit: READ_PAGE },\n ...(target.sortings ? { sortings: target.sortings } : {})\n })\n if (answer.errors?.length) throw new Error(answer.errors.map(one => one.message).join('; '))\n const read = target.rowsOf(answer.data)\n total = Number(read?.total) || 0\n const items = read?.rows ?? []\n rows.push(...items.slice(0, MAX_READ_ROWS - rows.length).map(item => rowOf(target, item)))\n if (items.length < READ_PAGE || rows.length >= total) break\n }\n return { rows, total, complete: rows.length >= total }\n}\n\n/** A column's sum - one number, or one per currency when the column names its currency (`{ KRW: 1000, USD: 100 }`). */\nexport type ReadSum = number | Record<string, number>\n\nexport interface ReadGroup {\n key: string\n count: number\n sums: Record<string, ReadSum>\n}\n\n/** The currency a row without one is summed under - said as it is, never taken for the domain's own. */\nexport const UNKNOWN_CURRENCY = 'unknown'\n\nconst sumOf = (rows: Record<string, any>[], name: string) => Math.round(rows.reduce((sum, row) => sum + (typeof row[name] === 'number' ? row[name] : 0), 0) * 100) / 100\n\n/** The sums of the number columns marked total - the server's, never the model's; a column with a currency per currency. */\nexport function sumsOf(target: ReadTarget, rows: Record<string, any>[]): Record<string, ReadSum> {\n const sums: Record<string, ReadSum> = {}\n for (const column of target.columns.filter(one => one.total && one.type === 'number')) {\n if (!column.currency) {\n sums[column.name] = sumOf(rows, column.name)\n continue\n }\n const byCurrency = new Map<string, Record<string, any>[]>()\n for (const row of rows) {\n const code = typeof row[column.currency] === 'string' && row[column.currency] ? row[column.currency] : UNKNOWN_CURRENCY\n byCurrency.set(code, [...(byCurrency.get(code) ?? []), row])\n }\n sums[column.name] = Object.fromEntries([...byCurrency].sort(([a], [b]) => a.localeCompare(b)).map(([code, members]) => [code, sumOf(members, column.name)]))\n }\n return sums\n}\n\n/** The currencies a set of sums holds for one column - none for a column summed as one number. */\nexport function currenciesOf(sums: Record<string, ReadSum>[], column: string): string[] {\n const codes = new Set<string>()\n for (const one of sums) if (one[column] && typeof one[column] === 'object') for (const code of Object.keys(one[column] as object)) codes.add(code)\n return [...codes].sort()\n}\n\n/** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */\nexport function groupsOf(target: ReadTarget, rows: Record<string, any>[], by: string): ReadGroup[] | undefined {\n const column = target.columns.find(one => one.name === by)\n if (!column) return undefined\n const groups = new Map<string, Record<string, any>[]>()\n for (const row of rows) {\n const value = row[by]\n const key = value === null || value === undefined ? '' : column.type === 'date' ? String(value).slice(0, 7) : String(value)\n groups.set(key, [...(groups.get(key) ?? []), row])\n }\n const list = [...groups].map(([key, members]) => ({ key, count: members.length, sums: sumsOf(target, members) }))\n if (column.type === 'date') return list.sort((a, b) => a.key.localeCompare(b.key))\n /* Largest first by the first sum - in its one currency; groups in two currencies have no common size, so by count. */\n const first = target.columns.find(one => one.total)?.name\n const codes = first ? currenciesOf(list.map(group => group.sums), first) : []\n const size = (group: ReadGroup) => {\n const sum = first ? group.sums[first] : undefined\n return typeof sum === 'number' ? Math.abs(sum) : codes.length === 1 && sum ? Math.abs(sum[codes[0]] ?? 0) : undefined\n }\n return list.sort((a, b) => {\n const [x, y] = [size(a), size(b)]\n return x !== undefined && y !== undefined && codes.length <= 1 ? y - x : b.count - a.count\n })\n}\n\n/** The address of the target's list screen with the conditions it takes, and the conditions it does not. */\nexport function screenAddress(target: ReadTarget, criteria: Record<string, unknown>): { href: string; applied: string[]; notApplied: string[] } | undefined {\n const screen = target.screen\n if (!screen) return undefined\n const params = new URLSearchParams()\n for (const [name, value] of Object.entries(screen.params(criteria))) if (value !== undefined && value !== null && value !== '') params.set(name, String(value))\n const given = Object.keys(criteria)\n const query = params.toString()\n return {\n href: query ? `${screen.route}?${query}` : screen.route,\n applied: given.filter(name => screen.takes.includes(name)),\n notApplied: given.filter(name => !screen.takes.includes(name))\n }\n}\n"]}
1
+ {"version":3,"file":"read-target.js","sourceRoot":"","sources":["../../../server/service/data-read/read-target.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * AI 조회 - an application's list, read through its own query as the person (operato-application/design/ai/read-targets.md).\n *\n * The application declares what it lets the assistant read: its list query, the conditions the model may set and\n * how each becomes one of the query's filters, the columns to show, and the screen that lists the same records.\n * The framework reads the rows through that query with the person's request state - the same @privilege and\n * domain as the screen - counts and sums them itself, and puts a long result on the AI 작업판 as the query returned\n * it. The model is told the count, the sums and a few rows; it never copies a table.\n *\n * Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.\n */\nimport { readFieldValue, rootFieldOf, type EntryField } from '../data-entry/entry-target.js'\nimport { encodeListQuery } from '@operato/utils/list-query.js'\nimport type { GraphqlRunner } from '../data-entry/entry-update.js'\nimport { parse } from 'graphql'\n\nexport interface ReadFilter {\n name: string\n operator: string\n value: unknown\n}\n\n/** A condition the model may set - read with the entry fields' rules, turned into the query's filters by the application. */\nexport interface ReadCriterion extends Omit<EntryField, 'required' | 'reference' | 'total'> {\n filter: (value: any) => ReadFilter | ReadFilter[]\n}\n\nexport interface ReadColumn {\n /** The key in the workspace table. */\n name: string\n /** Where the value is in a row - a dotted path into the query's item (`account.name`). Defaults to `name`. */\n path?: string\n type: 'string' | 'number' | 'date'\n labelKey?: string\n /** A number column the server sums. */\n total?: boolean\n /**\n * The column that holds this amount's currency (ISO 4217, `KRW`). Its sums are kept per currency - amounts of two\n * currencies are never added into one number.\n */\n currency?: string\n}\n\nexport interface ReadScreen {\n /** A URL-bound grid accepts the same predicates used by readRows, including conditions outside its form. */\n queryState?: boolean\n /** The application's route of the list screen (`cash-transaction-list`). */\n route: string\n /** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */\n params: (criteria: Record<string, any>) => Record<string, string | number | undefined>\n /** The conditions `params` carries - the card names the others as not applied on the screen. */\n takes: string[]\n labelKey?: string\n}\n\nexport interface ReadTarget {\n /** `<app>.<record>` - the tools are named after it. */\n key: string\n /** i18n key naming the records (「은행 입출금」). */\n labelKey?: string\n /** What the records are, for the model. */\n description: string\n /**\n * The list query - takes `$filters: [Filter!]`, `$pagination: Pagination` and `$sortings: [Sorting!]`, and\n * `$search: String` when `search` is declared.\n */\n query: string\n /**\n * The list takes ListParam's free-text term, matched by the resolver across its own searchable fields - the\n * condition `text`. Say what it finds in, for the model.\n */\n search?: { description: string }\n criteria: ReadCriterion[]\n /**\n * Filters every read of this list applies - the records it is of (a book's confirmed lines, not its forecasts). Not\n * a condition: the model neither sees nor sets them, so a read can narrow the list but never widen it.\n */\n fixedFilters?: ReadFilter[]\n columns: ReadColumn[]\n rowsOf: (data: any) => { rows: any[]; total: number }\n /** Orders the rows; the query's own order when left out. */\n sortings?: { name: string; desc?: boolean }[]\n screen?: ReadScreen\n}\n\n/** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */\nexport const MAX_READ_ROWS = 1000\n/** Rows the model is shown; the rest stay on the card. */\nexport const ROWS_FOR_MODEL = 20\n/** A page of the list query. */\nexport const READ_PAGE = 200\n\nconst registry = new Map<string, ReadTarget>()\n\nconst pascalOf = (key: string) =>\n key\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map(part => part[0].toUpperCase() + part.slice(1))\n .join('')\n\nexport const queryToolName = (key: string) => `query${pascalOf(key)}`\nexport const screenToolName = (key: string) => `open${pascalOf(key)}Screen`\nexport const readCategoryName = (key: string) => `data-read:${key}`\n\nexport function declareReadTarget(target: ReadTarget): void {\n if (!/^[a-z0-9-]+\\.[a-z0-9-]+$/.test(target.key)) throw new Error(`read target key '${target.key}' is not <app>.<record>`)\n if (registry.has(target.key) || details.has(target.key)) throw new Error(`read target '${target.key}' is declared twice`)\n rootFieldOf(target.query, 'query')\n if (!target.columns.length) throw new Error(`read target '${target.key}' shows no column`)\n for (const column of target.columns.filter(one => one.currency)) {\n if (!column.total || column.type !== 'number') throw new Error(`column '${column.name}' of '${target.key}' names a currency but is not a summed number`)\n if (target.columns.find(one => one.name === column.currency)?.type !== 'string') throw new Error(`column '${column.name}' of '${target.key}' takes its currency from '${column.currency}', which is no string column`)\n }\n const names = new Set<string>(['id'])\n for (const criterion of target.criteria) {\n if (criterion.name === 'id') throw new Error(`read target '${target.key}' declares 'id' - every target takes it`)\n if (criterion.name === 'text' && target.search) throw new Error(`read target '${target.key}' declares 'text' and search - search is the text condition`)\n if (names.has(criterion.name)) throw new Error(`read target '${target.key}' names the condition '${criterion.name}' twice`)\n names.add(criterion.name)\n if (criterion.type === 'enum' && !criterion.values?.length) throw new Error(`condition '${criterion.name}' of '${target.key}' lists no values`)\n }\n if (target.search) names.add('text')\n for (const taken of target.screen?.takes ?? []) if (!names.has(taken)) throw new Error(`the screen of '${target.key}' takes '${taken}', which is no condition`)\n registry.set(target.key, target)\n}\n\nexport function readTargetOf(key: string | undefined): ReadTarget | undefined {\n return key ? registry.get(key) : undefined\n}\n\nexport function clearReadTargets(): void {\n registry.clear()\n details.clear()\n}\n\n/* ── One answer of a query ──────────────────────────────────────────────────────────────────────────────────── */\n\n/** An argument of a detail read - one variable of its query, read with the entry fields' rules. */\nexport interface DetailArg {\n name: string\n type: 'string' | 'integer' | 'number' | 'boolean' | 'date' | 'enum'\n values?: string[]\n description?: string\n required?: boolean\n}\n\n/**\n * A query whose answer is one thing, not a list - a 360's summary, the receivables, a statement (design/ai/read-targets.md\n * §9). The application declares the query with the fields its screen reads; the framework runs it as the person, with\n * the arguments the model set, and hands the answer over as the query returned it. Nothing is summed or cut here: the\n * selection is the projection, and an answer too long to hand over is refused, never cut.\n */\nexport interface DetailRead {\n /** `<app>.<thing>` - the tool is named after it. */\n key: string\n labelKey?: string\n /** What it answers, for the model. */\n description: string\n /** One query with one root field. Its variables are the arguments - each declared in `args`. */\n query: string\n args?: DetailArg[]\n}\n\n/** Characters of an answer handed to the model - a longer one is refused, and the model narrows it. */\nexport const MAX_DETAIL_CHARS = 60_000\n\nconst details = new Map<string, DetailRead>()\n\nexport const detailToolName = (key: string) => `read${pascalOf(key)}`\n\n/** The variables a query declares, and whether each is required (`!`). */\nfunction variablesOf(document: string): { name: string; required: boolean }[] {\n const operation: any = parse(document).definitions.find((one: any) => one.kind === 'OperationDefinition')\n return (operation?.variableDefinitions ?? []).map((one: any) => ({ name: one.variable.name.value, required: one.type.kind === 'NonNullType' && !one.defaultValue }))\n}\n\nexport function declareDetailRead(target: DetailRead): void {\n if (!/^[a-z0-9-]+\\.[a-z0-9-]+$/.test(target.key)) throw new Error(`detail read key '${target.key}' is not <app>.<thing>`)\n if (details.has(target.key) || registry.has(target.key)) throw new Error(`read target '${target.key}' is declared twice`)\n rootFieldOf(target.query, 'query')\n const args = target.args ?? []\n for (const variable of variablesOf(target.query)) {\n const arg = args.find(one => one.name === variable.name)\n if (!arg) throw new Error(`detail read '${target.key}' takes $${variable.name}, which is no argument`)\n if (variable.required && !arg.required) throw new Error(`detail read '${target.key}': $${variable.name} is required by the query - declare it required`)\n }\n for (const arg of args) {\n if (!variablesOf(target.query).some(one => one.name === arg.name)) throw new Error(`detail read '${target.key}' declares '${arg.name}', which its query does not take`)\n if (arg.type === 'enum' && !arg.values?.length) throw new Error(`argument '${arg.name}' of '${target.key}' lists no values`)\n }\n details.set(target.key, target)\n}\n\nexport function detailReadOf(key: string | undefined): DetailRead | undefined {\n return key ? details.get(key) : undefined\n}\n\n/** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */\nexport function readCriteria(target: ReadTarget, args: any): { criteria: Record<string, unknown>; refused: { name: string; code: string }[] } {\n const criteria: Record<string, unknown> = {}\n const refused: { name: string; code: string }[] = []\n /* One record by its id - every target takes it (the id a list answer or a find tool gave). */\n if (typeof args?.id === 'string' && args.id.trim()) criteria.id = args.id.trim()\n if (target.search && typeof args?.text === 'string' && args.text.trim()) criteria.text = args.text.trim()\n for (const criterion of target.criteria) {\n const value = args?.[criterion.name]\n if (value === undefined || value === null || value === '') continue\n const { read, code } = readFieldValue(criterion as EntryField, value)\n if (read === undefined) refused.push({ name: criterion.name, code: code ?? 'unreadable' })\n else criteria[criterion.name] = read\n }\n return { criteria, refused }\n}\n\nexport function filtersOf(target: ReadTarget, criteria: Record<string, unknown>): ReadFilter[] {\n return [\n ...(typeof criteria.id === 'string' ? [{ name: 'id', operator: 'eq', value: criteria.id }] : []),\n ...target.criteria.flatMap(criterion =>\n criterion.name in criteria ? [criterion.filter(criteria[criterion.name])].flat() : []\n )\n ]\n}\n\nconst valueAt = (row: any, path: string) => path.split('.').reduce((at, part) => (at === null || at === undefined ? undefined : at[part]), row)\n\n/** One row as the columns say - a number column as a number, a missing value as null. */\nexport function rowOf(target: ReadTarget, item: any): Record<string, string | number | null> {\n return Object.fromEntries([\n /* The id names the record to the model - for one record's detail, a change, or its screen. */\n ...(item?.id ? [['id', String(item.id)] as [string, string]] : []),\n ...target.columns.map(column => {\n const raw = valueAt(item, column.path ?? column.name)\n if (raw === undefined || raw === null || raw === '') return [column.name, null]\n if (column.type === 'number') {\n const number = Number(raw)\n return [column.name, Number.isFinite(number) ? number : null]\n }\n return [column.name, String(raw)]\n })\n ])\n}\n\n/**\n * Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the\n * list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.\n */\nexport async function readRows(target: ReadTarget, filters: ReadFilter[], run: GraphqlRunner, search?: string): Promise<{ rows: Record<string, any>[]; total: number; complete: boolean }> {\n const rows: Record<string, any>[] = []\n let total = 0\n for (let page = 1; rows.length < MAX_READ_ROWS; page++) {\n const answer = await run(target.query, {\n filters: [...filters, ...(target.fixedFilters ?? [])],\n ...(search ? { search } : {}),\n pagination: { page, limit: READ_PAGE },\n ...(target.sortings ? { sortings: target.sortings } : {})\n })\n if (answer.errors?.length) throw new Error(answer.errors.map(one => one.message).join('; '))\n const read = target.rowsOf(answer.data)\n total = Number(read?.total) || 0\n const items = read?.rows ?? []\n rows.push(...items.slice(0, MAX_READ_ROWS - rows.length).map(item => rowOf(target, item)))\n if (items.length < READ_PAGE || rows.length >= total) break\n }\n return { rows, total, complete: rows.length >= total }\n}\n\n/** A column's sum - one number, or one per currency when the column names its currency (`{ KRW: 1000, USD: 100 }`). */\nexport type ReadSum = number | Record<string, number>\n\nexport interface ReadGroup {\n key: string\n count: number\n sums: Record<string, ReadSum>\n}\n\n/** The currency a row without one is summed under - said as it is, never taken for the domain's own. */\nexport const UNKNOWN_CURRENCY = 'unknown'\n\nconst sumOf = (rows: Record<string, any>[], name: string) => Math.round(rows.reduce((sum, row) => sum + (typeof row[name] === 'number' ? row[name] : 0), 0) * 100) / 100\n\n/** The sums of the number columns marked total - the server's, never the model's; a column with a currency per currency. */\nexport function sumsOf(target: ReadTarget, rows: Record<string, any>[]): Record<string, ReadSum> {\n const sums: Record<string, ReadSum> = {}\n for (const column of target.columns.filter(one => one.total && one.type === 'number')) {\n if (!column.currency) {\n sums[column.name] = sumOf(rows, column.name)\n continue\n }\n const byCurrency = new Map<string, Record<string, any>[]>()\n for (const row of rows) {\n const code = typeof row[column.currency] === 'string' && row[column.currency] ? row[column.currency] : UNKNOWN_CURRENCY\n byCurrency.set(code, [...(byCurrency.get(code) ?? []), row])\n }\n sums[column.name] = Object.fromEntries([...byCurrency].sort(([a], [b]) => a.localeCompare(b)).map(([code, members]) => [code, sumOf(members, column.name)]))\n }\n return sums\n}\n\n/** The currencies a set of sums holds for one column - none for a column summed as one number. */\nexport function currenciesOf(sums: Record<string, ReadSum>[], column: string): string[] {\n const codes = new Set<string>()\n for (const one of sums) if (one[column] && typeof one[column] === 'object') for (const code of Object.keys(one[column] as object)) codes.add(code)\n return [...codes].sort()\n}\n\n/** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */\nexport function groupsOf(target: ReadTarget, rows: Record<string, any>[], by: string): ReadGroup[] | undefined {\n const column = target.columns.find(one => one.name === by)\n if (!column) return undefined\n const groups = new Map<string, Record<string, any>[]>()\n for (const row of rows) {\n const value = row[by]\n const key = value === null || value === undefined ? '' : column.type === 'date' ? String(value).slice(0, 7) : String(value)\n groups.set(key, [...(groups.get(key) ?? []), row])\n }\n const list = [...groups].map(([key, members]) => ({ key, count: members.length, sums: sumsOf(target, members) }))\n if (column.type === 'date') return list.sort((a, b) => a.key.localeCompare(b.key))\n /* Largest first by the first sum - in its one currency; groups in two currencies have no common size, so by count. */\n const first = target.columns.find(one => one.total)?.name\n const codes = first ? currenciesOf(list.map(group => group.sums), first) : []\n const size = (group: ReadGroup) => {\n const sum = first ? group.sums[first] : undefined\n return typeof sum === 'number' ? Math.abs(sum) : codes.length === 1 && sum ? Math.abs(sum[codes[0]] ?? 0) : undefined\n }\n return list.sort((a, b) => {\n const [x, y] = [size(a), size(b)]\n return x !== undefined && y !== undefined && codes.length <= 1 ? y - x : b.count - a.count\n })\n}\n\n/** The address of the target's list screen with the conditions it takes, and the conditions it does not. */\nexport function screenAddress(target: ReadTarget, criteria: Record<string, unknown>): { href: string; applied: string[]; notApplied: string[] } | undefined {\n const screen = target.screen\n if (!screen) return undefined\n const params = new URLSearchParams()\n for (const [name, value] of Object.entries(screen.params(criteria))) if (value !== undefined && value !== null && value !== '') params.set(name, String(value))\n const given = Object.keys(criteria)\n const applicable = Object.fromEntries(Object.entries(criteria).filter(([name]) => screen.takes.includes(name)))\n const query = screen.queryState\n ? encodeListQuery({\n filters: filtersOf(target, applicable),\n search: typeof applicable.text === 'string' ? applicable.text : undefined,\n sorters: target.sortings\n })\n : params.toString()\n return {\n href: query ? `${screen.route}?${query}` : screen.route,\n applied: given.filter(name => screen.takes.includes(name)),\n notApplied: given.filter(name => !screen.takes.includes(name))\n }\n}\n"]}