@things-factory/ai-assistant 10.1.112 → 10.1.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +19 -0
- package/client/utils/open-page.ts +21 -0
- package/dist-client/components/assistant-chat.d.ts +2 -0
- package/dist-client/components/assistant-chat.js +19 -0
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/open-page.d.ts +5 -0
- package/dist-client/utils/open-page.js +19 -0
- package/dist-client/utils/open-page.js.map +1 -0
- package/dist-server/service/assistant-mode/resolve.d.ts +1 -0
- package/dist-server/service/assistant-mode/resolve.js +2 -1
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +2 -0
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/chat-session/attachment-tool-result.d.ts +28 -0
- package/dist-server/service/chat-session/attachment-tool-result.js +9 -0
- package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -0
- package/dist-server/service/chat-session/attachment-tools.js +4 -5
- package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
- package/dist-server/service/data-read/list-read.d.ts +36 -0
- package/dist-server/service/data-read/list-read.js +64 -0
- package/dist-server/service/data-read/list-read.js.map +1 -0
- package/dist-server/service/data-read/read-target.d.ts +109 -0
- package/dist-server/service/data-read/read-target.js +189 -0
- package/dist-server/service/data-read/read-target.js.map +1 -0
- package/dist-server/service/data-read/read-tools.d.ts +228 -0
- package/dist-server/service/data-read/read-tools.js +197 -0
- package/dist-server/service/data-read/read-tools.js.map +1 -0
- package/dist-server/service/index.d.ts +3 -0
- package/dist-server/service/index.js +7 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/assistant-mode/resolve.ts +3 -2
- package/server/service/assistant-mode/types.ts +2 -0
- package/server/service/chat-session/attachment-tool-result.test.ts +26 -0
- package/server/service/chat-session/attachment-tool-result.ts +18 -0
- package/server/service/chat-session/attachment-tools.ts +4 -5
- package/server/service/data-read/data-read.test.ts +229 -0
- package/server/service/data-read/list-read.ts +98 -0
- package/server/service/data-read/read-target.ts +230 -0
- package/server/service/data-read/read-tools.ts +212 -0
- package/server/service/index.ts +4 -0
- package/test/open-page.test.ts +31 -0
- package/translations/en.json +14 -0
- package/translations/ja.json +14 -0
- package/translations/ko.json +14 -0
- package/translations/ms.json +14 -0
- package/translations/zh.json +14 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** An address inside the application - never another origin, never a script. */
|
|
2
|
+
export declare function isAppAddress(href: unknown): href is string;
|
|
3
|
+
export declare function isOpenPageProposal(proposal: any): boolean;
|
|
4
|
+
/** The line under the card's title - the conditions the screen does not take, said, never dropped quietly. */
|
|
5
|
+
export declare function openPageNote(proposal: any, t: (key: string, options?: any) => string): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The card that opens an application's list screen with the conditions of a question (design/ai/read-targets.md §5).
|
|
3
|
+
* It reads nothing and runs nothing - the person presses it and the page opens beside the dock.
|
|
4
|
+
*/
|
|
5
|
+
/** An address inside the application - never another origin, never a script. */
|
|
6
|
+
export function isAppAddress(href) {
|
|
7
|
+
return typeof href === 'string' && !!href.trim() && !/^[a-z][a-z0-9+.-]*:/i.test(href.trim()) && !href.trim().startsWith('//');
|
|
8
|
+
}
|
|
9
|
+
export function isOpenPageProposal(proposal) {
|
|
10
|
+
return proposal?.kind === 'open-page' && isAppAddress(proposal?.href);
|
|
11
|
+
}
|
|
12
|
+
/** The line under the card's title - the conditions the screen does not take, said, never dropped quietly. */
|
|
13
|
+
export function openPageNote(proposal, t) {
|
|
14
|
+
const missing = Array.isArray(proposal?.notAppliedLabels) ? proposal.notAppliedLabels : [];
|
|
15
|
+
return missing.length
|
|
16
|
+
? t('ai-assistant.screen.not-applied', { defaultValue: 'the screen does not take: {conditions}', conditions: missing.join(', ') })
|
|
17
|
+
: '';
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=open-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-page.js","sourceRoot":"","sources":["../../client/utils/open-page.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAAC,IAAa;IACxC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;AAChI,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAa;IAC9C,OAAO,QAAQ,EAAE,IAAI,KAAK,WAAW,IAAI,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;AACvE,CAAC;AAED,8GAA8G;AAC9G,MAAM,UAAU,YAAY,CAAC,QAAa,EAAE,CAAyC;IACnF,MAAM,OAAO,GAAa,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAA;IACpG,OAAO,OAAO,CAAC,MAAM;QACnB,CAAC,CAAC,CAAC,CAAC,iCAAiC,EAAE,EAAE,YAAY,EAAE,wCAAwC,EAAE,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAClI,CAAC,CAAC,EAAE,CAAA;AACR,CAAC","sourcesContent":["/*\n * The card that opens an application's list screen with the conditions of a question (design/ai/read-targets.md §5).\n * It reads nothing and runs nothing - the person presses it and the page opens beside the dock.\n */\n\n/** An address inside the application - never another origin, never a script. */\nexport function isAppAddress(href: unknown): href is string {\n return typeof href === 'string' && !!href.trim() && !/^[a-z][a-z0-9+.-]*:/i.test(href.trim()) && !href.trim().startsWith('//')\n}\n\nexport function isOpenPageProposal(proposal: any): boolean {\n return proposal?.kind === 'open-page' && isAppAddress(proposal?.href)\n}\n\n/** The line under the card's title - the conditions the screen does not take, said, never dropped quietly. */\nexport function openPageNote(proposal: any, t: (key: string, options?: any) => string): string {\n const missing: string[] = Array.isArray(proposal?.notAppliedLabels) ? proposal.notAppliedLabels : []\n return missing.length\n ? t('ai-assistant.screen.not-applied', { defaultValue: 'the screen does not take: {conditions}', conditions: missing.join(', ') })\n : ''\n}\n"]}
|
|
@@ -24,6 +24,7 @@ export declare function modeToolCategories(mode: {
|
|
|
24
24
|
kinds?: string[];
|
|
25
25
|
};
|
|
26
26
|
entryTargets?: string[];
|
|
27
|
+
readTargets?: string[];
|
|
27
28
|
}): string[] | undefined;
|
|
28
29
|
/** The conversation's settings, from the server's declaration. */
|
|
29
30
|
export declare function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings;
|
|
@@ -33,7 +33,8 @@ function modeToolCategories(mode) {
|
|
|
33
33
|
/* Every conversation may put a large result on the AI 작업판 (design/ai/workspace.md). */
|
|
34
34
|
'assistant-workspace',
|
|
35
35
|
...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),
|
|
36
|
-
...(mode.entryTargets ?? []).map(key => `data-entry:${key}`)
|
|
36
|
+
...(mode.entryTargets ?? []).map(key => `data-entry:${key}`),
|
|
37
|
+
...(mode.readTargets ?? []).map(key => `data-read:${key}`)
|
|
37
38
|
];
|
|
38
39
|
return [...new Set([...(mode.toolCategories ?? []), ...extra])];
|
|
39
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":";;AAiDA,
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What the door and the screen each take from a mode.\n *\n * Both start from the same declaration, so a screen cannot show `/` sentences for a conversation\n * the server would answer differently.\n */\nimport { getAssistantMode } from './registry'\nimport type { AssistantModeSlash } from './types'\n\nexport type Translate = (key: string, params?: Record<string, any>) => string\n\nexport interface AssistantModeRef {\n /** Which conversation this is — a registered `<app>.<surface>`. */\n kind: string\n /** What it is about. Part of the conversation's lifetime; the tools read it, not the settings. */\n target?: string\n}\n\n/**\n * A kind nobody registered. The sentence is the person's (ADR-0054); the code and the kind are for\n * whoever reads the log — a screen naming a conversation the server does not have is a mismatched\n * deployment, not something the person did.\n */\nfunction unknownModeError(kind: string, t: Translate): Error {\n const error: any = new Error(\n t('ai-assistant.text.unknown-mode', {\n defaultValue: 'this screen asked for a conversation this server does not have ({kind}). reload the page.',\n kind: kind || '—'\n })\n )\n error.code = 'unknown-assistant-mode'\n error.params = { kind }\n return error\n}\n\nexport interface ModeSettings {\n systemPrompt: string\n toolCategories?: string[]\n requireToolOnFirstTurn?: boolean\n model?: string\n temperature?: number\n maxTokens?: number\n}\n\n/**\n * The categories a mode opens: its own, the AI 작업판 (design/ai/workspace.md), the attachment reader when it\n * takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).\n */\nexport function modeToolCategories(mode: { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[]; readTargets?: string[] }): string[] | undefined {\n const extra = [\n /* Every conversation may put a large result on the AI 작업판 (design/ai/workspace.md). */\n 'assistant-workspace',\n ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),\n ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`),\n ...(mode.readTargets ?? []).map(key => `data-read:${key}`)\n ]\n return [...new Set([...(mode.toolCategories ?? []), ...extra])]\n}\n\n/** The conversation's settings, from the server's declaration. */\nexport function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings {\n const kind = String(ref?.kind ?? '')\n const mode = kind ? getAssistantMode(kind) : undefined\n\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n systemPrompt: mode.systemPrompt,\n /* A mode that takes attachments can open the files sent in earlier turns (design/ai/shared-dock.md §9-4). */\n toolCategories: modeToolCategories(mode),\n requireToolOnFirstTurn: mode.requireToolOnFirstTurn,\n model: mode.model,\n temperature: mode.temperature,\n maxTokens: mode.maxTokens\n }\n}\n\nexport interface AssistantModeSlashView {\n name: string\n description: string\n template: string\n}\n\nexport interface AssistantModeView {\n kind: string\n slashTemplates: AssistantModeSlashView[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n /** 빈 대화의 제목과 한 줄 — 이 대화가 무엇을 하는 자리인지. */\n intro?: { title: string; subtitle: string }\n /** 빈 대화의 예시 — 이 대화에서 실제로 할 수 있는 말. */\n examples?: Array<{ label: string; items: string[] }>\n footerNotice?: string\n stagedNotice?: string\n /** What the screen may attach - the same policy the server enforces. */\n attachments?: AttachmentPolicy\n}\n\n/** What the screen shows, in the words of the request's locale. */\nexport function assistantModeView(kind: string, t: Translate): AssistantModeView {\n const mode = getAssistantMode(kind)\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n kind: mode.kind,\n slashTemplates: (mode.slashTemplates ?? []).map((slash: AssistantModeSlash) => ({\n name: slash.name,\n description: t(slash.descriptionKey),\n template: t(slash.templateKey)\n })),\n catalogEntries: mode.catalogEntries ?? [],\n proposalChoices: mode.proposalChoices ?? [],\n placeholder: mode.placeholderKey ? t(mode.placeholderKey) : undefined,\n intro: mode.introKeys ? { title: t(mode.introKeys.title), subtitle: t(mode.introKeys.subtitle) } : undefined,\n examples: mode.exampleKeys?.map(group => ({\n label: t(group.labelKey),\n items: group.itemKeys.map(key => t(key))\n })),\n footerNotice: mode.footerNoticeKey ? t(mode.footerNoticeKey) : undefined,\n stagedNotice: mode.stagedNoticeKey ? t(mode.stagedNoticeKey) : undefined,\n attachments: mode.attachments\n }\n}\n"]}
|
|
@@ -103,6 +103,8 @@ export interface AssistantModeSpec {
|
|
|
103
103
|
* with `registerEntryTarget`. Each opens that target's propose tool; its door still decides who gets it.
|
|
104
104
|
*/
|
|
105
105
|
entryTargets?: string[];
|
|
106
|
+
/** The lists of the application the conversation may read - read targets by key (design/ai/read-targets.md). */
|
|
107
|
+
readTargets?: string[];
|
|
106
108
|
/** i18n key for the line under an empty conversation. */
|
|
107
109
|
footerNoticeKey?: string;
|
|
108
110
|
/** i18n key for the line shown once the host reports a proposal staged. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What a conversation is, declared by the app server that owns the screen (ADR-0061 decision 7).\n *\n * ADR-0058 put these five in one value so they could not fall out of step, but the value lived in\n * the browser and travelled on every request: the persona, the tool categories, the model and the\n * limits were the caller's to choose. A caller choosing its own tool categories is a caller\n * choosing its own authority, so the declaration moved here and the client sends only which one it\n * is talking in (`kind`) and what about (`target`).\n */\n\n/** A `/` sentence this mode offers, and what it needs to be answerable. */\nexport interface AssistantModeSlash {\n /** Short name shown in the list. */\n name: string\n /** i18n key for the one-line description. The server translates it into the request's locale. */\n descriptionKey: string\n /** i18n key for the sentence that lands in the input. */\n templateKey: string\n /**\n * The tools this sentence needs. A mode must not offer work it cannot do (ADR-0058 decision 3),\n * and a name outside `toolCategories` is reported at boot rather than when a person presses it.\n */\n tools?: string[]\n}\n\nexport interface AssistantModeSpec {\n /** `<app>.<surface>` — `twin.space`, `plant.station`, `figure.modeller`. */\n kind: string\n\n /**\n * Which kinds of subject this conversation stands for — `['board', 'space']` (ADR-0072 A17 ①).\n *\n * A screen says what it is showing (`context.subject`), and these say which conversations that\n * subject can carry. It is a list of types and not a predicate on purpose: a list can be counted\n * at boot and pinned by a test, while a function can only be run.\n *\n * On the twin modeller nothing could be asked about operations, because an open board pinned the\n * authoring conversation and that conversation opens one category. A conversation that also\n * stands for `board` is how the same screen carries both.\n */\n subjects?: string[]\n\n /**\n * The conversation a screen opens when the person has not chosen — one per subject type\n * (ADR-0072 A17 ②).\n *\n * Registration order must not decide this: whichever module booted last would silently become\n * the default. Exactly one mode declares each subject type, and the boot report says so when a\n * type has none or more than one.\n */\n defaultFor?: string[]\n\n /**\n * i18n key for what this conversation is called where a person chooses between them —\n * \"이 그림 고치기\", \"이 현장 지금 어때\". A mode a screen may offer needs one.\n */\n labelKey?: string\n /** Who the assistant is here, and what it must not claim to do. The server's, not the caller's. */\n systemPrompt: string\n /** Which tool categories this conversation may use. Absent or empty exposes none. */\n toolCategories?: string[]\n /** Make the model read before it answers — for surfaces whose subject is live state. */\n requireToolOnFirstTurn?: boolean\n /** Model and limits are policy, not a request field. */\n model?: string\n temperature?: number\n maxTokens?: number\n /** What `/` offers. Declared as keys; the server translates. */\n slashTemplates?: AssistantModeSlash[]\n /** What `@` offers. */\n catalogEntries?: any[]\n /**\n * i18n key for the words in the empty input.\n *\n * The screen says what this conversation is for, and that differs by mode: figure's modeller had\n * \"describe the figure you want\" while the person was there to change the figure already open.\n */\n placeholderKey?: string\n\n /**\n * 빈 대화가 무엇을 말하는가 — 제목과 한 줄 (ADR-0061 결정 7).\n *\n * 자리표와 `/` 는 대화를 따라가는데 이 블록만 host 속성에 남아 있었다. 그래서 머리에는 「이 도형\n * 고치기」가 서 있고 본문은 「만들 Figure 를 설명하세요」라고 적혀 있었다(2026-09-18, Figure 레인\n * :3300). 한 대화가 화면에 무엇을 말하는가는 한 자리에서 나와야 한다.\n */\n introKeys?: { title: string; subtitle: string }\n\n /**\n * 빈 대화의 예시 — 「이 대화에서 할 수 있는 말」.\n *\n * 예시는 그 도구로 **실제로 할 수 있는 말**이어야 한다. 할 수 없는 것을 예시로 두면 빈 화면이 거짓을\n * 가르친다. 대화마다 열리는 도구가 다르므로 예시도 대화를 따라간다.\n */\n exampleKeys?: Array<{ labelKey: string; itemKeys: string[] }>\n\n /**\n * What this conversation accepts as attachments (design/ai/shared-dock.md §9-1). Absent: nothing - the\n * screen offers no attach button, so no file is taken that the model would never see.\n */\n attachments?: AttachmentPolicy\n /**\n * Which records this conversation may enter from material (design/ai/data-entry.md) - keys registered\n * with `registerEntryTarget`. Each opens that target's propose tool; its door still decides who gets it.\n */\n entryTargets?: string[]\n /** i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n /** i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n}\n\n/** A declaration that registered but is not what the contract asks for. Code + params, not prose. */\nexport interface AssistantModeWarning {\n /**\n * `assistant-mode-slash-outside-tools` — a `/` sentence needs a tool this mode does not open.\n * `assistant-mode-subject-without-default` — a subject type no conversation claims as its default.\n * `assistant-mode-subject-default-twice` — two conversations claim the same subject type.\n * `assistant-mode-offered-without-label` — a conversation a screen may offer with nothing to call it.\n */\n code:\n | 'assistant-mode-slash-outside-tools'\n | 'assistant-mode-subject-without-default'\n | 'assistant-mode-subject-default-twice'\n | 'assistant-mode-offered-without-label'\n | 'assistant-mode-entry-target-unknown'\n params: { kind: string; slash?: string; tool?: string; subject?: string; kinds?: string[]; target?: string }\n /** For a person reading the boot log; callers act on the code and params. */\n message: string\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What a conversation is, declared by the app server that owns the screen (ADR-0061 decision 7).\n *\n * ADR-0058 put these five in one value so they could not fall out of step, but the value lived in\n * the browser and travelled on every request: the persona, the tool categories, the model and the\n * limits were the caller's to choose. A caller choosing its own tool categories is a caller\n * choosing its own authority, so the declaration moved here and the client sends only which one it\n * is talking in (`kind`) and what about (`target`).\n */\n\n/** A `/` sentence this mode offers, and what it needs to be answerable. */\nexport interface AssistantModeSlash {\n /** Short name shown in the list. */\n name: string\n /** i18n key for the one-line description. The server translates it into the request's locale. */\n descriptionKey: string\n /** i18n key for the sentence that lands in the input. */\n templateKey: string\n /**\n * The tools this sentence needs. A mode must not offer work it cannot do (ADR-0058 decision 3),\n * and a name outside `toolCategories` is reported at boot rather than when a person presses it.\n */\n tools?: string[]\n}\n\nexport interface AssistantModeSpec {\n /** `<app>.<surface>` — `twin.space`, `plant.station`, `figure.modeller`. */\n kind: string\n\n /**\n * Which kinds of subject this conversation stands for — `['board', 'space']` (ADR-0072 A17 ①).\n *\n * A screen says what it is showing (`context.subject`), and these say which conversations that\n * subject can carry. It is a list of types and not a predicate on purpose: a list can be counted\n * at boot and pinned by a test, while a function can only be run.\n *\n * On the twin modeller nothing could be asked about operations, because an open board pinned the\n * authoring conversation and that conversation opens one category. A conversation that also\n * stands for `board` is how the same screen carries both.\n */\n subjects?: string[]\n\n /**\n * The conversation a screen opens when the person has not chosen — one per subject type\n * (ADR-0072 A17 ②).\n *\n * Registration order must not decide this: whichever module booted last would silently become\n * the default. Exactly one mode declares each subject type, and the boot report says so when a\n * type has none or more than one.\n */\n defaultFor?: string[]\n\n /**\n * i18n key for what this conversation is called where a person chooses between them —\n * \"이 그림 고치기\", \"이 현장 지금 어때\". A mode a screen may offer needs one.\n */\n labelKey?: string\n /** Who the assistant is here, and what it must not claim to do. The server's, not the caller's. */\n systemPrompt: string\n /** Which tool categories this conversation may use. Absent or empty exposes none. */\n toolCategories?: string[]\n /** Make the model read before it answers — for surfaces whose subject is live state. */\n requireToolOnFirstTurn?: boolean\n /** Model and limits are policy, not a request field. */\n model?: string\n temperature?: number\n maxTokens?: number\n /** What `/` offers. Declared as keys; the server translates. */\n slashTemplates?: AssistantModeSlash[]\n /** What `@` offers. */\n catalogEntries?: any[]\n /**\n * i18n key for the words in the empty input.\n *\n * The screen says what this conversation is for, and that differs by mode: figure's modeller had\n * \"describe the figure you want\" while the person was there to change the figure already open.\n */\n placeholderKey?: string\n\n /**\n * 빈 대화가 무엇을 말하는가 — 제목과 한 줄 (ADR-0061 결정 7).\n *\n * 자리표와 `/` 는 대화를 따라가는데 이 블록만 host 속성에 남아 있었다. 그래서 머리에는 「이 도형\n * 고치기」가 서 있고 본문은 「만들 Figure 를 설명하세요」라고 적혀 있었다(2026-09-18, Figure 레인\n * :3300). 한 대화가 화면에 무엇을 말하는가는 한 자리에서 나와야 한다.\n */\n introKeys?: { title: string; subtitle: string }\n\n /**\n * 빈 대화의 예시 — 「이 대화에서 할 수 있는 말」.\n *\n * 예시는 그 도구로 **실제로 할 수 있는 말**이어야 한다. 할 수 없는 것을 예시로 두면 빈 화면이 거짓을\n * 가르친다. 대화마다 열리는 도구가 다르므로 예시도 대화를 따라간다.\n */\n exampleKeys?: Array<{ labelKey: string; itemKeys: string[] }>\n\n /**\n * What this conversation accepts as attachments (design/ai/shared-dock.md §9-1). Absent: nothing - the\n * screen offers no attach button, so no file is taken that the model would never see.\n */\n attachments?: AttachmentPolicy\n /**\n * Which records this conversation may enter from material (design/ai/data-entry.md) - keys registered\n * with `registerEntryTarget`. Each opens that target's propose tool; its door still decides who gets it.\n */\n entryTargets?: string[]\n /** The lists of the application the conversation may read - read targets by key (design/ai/read-targets.md). */\n readTargets?: string[]\n /** i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n /** i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n}\n\n/** A declaration that registered but is not what the contract asks for. Code + params, not prose. */\nexport interface AssistantModeWarning {\n /**\n * `assistant-mode-slash-outside-tools` — a `/` sentence needs a tool this mode does not open.\n * `assistant-mode-subject-without-default` — a subject type no conversation claims as its default.\n * `assistant-mode-subject-default-twice` — two conversations claim the same subject type.\n * `assistant-mode-offered-without-label` — a conversation a screen may offer with nothing to call it.\n */\n code:\n | 'assistant-mode-slash-outside-tools'\n | 'assistant-mode-subject-without-default'\n | 'assistant-mode-subject-default-twice'\n | 'assistant-mode-offered-without-label'\n | 'assistant-mode-entry-target-unknown'\n params: { kind: string; slash?: string; tool?: string; subject?: string; kinds?: string[]; target?: string }\n /** For a person reading the boot log; callers act on the code and params. */\n message: string\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type FileForModel = {
|
|
2
|
+
type: 'image';
|
|
3
|
+
data: Buffer;
|
|
4
|
+
mediaType: string;
|
|
5
|
+
name: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'text';
|
|
8
|
+
text: string;
|
|
9
|
+
name: string;
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function attachmentToolResult(attachmentId: string, file: FileForModel): {
|
|
13
|
+
attachmentId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
images: {
|
|
17
|
+
data: Buffer<ArrayBufferLike>;
|
|
18
|
+
mediaType: string;
|
|
19
|
+
}[];
|
|
20
|
+
note: string;
|
|
21
|
+
} | {
|
|
22
|
+
note?: string;
|
|
23
|
+
attachmentId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
text: string;
|
|
27
|
+
images?: undefined;
|
|
28
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachmentToolResult = attachmentToolResult;
|
|
4
|
+
function attachmentToolResult(attachmentId, file) {
|
|
5
|
+
return file.type === 'image'
|
|
6
|
+
? { attachmentId, name: file.name, type: 'image', images: [{ data: file.data, mediaType: file.mediaType }], note: 'the picture is attached right after this result - look at it.' }
|
|
7
|
+
: { attachmentId, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) };
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=attachment-tool-result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-tool-result.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tool-result.ts"],"names":[],"mappings":";;AAaA,oDAIC;AAJD,SAAgB,oBAAoB,CAAC,YAAoB,EAAE,IAAkB;IAC3E,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO;QAC1B,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,+DAA+D,EAAE;QACnL,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,4CAA4C,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AACzJ,CAAC","sourcesContent":["/*\n * What readChatAttachment answers for a file of the conversation (design/ai/shared-dock.md §9-4).\n *\n * A text file comes back as its text. A picture comes back as the picture: the reserved `images` field is taken out\n * of the JSON by the loop (ai-client-base takeToolImages) and sent as an image part of the same turn - so a receipt\n * sent three turns ago can be read again without asking the person to send it again.\n *\n * Pure, so it is tested without storage or the shell.\n */\nexport type FileForModel =\n | { type: 'image'; data: Buffer; mediaType: string; name: string }\n | { type: 'text'; text: string; name: string; truncated: boolean }\n\nexport function attachmentToolResult(attachmentId: string, file: FileForModel) {\n return file.type === 'image'\n ? { attachmentId, name: file.name, type: 'image', images: [{ data: file.data, mediaType: file.mediaType }], note: 'the picture is attached right after this result - look at it.' }\n : { attachmentId, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) }\n}\n"]}
|
|
@@ -5,7 +5,7 @@ exports.chatAttachmentCategory = exports.CHAT_ATTACHMENT_CATEGORY = void 0;
|
|
|
5
5
|
* readChatAttachment - the model opens a file sent in an earlier turn (design/ai/shared-dock.md §9-4).
|
|
6
6
|
*
|
|
7
7
|
* A file reaches the model in full only in the turn it was sent; later turns carry one line naming it.
|
|
8
|
-
* When the model needs it again it asks here. The caller must be able to read the conversation the file
|
|
8
|
+
* When the model needs it again it asks here - a text file comes back as its text, a picture as the picture itself. The caller must be able to read the conversation the file
|
|
9
9
|
* belongs to - the same gate as reading its messages (ADR-0073 decision 1) - so a file id from elsewhere
|
|
10
10
|
* opens nothing.
|
|
11
11
|
*
|
|
@@ -20,13 +20,14 @@ const participant_lookup_js_1 = require("../chat-session-participant/participant
|
|
|
20
20
|
const session_attachments_js_1 = require("./session-attachments.js");
|
|
21
21
|
const attachment_policy_js_1 = require("./attachment-policy.js");
|
|
22
22
|
const registry_js_1 = require("../assistant-mode/registry.js");
|
|
23
|
+
const attachment_tool_result_js_1 = require("./attachment-tool-result.js");
|
|
23
24
|
exports.CHAT_ATTACHMENT_CATEGORY = 'chat-attachments';
|
|
24
25
|
const readChatAttachment = {
|
|
25
26
|
kind: 'read',
|
|
26
27
|
name: 'readChatAttachment',
|
|
27
28
|
doors: ['Query.chatMessages'],
|
|
28
29
|
description: 'Open a file that was sent earlier in this conversation. History names such files as "[attachment <id>: <name> ...]"; ' +
|
|
29
|
-
'pass that id. An image comes back as
|
|
30
|
+
'pass that id. An image comes back as the picture itself, a text file as its text (cut when long - the answer says so).',
|
|
30
31
|
schema: {
|
|
31
32
|
type: 'object',
|
|
32
33
|
properties: { attachmentId: { type: 'string', description: 'The id from the "[attachment <id>: ...]" line.' } },
|
|
@@ -47,9 +48,7 @@ const readChatAttachment = {
|
|
|
47
48
|
return { absent: 'not-found', attachmentId: id, message: 'there is no such attachment in a conversation here.' };
|
|
48
49
|
}
|
|
49
50
|
const file = await (0, session_attachments_js_1.attachmentForModel)(row, (0, attachment_policy_js_1.textCharsOf)((0, registry_js_1.getAssistantMode)(session.kind || '')?.attachments));
|
|
50
|
-
return
|
|
51
|
-
? { attachmentId: id, name: file.name, type: 'image', note: 'images are shown to you only in the turn they were sent; ask the person to send it again to look at it.' }
|
|
52
|
-
: { attachmentId: id, name: file.name, type: 'text', text: file.text, ...(file.truncated ? { note: 'the file is longer; this is its beginning.' } : {}) };
|
|
51
|
+
return (0, attachment_tool_result_js_1.attachmentToolResult)(id, file);
|
|
53
52
|
}
|
|
54
53
|
};
|
|
55
54
|
exports.chatAttachmentCategory = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,mEAAuG;AACvG,qEAA4D;AAC5D,iDAAqD;AAErD,uDAA+C;AAC/C,mEAAwD;AACxD,6FAAwF;AACxF,qEAAkF;AAClF,iEAAoD;AACpD,+DAAgE;
|
|
1
|
+
{"version":3,"file":"attachment-tools.js","sourceRoot":"","sources":["../../../server/service/chat-session/attachment-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;GASG;AACH,mEAAuG;AACvG,qEAA4D;AAC5D,iDAAqD;AAErD,uDAA+C;AAC/C,mEAAwD;AACxD,6FAAwF;AACxF,qEAAkF;AAClF,iEAAoD;AACpD,+DAAgE;AAChE,2EAAkE;AAErD,QAAA,wBAAwB,GAAG,kBAAkB,CAAA;AAE1D,MAAM,kBAAkB,GAAa;IACnC,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,oBAAoB;IAC1B,KAAK,EAAE,CAAC,oBAAoB,CAAC;IAC7B,WAAW,EACT,uHAAuH;QACvH,wHAAwH;IAC1H,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gDAAgD,EAAE,EAAE;QAC/G,QAAQ,EAAE,CAAC,cAAc,CAAC;KAC3B;IACD,OAAO,EAAE,KAAK,EAAE,IAAS,EAAE,OAAY,EAAE,EAAE;QACzC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;QAC7C,MAAM,EAAE,GAAG,OAAO,IAAI,EAAE,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACjF,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAA;QAElG,MAAM,GAAG,GAAQ,MAAM,IAAA,qBAAa,EAAC,4BAAU,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,4CAAmB,EAAS,EAAE,CAAC,CAAA;QAC3I,MAAM,OAAO,GAAQ,GAAG,IAAI,CAAC,MAAM,IAAA,qBAAa,EAAC,6BAAW,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAS,EAAE,CAAC,CAAC,CAAA;QACtI,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,qDAAqD,EAAE,CAAA;QAEtI,MAAM,WAAW,GAAG,MAAM,IAAA,4CAAoB,EAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAC/E,IAAI,CAAC,IAAA,sCAAc,EAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;YACrD,kFAAkF;YAClF,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,qDAAqD,EAAE,CAAA;QAClH,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAA,2CAAkB,EAAC,GAAG,EAAE,IAAA,kCAAW,EAAC,IAAA,8BAAgB,EAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAA;QAC1G,OAAO,IAAA,gDAAoB,EAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IACvC,CAAC;CACU,CAAA;AAEA,QAAA,sBAAsB,GAAiB;IAClD,IAAI,EAAE,gCAAwB;IAC9B,QAAQ,EACN,wLAAwL;IAC1L,KAAK,EAAE,CAAC,kBAAkB,CAAC;CAC5B,CAAA;AAED,IAAA,qCAAoB,EAAC,8BAAsB,CAAC,CAAA","sourcesContent":["/*\n * readChatAttachment - the model opens a file sent in an earlier turn (design/ai/shared-dock.md §9-4).\n *\n * A file reaches the model in full only in the turn it was sent; later turns carry one line naming it.\n * When the model needs it again it asks here - a text file comes back as its text, a picture as the picture itself. The caller must be able to read the conversation the file\n * belongs to - the same gate as reading its messages (ADR-0073 decision 1) - so a file id from elsewhere\n * opens nothing.\n *\n * The category is opened for every mode that declares an attachment policy (resolve.ts).\n */\nimport { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'\nimport { Attachment } from '@things-factory/attachment-base'\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from './chat-session.js'\nimport { mayReadSession } from './session-visibility.js'\nimport { isSessionParticipant } from '../chat-session-participant/participant-lookup.js'\nimport { attachmentForModel, CHAT_ATTACHMENT_REF } from './session-attachments.js'\nimport { textCharsOf } from './attachment-policy.js'\nimport { getAssistantMode } from '../assistant-mode/registry.js'\nimport { attachmentToolResult } from './attachment-tool-result.js'\n\nexport const CHAT_ATTACHMENT_CATEGORY = 'chat-attachments'\n\nconst readChatAttachment: ToolSpec = {\n kind: 'read',\n name: 'readChatAttachment',\n doors: ['Query.chatMessages'],\n description:\n 'Open a file that was sent earlier in this conversation. History names such files as \"[attachment <id>: <name> ...]\"; ' +\n 'pass that id. An image comes back as the picture itself, a text file as its text (cut when long - the answer says so).',\n schema: {\n type: 'object',\n properties: { attachmentId: { type: 'string', description: 'The id from the \"[attachment <id>: ...]\" line.' } },\n required: ['attachmentId']\n },\n builder: async (args: any, context: any) => {\n const { domain, user } = context?.state ?? {}\n const id = typeof args?.attachmentId === 'string' ? args.attachmentId.trim() : ''\n if (!domain?.id || !id) return { absent: 'no-attachment', message: 'no attachment id was given.' }\n\n const row: any = await getRepository(Attachment).findOne({ where: { id, domain: { id: domain.id }, refType: CHAT_ATTACHMENT_REF } as any })\n const session: any = row && (await getRepository(ChatSession).findOne({ where: { id: row.refBy, domain: { id: domain.id } } as any }))\n if (!row || !session) return { absent: 'not-found', attachmentId: id, message: 'there is no such attachment in a conversation here.' }\n\n const participant = await isSessionParticipant(domain.id, session.id, user?.id)\n if (!mayReadSession(session.visibility, participant)) {\n /* Same answer as absent - saying \"it exists but is private\" would itself leak. */\n return { absent: 'not-found', attachmentId: id, message: 'there is no such attachment in a conversation here.' }\n }\n\n const file = await attachmentForModel(row, textCharsOf(getAssistantMode(session.kind || '')?.attachments))\n return attachmentToolResult(id, file)\n }\n} as ToolSpec\n\nexport const chatAttachmentCategory: ToolCategory = {\n name: CHAT_ATTACHMENT_CATEGORY,\n guidance:\n '- A line \"[attachment <id>: <name> ...]\" names a file sent in an earlier turn. Call readChatAttachment with that id before answering about its contents; never guess what a file says.',\n specs: [readChatAttachment]\n}\n\nregisterToolCategory(chatAttachmentCategory)\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReadColumn, ReadTarget } from './read-target.js';
|
|
2
|
+
export interface ListRead {
|
|
3
|
+
/** `<app>.<record>`. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** i18n key of the screen's title - the card's name. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** The screen's name as the person knows it - the model picks the list by it. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** The ListParam query (`cashTransactions`). */
|
|
10
|
+
listQuery: string;
|
|
11
|
+
/** The fields of each item it reads - the screen's own selection where there is one. */
|
|
12
|
+
selection: string;
|
|
13
|
+
columns: ReadColumn[];
|
|
14
|
+
/** A date (or date-time) field the `from` / `to` conditions range over. */
|
|
15
|
+
dateField?: string;
|
|
16
|
+
/** Integer year and month fields, and an in/out field (INCOME | EXPENSE) - the `year`, `month`, `flow` conditions. */
|
|
17
|
+
narrowing?: {
|
|
18
|
+
year?: string;
|
|
19
|
+
month?: string;
|
|
20
|
+
flow?: string;
|
|
21
|
+
};
|
|
22
|
+
/** A field holding the period as YYYY-MM - the `period` condition. */
|
|
23
|
+
monthField?: string;
|
|
24
|
+
/** A status field and its values - the `status` condition. */
|
|
25
|
+
status?: {
|
|
26
|
+
field: string;
|
|
27
|
+
values: string[];
|
|
28
|
+
};
|
|
29
|
+
/** The resolver's searchables, said for the model - the list takes ListParam search (the `text` condition). */
|
|
30
|
+
search?: string;
|
|
31
|
+
/** The screen's route. Left out, the list has no screen card. */
|
|
32
|
+
route?: string;
|
|
33
|
+
/** The conditions the screen reads from its address - `year`, `month`, `flow` and `text` (as `search`). Empty: it opens as it is. */
|
|
34
|
+
screenTakes?: ('year' | 'month' | 'flow' | 'text')[];
|
|
35
|
+
}
|
|
36
|
+
export declare function listReadTarget(list: ListRead): ReadTarget;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listReadTarget = listReadTarget;
|
|
4
|
+
/** The money column a minimum or maximum amount means - `amount` when there is one, else the first summed column. */
|
|
5
|
+
const amountOf = (list) => list.columns.find(one => one.name === 'amount' && one.type === 'number')?.name ?? list.columns.find(one => one.total)?.name;
|
|
6
|
+
function listReadTarget(list) {
|
|
7
|
+
const search = !!list.search;
|
|
8
|
+
const amount = amountOf(list);
|
|
9
|
+
const narrowing = list.narrowing ?? {};
|
|
10
|
+
const criteria = [
|
|
11
|
+
...(list.dateField
|
|
12
|
+
? [
|
|
13
|
+
{ name: 'from', type: 'date', labelKey: 'ai-assistant.read.from', description: `the first ${list.dateField}, inclusive`, filter: (v) => ({ name: list.dateField, operator: 'gte', value: v }) },
|
|
14
|
+
{ name: 'to', type: 'date', labelKey: 'ai-assistant.read.to', description: `the last ${list.dateField}, inclusive`, filter: (v) => ({ name: list.dateField, operator: 'lte', value: `${v}${/T/.test(String(v)) ? '' : 'T23:59:59.999'}` }) }
|
|
15
|
+
]
|
|
16
|
+
: []),
|
|
17
|
+
...(narrowing.year ? [{ name: 'year', type: 'integer', labelKey: 'ai-assistant.read.year', description: 'a year (2026)', filter: (v) => ({ name: narrowing.year, operator: 'eq', value: v }) }] : []),
|
|
18
|
+
...(narrowing.month ? [{ name: 'month', type: 'integer', labelKey: 'ai-assistant.read.month', description: '1-12, with a year', filter: (v) => ({ name: narrowing.month, operator: 'eq', value: v }) }] : []),
|
|
19
|
+
...(narrowing.flow
|
|
20
|
+
? [{ name: 'flow', type: 'enum', values: ['INCOME', 'EXPENSE'], labelKey: 'ai-assistant.read.flow', description: 'money in or out', filter: (v) => ({ name: narrowing.flow, operator: 'eq', value: v }) }]
|
|
21
|
+
: []),
|
|
22
|
+
...(list.monthField
|
|
23
|
+
? [{ name: 'period', type: 'string', labelKey: 'ai-assistant.read.period', description: 'a month as YYYY-MM', filter: (v) => ({ name: list.monthField, operator: 'eq', value: v }) }]
|
|
24
|
+
: []),
|
|
25
|
+
...(list.status
|
|
26
|
+
? [{ name: 'status', type: 'enum', values: list.status.values, labelKey: 'ai-assistant.read.status', filter: (v) => ({ name: list.status.field, operator: 'eq', value: v }) }]
|
|
27
|
+
: []),
|
|
28
|
+
...(amount
|
|
29
|
+
? [
|
|
30
|
+
{ name: 'minAmount', type: 'number', labelKey: 'ai-assistant.read.min-amount', description: `at least this ${amount}`, filter: (v) => ({ name: amount, operator: 'gte', value: v }) },
|
|
31
|
+
{ name: 'maxAmount', type: 'number', labelKey: 'ai-assistant.read.max-amount', description: `at most this ${amount}`, filter: (v) => ({ name: amount, operator: 'lte', value: v }) }
|
|
32
|
+
]
|
|
33
|
+
: [])
|
|
34
|
+
];
|
|
35
|
+
const takes = (list.screenTakes ?? []).filter(name => (name === 'text' ? search : !!narrowing[name]));
|
|
36
|
+
return {
|
|
37
|
+
key: list.key,
|
|
38
|
+
labelKey: list.title,
|
|
39
|
+
description: `the rows of the list 「${list.name}」 (${list.columns.map(one => one.name).join(', ')}).`,
|
|
40
|
+
query: `query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]${search ? ', $search: String' : ''}) ` +
|
|
41
|
+
`{ ${list.listQuery}(filters: $filters, pagination: $pagination, sortings: $sortings${search ? ', search: $search' : ''}) { items { ${list.selection} } total } }`,
|
|
42
|
+
criteria,
|
|
43
|
+
columns: list.columns,
|
|
44
|
+
rowsOf: data => ({ rows: data?.[list.listQuery]?.items ?? [], total: data?.[list.listQuery]?.total ?? 0 }),
|
|
45
|
+
...(search ? { search: { description: `the ${list.search}` } } : {}),
|
|
46
|
+
...(list.dateField ? { sortings: [{ name: list.dateField, desc: true }] } : {}),
|
|
47
|
+
...(list.route
|
|
48
|
+
? {
|
|
49
|
+
screen: {
|
|
50
|
+
route: list.route,
|
|
51
|
+
labelKey: list.title,
|
|
52
|
+
takes,
|
|
53
|
+
params: (c) => ({
|
|
54
|
+
...(takes.includes('year') ? { year: c.year } : {}),
|
|
55
|
+
...(takes.includes('month') && c.year ? { month: c.month } : {}),
|
|
56
|
+
...(takes.includes('flow') ? { flow: c.flow } : {}),
|
|
57
|
+
...(takes.includes('text') ? { search: c.text } : {})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
: {})
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=list-read.js.map
|
|
@@ -0,0 +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, 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 (`cashTransactions`). */\n listQuery: string\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 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}`, 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}`, 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 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"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type EntryField } from '../data-entry/entry-target.js';
|
|
2
|
+
import type { GraphqlRunner } from '../data-entry/entry-update.js';
|
|
3
|
+
export interface ReadFilter {
|
|
4
|
+
name: string;
|
|
5
|
+
operator: string;
|
|
6
|
+
value: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** A condition the model may set - read with the entry fields' rules, turned into the query's filters by the application. */
|
|
9
|
+
export interface ReadCriterion extends Omit<EntryField, 'required' | 'reference' | 'total'> {
|
|
10
|
+
filter: (value: any) => ReadFilter | ReadFilter[];
|
|
11
|
+
}
|
|
12
|
+
export interface ReadColumn {
|
|
13
|
+
/** The key in the workspace table. */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Where the value is in a row - a dotted path into the query's item (`account.name`). Defaults to `name`. */
|
|
16
|
+
path?: string;
|
|
17
|
+
type: 'string' | 'number' | 'date';
|
|
18
|
+
labelKey?: string;
|
|
19
|
+
/** A number column the server sums. */
|
|
20
|
+
total?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ReadScreen {
|
|
23
|
+
/** The application's route of the list screen (`cash-transaction-list`). */
|
|
24
|
+
route: string;
|
|
25
|
+
/** The conditions the screen takes, as its address's query arguments. A condition left out is said on the card. */
|
|
26
|
+
params: (criteria: Record<string, any>) => Record<string, string | number | undefined>;
|
|
27
|
+
/** The conditions `params` carries - the card names the others as not applied on the screen. */
|
|
28
|
+
takes: string[];
|
|
29
|
+
labelKey?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ReadTarget {
|
|
32
|
+
/** `<app>.<record>` - the tools are named after it. */
|
|
33
|
+
key: string;
|
|
34
|
+
/** i18n key naming the records (「은행 입출금」). */
|
|
35
|
+
labelKey?: string;
|
|
36
|
+
/** What the records are, for the model. */
|
|
37
|
+
description: string;
|
|
38
|
+
/**
|
|
39
|
+
* The list query - takes `$filters: [Filter!]`, `$pagination: Pagination` and `$sortings: [Sorting!]`, and
|
|
40
|
+
* `$search: String` when `search` is declared.
|
|
41
|
+
*/
|
|
42
|
+
query: string;
|
|
43
|
+
/**
|
|
44
|
+
* The list takes ListParam's free-text term, matched by the resolver across its own searchable fields - the
|
|
45
|
+
* condition `text`. Say what it finds in, for the model.
|
|
46
|
+
*/
|
|
47
|
+
search?: {
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
criteria: ReadCriterion[];
|
|
51
|
+
columns: ReadColumn[];
|
|
52
|
+
rowsOf: (data: any) => {
|
|
53
|
+
rows: any[];
|
|
54
|
+
total: number;
|
|
55
|
+
};
|
|
56
|
+
/** Orders the rows; the query's own order when left out. */
|
|
57
|
+
sortings?: {
|
|
58
|
+
name: string;
|
|
59
|
+
desc?: boolean;
|
|
60
|
+
}[];
|
|
61
|
+
screen?: ReadScreen;
|
|
62
|
+
}
|
|
63
|
+
/** At most this many rows are read - the AI 작업판 table's own limit (workspace-blocks MAX_TABLE_ROWS). */
|
|
64
|
+
export declare const MAX_READ_ROWS = 1000;
|
|
65
|
+
/** Rows the model is shown; the rest stay on the card. */
|
|
66
|
+
export declare const ROWS_FOR_MODEL = 20;
|
|
67
|
+
/** A page of the list query. */
|
|
68
|
+
export declare const READ_PAGE = 200;
|
|
69
|
+
export declare const queryToolName: (key: string) => string;
|
|
70
|
+
export declare const screenToolName: (key: string) => string;
|
|
71
|
+
export declare const readCategoryName: (key: string) => string;
|
|
72
|
+
export declare function declareReadTarget(target: ReadTarget): void;
|
|
73
|
+
export declare function readTargetOf(key: string | undefined): ReadTarget | undefined;
|
|
74
|
+
export declare function clearReadTargets(): void;
|
|
75
|
+
/** The conditions the model set, read by their declared types; a value that does not fit is said, never guessed at. */
|
|
76
|
+
export declare function readCriteria(target: ReadTarget, args: any): {
|
|
77
|
+
criteria: Record<string, unknown>;
|
|
78
|
+
refused: {
|
|
79
|
+
name: string;
|
|
80
|
+
code: string;
|
|
81
|
+
}[];
|
|
82
|
+
};
|
|
83
|
+
export declare function filtersOf(target: ReadTarget, criteria: Record<string, unknown>): ReadFilter[];
|
|
84
|
+
/** One row as the columns say - a number column as a number, a missing value as null. */
|
|
85
|
+
export declare function rowOf(target: ReadTarget, item: any): Record<string, string | number | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Reads the rows through the application's query, page by page, up to MAX_READ_ROWS. `complete` is false when the
|
|
88
|
+
* list holds more - a sum of the rows read is then not the sum of the list, and is never said as one.
|
|
89
|
+
*/
|
|
90
|
+
export declare function readRows(target: ReadTarget, filters: ReadFilter[], run: GraphqlRunner, search?: string): Promise<{
|
|
91
|
+
rows: Record<string, any>[];
|
|
92
|
+
total: number;
|
|
93
|
+
complete: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
export interface ReadGroup {
|
|
96
|
+
key: string;
|
|
97
|
+
count: number;
|
|
98
|
+
sums: Record<string, number>;
|
|
99
|
+
}
|
|
100
|
+
/** The sums of the number columns marked total - the server's, never the model's. */
|
|
101
|
+
export declare function sumsOf(target: ReadTarget, rows: Record<string, any>[]): Record<string, number>;
|
|
102
|
+
/** The rows grouped by one column - a date column by its month - each with its count and sums, largest first. */
|
|
103
|
+
export declare function groupsOf(target: ReadTarget, rows: Record<string, any>[], by: string): ReadGroup[] | undefined;
|
|
104
|
+
/** The address of the target's list screen with the conditions it takes, and the conditions it does not. */
|
|
105
|
+
export declare function screenAddress(target: ReadTarget, criteria: Record<string, unknown>): {
|
|
106
|
+
href: string;
|
|
107
|
+
applied: string[];
|
|
108
|
+
notApplied: string[];
|
|
109
|
+
} | undefined;
|