UNPKG

@stll/folio-agents

Version:

Framework-neutral LLM tool layer over folio's ai-edits engine: function-calling tools so an agent can read and mutate .docx documents through @stll/folio-core.

1,113 lines (1,112 loc) • 34 kB
import { FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_LINE_SPACING_RULE_VALUES, FOLIO_PAGE_BREAK_CLEAR_VALUES, FOLIO_PARAGRAPH_ALIGNMENT_VALUES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, parseFolioDocumentOperationBatch } from "@stll/folio-core/server"; //#region src/operation-schema.ts /** * Reusable projections of folio's versioned document-operation contract for * tool builders: a JSON Schema for one operation, a JSON Schema for the * versioned batch envelope, and a Standard Schema V1 wrapper around * `parseFolioDocumentOperationBatch` so downstream tool definitions (e.g. * TanStack AI tools) can consume the contract directly instead of * hand-mirroring it in valibot/zod. * * The strict throwing parser in `@stll/folio-core` stays the single source of * truth for semantics; everything here is a projection of it. Constraints * JSON Schema cannot express (`endOffset > startOffset`, unique operation * ids within a batch, per-type mode support) are noted in `description`s and * enforced by the parser. */ /** * Pattern for the normalized text hashes the contract uses to detect stale * targets (`selectedTextHash`, `precondition.blockTextHash`). Mirrors the * parser's check in `@stll/folio-core`'s `document-operations.ts`. */ const NORMALIZED_TEXT_HASH_PATTERN = "^h[0-9a-z]+$"; /** * JSON Schema for the contract's `textRange` handle (a serializable range * over the visible text of one main-story block, as returned by `find_text` * style tools). Shared between {@link FOLIO_DOCUMENT_OPERATION_JSON_SCHEMA} * and the `suggest_changes` tool schema in `tools.ts` so the two cannot * drift. `endOffset` must be greater than `startOffset`; JSON Schema cannot * express that relation, the parser enforces it. */ const FOLIO_TEXT_RANGE_JSON_SCHEMA = { type: "object", description: "A range over the visible text of one main-story block. Offsets are zero-based UTF-16 boundaries; `endOffset` must be greater than `startOffset`. Copy range objects verbatim from the tool that produced them — `selectedTextHash` makes a shifted or edited selection fail as stale instead of hitting the wrong text.", properties: { type: { type: "string", enum: ["textRange"] }, story: { type: "string", enum: ["main"] }, blockId: { type: "string", minLength: 1 }, startOffset: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER }, endOffset: { type: "integer", minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: "Exclusive end offset; must be greater than `startOffset`." }, selectedTextHash: { type: "string", pattern: NORMALIZED_TEXT_HASH_PATTERN, description: "Normalized hash of the selected text, used to detect stale ranges." } }, required: [ "type", "story", "blockId", "startOffset", "endOffset", "selectedTextHash" ], additionalProperties: false }; /** * JSON Schema for the contract's optional `precondition` guard: `{ * blockTextHash }`, echoed from a `blockTextHash` returned by a document * read (`read_document`, `read_section`, `find_text`). Exported so * `tools.ts` can attach the same shape to `suggest_changes` and * `add_comment` without redeclaring it. */ const FOLIO_PRECONDITION_JSON_SCHEMA = { type: "object", description: "Optional guard against editing a block that changed since you read it: echo the `blockTextHash` returned by read_document / read_section / find_text for this block. The operation is skipped (reason `preconditionFailed`) unless the target block's current normalized text hash still matches.", properties: { blockTextHash: { type: "string", pattern: NORMALIZED_TEXT_HASH_PATTERN, description: "Normalized hash of the target block's text, from a prior document read." } }, required: ["blockTextHash"], additionalProperties: false }; const commentJsonSchema = { type: "object", description: "A comment attached to the text affected by this operation.", properties: { text: { type: "string", description: "The comment body." } }, required: ["text"], additionalProperties: false }; /** * Properties every operation variant accepts: the required `id` plus the * optional review metadata (`severity`, `area`) and `precondition` guard. * `suggestionId` joins them on every variant except `commentOnRange` * (comments are not tracked changes, so there is nothing to group). */ const operationMetaProperties = { id: { type: "string", description: "Caller-supplied operation id, echoed back in `applied` / `skipped` results. Must be unique within a batch (enforced by the parser)." }, severity: { type: "string", enum: [ "low", "medium", "high" ], description: "Optional review severity for structured-review workflows." }, area: { type: "string", description: "Optional review area label (e.g. \"Penalty\") for structured-review workflows." }, precondition: FOLIO_PRECONDITION_JSON_SCHEMA }; const suggestionIdProperty = { suggestionId: { type: "string", description: "Optional host-side grouping key: in \"suggested\" mode every mark this operation produces carries it so the whole suggestion is accepted or rejected as one unit." } }; const blockIdProperty = { type: "string", description: "Id of the target block, from a prior document read." }; const FOLIO_CLEARABLE_PARAGRAPH_STYLE_ID_JSON_SCHEMA = { oneOf: [{ type: "string" }, { type: "null" }], description: "Paragraph style id; it must name a paragraph style the document defines, or the operation skips with `missingStyle`. null clears the direct style." }; /** Direct `w:outlineLvl`, or null to restore style inheritance. */ const FOLIO_CLEARABLE_OUTLINE_LEVEL_JSON_SCHEMA = { oneOf: [ { type: "object", properties: { kind: { type: "string", enum: ["heading"] }, level: { type: "integer", minimum: 0, maximum: 8 } }, required: ["kind", "level"], additionalProperties: false }, { type: "object", properties: { kind: { type: "string", enum: ["bodyText"] } }, required: ["kind"], additionalProperties: false }, { type: "null" } ], description: "Direct outline level (`w:outlineLvl`): a heading level from 0 (top) to 8, or body text. null removes it and lets the paragraph style decide." }; /** Complete direct `w:ind` attributes, or null to restore style inheritance. */ const FOLIO_CLEARABLE_PARAGRAPH_INDENTATION_JSON_SCHEMA = { oneOf: [{ type: "object", properties: { indentLeft: { type: "integer", minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }, indentRight: { type: "integer", minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }, indentFirstLine: { type: "integer", minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }, hangingIndent: { type: "boolean" } }, minProperties: 1, additionalProperties: false }, { type: "null" }], description: "Complete direct paragraph indentation (`w:ind`); null removes the direct child and restores style inheritance." }; const FOLIO_INSERT_FORMATTING_SCOPE_JSON_SCHEMA = { type: "string", enum: ["firstParagraph", "allParagraphs"], description: "Which paragraphs split from `text` receive the operation's paragraph formatting: `firstParagraph` (default) formats only the first, for a heading followed by its body; `allParagraphs` formats every paragraph alike, for several list items in one operation." }; const FOLIO_PARAGRAPH_NUMBERING_JSON_SCHEMA = { oneOf: [ { type: "object", properties: { kind: { type: "string", enum: ["none"] } }, required: ["kind"], additionalProperties: false }, { type: "object", properties: { kind: { type: "string", enum: ["inherit"] } }, required: ["kind"], additionalProperties: false }, { type: "object", properties: { kind: { type: "string", enum: ["levelOnly"] }, ilvl: { type: "integer", minimum: 0, maximum: 8 } }, required: ["kind", "ilvl"], additionalProperties: false }, { type: "object", properties: { kind: { type: "string", enum: ["reference"] }, numId: { type: "integer", minimum: 1, maximum: Number.MAX_SAFE_INTEGER }, ilvl: { type: "integer", minimum: 0, maximum: 8 } }, required: ["kind", "numId"], additionalProperties: false }, { type: "object", properties: { kind: { type: "string", enum: ["newList"] }, format: { type: "string", enum: ["bullet", "numbered"] }, level: { type: "integer", minimum: 0, maximum: 8 } }, required: ["kind", "format"], additionalProperties: false } ], description: "A paragraph numbering request: `none` cancels numbering, `inherit` removes the direct numbering reference, `levelOnly` sets only `w:ilvl`, `reference` selects a defined numbering instance with an optional level override, and `newList` starts a list with the requested format." }; const FOLIO_HARD_PAGE_BREAK_JSON_SCHEMA = { type: "object", properties: { clear: { type: "string", enum: FOLIO_PAGE_BREAK_CLEAR_VALUES } }, additionalProperties: false, description: "Insert an empty hard page-break run; it cannot be combined with text, pageBreakBefore, or lineBreakMode." }; /** Shared cross-field constraint for insertion and suggest-changes projections. */ const FOLIO_HARD_PAGE_BREAK_OPERATION_CONSTRAINT = { oneOf: [{ not: { required: ["hardPageBreak"] } }, { required: ["hardPageBreak", "text"], properties: { type: { enum: ["insertAfterBlock", "insertBeforeBlock"] }, text: { enum: [""] }, pageBreakBefore: { not: {} }, lineBreakMode: { not: {} } } }] }; /** Complete direct `w:spacing` attributes, or null to restore style inheritance. */ const FOLIO_CLEARABLE_PARAGRAPH_SPACING_JSON_SCHEMA = { oneOf: [{ type: "object", properties: { spaceBefore: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "Non-negative space before, in twips." }, spaceAfter: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "Non-negative space after, in twips." }, lineSpacing: { type: "integer", minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER, description: "Signed line spacing: 240ths of a line with the auto rule, otherwise twips." }, lineSpacingRule: { type: "string", enum: FOLIO_LINE_SPACING_RULE_VALUES, description: "How `lineSpacing` is interpreted." }, beforeAutospacing: { type: "boolean", description: "Whether the application determines spacing before automatically." }, afterAutospacing: { type: "boolean", description: "Whether the application determines spacing after automatically." } }, minProperties: 1, additionalProperties: false }, { type: "null" }], description: "Complete direct paragraph spacing (`w:spacing`); omitted attributes stay absent, while zero and false remain explicit. Null removes the direct spacing child and restores style inheritance." }; const FOLIO_BLOCK_PARAGRAPH_PROPERTIES_JSON_SCHEMA = { type: "object", description: "The paragraph properties to set; at least one.", properties: { styleId: FOLIO_CLEARABLE_PARAGRAPH_STYLE_ID_JSON_SCHEMA, outlineLevel: FOLIO_CLEARABLE_OUTLINE_LEVEL_JSON_SCHEMA, numbering: FOLIO_PARAGRAPH_NUMBERING_JSON_SCHEMA, indentation: FOLIO_CLEARABLE_PARAGRAPH_INDENTATION_JSON_SCHEMA, alignment: { oneOf: [{ type: "string", enum: FOLIO_PARAGRAPH_ALIGNMENT_VALUES }, { type: "null" }], description: "Direct paragraph alignment; null restores style inheritance." }, spacing: FOLIO_CLEARABLE_PARAGRAPH_SPACING_JSON_SCHEMA }, minProperties: 1, additionalProperties: false }; /** * JSON Schema (draft-07 compatible) for ONE document operation: the full * union accepted by `parseFolioDocumentOperationBatch` in * `@stll/folio-core`, one `oneOf` variant per entry in * `FOLIO_DOCUMENT_OPERATION_TYPES`. Intended for LLM tool definitions and * other consumers that need the contract's wire shape without re-declaring * it; note that the `suggest_changes` tool in `tools.ts` deliberately * narrows this union (see the comment there). */ const FOLIO_DOCUMENT_OPERATION_JSON_SCHEMA = { description: "One document operation, discriminated by `type`. Mirrors the operation union accepted by `parseFolioDocumentOperationBatch` in @stll/folio-core.", oneOf: [ { type: "object", description: "Replace an exact text match inside one block.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["replaceInBlock"] }, blockId: blockIdProperty, find: { type: "string", description: "The exact text to find within the block." }, replace: { type: "string", description: "The replacement text." }, comment: commentJsonSchema }, required: [ "id", "type", "blockId", "find", "replace" ], additionalProperties: false }, { type: "object", description: "Replace the text covered by a range handle.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["replaceRange"] }, range: FOLIO_TEXT_RANGE_JSON_SCHEMA, replace: { type: "string", description: "The replacement text." }, comment: commentJsonSchema }, required: [ "id", "type", "range", "replace" ], additionalProperties: false }, { type: "object", description: "Attach a comment to the text covered by a range handle.", properties: { ...operationMetaProperties, type: { type: "string", enum: ["commentOnRange"] }, range: FOLIO_TEXT_RANGE_JSON_SCHEMA, comment: commentJsonSchema }, required: [ "id", "type", "range", "comment" ], additionalProperties: false }, { type: "object", description: "Apply inline formatting to the text covered by a range handle in direct or tracked mode.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["formatRange"] }, range: FOLIO_TEXT_RANGE_JSON_SCHEMA, formatting: { type: "object", description: "Inline formatting to apply; at least one property is required.", properties: { bold: { type: "boolean" }, italic: { type: "boolean" }, underline: { type: "boolean" } }, minProperties: 1, additionalProperties: false } }, required: [ "id", "type", "range", "formatting" ], additionalProperties: false }, { type: "object", description: "Insert a new paragraph after the anchor block.", ...FOLIO_HARD_PAGE_BREAK_OPERATION_CONSTRAINT, properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["insertAfterBlock"] }, blockId: blockIdProperty, text: { type: "string", description: "The paragraph text to insert. A line break splits this into consecutive paragraphs at the same anchor instead of one paragraph with embedded newlines. By default only the first paragraph gets `styleId` / `numbering` / `alignment` / `spacing` / `inheritFormatting` and later ones use body formatting; set `formattingScope: \"allParagraphs\"` to format every paragraph alike, for example several list items. Prefer one paragraph per operation otherwise." }, inheritFormatting: { type: "boolean", description: "Inherit the anchor block's formatting for the inserted paragraph." }, pageBreakBefore: { type: "boolean", description: "Start the inserted paragraph on a new page (`pageBreakBefore`)." }, styleId: FOLIO_CLEARABLE_PARAGRAPH_STYLE_ID_JSON_SCHEMA, outlineLevel: FOLIO_CLEARABLE_OUTLINE_LEVEL_JSON_SCHEMA, alignment: { oneOf: [{ type: "string", enum: FOLIO_PARAGRAPH_ALIGNMENT_VALUES }, { type: "null" }], description: "Direct paragraph alignment; null restores style inheritance." }, spacing: FOLIO_CLEARABLE_PARAGRAPH_SPACING_JSON_SCHEMA, indentation: FOLIO_CLEARABLE_PARAGRAPH_INDENTATION_JSON_SCHEMA, lineBreakMode: { type: "string", enum: ["paragraph", "inline"] }, formattingScope: FOLIO_INSERT_FORMATTING_SCOPE_JSON_SCHEMA, numbering: FOLIO_PARAGRAPH_NUMBERING_JSON_SCHEMA, hardPageBreak: FOLIO_HARD_PAGE_BREAK_JSON_SCHEMA, moveId: { type: "string", description: "Links this operation to the other half of one relocation: a `deleteBlock` and an insertion sharing a `moveId` are written as `w:moveFrom` / `w:moveTo`. An id that does not name exactly one of each applies as an ordinary edit." }, comment: commentJsonSchema }, required: [ "id", "type", "blockId", "text" ], additionalProperties: false }, { type: "object", description: "Insert a new paragraph before the anchor block.", ...FOLIO_HARD_PAGE_BREAK_OPERATION_CONSTRAINT, properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["insertBeforeBlock"] }, blockId: blockIdProperty, text: { type: "string", description: "The paragraph text to insert. A line break splits this into consecutive paragraphs at the same anchor instead of one paragraph with embedded newlines. By default only the first paragraph gets `styleId` / `numbering` / `alignment` / `spacing` / `inheritFormatting` and later ones use body formatting; set `formattingScope: \"allParagraphs\"` to format every paragraph alike, for example several list items. Prefer one paragraph per operation otherwise." }, inheritFormatting: { type: "boolean", description: "Inherit the anchor block's formatting for the inserted paragraph." }, pageBreakBefore: { type: "boolean", description: "Start the inserted paragraph on a new page (`pageBreakBefore`)." }, styleId: FOLIO_CLEARABLE_PARAGRAPH_STYLE_ID_JSON_SCHEMA, outlineLevel: FOLIO_CLEARABLE_OUTLINE_LEVEL_JSON_SCHEMA, alignment: { oneOf: [{ type: "string", enum: FOLIO_PARAGRAPH_ALIGNMENT_VALUES }, { type: "null" }], description: "Direct paragraph alignment; null restores style inheritance." }, spacing: FOLIO_CLEARABLE_PARAGRAPH_SPACING_JSON_SCHEMA, indentation: FOLIO_CLEARABLE_PARAGRAPH_INDENTATION_JSON_SCHEMA, lineBreakMode: { type: "string", enum: ["paragraph", "inline"] }, formattingScope: FOLIO_INSERT_FORMATTING_SCOPE_JSON_SCHEMA, numbering: FOLIO_PARAGRAPH_NUMBERING_JSON_SCHEMA, hardPageBreak: FOLIO_HARD_PAGE_BREAK_JSON_SCHEMA, moveId: { type: "string", description: "Links this operation to the other half of one relocation: a `deleteBlock` and an insertion sharing a `moveId` are written as `w:moveFrom` / `w:moveTo`. An id that does not name exactly one of each applies as an ordinary edit." }, comment: commentJsonSchema }, required: [ "id", "type", "blockId", "text" ], additionalProperties: false }, { type: "object", description: "Replace one block's entire text.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["replaceBlock"] }, blockId: blockIdProperty, text: { type: "string", description: "The new block text." }, preserveFormatting: { type: "boolean", description: "Keep the block's existing formatting for the replacement text." }, styleId: FOLIO_CLEARABLE_PARAGRAPH_STYLE_ID_JSON_SCHEMA, comment: commentJsonSchema }, required: [ "id", "type", "blockId", "text" ], additionalProperties: false }, { type: "object", description: "Delete one block.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["deleteBlock"] }, blockId: blockIdProperty, moveId: { type: "string", description: "Links this operation to the other half of one relocation: a `deleteBlock` and an insertion sharing a `moveId` are written as `w:moveFrom` / `w:moveTo`. An id that does not name exactly one of each applies as an ordinary edit." }, comment: commentJsonSchema }, required: [ "id", "type", "blockId" ], additionalProperties: false }, { type: "object", description: "Break one block in two at `offset`: a paragraph mark is inserted and no words change.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["splitBlock"] }, blockId: blockIdProperty, offset: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "Character offset in the block's text where the break goes. Must fall strictly inside it." }, separator: { type: "string", description: "Text at `offset` the break replaces, usually the space between the halves." }, firstParagraphProperties: { ...FOLIO_BLOCK_PARAGRAPH_PROPERTIES_JSON_SCHEMA, description: "Properties for the first result; omitted properties keep the source paragraph's value." }, secondParagraphProperties: { ...FOLIO_BLOCK_PARAGRAPH_PROPERTIES_JSON_SCHEMA, description: "Properties for the second result; omitted properties keep the source paragraph's value." } }, required: [ "id", "type", "blockId", "offset" ], additionalProperties: false }, { type: "object", description: "Join one block with the block after it in the same container: a paragraph mark is deleted.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["mergeBlockWithNext"] }, blockId: blockIdProperty, separator: { type: "string", description: "Text the join inserts between the two halves, usually a space." }, mergedParagraphProperties: { ...FOLIO_BLOCK_PARAGRAPH_PROPERTIES_JSON_SCHEMA, description: "Properties for the joined result; omitted properties keep the first paragraph's value." } }, required: [ "id", "type", "blockId" ], additionalProperties: false }, { type: "object", description: "Change one block's paragraph properties without touching its words, recorded as a `w:pPrChange`.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["setBlockParagraphProperties"] }, blockId: blockIdProperty, properties: FOLIO_BLOCK_PARAGRAPH_PROPERTIES_JSON_SCHEMA }, required: [ "id", "type", "blockId", "properties" ], additionalProperties: false }, { type: "object", description: "Insert a whole table next to the anchor block.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["insertTable"] }, blockId: blockIdProperty, position: { type: "string", enum: ["after", "before"], description: "Place the table after the anchor (default) or before it." }, rows: { type: "array", description: "Cell texts row by row; every row must hold the same number of cells.", items: { type: "array", items: { type: "string" } } } }, required: [ "id", "type", "blockId", "rows" ], additionalProperties: false }, { type: "object", description: "Delete the whole table the anchor block sits in.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["deleteTable"] }, blockId: blockIdProperty }, required: [ "id", "type", "blockId" ], additionalProperties: false }, { type: "object", description: "Attach a comment to one block, optionally quoting text within it.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["commentOnBlock"] }, blockId: blockIdProperty, quote: { type: "string", description: "Exact text within the block the comment is about." }, comment: commentJsonSchema }, required: [ "id", "type", "blockId", "comment" ], additionalProperties: false }, { type: "object", description: "Insert a signature table next to the anchor block. Direct mode only (table insertion is not representable as a tracked change).", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["insertSignatureTable"] }, blockId: blockIdProperty, position: { type: "string", enum: ["after", "before"], description: "Insert after the anchor block (default) or before it. Defaults to \"after\"." }, parties: { type: "array", description: "The signing parties, one table cell per party.", items: { type: "object", properties: { name: { type: "string", description: "Party name (rendered bold)." }, signatory: { type: "string", description: "Name of the person signing." }, title: { type: "string", description: "Signatory title (rendered in italics)." } }, required: ["name"], additionalProperties: false } }, comment: commentJsonSchema }, required: [ "id", "type", "blockId", "parties" ], additionalProperties: false }, { type: "object", description: "Insert a row next to the row containing the anchor block.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["insertTableRow"] }, blockId: blockIdProperty, position: { type: "string", enum: ["after", "before"], description: "Insert after the anchor row (default) or before it. Defaults to \"after\"." }, cellTexts: { type: "array", description: "Initial text for physical cells in source order; omitted cells stay empty.", items: { type: "string" } } }, required: [ "id", "type", "blockId" ], additionalProperties: false }, { type: "object", description: "Delete the row containing the anchor block.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["deleteTableRow"] }, blockId: blockIdProperty }, required: [ "id", "type", "blockId" ], additionalProperties: false }, { type: "object", description: "Insert a column next to the column containing the anchor block.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["insertTableColumn"] }, blockId: blockIdProperty, position: { type: "string", enum: ["after", "before"], description: "Insert after the anchor column (default) or before it. Defaults to \"after\"." }, cellTexts: { type: "array", description: "Initial text for newly created physical cells in row order.", items: { type: "string" } } }, required: [ "id", "type", "blockId" ], additionalProperties: false }, { type: "object", description: "Delete the column containing the anchor block.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["deleteTableColumn"] }, blockId: blockIdProperty }, required: [ "id", "type", "blockId" ], additionalProperties: false }, { type: "object", description: "Merge a region targeted by an opposite-cell anchor or a downward row count. Tracked mode supports vertical-only regions with empty continuation cells.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["mergeTableCells"] }, blockId: blockIdProperty, endBlockId: { type: "string", minLength: 1, description: "Stable paragraph anchor inside the opposite corner cell." }, rowCount: { type: "integer", minimum: 2, maximum: Number.MAX_SAFE_INTEGER, description: "Number of grid rows to merge downward from the anchored cell." } }, required: [ "id", "type", "blockId" ], oneOf: [{ required: ["endBlockId"] }, { required: ["rowCount"] }], additionalProperties: false }, { type: "object", description: "Split one spanned table cell into individual cells. Tracked mode supports vertical-only spans.", properties: { ...operationMetaProperties, ...suggestionIdProperty, type: { type: "string", enum: ["splitTableCell"] }, blockId: blockIdProperty }, required: [ "id", "type", "blockId" ], additionalProperties: false } ] }; /** * JSON Schema (draft-07 compatible) for the versioned batch envelope accepted * by `parseFolioDocumentOperationBatch`: `version` (always * `FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION`), `operations`, and the * optional `mode` / `atomic` / `dryRun` flags — the exact wire shape of * `FolioDocumentOperationBatch`. Hand this to an LLM tool definition (or any * JSON Schema consumer) instead of re-declaring the contract. */ const FOLIO_DOCUMENT_OPERATION_BATCH_JSON_SCHEMA = { type: "object", description: "A versioned batch of document operations. Operation ids must be unique within the batch (enforced by the parser, not expressible in JSON Schema).", properties: { version: { type: "integer", enum: [FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION], description: "Document-operation contract version." }, operations: { type: "array", description: "The operations to apply, in order.", items: FOLIO_DOCUMENT_OPERATION_JSON_SCHEMA }, mode: { type: "string", enum: FOLIO_DOCUMENT_OPERATION_MODES, description: "How edits land: \"tracked-changes\" (default) proposes revisions for human review, \"direct\" applies immediately. `insertSignatureTable` supports \"direct\" only. Tracked cell merge and split operations support unambiguous vertical-only topology." }, atomic: { type: "boolean", description: "Reject the whole batch when any operation would be skipped." }, dryRun: { type: "boolean", description: "Preview the batch without mutating the document." }, precondition: { type: "object", description: "Batch-level guard: the host document version this batch was authored against. A surface that knows its document version skips the whole batch (reason `documentVersionMismatch`) when the token differs.", properties: { documentVersion: { type: "string", minLength: 1, description: "Opaque host version token (an entity version id, a checkpoint)." } }, required: ["documentVersion"], additionalProperties: false } }, required: ["version", "operations"], additionalProperties: false }; /** * Convert the parser's JSONPath-style location (`$.operations[0].blockId`) * into a Standard Schema issue path (`["operations", 0, "blockId"]`). * Returns `undefined` for the root path so root-level issues carry no path. */ const toStandardSchemaPath = (path) => { const segments = []; for (const match of path.matchAll(/\.(?<key>[^.[\]]+)|\[(?<index>\d+)\]/gu)) { const { key, index } = match.groups ?? {}; if (key !== void 0) segments.push(key); else if (index !== void 0) segments.push(Number(index)); } return segments.length > 0 ? segments : void 0; }; const toStandardSchemaIssue = (error) => { if (error instanceof InvalidFolioDocumentOperationBatchError) { const path = toStandardSchemaPath(error.path); return { message: error.message, ...path !== void 0 && { path } }; } if (error instanceof UnsupportedFolioDocumentOperationVersionError) return { message: error.message, path: ["version"] }; return { message: error instanceof Error ? error.message : String(error) }; }; /** * Standard Schema V1 (https://standardschema.dev) validator for a document * operation batch, for spec-aware consumers (TanStack AI tool `inputSchema`, * tRPC input, etc.). Delegates to `parseFolioDocumentOperationBatch` from * `@stll/folio-core`, so the strict parser stays the single source of truth: * `validate` never throws, returning `{ value }` with the parser's exact * output on success and `{ issues }` (message plus a key path when the parser * reported one) on failure. The matching LLM-facing JSON schema is attached * as {@link FolioDocumentOperationBatchSchema.jsonSchema}. */ const folioDocumentOperationBatchSchema = { "~standard": { version: 1, vendor: "folio", validate: (value) => { try { return { value: parseFolioDocumentOperationBatch(value) }; } catch (error) { return { issues: [toStandardSchemaIssue(error)] }; } } }, jsonSchema: FOLIO_DOCUMENT_OPERATION_BATCH_JSON_SCHEMA }; //#endregion export { FOLIO_BLOCK_PARAGRAPH_PROPERTIES_JSON_SCHEMA, FOLIO_CLEARABLE_OUTLINE_LEVEL_JSON_SCHEMA, FOLIO_CLEARABLE_PARAGRAPH_INDENTATION_JSON_SCHEMA, FOLIO_CLEARABLE_PARAGRAPH_SPACING_JSON_SCHEMA, FOLIO_CLEARABLE_PARAGRAPH_STYLE_ID_JSON_SCHEMA, FOLIO_DOCUMENT_OPERATION_BATCH_JSON_SCHEMA, FOLIO_DOCUMENT_OPERATION_JSON_SCHEMA, FOLIO_HARD_PAGE_BREAK_JSON_SCHEMA, FOLIO_HARD_PAGE_BREAK_OPERATION_CONSTRAINT, FOLIO_INSERT_FORMATTING_SCOPE_JSON_SCHEMA, FOLIO_PARAGRAPH_NUMBERING_JSON_SCHEMA, FOLIO_PRECONDITION_JSON_SCHEMA, FOLIO_TEXT_RANGE_JSON_SCHEMA, folioDocumentOperationBatchSchema };