@skaile/forge-editor-compose 0.3.0 → 0.4.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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-11-05
4
+
5
+ ### Changed
6
+
7
+ - **Breaking:** `ComposeOptions.sameOwner(documentId, caseDocumentId, user)` also receives the case document of the `material` request, so the host can answer „same person as this case?“ without ambient state.
8
+
9
+ ### Added
10
+
11
+ - `ComposeContext.previousIds`: earlier documents of the same person, newest first. `material` returns the block of the same slot and element from the first of them as `previous` (forge-compose `previousVersionOf`); `materialToData` labels it „Vorfassung – fortschreiben, nicht übernehmen“, the chat tool `compose_material` answers it as `vorfassung`.
12
+ - Requires `@skaile/forge-compose` ^0.4.0.
13
+
3
14
  ## 0.3.0 - 2026-11-05
4
15
 
5
16
  ### Changed
package/README.md CHANGED
@@ -64,10 +64,10 @@ const ops = composeOperations({
64
64
  hierarchy: icdHierarchy,
65
65
  async contextOf(documentId, user) { // null = unknown document → ComposeError(404)
66
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'], snippetIds: ['bausteine'] }
67
+ return c && { case: { tags: c.tags, data: c.data }, templateId: c.templateId, sourceIds: c.sourceIds, exampleIds: ['beispiele'], snippetIds: ['bausteine'], previousIds: c.earlierDocumentsOfThePerson }
68
68
  },
69
69
  snippetKind: 'baustein', // block kind of the snippets in `snippetIds`; default 'snippet'
70
- // sameOwner: (documentId, user) => … // optional: the `person` signal (same person as the case)
70
+ // sameOwner: (documentId, caseDocumentId, user) => … // optional: the `person` signal (same person as the case of caseDocumentId)
71
71
  segmentation: {
72
72
  rules: [{ pattern: /^Therapie/i, id: 'therapie' }], // heading → slot (confidence 1, origin `rule`)
73
73
  tagRules: [], // text → tag, e.g. `icdAnchorRule`
@@ -94,7 +94,7 @@ export default defineEventHandler(async event => {
94
94
  })
95
95
  ```
96
96
 
97
- **Operations** – `material` (`{ document, block }`: the `slot` block `block` of the instance `document` → `materialFor` of forge-compose: the facts of the case's sources for its identifier, the best snippets (`snippetIds`, blocks of `snippetKind`: content to adopt) and examples (`exampleIds`: style only), each with `score` and `parts`. Library entries are filtered (same slot, accepted, their `nur`/`nie` condition against the case tags ∪ the block's `meta.element`) and ranked by the weighted signals `tags`, `element` (the block's `meta.element`), `fall` (made from `document`), `person` (`sameOwner`, asked per origin document), length and the `inhalt` scorers `scorers`, default `[embeddingScorer]` (0 without `ports.embeddings`); the template variant the block was made from (`templateSlotOf`, by its `derivation`) is the slot those scorers see; a block that is missing or no `slot` is a 404), `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 with the case tags ∪ its tags and element; 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).
97
+ **Operations** – `material` (`{ document, block }`: the `slot` block `block` of the instance `document` → `materialFor` of forge-compose: the facts of the case's sources for its identifier, the previous version (the block of the same slot and element in the first of `ComposeContext.previousIds`, the earlier documents of the same person, newest first: `Material.previous`, labelled „Vorfassung – fortschreiben, nicht übernehmen“ by `materialToData`), the best snippets (`snippetIds`, blocks of `snippetKind`: content to adopt) and examples (`exampleIds`: style only), each with `score` and `parts`. Library entries are filtered (same slot, accepted, their `nur`/`nie` condition against the case tags ∪ the block's `meta.element`) and ranked by the weighted signals `tags`, `element` (the block's `meta.element`), `fall` (made from `document`), `person` (`sameOwner(origin, document, user)`, asked per origin document), length and the `inhalt` scorers `scorers`, default `[embeddingScorer]` (0 without `ports.embeddings`); the template variant the block was made from (`templateSlotOf`, by its `derivation`) is the slot those scorers see; a block that is missing or no `slot` is a 404), `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 with the case tags ∪ its tags and element; 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).
98
98
 
99
99
  **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.
100
100
 
@@ -15,6 +15,12 @@ export interface ComposeContext {
15
15
  exampleIds: string[] | 'all';
16
16
  /** Documents searched for snippet blocks (`snippetKind`: content to adopt). Default none. */
17
17
  snippetIds?: string[];
18
+ /**
19
+ * Earlier documents of the same person as the case, newest first: the block of the same slot and element in the first of
20
+ * them that has one is the previous version (`Material.previous`, „Vorfassung – fortschreiben, nicht übernehmen“). Default
21
+ * none. Never a person key: document ids only.
22
+ */
23
+ previousIds?: string[];
18
24
  }
19
25
  export interface ComposeOptions {
20
26
  /** `documents` is required (`putDocument` for `example` and `instance`); `embeddings` is reused as the pipeline's and the scorers' `Embedder` when present. */
@@ -50,18 +56,19 @@ export interface ComposeOptions {
50
56
  /** Block kind of the snippets in `snippetIds`; default `'snippet'`. */
51
57
  snippetKind?: string;
52
58
  /**
53
- * Whether a document belongs to the same person as the case (the `person` signal of `material`, asked for each origin
54
- * document of the library entries); absent = the signal is 0.
59
+ * Whether `documentId` belongs to the same person as the case of `caseDocumentId` (the `person` signal of `material`,
60
+ * asked for each origin document of the library entries); absent = the signal is 0.
55
61
  */
56
- sameOwner?: (documentId: string, user: object) => Promise<boolean> | boolean;
62
+ sameOwner?: (documentId: string, caseDocumentId: string, user: object) => Promise<boolean> | boolean;
57
63
  /** System message of the `facts` operation; default `FACTS_SYSTEM` (domain-neutral). A host with a domain passes its own. */
58
64
  factsSystem?: string;
59
65
  }
60
66
  export interface ComposeOperations {
61
67
  /**
62
- * Facts, ranked snippets and ranked examples for the `slot` block `block` of `document` (`materialFor`): the block's
63
- * identifier picks facts and library entries, its `meta.element` ranks by the `element` signal, `document` by `fall`, the
64
- * template variant it was made from (`templateSlotOf`) is the slot the injected scorers see.
68
+ * Facts, the previous version, ranked snippets and ranked examples for the `slot` block `block` of `document`
69
+ * (`materialFor`): the block's identifier picks facts and library entries, its `meta.element` ranks by the `element` signal,
70
+ * `document` by `fall`, the template variant it was made from (`templateSlotOf`) is the slot the injected scorers see; the
71
+ * previous version comes from `ComposeContext.previousIds`.
65
72
  */
66
73
  material(input: ComposeInput<'material'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'material'>>;
67
74
  split(input: ComposeInput<'split'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'split'>>;
@@ -108,15 +108,22 @@ function composeOperations(o) {
108
108
  async function snippetsOfCase(context) {
109
109
  return (await Promise.all((context.snippetIds ?? []).map(async (id) => examplesOf(await documents.loadContent(id), id, snippetKind)))).flat();
110
110
  }
111
- /** `o.sameOwner` asked once per origin document of `entries`, as the synchronous predicate `materialFor` takes. */
112
- async function ownerCheckOf(entries, user) {
111
+ /** `o.sameOwner` asked once per origin document of `entries` for the case of `caseDocumentId`, as the synchronous predicate `materialFor` takes. */
112
+ async function ownerCheckOf(entries, caseDocumentId, user) {
113
113
  const sameOwner = o.sameOwner;
114
114
  if (!sameOwner) return void 0;
115
115
  const ids = [...new Set(entries.flatMap((entry) => entry.origin ? [entry.origin.document] : []))];
116
- const answers = await Promise.all(ids.map((id) => sameOwner(id, user)));
116
+ const answers = await Promise.all(ids.map((id) => sameOwner(id, caseDocumentId, user)));
117
117
  const owned = new Set(ids.filter((_, index) => answers[index]));
118
118
  return (documentId) => owned.has(documentId);
119
119
  }
120
+ /** The earlier documents of the case (`previousIds`) with their content, in the given order. */
121
+ async function previousOfCase(context) {
122
+ return Promise.all((context.previousIds ?? []).map(async (id) => ({
123
+ id,
124
+ content: await documents.loadContent(id)
125
+ })));
126
+ }
120
127
  /** The block `blockId` of `documentId` as a typed block of `kind`; `ComposeError(404)` otherwise. */
121
128
  async function blockOf(documentId, blockId, kind) {
122
129
  const node = findBlockNode(await documentOf(documentId), blockId);
@@ -154,10 +161,11 @@ function composeOperations(o) {
154
161
  async material({ document, block }, user, signal) {
155
162
  const context = await contextFor(document, user);
156
163
  const node = await blockOf(document, block, KIND.slot);
157
- const [facts, examples, snippets, template] = await Promise.all([
164
+ const [facts, examples, snippets, previous, template] = await Promise.all([
158
165
  factsOfCase(context),
159
166
  examplesOfCase(context),
160
167
  snippetsOfCase(context),
168
+ previousOfCase(context),
161
169
  documents.loadContent(context.templateId)
162
170
  ]);
163
171
  const meta = compositeMeta(node);
@@ -173,10 +181,11 @@ function composeOperations(o) {
173
181
  ...slotBlock && { slot: slotBlock }
174
182
  };
175
183
  const embedder = embedderFor(user);
176
- const sameOwner = await ownerCheckOf([...examples, ...snippets], user);
184
+ const sameOwner = await ownerCheckOf([...examples, ...snippets], document, user);
177
185
  return materialFor(target, context.case, facts, {
178
186
  examples,
179
- snippets
187
+ snippets,
188
+ previous
180
189
  }, {
181
190
  scorers,
182
191
  ...h && { h },
@@ -1 +1 @@
1
- {"version":3,"file":"operations.js","names":[],"sources":["../../src/server/operations.ts"],"sourcesContent":["// The domain operations of the compose editor: material, split, facts, example, instance and classify. One function each,\n// framework-free: routes, forge-app-actions definitions and chat tools are projections of them. Authorization is the host's\n// (its action `authorize` or route check runs before); the operations trust `user` for port reads only. Model access goes\n// through the host's `generate`; every failure the caller can act on is a `ComposeError`.\nimport type { JSONContent } from '@tiptap/core'\nimport type { EditorPorts } from '@skaile/forge-editor/server'\nimport { gate } from '@skaile/forge-compose'\nimport type { CaseProfile, Generate, Hierarchy, Selector } from '@skaile/forge-compose'\nimport {\n KIND, META, METHOD, STATUS, TYPED_BLOCK, blockIdOf, bodyOf, bodyText, compositeMeta, compositeTags, compositeTitle,\n compositesOfKind, exampleFromProposal, exampleFromSlot, examplesOf, factNode, factsOf, instanceFromTemplate, kindOf,\n materialFor, plainText, slotsOf, templateSlotOf,\n} from '@skaile/forge-compose/documents'\nimport type { DocNode, ExampleBlock, FactBlock, MaterialTarget, SlotBlock } from '@skaile/forge-compose/documents'\nimport {\n defaultPipeline, embeddingScorer, lengthScorer, proposals, scoreAll, segment, tagFitScorer,\n} from '@skaile/forge-compose/segment'\nimport type { Decision, Embedder, RuleSpec, Scorer, SegmentPipeline, Splitter } from '@skaile/forge-compose/segment'\nimport { catalogOf } from './catalog'\nimport { ComposeError } from './errors'\nimport { buildFactsPrompt, FactsReplySchema, parseFactsReply } from './facts-prompt'\nimport type { CatalogEntry, FactDraft, FactsReply } from './facts-prompt'\nimport type { ComposeInput, ComposeOutput } from './inputs'\n\n/** What the host knows about the case a document belongs to. */\nexport interface ComposeContext {\n case: CaseProfile\n /** Document that holds the slots. */\n templateId: string\n /** Documents whose `fact`/`source` blocks belong to the case. */\n sourceIds: string[]\n /** Documents searched for `example` blocks (style only). */\n exampleIds: string[] | 'all'\n /** Documents searched for snippet blocks (`snippetKind`: content to adopt). Default none. */\n snippetIds?: string[]\n}\n\nexport interface ComposeOptions {\n /** `documents` is required (`putDocument` for `example` and `instance`); `embeddings` is reused as the pipeline's and the scorers' `Embedder` when present. */\n ports: Pick<EditorPorts, 'documents'> & Partial<Pick<EditorPorts, 'embeddings' | 'index'>>\n /** The host's model seam; every model call of the operations goes through it. */\n generate: Generate\n /** The case of a document (a case document, a source of a case, an instance); `null` = unknown document, answered as `ComposeError(404)`. */\n contextOf(documentId: string, user: object): Promise<ComposeContext | null>\n /** The document „Als Beispiel übernehmen“ appends to; created when missing. */\n exampleLibraryId: string\n hierarchy?: Hierarchy\n /**\n * Merged over `defaultPipeline({ embedder: ports.embeddings, generate: llm ? generate : undefined, rules, tagRules })`: a\n * given `splitter`, `merge`, `slotLabelers`, `tagLabelers` or `resolve` replaces the default one (a host that passes\n * `slotLabelers` replaces the default list entirely; an explicit `merge: undefined` switches merging off).\n */\n segmentation?: Partial<SegmentPipeline> & {\n rules?: RuleSpec[]\n tagRules?: RuleSpec[]\n /** Default `true`: `llmLabeler({ generate })` as the last slot and tag labeler. */\n llm?: boolean\n /** The splitter for one source (by its `meta`, e.g. `sourceType`); `null` = the pipeline's. */\n splitterFor?(source: { meta: Record<string, unknown> }): Splitter | null\n }\n /**\n * Extra scorers of the `inhalt` signal, added to the built-in signals of `material` (`tags`, `element`, `fall`, `person`,\n * length); `compose_examples` ranks with `[tagFitScorer(hierarchy), ...scorers, lengthScorer]`. Default `[embeddingScorer]`,\n * which scores 0 without `ports.embeddings`.\n */\n scorers?: Scorer[]\n /** Block kind of the snippets in `snippetIds`; default `'snippet'`. */\n snippetKind?: string\n /**\n * Whether a document belongs to the same person as the case (the `person` signal of `material`, asked for each origin\n * document of the library entries); absent = the signal is 0.\n */\n sameOwner?: (documentId: string, user: object) => Promise<boolean> | boolean\n /** System message of the `facts` operation; default `FACTS_SYSTEM` (domain-neutral). A host with a domain passes its own. */\n factsSystem?: string\n}\n\nexport interface ComposeOperations {\n /**\n * Facts, ranked snippets and ranked examples for the `slot` block `block` of `document` (`materialFor`): the block's\n * identifier picks facts and library entries, its `meta.element` ranks by the `element` signal, `document` by `fall`, the\n * template variant it was made from (`templateSlotOf`) is the slot the injected scorers see.\n */\n material(input: ComposeInput<'material'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'material'>>\n split(input: ComposeInput<'split'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'split'>>\n facts(input: ComposeInput<'facts'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'facts'>>\n example(input: ComposeInput<'example'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'example'>>\n instance(input: ComposeInput<'instance'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'instance'>>\n /** Which slot and tags a text belongs to, by the configured labelers. Not an operation id: chat only, read only. */\n classify(input: { document: string; text: string }, user: object, signal?: AbortSignal): Promise<Decision>\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n// Source text\n\nconst BOLD_HEADING_CHARS = 120\n\nconst isBold = (node: DocNode): boolean =>\n Array.isArray(node.marks) && node.marks.some(mark => typeof mark === 'object' && mark !== null && 'type' in mark && mark.type === 'bold')\n\n/** A one-line paragraph that is bold throughout: how sources written by the import mark their section headings. */\nfunction isHeadingLine(node: DocNode): boolean {\n const content = node.content ?? []\n return content.length > 0 && content.every(child => child.type === 'text' && isBold(child)) && plainText(node).trim().length <= BOLD_HEADING_CHARS\n}\n\nfunction blockMarkdown(node: DocNode): string {\n switch (node.type) {\n case 'heading': {\n const text = plainText(node).trim()\n const level = Math.min(3, Math.max(1, Number(node.attrs?.level) || 1))\n return text ? `${'#'.repeat(level)} ${text}` : ''\n }\n case 'paragraph': {\n const text = plainText(node).trim()\n return text && isHeadingLine(node) ? `## ${text}` : text\n }\n case 'bulletList':\n case 'orderedList':\n return (node.content ?? [])\n .map((item, index) => ({ text: plainText(item).replace(/\\s*\\n+\\s*/g, ' ').trim(), index }))\n .filter(item => item.text)\n .map(item => `${node.type === 'orderedList' ? `${item.index + 1}.` : '-'} ${item.text}`)\n .join('\\n')\n case TYPED_BLOCK:\n return markdownOf(bodyOf(node))\n default:\n return plainText(node).trim()\n }\n}\n\n/**\n * Blocks as the Markdown the splitters read: `heading` nodes and bold one-line paragraphs become `#` lines (so\n * `structuralSplitter` sees the sections), lists become list lines, everything else plain paragraphs separated by a blank line.\n */\nfunction markdownOf(nodes: readonly DocNode[]): string {\n return nodes.map(blockMarkdown).filter(text => text.trim()).join('\\n\\n')\n}\n\nasync function sha256Hex(text: string): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))\n return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('')\n}\n\n/** The typed block with `id`, wherever it nests; `null` when there is none. */\nfunction findBlockNode(doc: DocNode, id: string): DocNode | null {\n const walk = (nodes: readonly DocNode[] | undefined): DocNode | null => {\n for (const node of nodes ?? []) {\n if (node.type === TYPED_BLOCK && blockIdOf(node) === id) return node\n const inner = walk(node.content)\n if (inner) return inner\n }\n return null\n }\n return walk(doc.content)\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n// Support for the chat tools\n\n/** What `composeTools` reads besides the public operations; `composeOperations` registers it for the object it returns. */\nexport interface ComposeToolSupport {\n /** The content of a document as stored, `null` = no such document. */\n load(id: string): Promise<DocNode | null>\n /** The accepted examples of the case `document` belongs to, gated by `tags`, ranked by tag fit, the configured scorers and length, best first. */\n examples(\n input: { document: string; slot?: string; tags?: string[]; query?: string; limit?: number },\n user: object,\n signal?: AbortSignal,\n ): Promise<{ slot: string; tags: string[]; text: string; score: number; parts: Record<string, number> }[]>\n}\n\nconst support = new WeakMap<ComposeOperations, ComposeToolSupport>()\n\n/** The tool support of `ops`; `ops` must be what `composeOperations` returned. */\nexport function composeToolSupportOf(ops: ComposeOperations): ComposeToolSupport {\n const found = support.get(ops)\n if (!found) throw new Error('@skaile/forge-editor-compose: composeTools needs the object composeOperations returned')\n return found\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n\nconst asJson = (doc: DocNode): JSONContent => doc as JSONContent // DocNode is the structural twin of JSONContent\n\nconst emptySlot = (identifier: string): SlotBlock => ({\n id: '', identifier, title: identifier, description: '', tags: [], meta: {}, chapterId: null, node: {},\n})\n\n/** The model name recorded in the derivation of a proposal: what decided it. */\nconst modelOf = (origin: Decision['origin'], embedder: Embedder | undefined): string =>\n origin === 'embedding' && embedder ? embedder.model : (origin ?? 'none')\n\nexport function composeOperations(o: ComposeOptions): ComposeOperations {\n const { documents } = o.ports\n const h = o.hierarchy\n const scorers = o.scorers ?? [embeddingScorer]\n const snippetKind = o.snippetKind ?? 'snippet'\n\n const embedderFor = (user: object): Embedder | undefined => {\n const embeddings = o.ports.embeddings\n return embeddings && { model: embeddings.model, dim: embeddings.dim, embed: texts => embeddings.embed(texts, user) }\n }\n\n async function contextFor(documentId: string, user: object): Promise<ComposeContext> {\n const context = await o.contextOf(documentId, user)\n if (!context) throw new ComposeError(404, `Für das Dokument „${documentId}“ gibt es keinen Fall.`)\n return context\n }\n\n async function documentOf(id: string): Promise<DocNode> {\n const content = await documents.loadContent(id)\n if (!content) throw new ComposeError(404, `Das Dokument „${id}“ existiert nicht.`)\n return content\n }\n\n async function factsOfCase(context: ComposeContext): Promise<FactBlock[]> {\n const lists = await Promise.all(context.sourceIds.map(async id => factsOf(await documents.loadContent(id))))\n return lists.flat()\n }\n\n async function examplesOfCase(context: ComposeContext): Promise<ExampleBlock[]> {\n if (context.exampleIds === 'all') {\n const all = await documents.listContents()\n return all.flatMap(({ id, content }) => examplesOf(content, id))\n }\n const lists = await Promise.all(context.exampleIds.map(async id => examplesOf(await documents.loadContent(id), id)))\n return lists.flat()\n }\n\n async function snippetsOfCase(context: ComposeContext): Promise<ExampleBlock[]> {\n const lists = await Promise.all((context.snippetIds ?? []).map(async id => examplesOf(await documents.loadContent(id), id, snippetKind)))\n return lists.flat()\n }\n\n /** `o.sameOwner` asked once per origin document of `entries`, as the synchronous predicate `materialFor` takes. */\n async function ownerCheckOf(entries: readonly ExampleBlock[], user: object): Promise<((documentId: string) => boolean) | undefined> {\n const sameOwner = o.sameOwner\n if (!sameOwner) return undefined\n const ids = [...new Set(entries.flatMap(entry => (entry.origin ? [entry.origin.document] : [])))]\n const answers = await Promise.all(ids.map(id => sameOwner(id, user)))\n const owned = new Set(ids.filter((_, index) => answers[index]))\n return documentId => owned.has(documentId)\n }\n\n /** The block `blockId` of `documentId` as a typed block of `kind`; `ComposeError(404)` otherwise. */\n async function blockOf(documentId: string, blockId: string, kind: string): Promise<DocNode> {\n const node = findBlockNode(await documentOf(documentId), blockId)\n if (!node) throw new ComposeError(404, `Der Block „${blockId}“ existiert im Dokument „${documentId}“ nicht.`)\n if (kindOf(node) !== kind) throw new ComposeError(404, `Der Block „${blockId}“ ist kein Block vom Typ „${kind}“.`)\n return node\n }\n\n /** The pipeline: the default of the host's embedder and `generate`, the `segmentation` fields merged over it. */\n function pipelineOf(embedder: Embedder | undefined, source?: { meta: Record<string, unknown> }): SegmentPipeline {\n const { rules, tagRules, llm, splitterFor, ...override } = o.segmentation ?? {}\n const base = defaultPipeline({\n ...(embedder && { embedder }),\n ...(llm !== false && { generate: o.generate }),\n ...(rules && { rules }),\n ...(tagRules && { tagRules }),\n })\n const merge = 'merge' in override ? override.merge : base.merge\n const resolveOptions = 'resolve' in override ? override.resolve : base.resolve\n return {\n splitter: (source && splitterFor?.(source)) || override.splitter || base.splitter,\n slotLabelers: override.slotLabelers ?? base.slotLabelers,\n tagLabelers: override.tagLabelers ?? base.tagLabelers,\n ...(merge && { merge }),\n ...(resolveOptions && { resolve: resolveOptions }),\n }\n }\n\n /** `example` appends read-modify-write one after another, so two adoptions at once keep both. */\n let appending: Promise<unknown> = Promise.resolve()\n const exclusive = <T>(run: () => Promise<T>): Promise<T> => {\n const result = appending.then(run)\n appending = result.catch(() => undefined)\n return result\n }\n\n const ops: ComposeOperations = {\n async material({ document, block }, user, signal) {\n const context = await contextFor(document, user)\n const node = await blockOf(document, block, KIND.slot)\n const [facts, examples, snippets, template] = await Promise.all([\n factsOfCase(context),\n examplesOfCase(context),\n snippetsOfCase(context),\n documents.loadContent(context.templateId),\n ])\n const meta = compositeMeta(node)\n const identifier = meta[META.identifier]\n const rawElement = meta[META.element]\n const element = typeof rawElement === 'string' ? rawElement.trim() : ''\n const slotBlock = template ? templateSlotOf(node, template) : undefined\n const target: MaterialTarget = {\n identifier: typeof identifier === 'string' ? identifier : '',\n ...(element ? { element } : {}),\n tags: compositeTags(node),\n document,\n ...(slotBlock && { slot: slotBlock }),\n }\n const embedder = embedderFor(user)\n const sameOwner = await ownerCheckOf([...examples, ...snippets], user)\n return materialFor(target, context.case, facts, { examples, snippets }, {\n scorers,\n ...(h && { h }),\n ...(sameOwner && { sameOwner }),\n ctx: { ...(embedder && { embedder }), ...(signal && { signal }) },\n })\n },\n\n async split({ document, block }, user, signal) {\n const context = await contextFor(document, user)\n const source = await blockOf(document, block, KIND.source)\n const body = markdownOf(bodyOf(source))\n if (!body.trim()) throw new ComposeError(409, 'Die Quelle enthält keinen Text.')\n const [template, examples] = await Promise.all([documents.loadContent(context.templateId), examplesOfCase(context)])\n const embedder = embedderFor(user)\n const { units, decisions } = await segment(\n body,\n pipelineOf(embedder, { meta: compositeMeta(source) }),\n catalogOf(template, examples),\n { ...(embedder && { embedder }), ...(signal && { signal }) },\n )\n const inputHash = await sha256Hex(body)\n const now = new Date().toISOString()\n // A unit without slot stays a proposal (slot empty): nothing of the corpus is lost, the person assigns it.\n const nodes = proposals(units, decisions).map(proposal =>\n exampleFromProposal(proposal, {\n sourceBlockId: block,\n sourceDocumentId: document,\n caseTags: context.case.tags,\n model: modelOf(proposal.origin, embedder),\n now,\n inputHash,\n }),\n )\n return { nodes }\n },\n\n async facts({ document, block }, user, signal) {\n const context = await contextFor(document, user)\n const source = await blockOf(document, block, KIND.source)\n const body = markdownOf(bodyOf(source))\n if (!body.trim()) throw new ComposeError(409, 'Die Quelle enthält keinen Text.')\n const template = await documentOf(context.templateId)\n\n const chapters = new Map(compositesOfKind(template, KIND.section).map(section => [blockIdOf(section), compositeTitle(section)]))\n const seen = new Set<string>()\n const catalog: CatalogEntry[] = []\n for (const slot of slotsOf(template)) {\n if (!slot.identifier || seen.has(slot.identifier)) continue\n seen.add(slot.identifier)\n catalog.push({\n identifier: slot.identifier,\n chapter: (slot.chapterId && chapters.get(slot.chapterId)) || '',\n title: slot.title,\n description: slot.description,\n tags: slot.tags,\n })\n }\n\n const { system, prompt } = buildFactsPrompt(catalog, body, o.factsSystem)\n const reply = await o.generate<FactsReply>({ stage: 'facts', system, prompt, schema: FactsReplySchema, ...(signal && { signal }) })\n let drafts: FactDraft[]\n try {\n drafts = parseFactsReply(reply, seen)\n }\n catch (error) {\n throw new ComposeError(422, error instanceof Error ? error.message : 'Antwort des Modells unbrauchbar')\n }\n const inputHash = await sha256Hex(body)\n const now = new Date().toISOString()\n // The host's `generate` knows the model and logs it; it is not known here.\n const nodes = drafts.map(draft =>\n factNode({\n text: draft.text,\n slot: draft.slot,\n derivation: { from: { document, block }, method: METHOD.aiFact, createdAt: now, inputHash },\n }),\n )\n return { nodes }\n },\n\n async example({ document, block }, user) {\n const context = await contextFor(document, user)\n const slotNode = await blockOf(document, block, KIND.slot)\n const slot = slotsOf({ type: 'doc', content: [slotNode] })[0]\n if (!slot || !bodyText(slotNode).trim()) throw new ComposeError(409, 'Der Block hat keinen Text, der als Beispiel übernommen werden könnte.')\n if (!documents.putDocument) throw new ComposeError(503, 'Der Dokumentspeicher kann keine Dokumente schreiben.')\n const put = documents.putDocument.bind(documents)\n const node = exampleFromSlot(slot, {\n sourceDocumentId: document,\n caseTags: context.case.tags,\n now: new Date().toISOString(),\n })\n await exclusive(async () => {\n const library = (await documents.loadContent(o.exampleLibraryId)) ?? { type: 'doc', content: [] }\n await put(o.exampleLibraryId, asJson({ ...library, content: [...(library.content ?? []), node] }))\n })\n return { libraryId: o.exampleLibraryId, blockId: blockIdOf(node) }\n },\n\n async instance({ templateId, instanceId, force }, user) {\n if (!force && (await documents.loadContent(instanceId))) throw new ComposeError(409, 'Instanz existiert')\n const context = await contextFor(instanceId, user)\n const template = await documentOf(templateId)\n if (!documents.putDocument) throw new ComposeError(503, 'Der Dokumentspeicher kann keine Dokumente schreiben.')\n const { doc, chosen, dropped, unresolved, elements } = instanceFromTemplate(template, context.case, {\n templateId,\n now: new Date().toISOString(),\n ...(h && { h }),\n })\n await documents.putDocument(instanceId, asJson(doc))\n return { chosen, dropped, unresolved, elements }\n },\n\n async classify({ document, text }, user, signal) {\n if (!text.trim()) throw new ComposeError(400, 'Der Text ist leer.')\n const context = await contextFor(document, user)\n const [template, examples] = await Promise.all([documents.loadContent(context.templateId), examplesOfCase(context)])\n const embedder = embedderFor(user)\n // The whole text is one unit: no splitting, no merging, then the configured labelers and the resolver as in `split`.\n const single: Splitter = { name: 'single', split: () => [{ text, start: 0, end: text.length, level: 0, path: [] }] }\n const { merge: _merge, ...pipeline } = pipelineOf(embedder)\n const { decisions } = await segment(\n text,\n { ...pipeline, splitter: single },\n catalogOf(template, examples),\n { ...(embedder && { embedder }), ...(signal && { signal }) },\n )\n return decisions[0] ?? { unit: 0, slot: null, tags: [], confidence: 0, origin: null }\n },\n }\n\n support.set(ops, {\n load: id => documents.loadContent(id),\n\n async examples({ document, slot, tags, query, limit }, user, signal) {\n const context = await contextFor(document, user)\n const selector: Selector = { require: tags ?? [], prefer: [], exclude: [] }\n const candidates = (await examplesOfCase(context)).filter(\n example => example.status !== STATUS.proposal && (!slot || example.slot === slot) && gate(selector, example.tags, h),\n )\n if (candidates.length === 0) return []\n const template = await documents.loadContent(context.templateId)\n const slotBlock = slotsOf(template).find(candidate => candidate.identifier === slot) ?? emptySlot(slot ?? '')\n const facts: FactBlock[] = query?.trim() ? [{ id: '', slot: slotBlock.identifier, text: query.trim(), sourceTitle: '' }] : []\n // Asked for explicit tags, the case to measure against is those tags.\n const profile: CaseProfile = tags?.length ? { ...context.case, tags } : context.case\n const embedder = embedderFor(user)\n const scored = await scoreAll(candidates, { case: profile, slot: slotBlock, facts }, [tagFitScorer(h), ...scorers, lengthScorer], {\n ...(embedder && { embedder }),\n ...(signal && { signal }),\n })\n return candidates\n .map((example, index) => ({ example, index, result: scored[index] ?? { score: 0, parts: {} } }))\n .sort((a, b) => b.result.score - a.result.score || a.index - b.index)\n .slice(0, Math.max(1, limit ?? 5))\n .map(({ example, result }) => ({ slot: example.slot, tags: example.tags, text: example.text, score: result.score, parts: result.parts }))\n },\n })\n return ops\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"operations.js","names":[],"sources":["../../src/server/operations.ts"],"sourcesContent":["// The domain operations of the compose editor: material, split, facts, example, instance and classify. One function each,\n// framework-free: routes, forge-app-actions definitions and chat tools are projections of them. Authorization is the host's\n// (its action `authorize` or route check runs before); the operations trust `user` for port reads only. Model access goes\n// through the host's `generate`; every failure the caller can act on is a `ComposeError`.\nimport type { JSONContent } from '@tiptap/core'\nimport type { EditorPorts } from '@skaile/forge-editor/server'\nimport { gate } from '@skaile/forge-compose'\nimport type { CaseProfile, Generate, Hierarchy, Selector } from '@skaile/forge-compose'\nimport {\n KIND, META, METHOD, STATUS, TYPED_BLOCK, blockIdOf, bodyOf, bodyText, compositeMeta, compositeTags, compositeTitle,\n compositesOfKind, exampleFromProposal, exampleFromSlot, examplesOf, factNode, factsOf, instanceFromTemplate, kindOf,\n materialFor, plainText, slotsOf, templateSlotOf,\n} from '@skaile/forge-compose/documents'\nimport type { DocNode, ExampleBlock, FactBlock, MaterialTarget, PreviousDocument, SlotBlock } from '@skaile/forge-compose/documents'\nimport {\n defaultPipeline, embeddingScorer, lengthScorer, proposals, scoreAll, segment, tagFitScorer,\n} from '@skaile/forge-compose/segment'\nimport type { Decision, Embedder, RuleSpec, Scorer, SegmentPipeline, Splitter } from '@skaile/forge-compose/segment'\nimport { catalogOf } from './catalog'\nimport { ComposeError } from './errors'\nimport { buildFactsPrompt, FactsReplySchema, parseFactsReply } from './facts-prompt'\nimport type { CatalogEntry, FactDraft, FactsReply } from './facts-prompt'\nimport type { ComposeInput, ComposeOutput } from './inputs'\n\n/** What the host knows about the case a document belongs to. */\nexport interface ComposeContext {\n case: CaseProfile\n /** Document that holds the slots. */\n templateId: string\n /** Documents whose `fact`/`source` blocks belong to the case. */\n sourceIds: string[]\n /** Documents searched for `example` blocks (style only). */\n exampleIds: string[] | 'all'\n /** Documents searched for snippet blocks (`snippetKind`: content to adopt). Default none. */\n snippetIds?: string[]\n /**\n * Earlier documents of the same person as the case, newest first: the block of the same slot and element in the first of\n * them that has one is the previous version (`Material.previous`, „Vorfassung – fortschreiben, nicht übernehmen“). Default\n * none. Never a person key: document ids only.\n */\n previousIds?: string[]\n}\n\nexport interface ComposeOptions {\n /** `documents` is required (`putDocument` for `example` and `instance`); `embeddings` is reused as the pipeline's and the scorers' `Embedder` when present. */\n ports: Pick<EditorPorts, 'documents'> & Partial<Pick<EditorPorts, 'embeddings' | 'index'>>\n /** The host's model seam; every model call of the operations goes through it. */\n generate: Generate\n /** The case of a document (a case document, a source of a case, an instance); `null` = unknown document, answered as `ComposeError(404)`. */\n contextOf(documentId: string, user: object): Promise<ComposeContext | null>\n /** The document „Als Beispiel übernehmen“ appends to; created when missing. */\n exampleLibraryId: string\n hierarchy?: Hierarchy\n /**\n * Merged over `defaultPipeline({ embedder: ports.embeddings, generate: llm ? generate : undefined, rules, tagRules })`: a\n * given `splitter`, `merge`, `slotLabelers`, `tagLabelers` or `resolve` replaces the default one (a host that passes\n * `slotLabelers` replaces the default list entirely; an explicit `merge: undefined` switches merging off).\n */\n segmentation?: Partial<SegmentPipeline> & {\n rules?: RuleSpec[]\n tagRules?: RuleSpec[]\n /** Default `true`: `llmLabeler({ generate })` as the last slot and tag labeler. */\n llm?: boolean\n /** The splitter for one source (by its `meta`, e.g. `sourceType`); `null` = the pipeline's. */\n splitterFor?(source: { meta: Record<string, unknown> }): Splitter | null\n }\n /**\n * Extra scorers of the `inhalt` signal, added to the built-in signals of `material` (`tags`, `element`, `fall`, `person`,\n * length); `compose_examples` ranks with `[tagFitScorer(hierarchy), ...scorers, lengthScorer]`. Default `[embeddingScorer]`,\n * which scores 0 without `ports.embeddings`.\n */\n scorers?: Scorer[]\n /** Block kind of the snippets in `snippetIds`; default `'snippet'`. */\n snippetKind?: string\n /**\n * Whether `documentId` belongs to the same person as the case of `caseDocumentId` (the `person` signal of `material`,\n * asked for each origin document of the library entries); absent = the signal is 0.\n */\n sameOwner?: (documentId: string, caseDocumentId: string, user: object) => Promise<boolean> | boolean\n /** System message of the `facts` operation; default `FACTS_SYSTEM` (domain-neutral). A host with a domain passes its own. */\n factsSystem?: string\n}\n\nexport interface ComposeOperations {\n /**\n * Facts, the previous version, ranked snippets and ranked examples for the `slot` block `block` of `document`\n * (`materialFor`): the block's identifier picks facts and library entries, its `meta.element` ranks by the `element` signal,\n * `document` by `fall`, the template variant it was made from (`templateSlotOf`) is the slot the injected scorers see; the\n * previous version comes from `ComposeContext.previousIds`.\n */\n material(input: ComposeInput<'material'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'material'>>\n split(input: ComposeInput<'split'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'split'>>\n facts(input: ComposeInput<'facts'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'facts'>>\n example(input: ComposeInput<'example'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'example'>>\n instance(input: ComposeInput<'instance'>, user: object, signal?: AbortSignal): Promise<ComposeOutput<'instance'>>\n /** Which slot and tags a text belongs to, by the configured labelers. Not an operation id: chat only, read only. */\n classify(input: { document: string; text: string }, user: object, signal?: AbortSignal): Promise<Decision>\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n// Source text\n\nconst BOLD_HEADING_CHARS = 120\n\nconst isBold = (node: DocNode): boolean =>\n Array.isArray(node.marks) && node.marks.some(mark => typeof mark === 'object' && mark !== null && 'type' in mark && mark.type === 'bold')\n\n/** A one-line paragraph that is bold throughout: how sources written by the import mark their section headings. */\nfunction isHeadingLine(node: DocNode): boolean {\n const content = node.content ?? []\n return content.length > 0 && content.every(child => child.type === 'text' && isBold(child)) && plainText(node).trim().length <= BOLD_HEADING_CHARS\n}\n\nfunction blockMarkdown(node: DocNode): string {\n switch (node.type) {\n case 'heading': {\n const text = plainText(node).trim()\n const level = Math.min(3, Math.max(1, Number(node.attrs?.level) || 1))\n return text ? `${'#'.repeat(level)} ${text}` : ''\n }\n case 'paragraph': {\n const text = plainText(node).trim()\n return text && isHeadingLine(node) ? `## ${text}` : text\n }\n case 'bulletList':\n case 'orderedList':\n return (node.content ?? [])\n .map((item, index) => ({ text: plainText(item).replace(/\\s*\\n+\\s*/g, ' ').trim(), index }))\n .filter(item => item.text)\n .map(item => `${node.type === 'orderedList' ? `${item.index + 1}.` : '-'} ${item.text}`)\n .join('\\n')\n case TYPED_BLOCK:\n return markdownOf(bodyOf(node))\n default:\n return plainText(node).trim()\n }\n}\n\n/**\n * Blocks as the Markdown the splitters read: `heading` nodes and bold one-line paragraphs become `#` lines (so\n * `structuralSplitter` sees the sections), lists become list lines, everything else plain paragraphs separated by a blank line.\n */\nfunction markdownOf(nodes: readonly DocNode[]): string {\n return nodes.map(blockMarkdown).filter(text => text.trim()).join('\\n\\n')\n}\n\nasync function sha256Hex(text: string): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))\n return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('')\n}\n\n/** The typed block with `id`, wherever it nests; `null` when there is none. */\nfunction findBlockNode(doc: DocNode, id: string): DocNode | null {\n const walk = (nodes: readonly DocNode[] | undefined): DocNode | null => {\n for (const node of nodes ?? []) {\n if (node.type === TYPED_BLOCK && blockIdOf(node) === id) return node\n const inner = walk(node.content)\n if (inner) return inner\n }\n return null\n }\n return walk(doc.content)\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n// Support for the chat tools\n\n/** What `composeTools` reads besides the public operations; `composeOperations` registers it for the object it returns. */\nexport interface ComposeToolSupport {\n /** The content of a document as stored, `null` = no such document. */\n load(id: string): Promise<DocNode | null>\n /** The accepted examples of the case `document` belongs to, gated by `tags`, ranked by tag fit, the configured scorers and length, best first. */\n examples(\n input: { document: string; slot?: string; tags?: string[]; query?: string; limit?: number },\n user: object,\n signal?: AbortSignal,\n ): Promise<{ slot: string; tags: string[]; text: string; score: number; parts: Record<string, number> }[]>\n}\n\nconst support = new WeakMap<ComposeOperations, ComposeToolSupport>()\n\n/** The tool support of `ops`; `ops` must be what `composeOperations` returned. */\nexport function composeToolSupportOf(ops: ComposeOperations): ComposeToolSupport {\n const found = support.get(ops)\n if (!found) throw new Error('@skaile/forge-editor-compose: composeTools needs the object composeOperations returned')\n return found\n}\n\n// ---------------------------------------------------------------------------------------------------------------\n\nconst asJson = (doc: DocNode): JSONContent => doc as JSONContent // DocNode is the structural twin of JSONContent\n\nconst emptySlot = (identifier: string): SlotBlock => ({\n id: '', identifier, title: identifier, description: '', tags: [], meta: {}, chapterId: null, node: {},\n})\n\n/** The model name recorded in the derivation of a proposal: what decided it. */\nconst modelOf = (origin: Decision['origin'], embedder: Embedder | undefined): string =>\n origin === 'embedding' && embedder ? embedder.model : (origin ?? 'none')\n\nexport function composeOperations(o: ComposeOptions): ComposeOperations {\n const { documents } = o.ports\n const h = o.hierarchy\n const scorers = o.scorers ?? [embeddingScorer]\n const snippetKind = o.snippetKind ?? 'snippet'\n\n const embedderFor = (user: object): Embedder | undefined => {\n const embeddings = o.ports.embeddings\n return embeddings && { model: embeddings.model, dim: embeddings.dim, embed: texts => embeddings.embed(texts, user) }\n }\n\n async function contextFor(documentId: string, user: object): Promise<ComposeContext> {\n const context = await o.contextOf(documentId, user)\n if (!context) throw new ComposeError(404, `Für das Dokument „${documentId}“ gibt es keinen Fall.`)\n return context\n }\n\n async function documentOf(id: string): Promise<DocNode> {\n const content = await documents.loadContent(id)\n if (!content) throw new ComposeError(404, `Das Dokument „${id}“ existiert nicht.`)\n return content\n }\n\n async function factsOfCase(context: ComposeContext): Promise<FactBlock[]> {\n const lists = await Promise.all(context.sourceIds.map(async id => factsOf(await documents.loadContent(id))))\n return lists.flat()\n }\n\n async function examplesOfCase(context: ComposeContext): Promise<ExampleBlock[]> {\n if (context.exampleIds === 'all') {\n const all = await documents.listContents()\n return all.flatMap(({ id, content }) => examplesOf(content, id))\n }\n const lists = await Promise.all(context.exampleIds.map(async id => examplesOf(await documents.loadContent(id), id)))\n return lists.flat()\n }\n\n async function snippetsOfCase(context: ComposeContext): Promise<ExampleBlock[]> {\n const lists = await Promise.all((context.snippetIds ?? []).map(async id => examplesOf(await documents.loadContent(id), id, snippetKind)))\n return lists.flat()\n }\n\n /** `o.sameOwner` asked once per origin document of `entries` for the case of `caseDocumentId`, as the synchronous predicate `materialFor` takes. */\n async function ownerCheckOf(\n entries: readonly ExampleBlock[],\n caseDocumentId: string,\n user: object,\n ): Promise<((documentId: string) => boolean) | undefined> {\n const sameOwner = o.sameOwner\n if (!sameOwner) return undefined\n const ids = [...new Set(entries.flatMap(entry => (entry.origin ? [entry.origin.document] : [])))]\n const answers = await Promise.all(ids.map(id => sameOwner(id, caseDocumentId, user)))\n const owned = new Set(ids.filter((_, index) => answers[index]))\n return documentId => owned.has(documentId)\n }\n\n /** The earlier documents of the case (`previousIds`) with their content, in the given order. */\n async function previousOfCase(context: ComposeContext): Promise<PreviousDocument[]> {\n return Promise.all((context.previousIds ?? []).map(async id => ({ id, content: await documents.loadContent(id) })))\n }\n\n /** The block `blockId` of `documentId` as a typed block of `kind`; `ComposeError(404)` otherwise. */\n async function blockOf(documentId: string, blockId: string, kind: string): Promise<DocNode> {\n const node = findBlockNode(await documentOf(documentId), blockId)\n if (!node) throw new ComposeError(404, `Der Block „${blockId}“ existiert im Dokument „${documentId}“ nicht.`)\n if (kindOf(node) !== kind) throw new ComposeError(404, `Der Block „${blockId}“ ist kein Block vom Typ „${kind}“.`)\n return node\n }\n\n /** The pipeline: the default of the host's embedder and `generate`, the `segmentation` fields merged over it. */\n function pipelineOf(embedder: Embedder | undefined, source?: { meta: Record<string, unknown> }): SegmentPipeline {\n const { rules, tagRules, llm, splitterFor, ...override } = o.segmentation ?? {}\n const base = defaultPipeline({\n ...(embedder && { embedder }),\n ...(llm !== false && { generate: o.generate }),\n ...(rules && { rules }),\n ...(tagRules && { tagRules }),\n })\n const merge = 'merge' in override ? override.merge : base.merge\n const resolveOptions = 'resolve' in override ? override.resolve : base.resolve\n return {\n splitter: (source && splitterFor?.(source)) || override.splitter || base.splitter,\n slotLabelers: override.slotLabelers ?? base.slotLabelers,\n tagLabelers: override.tagLabelers ?? base.tagLabelers,\n ...(merge && { merge }),\n ...(resolveOptions && { resolve: resolveOptions }),\n }\n }\n\n /** `example` appends read-modify-write one after another, so two adoptions at once keep both. */\n let appending: Promise<unknown> = Promise.resolve()\n const exclusive = <T>(run: () => Promise<T>): Promise<T> => {\n const result = appending.then(run)\n appending = result.catch(() => undefined)\n return result\n }\n\n const ops: ComposeOperations = {\n async material({ document, block }, user, signal) {\n const context = await contextFor(document, user)\n const node = await blockOf(document, block, KIND.slot)\n const [facts, examples, snippets, previous, template] = await Promise.all([\n factsOfCase(context),\n examplesOfCase(context),\n snippetsOfCase(context),\n previousOfCase(context),\n documents.loadContent(context.templateId),\n ])\n const meta = compositeMeta(node)\n const identifier = meta[META.identifier]\n const rawElement = meta[META.element]\n const element = typeof rawElement === 'string' ? rawElement.trim() : ''\n const slotBlock = template ? templateSlotOf(node, template) : undefined\n const target: MaterialTarget = {\n identifier: typeof identifier === 'string' ? identifier : '',\n ...(element ? { element } : {}),\n tags: compositeTags(node),\n document,\n ...(slotBlock && { slot: slotBlock }),\n }\n const embedder = embedderFor(user)\n const sameOwner = await ownerCheckOf([...examples, ...snippets], document, user)\n return materialFor(target, context.case, facts, { examples, snippets, previous }, {\n scorers,\n ...(h && { h }),\n ...(sameOwner && { sameOwner }),\n ctx: { ...(embedder && { embedder }), ...(signal && { signal }) },\n })\n },\n\n async split({ document, block }, user, signal) {\n const context = await contextFor(document, user)\n const source = await blockOf(document, block, KIND.source)\n const body = markdownOf(bodyOf(source))\n if (!body.trim()) throw new ComposeError(409, 'Die Quelle enthält keinen Text.')\n const [template, examples] = await Promise.all([documents.loadContent(context.templateId), examplesOfCase(context)])\n const embedder = embedderFor(user)\n const { units, decisions } = await segment(\n body,\n pipelineOf(embedder, { meta: compositeMeta(source) }),\n catalogOf(template, examples),\n { ...(embedder && { embedder }), ...(signal && { signal }) },\n )\n const inputHash = await sha256Hex(body)\n const now = new Date().toISOString()\n // A unit without slot stays a proposal (slot empty): nothing of the corpus is lost, the person assigns it.\n const nodes = proposals(units, decisions).map(proposal =>\n exampleFromProposal(proposal, {\n sourceBlockId: block,\n sourceDocumentId: document,\n caseTags: context.case.tags,\n model: modelOf(proposal.origin, embedder),\n now,\n inputHash,\n }),\n )\n return { nodes }\n },\n\n async facts({ document, block }, user, signal) {\n const context = await contextFor(document, user)\n const source = await blockOf(document, block, KIND.source)\n const body = markdownOf(bodyOf(source))\n if (!body.trim()) throw new ComposeError(409, 'Die Quelle enthält keinen Text.')\n const template = await documentOf(context.templateId)\n\n const chapters = new Map(compositesOfKind(template, KIND.section).map(section => [blockIdOf(section), compositeTitle(section)]))\n const seen = new Set<string>()\n const catalog: CatalogEntry[] = []\n for (const slot of slotsOf(template)) {\n if (!slot.identifier || seen.has(slot.identifier)) continue\n seen.add(slot.identifier)\n catalog.push({\n identifier: slot.identifier,\n chapter: (slot.chapterId && chapters.get(slot.chapterId)) || '',\n title: slot.title,\n description: slot.description,\n tags: slot.tags,\n })\n }\n\n const { system, prompt } = buildFactsPrompt(catalog, body, o.factsSystem)\n const reply = await o.generate<FactsReply>({ stage: 'facts', system, prompt, schema: FactsReplySchema, ...(signal && { signal }) })\n let drafts: FactDraft[]\n try {\n drafts = parseFactsReply(reply, seen)\n }\n catch (error) {\n throw new ComposeError(422, error instanceof Error ? error.message : 'Antwort des Modells unbrauchbar')\n }\n const inputHash = await sha256Hex(body)\n const now = new Date().toISOString()\n // The host's `generate` knows the model and logs it; it is not known here.\n const nodes = drafts.map(draft =>\n factNode({\n text: draft.text,\n slot: draft.slot,\n derivation: { from: { document, block }, method: METHOD.aiFact, createdAt: now, inputHash },\n }),\n )\n return { nodes }\n },\n\n async example({ document, block }, user) {\n const context = await contextFor(document, user)\n const slotNode = await blockOf(document, block, KIND.slot)\n const slot = slotsOf({ type: 'doc', content: [slotNode] })[0]\n if (!slot || !bodyText(slotNode).trim()) throw new ComposeError(409, 'Der Block hat keinen Text, der als Beispiel übernommen werden könnte.')\n if (!documents.putDocument) throw new ComposeError(503, 'Der Dokumentspeicher kann keine Dokumente schreiben.')\n const put = documents.putDocument.bind(documents)\n const node = exampleFromSlot(slot, {\n sourceDocumentId: document,\n caseTags: context.case.tags,\n now: new Date().toISOString(),\n })\n await exclusive(async () => {\n const library = (await documents.loadContent(o.exampleLibraryId)) ?? { type: 'doc', content: [] }\n await put(o.exampleLibraryId, asJson({ ...library, content: [...(library.content ?? []), node] }))\n })\n return { libraryId: o.exampleLibraryId, blockId: blockIdOf(node) }\n },\n\n async instance({ templateId, instanceId, force }, user) {\n if (!force && (await documents.loadContent(instanceId))) throw new ComposeError(409, 'Instanz existiert')\n const context = await contextFor(instanceId, user)\n const template = await documentOf(templateId)\n if (!documents.putDocument) throw new ComposeError(503, 'Der Dokumentspeicher kann keine Dokumente schreiben.')\n const { doc, chosen, dropped, unresolved, elements } = instanceFromTemplate(template, context.case, {\n templateId,\n now: new Date().toISOString(),\n ...(h && { h }),\n })\n await documents.putDocument(instanceId, asJson(doc))\n return { chosen, dropped, unresolved, elements }\n },\n\n async classify({ document, text }, user, signal) {\n if (!text.trim()) throw new ComposeError(400, 'Der Text ist leer.')\n const context = await contextFor(document, user)\n const [template, examples] = await Promise.all([documents.loadContent(context.templateId), examplesOfCase(context)])\n const embedder = embedderFor(user)\n // The whole text is one unit: no splitting, no merging, then the configured labelers and the resolver as in `split`.\n const single: Splitter = { name: 'single', split: () => [{ text, start: 0, end: text.length, level: 0, path: [] }] }\n const { merge: _merge, ...pipeline } = pipelineOf(embedder)\n const { decisions } = await segment(\n text,\n { ...pipeline, splitter: single },\n catalogOf(template, examples),\n { ...(embedder && { embedder }), ...(signal && { signal }) },\n )\n return decisions[0] ?? { unit: 0, slot: null, tags: [], confidence: 0, origin: null }\n },\n }\n\n support.set(ops, {\n load: id => documents.loadContent(id),\n\n async examples({ document, slot, tags, query, limit }, user, signal) {\n const context = await contextFor(document, user)\n const selector: Selector = { require: tags ?? [], prefer: [], exclude: [] }\n const candidates = (await examplesOfCase(context)).filter(\n example => example.status !== STATUS.proposal && (!slot || example.slot === slot) && gate(selector, example.tags, h),\n )\n if (candidates.length === 0) return []\n const template = await documents.loadContent(context.templateId)\n const slotBlock = slotsOf(template).find(candidate => candidate.identifier === slot) ?? emptySlot(slot ?? '')\n const facts: FactBlock[] = query?.trim() ? [{ id: '', slot: slotBlock.identifier, text: query.trim(), sourceTitle: '' }] : []\n // Asked for explicit tags, the case to measure against is those tags.\n const profile: CaseProfile = tags?.length ? { ...context.case, tags } : context.case\n const embedder = embedderFor(user)\n const scored = await scoreAll(candidates, { case: profile, slot: slotBlock, facts }, [tagFitScorer(h), ...scorers, lengthScorer], {\n ...(embedder && { embedder }),\n ...(signal && { signal }),\n })\n return candidates\n .map((example, index) => ({ example, index, result: scored[index] ?? { score: 0, parts: {} } }))\n .sort((a, b) => b.result.score - a.result.score || a.index - b.index)\n .slice(0, Math.max(1, limit ?? 5))\n .map(({ example, result }) => ({ slot: example.slot, tags: example.tags, text: example.text, score: result.score, parts: result.parts }))\n },\n })\n return ops\n}\n"],"mappings":"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"}
@@ -81,7 +81,7 @@ function composeTools(ops) {
81
81
  }),
82
82
  provider({
83
83
  name: "compose_material",
84
- description: "Liefert das Material für einen Slot-Block des geöffneten Dokuments (Block-Id aus compose_slots): die Fakten des Falls, die passenden Bausteine (Inhalt zum Übernehmen und Anpassen) und die passenden geprüften Beispiele (nur Stil, nie übernehmen), jeweils mit Bewertung und ihren Teilwerten. Das Material ist die Grundlage für den Text des Slots.",
84
+ description: "Liefert das Material für einen Slot-Block des geöffneten Dokuments (Block-Id aus compose_slots): die Fakten des Falls, die Vorfassung (derselbe Abschnitt aus einem früheren Dokument derselben Person: fortschreiben, nicht übernehmen), die passenden Bausteine (Inhalt zum Übernehmen und Anpassen) und die passenden geprüften Beispiele (nur Stil, nie übernehmen), jeweils mit Bewertung und ihren Teilwerten. Das Material ist die Grundlage für den Text des Slots.",
85
85
  schema: inputs.material,
86
86
  expected: "{ \"block\": \"Block-Id des Slots\" }",
87
87
  async run({ block }, ctx, documentId) {
@@ -95,6 +95,10 @@ function composeTools(ops) {
95
95
  quelle: fact.source,
96
96
  text: cut(fact.text)
97
97
  })),
98
+ vorfassung: material.previous ? {
99
+ titel: material.previous.title,
100
+ text: cut(material.previous.text)
101
+ } : null,
98
102
  bausteine: material.snippets.map((snippet) => ({
99
103
  titel: snippet.title,
100
104
  tags: snippet.tags,
@@ -108,9 +112,9 @@ function composeTools(ops) {
108
112
  text: cut(example.text)
109
113
  }))
110
114
  };
111
- return JSON.stringify(answer.fakten.length === 0 && answer.bausteine.length === 0 && answer.beispiele.length === 0 ? {
115
+ return JSON.stringify(answer.fakten.length === 0 && !answer.vorfassung && answer.bausteine.length === 0 && answer.beispiele.length === 0 ? {
112
116
  ...answer,
113
- hinweis: "Für diesen Slot gibt es weder Fakten noch passende Bausteine oder Beispiele."
117
+ hinweis: "Für diesen Slot gibt es weder Fakten noch eine Vorfassung, passende Bausteine oder Beispiele."
114
118
  } : answer);
115
119
  }
116
120
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","names":[],"sources":["../../src/server/tools.ts"],"sourcesContent":["// i18n-ignore-file: model-facing tool descriptions and results (German, like the chat's system prompt)\n// The compose operations as read-only tools of the document chat (`ToolProvider` of forge-editor): what slots the open\n// document has, which material and examples a slot gets, and where a text belongs. `edit_document` stays the chat's own\n// tool; nothing here writes. The tool definitions carry zod schemas and no `execute`, so this entry never imports the AI SDK.\nimport { z } from 'zod'\nimport type { ToolContext, ToolProvider } from '@skaile/forge-editor/server'\nimport { bodyText, slotsOf } from '@skaile/forge-compose/documents'\nimport { ComposeError } from './errors'\nimport { composeToolSupportOf } from './operations'\nimport type { ComposeOperations, ComposeToolSupport } from './operations'\n\n/** Characters of an example text a tool answer carries. */\nexport const TOOL_TEXT_LIMIT = 2000\n/** Examples `compose_examples` answers when `limit` is not given, and the most it answers. */\nconst DEFAULT_EXAMPLES = 5\nconst MAX_EXAMPLES = 20\n\nconst text = z.string().trim().min(1)\n\nconst inputs = {\n material: z.object({ block: text }),\n examples: z.object({\n slot: text.optional(),\n tags: z.array(text).optional(),\n query: z.string().optional(),\n limit: z.number().int().min(1).max(MAX_EXAMPLES).optional(),\n }),\n slots: z.object({}),\n classify: z.object({ text }),\n}\n\nconst cut = (value: string): string => (value.length > TOOL_TEXT_LIMIT ? `${value.slice(0, TOOL_TEXT_LIMIT)}…` : value)\nconst round = (value: number): number => Math.round(value * 1000) / 1000\n\n/** A refusal the model can act on: a German sentence naming the tool, the problem and the expected shape. */\nfunction invalid(name: string, expected: string, error: z.ZodError): string {\n const problems = error.issues.map(issue => `${issue.path.join('.') || 'Eingabe'}: ${issue.message}`).join('; ')\n return `Ungültige Eingabe für ${name} (${problems}). Erwartet: ${expected}.`\n}\n\ninterface ToolSpec<S extends z.ZodType> {\n name: string\n description: string\n schema: S\n /** The expected input as the model reads it in a refusal. */\n expected: string\n run(input: z.infer<S>, ctx: ToolContext, documentId: string): Promise<string>\n}\n\n/**\n * One provider: the open document comes from the chat (`ctx.documentId`), the input is validated here and a refusal of the\n * operations (`ComposeError`: no case, nothing found) is answered as text so the model can react; other failures throw.\n */\nfunction provider<S extends z.ZodType>(spec: ToolSpec<S>): ToolProvider {\n return {\n name: spec.name,\n tool: { description: spec.description, inputSchema: spec.schema },\n async handle(ctx, input) {\n if (!ctx.documentId) return 'Kein Dokument geöffnet'\n const parsed = spec.schema.safeParse(input ?? {})\n if (!parsed.success) return invalid(spec.name, spec.expected, parsed.error)\n try {\n return await spec.run(parsed.data, ctx, ctx.documentId)\n }\n catch (error) {\n if (error instanceof ComposeError) return error.message\n throw error\n }\n },\n }\n}\n\n/** The slots of the open document (live content first), with whether each holds text. */\nasync function slotCatalog(support: ComposeToolSupport, ctx: ToolContext, documentId: string): Promise<string> {\n const doc = ctx.liveContent(documentId) ?? (await support.load(documentId))\n if (!doc) return `Das Dokument „${documentId}“ existiert nicht.`\n const slots = slotsOf(doc)\n if (slots.length === 0) return 'Das Dokument enthält keine Slots.'\n return JSON.stringify({\n slots: slots.map(slot => ({\n block: slot.id,\n bezeichner: slot.identifier,\n titel: slot.title,\n gefuellt: bodyText(slot.node).trim().length > 0,\n })),\n })\n}\n\n/**\n * The read-only chat tools of the compose operations: `compose_slots` (the open document's slots with their block ids),\n * `compose_material` (facts, ranked snippets and examples of a slot block), `compose_examples` (examples by slot, tags and a free query) and `compose_classify`\n * (which slot and tags a text belongs to). `ops` must be what `composeOperations` returned. Pass the result as\n * `tools` of `provideEditorServer`; the tools use `ToolContext.documentId` (forge-editor ≥ 6.2), `user` and `signal`.\n */\nexport function composeTools(ops: ComposeOperations): ToolProvider[] {\n const support = composeToolSupportOf(ops)\n return [\n provider({\n name: 'compose_slots',\n description:\n 'Listet die Slots (Textabschnitte) des geöffneten Dokuments mit Block-Id, Bezeichner, Titel und ob schon Text darin steht. ' +\n 'Nutze die Block-Id für compose_material und den Bezeichner für compose_examples.',\n schema: inputs.slots,\n expected: '{}',\n run: (_input, ctx, documentId) => slotCatalog(support, ctx, documentId),\n }),\n provider({\n name: 'compose_material',\n description:\n 'Liefert das Material für einen Slot-Block des geöffneten Dokuments (Block-Id aus compose_slots): die Fakten des Falls, ' +\n 'die passenden Bausteine (Inhalt zum Übernehmen und Anpassen) und die passenden geprüften Beispiele (nur Stil, nie ' +\n 'übernehmen), jeweils mit Bewertung und ihren Teilwerten. Das Material ist die Grundlage für den Text des Slots.',\n schema: inputs.material,\n expected: '{ \"block\": \"Block-Id des Slots\" }',\n async run({ block }, ctx, documentId) {\n const material = await ops.material({ document: documentId, block }, ctx.user, ctx.signal)\n const answer = {\n block,\n fakten: material.facts.map(fact => ({ quelle: fact.source, text: cut(fact.text) })),\n bausteine: material.snippets.map(snippet => ({\n titel: snippet.title,\n tags: snippet.tags,\n wert: round(snippet.score),\n teile: snippet.parts,\n text: cut(snippet.text),\n })),\n beispiele: material.examples.map(example => ({ wert: round(example.score), teile: example.parts, text: cut(example.text) })),\n }\n return JSON.stringify(\n answer.fakten.length === 0 && answer.bausteine.length === 0 && answer.beispiele.length === 0\n ? { ...answer, hinweis: 'Für diesen Slot gibt es weder Fakten noch passende Bausteine oder Beispiele.' }\n : answer,\n )\n },\n }),\n provider({\n name: 'compose_examples',\n description:\n 'Sucht geprüfte Beispieltexte aus der Beispielbibliothek des Falls. Optional eingeschränkt auf einen Slot (slot), ' +\n 'auf Tags (tags, alle müssen zutreffen) und nach Nähe zu einer Freitextfrage (query) sortiert. Beste zuerst.',\n schema: inputs.examples,\n expected: '{ \"slot\"?: \"Bezeichner\", \"tags\"?: [\"facette/wert\"], \"query\"?: \"Text\", \"limit\"?: 1-20 }',\n async run({ slot, tags, query, limit }, ctx, documentId) {\n const found = await support.examples(\n {\n document: documentId,\n ...(slot && { slot }),\n ...(tags && { tags }),\n ...(query && { query }),\n limit: Math.min(MAX_EXAMPLES, limit ?? DEFAULT_EXAMPLES),\n },\n ctx.user,\n ctx.signal,\n )\n if (found.length === 0) return 'Keine passenden Beispiele gefunden.'\n return JSON.stringify({\n beispiele: found.map(example => ({\n slot: example.slot,\n tags: example.tags,\n wert: round(example.score),\n teile: example.parts,\n text: cut(example.text),\n })),\n })\n },\n }),\n provider({\n name: 'compose_classify',\n description:\n 'Ordnet einen Text dem passenden Slot der Vorlage zu und schlägt Tags vor (Regeln, Beispiele, Modell). ' +\n 'Nutze es für „Wohin gehört dieser Absatz?“. slot ist null, wenn kein Slot sicher passt.',\n schema: inputs.classify,\n expected: '{ \"text\": \"der zuzuordnende Text\" }',\n async run({ text: value }, ctx, documentId) {\n const decision = await ops.classify({ document: documentId, text: value }, ctx.user, ctx.signal)\n return JSON.stringify({\n slot: decision.slot,\n tags: decision.tags,\n sicherheit: round(decision.confidence),\n herkunft: decision.origin,\n })\n },\n }),\n ]\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tools.js","names":[],"sources":["../../src/server/tools.ts"],"sourcesContent":["// i18n-ignore-file: model-facing tool descriptions and results (German, like the chat's system prompt)\n// The compose operations as read-only tools of the document chat (`ToolProvider` of forge-editor): what slots the open\n// document has, which material and examples a slot gets, and where a text belongs. `edit_document` stays the chat's own\n// tool; nothing here writes. The tool definitions carry zod schemas and no `execute`, so this entry never imports the AI SDK.\nimport { z } from 'zod'\nimport type { ToolContext, ToolProvider } from '@skaile/forge-editor/server'\nimport { bodyText, slotsOf } from '@skaile/forge-compose/documents'\nimport { ComposeError } from './errors'\nimport { composeToolSupportOf } from './operations'\nimport type { ComposeOperations, ComposeToolSupport } from './operations'\n\n/** Characters of an example text a tool answer carries. */\nexport const TOOL_TEXT_LIMIT = 2000\n/** Examples `compose_examples` answers when `limit` is not given, and the most it answers. */\nconst DEFAULT_EXAMPLES = 5\nconst MAX_EXAMPLES = 20\n\nconst text = z.string().trim().min(1)\n\nconst inputs = {\n material: z.object({ block: text }),\n examples: z.object({\n slot: text.optional(),\n tags: z.array(text).optional(),\n query: z.string().optional(),\n limit: z.number().int().min(1).max(MAX_EXAMPLES).optional(),\n }),\n slots: z.object({}),\n classify: z.object({ text }),\n}\n\nconst cut = (value: string): string => (value.length > TOOL_TEXT_LIMIT ? `${value.slice(0, TOOL_TEXT_LIMIT)}…` : value)\nconst round = (value: number): number => Math.round(value * 1000) / 1000\n\n/** A refusal the model can act on: a German sentence naming the tool, the problem and the expected shape. */\nfunction invalid(name: string, expected: string, error: z.ZodError): string {\n const problems = error.issues.map(issue => `${issue.path.join('.') || 'Eingabe'}: ${issue.message}`).join('; ')\n return `Ungültige Eingabe für ${name} (${problems}). Erwartet: ${expected}.`\n}\n\ninterface ToolSpec<S extends z.ZodType> {\n name: string\n description: string\n schema: S\n /** The expected input as the model reads it in a refusal. */\n expected: string\n run(input: z.infer<S>, ctx: ToolContext, documentId: string): Promise<string>\n}\n\n/**\n * One provider: the open document comes from the chat (`ctx.documentId`), the input is validated here and a refusal of the\n * operations (`ComposeError`: no case, nothing found) is answered as text so the model can react; other failures throw.\n */\nfunction provider<S extends z.ZodType>(spec: ToolSpec<S>): ToolProvider {\n return {\n name: spec.name,\n tool: { description: spec.description, inputSchema: spec.schema },\n async handle(ctx, input) {\n if (!ctx.documentId) return 'Kein Dokument geöffnet'\n const parsed = spec.schema.safeParse(input ?? {})\n if (!parsed.success) return invalid(spec.name, spec.expected, parsed.error)\n try {\n return await spec.run(parsed.data, ctx, ctx.documentId)\n }\n catch (error) {\n if (error instanceof ComposeError) return error.message\n throw error\n }\n },\n }\n}\n\n/** The slots of the open document (live content first), with whether each holds text. */\nasync function slotCatalog(support: ComposeToolSupport, ctx: ToolContext, documentId: string): Promise<string> {\n const doc = ctx.liveContent(documentId) ?? (await support.load(documentId))\n if (!doc) return `Das Dokument „${documentId}“ existiert nicht.`\n const slots = slotsOf(doc)\n if (slots.length === 0) return 'Das Dokument enthält keine Slots.'\n return JSON.stringify({\n slots: slots.map(slot => ({\n block: slot.id,\n bezeichner: slot.identifier,\n titel: slot.title,\n gefuellt: bodyText(slot.node).trim().length > 0,\n })),\n })\n}\n\n/**\n * The read-only chat tools of the compose operations: `compose_slots` (the open document's slots with their block ids),\n * `compose_material` (facts, ranked snippets and examples of a slot block), `compose_examples` (examples by slot, tags and a free query) and `compose_classify`\n * (which slot and tags a text belongs to). `ops` must be what `composeOperations` returned. Pass the result as\n * `tools` of `provideEditorServer`; the tools use `ToolContext.documentId` (forge-editor ≥ 6.2), `user` and `signal`.\n */\nexport function composeTools(ops: ComposeOperations): ToolProvider[] {\n const support = composeToolSupportOf(ops)\n return [\n provider({\n name: 'compose_slots',\n description:\n 'Listet die Slots (Textabschnitte) des geöffneten Dokuments mit Block-Id, Bezeichner, Titel und ob schon Text darin steht. ' +\n 'Nutze die Block-Id für compose_material und den Bezeichner für compose_examples.',\n schema: inputs.slots,\n expected: '{}',\n run: (_input, ctx, documentId) => slotCatalog(support, ctx, documentId),\n }),\n provider({\n name: 'compose_material',\n description:\n 'Liefert das Material für einen Slot-Block des geöffneten Dokuments (Block-Id aus compose_slots): die Fakten des Falls, ' +\n 'die Vorfassung (derselbe Abschnitt aus einem früheren Dokument derselben Person: fortschreiben, nicht übernehmen), ' +\n 'die passenden Bausteine (Inhalt zum Übernehmen und Anpassen) und die passenden geprüften Beispiele (nur Stil, nie ' +\n 'übernehmen), jeweils mit Bewertung und ihren Teilwerten. Das Material ist die Grundlage für den Text des Slots.',\n schema: inputs.material,\n expected: '{ \"block\": \"Block-Id des Slots\" }',\n async run({ block }, ctx, documentId) {\n const material = await ops.material({ document: documentId, block }, ctx.user, ctx.signal)\n const answer = {\n block,\n fakten: material.facts.map(fact => ({ quelle: fact.source, text: cut(fact.text) })),\n vorfassung: material.previous ? { titel: material.previous.title, text: cut(material.previous.text) } : null,\n bausteine: material.snippets.map(snippet => ({\n titel: snippet.title,\n tags: snippet.tags,\n wert: round(snippet.score),\n teile: snippet.parts,\n text: cut(snippet.text),\n })),\n beispiele: material.examples.map(example => ({ wert: round(example.score), teile: example.parts, text: cut(example.text) })),\n }\n return JSON.stringify(\n answer.fakten.length === 0 && !answer.vorfassung && answer.bausteine.length === 0 && answer.beispiele.length === 0\n ? { ...answer, hinweis: 'Für diesen Slot gibt es weder Fakten noch eine Vorfassung, passende Bausteine oder Beispiele.' }\n : answer,\n )\n },\n }),\n provider({\n name: 'compose_examples',\n description:\n 'Sucht geprüfte Beispieltexte aus der Beispielbibliothek des Falls. Optional eingeschränkt auf einen Slot (slot), ' +\n 'auf Tags (tags, alle müssen zutreffen) und nach Nähe zu einer Freitextfrage (query) sortiert. Beste zuerst.',\n schema: inputs.examples,\n expected: '{ \"slot\"?: \"Bezeichner\", \"tags\"?: [\"facette/wert\"], \"query\"?: \"Text\", \"limit\"?: 1-20 }',\n async run({ slot, tags, query, limit }, ctx, documentId) {\n const found = await support.examples(\n {\n document: documentId,\n ...(slot && { slot }),\n ...(tags && { tags }),\n ...(query && { query }),\n limit: Math.min(MAX_EXAMPLES, limit ?? DEFAULT_EXAMPLES),\n },\n ctx.user,\n ctx.signal,\n )\n if (found.length === 0) return 'Keine passenden Beispiele gefunden.'\n return JSON.stringify({\n beispiele: found.map(example => ({\n slot: example.slot,\n tags: example.tags,\n wert: round(example.score),\n teile: example.parts,\n text: cut(example.text),\n })),\n })\n },\n }),\n provider({\n name: 'compose_classify',\n description:\n 'Ordnet einen Text dem passenden Slot der Vorlage zu und schlägt Tags vor (Regeln, Beispiele, Modell). ' +\n 'Nutze es für „Wohin gehört dieser Absatz?“. slot ist null, wenn kein Slot sicher passt.',\n schema: inputs.classify,\n expected: '{ \"text\": \"der zuzuordnende Text\" }',\n async run({ text: value }, ctx, documentId) {\n const decision = await ops.classify({ document: documentId, text: value }, ctx.user, ctx.signal)\n return JSON.stringify({\n slot: decision.slot,\n tags: decision.tags,\n sicherheit: round(decision.confidence),\n herkunft: decision.origin,\n })\n },\n }),\n ]\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaile/forge-editor-compose",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Corpus to composed text in @skaile/forge-editor: the Block types slot, source, fact and example, the editor features that split sources into examples, extract facts, fill slots with ranked material and adopt examples, and the operation input contract shared with the host's routes.",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  }
51
51
  },
52
52
  "dependencies": {
53
- "@skaile/forge-compose": "^0.3.0",
53
+ "@skaile/forge-compose": "^0.4.0",
54
54
  "zod": "^4.5.4"
55
55
  },
56
56
  "peerDependencies": {