@skaile/forge-editor-compose 0.0.0-stage → 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-10-28
4
+
5
+ ### Added
6
+
7
+ - First release.
8
+ - Entry `.`: `composeBlockTypes({ slotOptions?, labels? })` (the Block types `slot`, `source`, `fact`, `example`; `section` stays forge-editor's), `withAiBrief(blockType, { purpose, instructions? })` and `withProperties(blockType, definitions)` to adapt them per app (pure, they return new Block types), and `KIND` re-exported from `@skaile/forge-compose/documents`.
9
+ - Entry `./vue`: `composeFeatures({ documentId, invoke })` with the features `compose-material` („Block füllen (KI)“ material from facts and ranked examples), `compose-split` („In Beispiele zerlegen (KI)“, „Vorschläge annehmen“, a title-row chip with confidence and origin on proposals), `compose-facts` („Stichpunkte erzeugen (KI)“) and `compose-example` („Als Beispiel übernehmen“); `httpInvoke({ base?, fetch })`, the `ComposeInvoke` type and `createInstance(invoke, input)`.
10
+ - Entry `./server`, free of the AI SDK, h3 and forge-app-actions: the operation contract (`ComposeOperationId`, the zod `composeInputs`, `ComposeInput<K>`, `ComposeOutput<K>`), `ComposeError` (`status`, `statusCode`), `composeOperations(options)` with `material`, `split`, `facts`, `example`, `instance` and `classify` over the host's `ports`, `generate` (forge-compose `Generate`) and `contextOf`, `catalogOf(template, examples, vocabulary?)` (label catalogues from the template and the accepted examples) and `composeTools(ops)` (read-only chat tools `compose_slots`, `compose_material`, `compose_examples`, `compose_classify`, reading the open document from `ToolContext.documentId`).
11
+ - Entry `./server/actions`: `composeActions(define, ops, { authorize, userOf, audience? })`, the five operations as forge-app-actions server actions (`compose.*`); optional peer `@skaile/forge-app-actions`.
12
+ - Entry `./server/ai-sdk`: `aiSdkGenerate({ models, ref, user, onRun? })`, a `Generate` over the editor's `ModelProvider` and the AI SDK; optional peer `ai`.
13
+ - Needs `@skaile/forge-editor` `^6.2.0` and `@skaile/forge-compose` `^0.1.0`.
package/README.md CHANGED
@@ -1,3 +1,136 @@
1
- # Temporary Holding Version
1
+ # @skaile/forge-editor-compose
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Corpus → example blocks → tagged templates → generated text, as an add-on of [`@skaile/forge-editor`](../editor). The pure calculus (tag selectors, case profile, ranking, segmentation pipeline, document conventions) lives in [`@skaile/forge-compose`](https://github.com/skaile-ai/forge-compose); this package is its editor side: Block types, editor features and the operation contract the features and the host's routes share.
4
+
5
+ The documents are ordinary editor documents:
6
+
7
+ | Block type | Is | Fields (`meta`) |
8
+ |---|---|---|
9
+ | `section` | chapter of a template (forge-editor's own container type) | – |
10
+ | `slot` | one text block of a template, and of an instance made from it | `identifier`, `optional`, `heading`, `anweisung`, `skelett`, `selector` |
11
+ | `source` | a text facts and examples come from | `sourceType`, `date` |
12
+ | `fact` | one statement of a source, assigned to a slot | `slot`, `derivation` |
13
+ | `example` | one reusable text for a slot; tags carry the case (`alter/kind`, `icd/f32`) | `slot`, `status` (`vorschlag` \| `geprüft`), `confidence`, `origin`, `derivation` |
14
+
15
+ ## Entries
16
+
17
+ | Entry | Exports |
18
+ |---|---|
19
+ | `@skaile/forge-editor-compose` | `composeBlockTypes`, `withAiBrief`, `withProperties`, `KIND` |
20
+ | `@skaile/forge-editor-compose/vue` | `composeFeatures`, `httpInvoke`, `createInstance`, `ComposeInvoke` |
21
+ | `@skaile/forge-editor-compose/server` | `composeOperations`, `composeTools`, `catalogOf`, `ComposeError`, `composeInputs`, `ComposeInput`, `ComposeOutput`, `ComposeOperationId`, `isComposeOperationId`, `FACTS_SYSTEM` |
22
+ | `@skaile/forge-editor-compose/server/actions` | `composeActions` – the five operations as forge-app-actions server actions (`compose.material` …) |
23
+ | `@skaile/forge-editor-compose/server/ai-sdk` | `aiSdkGenerate` – a `Generate` over the editor's `ModelProvider` and the AI SDK |
24
+
25
+ `./server` never imports the AI SDK, h3 or forge-app-actions; the optional peers `ai` and `@skaile/forge-app-actions` are imported only by `./server/ai-sdk` and `./server/actions`.
26
+
27
+ ## Host wiring
28
+
29
+ ```ts
30
+ import { composeBlockTypes, withAiBrief, withProperties, KIND } from '@skaile/forge-editor-compose'
31
+ import { composeFeatures, httpInvoke } from '@skaile/forge-editor-compose/vue'
32
+
33
+ // Block types: the four compose types, adapted to the app.
34
+ const [slot, source, fact, example] = composeBlockTypes({ slotOptions: slotsOfTheTemplate /* { value, label }[] */ })
35
+ const blockTypes = [
36
+ withAiBrief(slot, { purpose: 'Ein Abschnitt eines Entlassbriefs …', instructions: 'Schreibe sachlich, dritte Person, Präteritum.' }),
37
+ withProperties(source, [{ key: 'format', label: 'Format', type: 'select', options: [{ value: 'verlauf', label: 'Verlauf' }] }]),
38
+ fact,
39
+ example,
40
+ ]
41
+
42
+ // Features: the editor calls the host through one function; the host picks the transport.
43
+ const features = composeFeatures({
44
+ documentId: () => props.documentId,
45
+ invoke: httpInvoke({ base: '/api/compose', fetch: $fetch }), // POST /api/compose/{material|split|facts|example|instance}
46
+ })
47
+ ```
48
+
49
+ `blockTypes` and `features` go into the host's editor preset (`composeEditor(ui, { blockTypes, add: features })` or the host's wrapper). The preset's `blockTypes` **replaces** the built-in Block types, so the host lists `section` itself (`[...defaultBlockTypes(t), ...composeTypes]`, `defaultBlockTypes` from `@skaile/forge-editor/composites`). The host mounts the routes (or exposes the operations another way): `composeInputs[op]` validates each body, `ComposeOutput<op>` is the answer.
50
+
51
+ ## Server
52
+
53
+ The operations are plain functions; routes, forge-app-actions definitions and chat tools are projections of them. The host brings the case (`contextOf`), the model (`generate`) and its ports; **authorization is the host's** (its action `authorize` or route check runs before the operation, which trusts `user` for port reads only).
54
+
55
+ ```ts
56
+ import { composeOperations, composeTools, isComposeOperationId, composeInputs, ComposeError } from '@skaile/forge-editor-compose/server'
57
+ import { icdHierarchy } from '@skaile/forge-compose'
58
+ import { entrySplitter } from '@skaile/forge-compose/segment'
59
+
60
+ const ops = composeOperations({
61
+ ports, // `documents` (with `putDocument`) required; `embeddings` and `index` optional
62
+ generate, // forge-compose `Generate`: every model call goes through it (see below)
63
+ exampleLibraryId: 'beispiele', // where „Als Beispiel übernehmen“ writes; created when missing
64
+ hierarchy: icdHierarchy,
65
+ async contextOf(documentId, user) { // null = unknown document → ComposeError(404)
66
+ const c = await caseOf(documentId, user)
67
+ return c && { case: { tags: c.tags, data: c.data }, templateId: c.templateId, sourceIds: c.sourceIds, exampleIds: ['beispiele'] }
68
+ },
69
+ segmentation: {
70
+ rules: [{ pattern: /^Therapie/i, id: 'therapie' }], // heading → slot (confidence 1, origin `rule`)
71
+ tagRules: [], // text → tag, e.g. `icdAnchorRule`
72
+ splitterFor: source => (source.meta.sourceType === 'verlauf' ? entrySplitter(VERLAUF_HEADER) : null),
73
+ // llm: false // no `llmLabeler`; slotLabelers/tagLabelers replace the default lists
74
+ },
75
+ })
76
+
77
+ // Chat: the read-only tools compose_slots, compose_material, compose_examples, compose_classify.
78
+ provideEditorServer({ ports, tools: composeTools(ops) }) // `ops` must be what composeOperations returned
79
+
80
+ // Route (Nuxt/h3): the transport `httpInvoke` calls, POST /api/compose/:op
81
+ export default defineEventHandler(async event => {
82
+ const op = getRouterParam(event, 'op')
83
+ if (!isComposeOperationId(op)) throw createError({ statusCode: 404 })
84
+ const user = await userOf(event) // the host authorizes here
85
+ try {
86
+ return await ops[op](composeInputs[op].parse(await readBody(event)), user)
87
+ }
88
+ catch (error) {
89
+ if (error instanceof ComposeError) throw createError({ statusCode: error.status, statusMessage: error.message })
90
+ throw error
91
+ }
92
+ })
93
+ ```
94
+
95
+ **Operations** – `material` (facts of the case's sources plus the best examples, scored by `scorers`, default `[tagFitScorer, embeddingScorer, lengthScorer]`; the embedding scorer scores 0 without `ports.embeddings`), `split` (source block → `example` proposals with `status: 'vorschlag'`, `confidence`, `origin`, derivation `ai:split` with the SHA-256 of the body), `facts` (one `generate` call with `stage: 'facts'`; a fact for a slot the template does not have keeps an empty `slot`), `example` (adopt an instance slot into the library; 409 without text), `instance` (409 „Instanz existiert“ unless `force`), `classify` (slot and tags of one text by the configured labelers; chat only). Each answers `ComposeOutput<op>`; failures the caller can act on are `ComposeError` (`status` 400, 404, 409, 422 or 503, `statusCode` the same, so forge-app-actions and h3 map it).
96
+
97
+ **Learning** – `catalogOf(template, examples, vocabulary?)` builds the slot and tag catalogues the labelers read; the `example` blocks with a status other than `vorschlag` are their examples (newest first), so each acceptance improves the next assignment without a training step. `FACTS_SYSTEM` is the default system message of `facts`; `factsSystem` replaces it for a domain.
98
+
99
+ **Model seam** – `generate` is the host's: a host with its own pipeline (local-only checks, run log, JSON repair) implements it itself; a host whose `ports.models` comes from `createEditorModelProvider` uses the adapter:
100
+
101
+ ```ts
102
+ import { aiSdkGenerate } from '@skaile/forge-editor-compose/server/ai-sdk'
103
+ const generate = aiSdkGenerate({ models: ports.models, ref, user, onRun: run => log(run) }) // rejects ComposeError(503) without a model
104
+ ```
105
+
106
+ **Actions** – a host with a forge-app-actions catalogue registers the five operations as `compose.material` … `compose.instance`:
107
+
108
+ ```ts
109
+ import { composeActions } from '@skaile/forge-editor-compose/server/actions'
110
+ const actions = composeActions(defineServerAction, ops, { authorize, userOf: ctx => ctx.principal, audience: ['user', 'llm'] })
111
+ serverActionRegistry.registerAll(actions)
112
+ ```
113
+
114
+ ## Features
115
+
116
+ - **`compose-material`** – „Block füllen (KI)“ on a `slot` with an identifier asks `material` and hands the facts and the best-ranked examples to the model (`aiBlockData`, at most 10 entries).
117
+ - **`compose-split`** – „In Beispiele zerlegen (KI)“ on a `source` asks `split` and inserts the returned `example` proposals right after the source in one transaction (one undo step). A proposal shows a chip `Vorschlag 0.82 · embed` (confidence · origin) in its title row until „Vorschläge annehmen“ on the source sets `status: 'geprüft'` on the proposals that follow it and came from it, and removes `confidence` and `origin`.
118
+ - **`compose-facts`** – „Stichpunkte erzeugen (KI)“ on a `source` asks `facts` and inserts the `fact` blocks after the source (and after the facts that already follow it).
119
+ - **`compose-example`** – „Als Beispiel übernehmen“ on a `slot` of an instance (one with `derivation.method === 'copy'`) asks `example`; the host appends it to its example library.
120
+ - **`createInstance(invoke, { templateId, instanceId, force? })`** – not a feature: a page calls it to make the instance document of a case; it answers `{ chosen, dropped, unresolved }`.
121
+
122
+ Failures of `invoke` (a rejected promise) show the error's message (`data.message`, `data.statusMessage` or `message`) in a notice; nothing is inserted.
123
+
124
+ ## Things to know
125
+
126
+ - The operations read the **stored** document (`documentId` plus a block id). An edit the debounced save has not reached yet is not seen by `split`, `facts` and `example`.
127
+ - `confidence` and `origin` of an example are declared `hidden` properties: the properties form has no read-only field, so the title-row chip shows them while `status` is `vorschlag`.
128
+ - The slot's AI brief has no `contentTemplate`: the editor leaves an unfilled `{skelett}` literally in the prompt. The `skelett` of a slot is sent by `compose-material` as the first „Block füllen (KI)“ data entry instead, only when it is set.
129
+ - `source` lists the `container` capability without `accepts`: facts and examples are siblings of the source, not nested in it.
130
+ - `split` and `facts` read a source's body as Markdown: `heading` nodes and bold one-line paragraphs (how imports mark section headings) become `#` lines, so the structural splitter sees the sections; lists become list lines.
131
+ - A unit `split` cannot assign still becomes an `example` proposal (empty `slot`): nothing of the corpus is lost, the person assigns it in the inspector. A unit a rule or classifier decided is never sent to the language model (`stage: 'label'` batches hold only undecided units).
132
+ - `contextOf` must answer for every document an operation names: the document with the source (`split`, `facts`), the instance (`material`, `example`, `instance`) and the document open in the chat. `null` is a 404.
133
+
134
+ ## Peers
135
+
136
+ `@skaile/forge-editor` `^6.2.0` (6.2 adds `ToolContext.documentId`, which the chat tools read), `@tiptap/core`, `@tiptap/pm`, `vue` `^3.5`; dependencies `@skaile/forge-compose` and `zod`. Optional: `ai` `^7.0.124` (only `./server/ai-sdk` imports it), `@skaile/forge-app-actions` `^0.4.1` (only `./server/actions` imports it).
@@ -0,0 +1,25 @@
1
+ import { KIND } from "@skaile/forge-compose/documents";
2
+ import { BlockType } from "@skaile/forge-editor/composites";
3
+ import { PropertyDefinition } from "@skaile/forge-editor/core";
4
+ //#region src/block-types.d.ts
5
+ export type ComposeKind = typeof KIND.slot | typeof KIND.source | typeof KIND.fact | typeof KIND.example;
6
+ export interface ComposeBlockTypeOptions {
7
+ /** The slot identifiers a fact or example can target: `value` = identifier, `label` = what the person sees. Without options `slot` is a text field. */
8
+ slotOptions?: {
9
+ value: string;
10
+ label: string;
11
+ }[];
12
+ /** Display names of the four types; defaults are German. */
13
+ labels?: Partial<Record<'slot' | 'source' | 'fact' | 'example', string>>;
14
+ }
15
+ /** The four compose Block types. */
16
+ export declare function composeBlockTypes(o?: ComposeBlockTypeOptions): BlockType[];
17
+ /** `blockType` with the `ai-brief` capability's purpose (and, when given, instructions) replaced; the other brief settings stay. Pure. */
18
+ export declare function withAiBrief(blockType: BlockType, brief: {
19
+ purpose: string;
20
+ instructions?: string;
21
+ }): BlockType;
22
+ /** `blockType` with `definitions` added to its `properties` capability; a definition whose key exists replaces it in place. Pure. */
23
+ export declare function withProperties(blockType: BlockType, definitions: readonly PropertyDefinition[]): BlockType;
24
+ //#endregion
25
+ //# sourceMappingURL=block-types.d.ts.map
@@ -0,0 +1,231 @@
1
+ import { KIND } from "@skaile/forge-compose/documents";
2
+ //#region src/block-types.ts
3
+ /** `ja`/`nein` as a select: the library has no boolean field; unset means the per-field default (`optional` nein, `heading` ja). */
4
+ const JA_NEIN = [{
5
+ value: "ja",
6
+ label: "Ja"
7
+ }, {
8
+ value: "nein",
9
+ label: "Nein"
10
+ }];
11
+ const BASKET = { id: "basket" };
12
+ const TITLE_HIDDEN = { id: "titleHidden" };
13
+ const DERIVATION = {
14
+ key: "derivation",
15
+ label: "Herkunft",
16
+ type: "derivation"
17
+ };
18
+ /**
19
+ * The slot type's AI brief. It has no `contentTemplate`: the editor leaves a `{key}` placeholder of the template literally in
20
+ * the prompt when the property is empty, so a template of `{skelett}` would reach the model as the text „{skelett}“ for every
21
+ * slot without a skeleton. The skeleton travels as an `aiBlockData` entry instead (`compose-material`), only when it is set.
22
+ */
23
+ const SLOT_AI = {
24
+ purpose: "Ein Abschnitt eines Dokuments: sachlicher Fließtext, der nur aus den genannten Fakten entsteht; Beispiele sind Stilvorlage.",
25
+ instructions: "Schreibe sachlichen Fließtext nur aus den genannten Fakten. Beispiele sind Stilvorlage für Aufbau und Ton; erfinde nichts hinzu.",
26
+ instructionsField: "anweisung"
27
+ };
28
+ /** The four compose Block types. */
29
+ function composeBlockTypes(o = {}) {
30
+ const label = {
31
+ slot: "Abschnitt",
32
+ source: "Quelle",
33
+ fact: "Fakt",
34
+ example: "Beispiel",
35
+ ...o.labels
36
+ };
37
+ const options = (o.slotOptions ?? []).filter((option) => option.value !== "");
38
+ const slotTarget = options.length > 0 ? {
39
+ key: "slot",
40
+ label: "Slot (Bezeichner im Template)",
41
+ type: "select",
42
+ options
43
+ } : {
44
+ key: "slot",
45
+ label: "Slot (Bezeichner im Template)",
46
+ type: "text"
47
+ };
48
+ return [
49
+ {
50
+ id: KIND.slot,
51
+ label: label.slot,
52
+ icon: "i-lucide-file-text",
53
+ titlePlaceholder: label.slot,
54
+ descriptionPlaceholder: "Beschreibung: Zuordnung, Stichpunkte, Zweck der Prosa",
55
+ capabilities: [
56
+ BASKET,
57
+ TITLE_HIDDEN,
58
+ {
59
+ id: "properties",
60
+ config: { definitions: [
61
+ {
62
+ key: "identifier",
63
+ label: "Bezeichner (z. B. therapie/pflege)",
64
+ type: "text"
65
+ },
66
+ {
67
+ key: "optional",
68
+ label: "Optional (Slot darf in der Instanz entfallen)",
69
+ type: "select",
70
+ options: JA_NEIN
71
+ },
72
+ {
73
+ key: "heading",
74
+ label: "Überschrift im Dokument",
75
+ type: "select",
76
+ options: JA_NEIN
77
+ },
78
+ {
79
+ key: "anweisung",
80
+ label: "KI-Anweisung (ersetzt die Vorgabe)",
81
+ type: "textarea"
82
+ },
83
+ {
84
+ key: "skelett",
85
+ label: "Skelett (Zieltext als Markdown, {{…}} = freie Stellen)",
86
+ type: "textarea"
87
+ },
88
+ {
89
+ key: "selector",
90
+ label: "Auswahl (#muss ~bevorzugt -nicht)",
91
+ type: "text"
92
+ }
93
+ ] }
94
+ },
95
+ {
96
+ id: "ai-brief",
97
+ config: { ...SLOT_AI }
98
+ }
99
+ ]
100
+ },
101
+ {
102
+ id: KIND.source,
103
+ label: label.source,
104
+ icon: "i-lucide-file-search",
105
+ titlePlaceholder: "Titel der Quelle",
106
+ descriptionPlaceholder: "",
107
+ capabilities: [
108
+ {
109
+ id: "container",
110
+ config: {}
111
+ },
112
+ BASKET,
113
+ TITLE_HIDDEN,
114
+ {
115
+ id: "properties",
116
+ config: { definitions: [{
117
+ key: "sourceType",
118
+ label: "Quelltyp",
119
+ type: "text"
120
+ }, {
121
+ key: "date",
122
+ label: "Datum",
123
+ type: "text"
124
+ }] }
125
+ }
126
+ ],
127
+ chrome: { tags: false }
128
+ },
129
+ {
130
+ id: KIND.fact,
131
+ label: label.fact,
132
+ icon: "i-lucide-lightbulb",
133
+ titlePlaceholder: "",
134
+ descriptionPlaceholder: "",
135
+ capabilities: [
136
+ BASKET,
137
+ TITLE_HIDDEN,
138
+ {
139
+ id: "properties",
140
+ config: { definitions: [slotTarget, DERIVATION] }
141
+ }
142
+ ],
143
+ chrome: { tags: false }
144
+ },
145
+ {
146
+ id: KIND.example,
147
+ label: label.example,
148
+ icon: "i-lucide-quote",
149
+ titlePlaceholder: label.example,
150
+ descriptionPlaceholder: "",
151
+ capabilities: [
152
+ BASKET,
153
+ TITLE_HIDDEN,
154
+ {
155
+ id: "properties",
156
+ config: { definitions: [
157
+ slotTarget,
158
+ {
159
+ key: "status",
160
+ label: "Status",
161
+ type: "select",
162
+ options: [{
163
+ value: "vorschlag",
164
+ label: "Vorschlag"
165
+ }, {
166
+ value: "geprüft",
167
+ label: "Geprüft"
168
+ }]
169
+ },
170
+ {
171
+ key: "confidence",
172
+ label: "Sicherheit",
173
+ type: "text",
174
+ hidden: true
175
+ },
176
+ {
177
+ key: "origin",
178
+ label: "Herkunft der Zuordnung",
179
+ type: "text",
180
+ hidden: true
181
+ },
182
+ DERIVATION
183
+ ] }
184
+ }
185
+ ]
186
+ }
187
+ ];
188
+ }
189
+ /** `blockType` with the `ai-brief` capability's purpose (and, when given, instructions) replaced; the other brief settings stay. Pure. */
190
+ function withAiBrief(blockType, brief) {
191
+ const config = (existing) => ({
192
+ ...existing && typeof existing === "object" ? existing : {},
193
+ purpose: brief.purpose,
194
+ ...brief.instructions !== void 0 && { instructions: brief.instructions }
195
+ });
196
+ const listed = blockType.capabilities.some((capability) => capability.id === "ai-brief");
197
+ return {
198
+ ...blockType,
199
+ capabilities: listed ? blockType.capabilities.map((capability) => capability.id === "ai-brief" ? {
200
+ ...capability,
201
+ config: config(capability.config)
202
+ } : capability) : [...blockType.capabilities, {
203
+ id: "ai-brief",
204
+ config: config(void 0)
205
+ }]
206
+ };
207
+ }
208
+ /** `blockType` with `definitions` added to its `properties` capability; a definition whose key exists replaces it in place. Pure. */
209
+ function withProperties(blockType, definitions) {
210
+ const merge = (existing) => {
211
+ const current = existing?.definitions ?? [];
212
+ const replaced = current.map((definition) => definitions.find((added) => added.key === definition.key) ?? definition);
213
+ const added = definitions.filter((definition) => !current.some((known) => known.key === definition.key));
214
+ return { definitions: [...replaced, ...added] };
215
+ };
216
+ const listed = blockType.capabilities.some((capability) => capability.id === "properties");
217
+ return {
218
+ ...blockType,
219
+ capabilities: listed ? blockType.capabilities.map((capability) => capability.id === "properties" ? {
220
+ ...capability,
221
+ config: merge(capability.config)
222
+ } : capability) : [...blockType.capabilities, {
223
+ id: "properties",
224
+ config: merge(void 0)
225
+ }]
226
+ };
227
+ }
228
+ //#endregion
229
+ export { composeBlockTypes, withAiBrief, withProperties };
230
+
231
+ //# sourceMappingURL=block-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-types.js","names":[],"sources":["../src/block-types.ts"],"sourcesContent":["// Block types of the compose documents. Pure data (type-only imports besides `KIND`), so seeds, tests and server code use the\n// same ids and properties as the editor. `section` is forge-editor's built-in container type and is not defined here.\n//\n// slot one text block of a template (and of an instance made from it); `identifier` is what facts and examples target\n// source a text the facts and examples come from (a letter, a chapter, a case file)\n// fact one short statement of a source, assigned to a slot\n// example one reusable text for a slot, tagged by case (`alter/*`, `icd/*`, …); a proposal until a person accepts it\nimport type { BlockType } from '@skaile/forge-editor/composites'\nimport type { PropertyDefinition } from '@skaile/forge-editor/core'\nimport { KIND } from '@skaile/forge-compose/documents'\n\nexport type ComposeKind = typeof KIND.slot | typeof KIND.source | typeof KIND.fact | typeof KIND.example\n\nexport interface ComposeBlockTypeOptions {\n /** The slot identifiers a fact or example can target: `value` = identifier, `label` = what the person sees. Without options `slot` is a text field. */\n slotOptions?: { value: string; label: string }[]\n /** Display names of the four types; defaults are German. */\n labels?: Partial<Record<'slot' | 'source' | 'fact' | 'example', string>>\n}\n\n/** `ja`/`nein` as a select: the library has no boolean field; unset means the per-field default (`optional` nein, `heading` ja). */\nconst JA_NEIN = [\n { value: 'ja', label: 'Ja' },\n { value: 'nein', label: 'Nein' },\n]\n\nconst BASKET = { id: 'basket' }\nconst TITLE_HIDDEN = { id: 'titleHidden' }\n\nconst DERIVATION: PropertyDefinition = { key: 'derivation', label: 'Herkunft', type: 'derivation' }\n\n/**\n * The slot type's AI brief. It has no `contentTemplate`: the editor leaves a `{key}` placeholder of the template literally in\n * the prompt when the property is empty, so a template of `{skelett}` would reach the model as the text „{skelett}“ for every\n * slot without a skeleton. The skeleton travels as an `aiBlockData` entry instead (`compose-material`), only when it is set.\n */\nconst SLOT_AI = {\n purpose: 'Ein Abschnitt eines Dokuments: sachlicher Fließtext, der nur aus den genannten Fakten entsteht; Beispiele sind Stilvorlage.',\n instructions: 'Schreibe sachlichen Fließtext nur aus den genannten Fakten. Beispiele sind Stilvorlage für Aufbau und Ton; erfinde nichts hinzu.',\n instructionsField: 'anweisung',\n}\n\n/** The four compose Block types. */\nexport function composeBlockTypes(o: ComposeBlockTypeOptions = {}): BlockType[] {\n const label = { slot: 'Abschnitt', source: 'Quelle', fact: 'Fakt', example: 'Beispiel', ...o.labels }\n const options = (o.slotOptions ?? []).filter(option => option.value !== '')\n\n // A select cannot hold an empty option (the Select component refuses an empty value): the placeholder shows while `slot` is unset.\n // A `slot` that is no longer in the template stays in the document but is not selectable.\n const slotTarget: PropertyDefinition = options.length > 0\n ? { key: 'slot', label: 'Slot (Bezeichner im Template)', type: 'select', options }\n : { key: 'slot', label: 'Slot (Bezeichner im Template)', type: 'text' }\n\n return [\n {\n id: KIND.slot,\n label: label.slot,\n icon: 'i-lucide-file-text',\n titlePlaceholder: label.slot,\n descriptionPlaceholder: 'Beschreibung: Zuordnung, Stichpunkte, Zweck der Prosa',\n capabilities: [\n BASKET,\n TITLE_HIDDEN,\n {\n id: 'properties',\n config: {\n definitions: [\n { key: 'identifier', label: 'Bezeichner (z. B. therapie/pflege)', type: 'text' },\n { key: 'optional', label: 'Optional (Slot darf in der Instanz entfallen)', type: 'select', options: JA_NEIN },\n { key: 'heading', label: 'Überschrift im Dokument', type: 'select', options: JA_NEIN },\n { key: 'anweisung', label: 'KI-Anweisung (ersetzt die Vorgabe)', type: 'textarea' },\n { key: 'skelett', label: 'Skelett (Zieltext als Markdown, {{…}} = freie Stellen)', type: 'textarea' },\n { key: 'selector', label: 'Auswahl (#muss ~bevorzugt -nicht)', type: 'text' },\n ],\n },\n },\n { id: 'ai-brief', config: { ...SLOT_AI } },\n ],\n },\n {\n id: KIND.source,\n label: label.source,\n icon: 'i-lucide-file-search',\n titlePlaceholder: 'Titel der Quelle',\n descriptionPlaceholder: '',\n capabilities: [\n { id: 'container', config: {} },\n BASKET,\n TITLE_HIDDEN,\n {\n id: 'properties',\n config: {\n definitions: [\n { key: 'sourceType', label: 'Quelltyp', type: 'text' },\n { key: 'date', label: 'Datum', type: 'text' },\n ],\n },\n },\n ],\n chrome: { tags: false },\n },\n {\n id: KIND.fact,\n label: label.fact,\n icon: 'i-lucide-lightbulb',\n titlePlaceholder: '',\n descriptionPlaceholder: '',\n capabilities: [BASKET, TITLE_HIDDEN, { id: 'properties', config: { definitions: [slotTarget, DERIVATION] } }],\n chrome: { tags: false },\n },\n {\n id: KIND.example,\n label: label.example,\n icon: 'i-lucide-quote',\n titlePlaceholder: label.example,\n descriptionPlaceholder: '',\n capabilities: [\n BASKET,\n TITLE_HIDDEN,\n {\n id: 'properties',\n config: {\n definitions: [\n slotTarget,\n { key: 'status', label: 'Status', type: 'select', options: [\n { value: 'vorschlag', label: 'Vorschlag' },\n { value: 'geprüft', label: 'Geprüft' },\n ] },\n // Written by the split, shown as the proposal chip of the title row (`composeFeatures`) while `status` is `vorschlag`;\n // the properties form has no read-only field, so both are hidden from it and removed when the proposal is accepted.\n { key: 'confidence', label: 'Sicherheit', type: 'text', hidden: true },\n { key: 'origin', label: 'Herkunft der Zuordnung', type: 'text', hidden: true },\n DERIVATION,\n ],\n },\n },\n ],\n },\n ]\n}\n\n/** `blockType` with the `ai-brief` capability's purpose (and, when given, instructions) replaced; the other brief settings stay. Pure. */\nexport function withAiBrief(blockType: BlockType, brief: { purpose: string; instructions?: string }): BlockType {\n const config = (existing: unknown) => ({\n ...(existing && typeof existing === 'object' ? existing : {}),\n purpose: brief.purpose,\n ...(brief.instructions !== undefined && { instructions: brief.instructions }),\n })\n const listed = blockType.capabilities.some(capability => capability.id === 'ai-brief')\n return {\n ...blockType,\n capabilities: listed\n ? blockType.capabilities.map(capability => (capability.id === 'ai-brief' ? { ...capability, config: config(capability.config) } : capability))\n : [...blockType.capabilities, { id: 'ai-brief', config: config(undefined) }],\n }\n}\n\n/** `blockType` with `definitions` added to its `properties` capability; a definition whose key exists replaces it in place. Pure. */\nexport function withProperties(blockType: BlockType, definitions: readonly PropertyDefinition[]): BlockType {\n const merge = (existing: unknown): { definitions: PropertyDefinition[] } => {\n const current = (existing as { definitions?: readonly PropertyDefinition[] } | undefined)?.definitions ?? []\n const replaced = current.map(definition => definitions.find(added => added.key === definition.key) ?? definition)\n const added = definitions.filter(definition => !current.some(known => known.key === definition.key))\n return { definitions: [...replaced, ...added] }\n }\n const listed = blockType.capabilities.some(capability => capability.id === 'properties')\n return {\n ...blockType,\n capabilities: listed\n ? blockType.capabilities.map(capability => (capability.id === 'properties' ? { ...capability, config: merge(capability.config) } : capability))\n : [...blockType.capabilities, { id: 'properties', config: merge(undefined) }],\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { ComposeBlockTypeOptions, ComposeKind, composeBlockTypes, withAiBrief, withProperties } from "./block-types.js";
2
+ import { KIND } from "@skaile/forge-compose/documents";
3
+ export { type ComposeBlockTypeOptions, type ComposeKind, KIND, composeBlockTypes, withAiBrief, withProperties };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { composeBlockTypes, withAiBrief, withProperties } from "./block-types.js";
2
+ import { KIND } from "@skaile/forge-compose/documents";
3
+ export { KIND, composeBlockTypes, withAiBrief, withProperties };
@@ -0,0 +1,45 @@
1
+ import { ComposeOperations } from "./operations.js";
2
+ import { z } from "zod";
3
+ import { DefineServerActionArgs, ServerActionAudience, ServerActionContext, createServerActionDefiner } from "@skaile/forge-app-actions/server";
4
+ //#region src/server/actions.d.ts
5
+ /** The app's action definer, `createServerActionDefiner<P, D>()`. */
6
+ export type ComposeActionDefiner<P, D> = ReturnType<typeof createServerActionDefiner<P, D>>;
7
+ export interface ComposeActionOptions<P, D> {
8
+ /** The host's authorization, run by each action after its input parsed and before the operation. Throw to deny; an error with a numeric `statusCode` keeps its status. */
9
+ authorize: NonNullable<DefineServerActionArgs<string, z.ZodType, P, D, unknown>['authorize']>;
10
+ /** The `user` the operations read ports with, derived from the action's context (usually its `principal`). */
11
+ userOf(ctx: ServerActionContext<P, D>): object;
12
+ /** Who is offered the actions. Default `['user']`. */
13
+ audience?: readonly ServerActionAudience[];
14
+ }
15
+ /**
16
+ * The five compose operations as server actions `compose.material`, `compose.split`, `compose.facts`, `compose.example` and
17
+ * `compose.instance`. `define` is the app's `createServerActionDefiner<P, D>()`. A `ComposeError` thrown by an operation carries
18
+ * `statusCode`, so `invokeAction` answers its status and message.
19
+ */
20
+ export declare function composeActions<P, D>(define: ComposeActionDefiner<P, D>, ops: ComposeOperations, o: ComposeActionOptions<P, D>): readonly [import("@skaile/forge-app-actions/server").ServerAction<"compose.material", {
21
+ document: string;
22
+ slot: string;
23
+ }, import("@skaile/forge-compose/documents").Material>, import("@skaile/forge-app-actions/server").ServerAction<"compose.split", {
24
+ document: string;
25
+ block: string;
26
+ }, {
27
+ nodes: import("@skaile/forge-compose/documents").DocNode[];
28
+ }>, import("@skaile/forge-app-actions/server").ServerAction<"compose.facts", {
29
+ document: string;
30
+ block: string;
31
+ }, {
32
+ nodes: import("@skaile/forge-compose/documents").DocNode[];
33
+ }>, import("@skaile/forge-app-actions/server").ServerAction<"compose.example", {
34
+ document: string;
35
+ block: string;
36
+ }, {
37
+ libraryId: string;
38
+ blockId: string;
39
+ }>, import("@skaile/forge-app-actions/server").ServerAction<"compose.instance", {
40
+ templateId: string;
41
+ instanceId: string;
42
+ force?: boolean | undefined;
43
+ }, import("@skaile/forge-compose/documents").InstanceReport>];
44
+ //#endregion
45
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { composeInputs } from "./inputs.js";
2
+ //#region src/server/actions.ts
3
+ /**
4
+ * The five compose operations as server actions `compose.material`, `compose.split`, `compose.facts`, `compose.example` and
5
+ * `compose.instance`. `define` is the app's `createServerActionDefiner<P, D>()`. A `ComposeError` thrown by an operation carries
6
+ * `statusCode`, so `invokeAction` answers its status and message.
7
+ */
8
+ function composeActions(define, ops, o) {
9
+ const audience = o.audience ?? ["user"];
10
+ const { authorize, userOf } = o;
11
+ return [
12
+ define({
13
+ id: "compose.material",
14
+ description: "Material eines Slots: Fakten und passende Beispiele",
15
+ input: composeInputs.material,
16
+ kind: "query",
17
+ audience,
18
+ authorize,
19
+ run: (input, ctx) => ops.material(input, userOf(ctx))
20
+ }),
21
+ define({
22
+ id: "compose.split",
23
+ description: "Quelle in Beispielvorschläge zerlegen",
24
+ input: composeInputs.split,
25
+ kind: "effect",
26
+ audience,
27
+ authorize,
28
+ run: (input, ctx) => ops.split(input, userOf(ctx))
29
+ }),
30
+ define({
31
+ id: "compose.facts",
32
+ description: "Stichpunkte aus einer Quelle erzeugen",
33
+ input: composeInputs.facts,
34
+ kind: "effect",
35
+ audience,
36
+ authorize,
37
+ run: (input, ctx) => ops.facts(input, userOf(ctx))
38
+ }),
39
+ define({
40
+ id: "compose.example",
41
+ description: "Slot als Beispiel in die Bibliothek übernehmen",
42
+ input: composeInputs.example,
43
+ kind: "effect",
44
+ audience,
45
+ authorize,
46
+ run: (input, ctx) => ops.example(input, userOf(ctx))
47
+ }),
48
+ define({
49
+ id: "compose.instance",
50
+ description: "Instanz aus einer Vorlage anlegen",
51
+ input: composeInputs.instance,
52
+ kind: "effect",
53
+ audience,
54
+ authorize,
55
+ run: (input, ctx) => ops.instance(input, userOf(ctx))
56
+ })
57
+ ];
58
+ }
59
+ //#endregion
60
+ export { composeActions };
61
+
62
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","names":[],"sources":["../../src/server/actions.ts"],"sourcesContent":["// The forge-app-actions projection of the compose operations, for hosts with an action catalogue. Registers nothing and mounts no\n// route: the host puts the returned actions into its catalogue (`serverActionRegistry.registerAll`) or behind its own route.\nimport type {\n DefineServerActionArgs,\n ServerActionAudience,\n ServerActionContext,\n createServerActionDefiner,\n} from '@skaile/forge-app-actions/server'\nimport type { z } from 'zod'\nimport { composeInputs } from './inputs'\nimport type { ComposeOperations } from './operations'\n\n/** The app's action definer, `createServerActionDefiner<P, D>()`. */\nexport type ComposeActionDefiner<P, D> = ReturnType<typeof createServerActionDefiner<P, D>>\n\nexport interface ComposeActionOptions<P, D> {\n /** The host's authorization, run by each action after its input parsed and before the operation. Throw to deny; an error with a numeric `statusCode` keeps its status. */\n authorize: NonNullable<DefineServerActionArgs<string, z.ZodType, P, D, unknown>['authorize']>\n /** The `user` the operations read ports with, derived from the action's context (usually its `principal`). */\n userOf(ctx: ServerActionContext<P, D>): object\n /** Who is offered the actions. Default `['user']`. */\n audience?: readonly ServerActionAudience[]\n}\n\n/**\n * The five compose operations as server actions `compose.material`, `compose.split`, `compose.facts`, `compose.example` and\n * `compose.instance`. `define` is the app's `createServerActionDefiner<P, D>()`. A `ComposeError` thrown by an operation carries\n * `statusCode`, so `invokeAction` answers its status and message.\n */\nexport function composeActions<P, D>(\n define: ComposeActionDefiner<P, D>,\n ops: ComposeOperations,\n o: ComposeActionOptions<P, D>,\n) {\n const audience = o.audience ?? ['user']\n const { authorize, userOf } = o\n return [\n define({\n id: 'compose.material',\n description: 'Material eines Slots: Fakten und passende Beispiele',\n input: composeInputs.material,\n kind: 'query',\n audience,\n authorize,\n run: (input, ctx) => ops.material(input, userOf(ctx)),\n }),\n define({\n id: 'compose.split',\n description: 'Quelle in Beispielvorschläge zerlegen',\n input: composeInputs.split,\n kind: 'effect',\n audience,\n authorize,\n run: (input, ctx) => ops.split(input, userOf(ctx)),\n }),\n define({\n id: 'compose.facts',\n description: 'Stichpunkte aus einer Quelle erzeugen',\n input: composeInputs.facts,\n kind: 'effect',\n audience,\n authorize,\n run: (input, ctx) => ops.facts(input, userOf(ctx)),\n }),\n define({\n id: 'compose.example',\n description: 'Slot als Beispiel in die Bibliothek übernehmen',\n input: composeInputs.example,\n kind: 'effect',\n audience,\n authorize,\n run: (input, ctx) => ops.example(input, userOf(ctx)),\n }),\n define({\n id: 'compose.instance',\n description: 'Instanz aus einer Vorlage anlegen',\n input: composeInputs.instance,\n kind: 'effect',\n audience,\n authorize,\n run: (input, ctx) => ops.instance(input, userOf(ctx)),\n }),\n ] as const\n}\n"],"mappings":";;;;;;;AA6BA,SAAgB,eACd,QACA,KACA,GACA;CACA,MAAM,WAAW,EAAE,YAAY,CAAC,MAAM;CACtC,MAAM,EAAE,WAAW,WAAW;CAC9B,OAAO;EACL,OAAO;GACL,IAAI;GACJ,aAAa;GACb,OAAO,cAAc;GACrB,MAAM;GACN;GACA;GACA,MAAM,OAAO,QAAQ,IAAI,SAAS,OAAO,OAAO,GAAG,CAAC;EACtD,CAAC;EACD,OAAO;GACL,IAAI;GACJ,aAAa;GACb,OAAO,cAAc;GACrB,MAAM;GACN;GACA;GACA,MAAM,OAAO,QAAQ,IAAI,MAAM,OAAO,OAAO,GAAG,CAAC;EACnD,CAAC;EACD,OAAO;GACL,IAAI;GACJ,aAAa;GACb,OAAO,cAAc;GACrB,MAAM;GACN;GACA;GACA,MAAM,OAAO,QAAQ,IAAI,MAAM,OAAO,OAAO,GAAG,CAAC;EACnD,CAAC;EACD,OAAO;GACL,IAAI;GACJ,aAAa;GACb,OAAO,cAAc;GACrB,MAAM;GACN;GACA;GACA,MAAM,OAAO,QAAQ,IAAI,QAAQ,OAAO,OAAO,GAAG,CAAC;EACrD,CAAC;EACD,OAAO;GACL,IAAI;GACJ,aAAa;GACb,OAAO,cAAc;GACrB,MAAM;GACN;GACA;GACA,MAAM,OAAO,QAAQ,IAAI,SAAS,OAAO,OAAO,GAAG,CAAC;EACtD,CAAC;CACH;AACF"}
@@ -0,0 +1,26 @@
1
+ import { Generate } from "@skaile/forge-compose";
2
+ import { ModelProvider } from "@skaile/forge-editor/server";
3
+ import { ModelRef } from "@skaile/forge-editor/ai/protocol";
4
+ //#region src/server/ai-sdk.d.ts
5
+ /** One model call as `onRun` reports it, for the host's run log. */
6
+ export interface ComposeRun {
7
+ stage: string;
8
+ /** The model id, as the language model names itself. */
9
+ model: string;
10
+ /** First 16 hex characters of the SHA-256 of `system + prompt`; identifies the prompt without storing it. */
11
+ promptHash: string;
12
+ ok: boolean;
13
+ ms: number;
14
+ error?: string;
15
+ }
16
+ export interface AiSdkGenerateOptions {
17
+ models: ModelProvider;
18
+ ref: ModelRef;
19
+ /** What the host's `authenticate` returned; `null` for a call without a user. */
20
+ user: object | null;
21
+ /** Called once per model call, after it succeeded or failed (a failure is rethrown afterwards). */
22
+ onRun?(run: ComposeRun): void;
23
+ }
24
+ export declare function aiSdkGenerate(o: AiSdkGenerateOptions): Generate;
25
+ //#endregion
26
+ //# sourceMappingURL=ai-sdk.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { ComposeError } from "./errors.js";
2
+ import { Output, generateText } from "ai";
3
+ //#region src/server/ai-sdk.ts
4
+ async function promptHashOf(text) {
5
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
6
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 16);
7
+ }
8
+ function aiSdkGenerate(o) {
9
+ return async ({ stage, system, prompt, schema, signal }) => {
10
+ const model = await o.models.languageModel?.(o.ref, o.user) ?? null;
11
+ if (!model) throw new ComposeError(503, "Kein Modell");
12
+ const started = Date.now();
13
+ const run = async (ok, error) => {
14
+ if (!o.onRun) return;
15
+ o.onRun({
16
+ stage,
17
+ model: typeof model === "string" ? model : model.modelId,
18
+ promptHash: await promptHashOf((system ?? "") + prompt),
19
+ ok,
20
+ ms: Date.now() - started,
21
+ ...error !== void 0 && { error: error instanceof Error ? error.message : String(error) }
22
+ });
23
+ };
24
+ try {
25
+ const answer = (await generateText({
26
+ model,
27
+ abortSignal: signal,
28
+ ...system !== void 0 && { system },
29
+ output: Output.object({ schema }),
30
+ messages: [{
31
+ role: "user",
32
+ content: prompt
33
+ }]
34
+ })).output;
35
+ await run(true);
36
+ return answer;
37
+ } catch (error) {
38
+ await run(false, error);
39
+ throw error;
40
+ }
41
+ };
42
+ }
43
+ //#endregion
44
+ export { aiSdkGenerate };
45
+
46
+ //# sourceMappingURL=ai-sdk.js.map