@tiwater/office-mcp 0.12.0 → 0.13.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/office/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
commandCandidate,
|
|
12
12
|
createToolResult,
|
|
13
13
|
requireString,
|
|
14
|
+
runCandidateChain,
|
|
14
15
|
runJsonCandidateChain,
|
|
15
16
|
withTempJsonFile,
|
|
16
17
|
} from '../_shared/tool-runtime.mjs';
|
|
@@ -35,97 +36,12 @@ const convertCandidates = [
|
|
|
35
36
|
];
|
|
36
37
|
|
|
37
38
|
const pathInput = z.string().trim().min(1);
|
|
38
|
-
const migrationQueryTargetAction = z.enum([
|
|
39
|
-
'place-content',
|
|
40
|
-
'keep-template-label',
|
|
41
|
-
'select-template-option',
|
|
42
|
-
]).describe('Business action: place-content moves the current source content into a target content position; keep-template-label preserves the target label and structure while migrating a uniquely identified current field value; select-template-option marks the target option represented by the current source fact.');
|
|
43
|
-
const targetActions = new Set(migrationQueryTargetAction.options);
|
|
44
|
-
|
|
45
|
-
const choiceReference = z.string().trim().regex(/^[ST][1-9][0-9]*-[0-9a-f]{8}$/);
|
|
46
|
-
const alternativeReference = z.string().trim().regex(/^S[1-9][0-9]*-[PLO][1-9][0-9]*-[0-9a-f]{8}$/);
|
|
47
|
-
|
|
48
|
-
const terminalMigrationChoiceInput = z.object({
|
|
49
|
-
sourceRef: choiceReference,
|
|
50
|
-
action: z.enum(['exclude-source', 'review-source']),
|
|
51
|
-
}).strict();
|
|
52
|
-
|
|
53
|
-
const migrationChoiceInput = z.union([
|
|
54
|
-
z.object({ alternativeRef: alternativeReference }).strict(),
|
|
55
|
-
terminalMigrationChoiceInput,
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
const templateCleanupInput = z.object({
|
|
59
|
-
targetRef: choiceReference,
|
|
60
|
-
scope: z.enum(['cell', 'row']),
|
|
61
|
-
}).strict();
|
|
62
|
-
|
|
63
|
-
const templateMigrationInput = z.object({
|
|
64
|
-
source: pathInput.describe('Path to the current source DOCX.'),
|
|
65
|
-
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
66
|
-
output: pathInput.describe('Path to the migrated output DOCX.'),
|
|
67
|
-
receiptOutput: pathInput.describe('New JSON receipt artifact path. Existing files are never overwritten.'),
|
|
68
|
-
choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source ref. Targeted choices use an alternativeRef returned by the query tool. Terminal exclusions and genuine local review use a sourceRef.'),
|
|
69
|
-
templateCleanup: z.array(templateCleanupInput).optional().describe('Optional baseline-owned placeholders or example rows to clear.'),
|
|
70
|
-
}).strict();
|
|
71
39
|
|
|
72
40
|
const runtimeIdentity = z.object({
|
|
73
41
|
command: z.string(),
|
|
74
42
|
cwd: z.string(),
|
|
75
43
|
}).strict();
|
|
76
44
|
|
|
77
|
-
const migrationChoiceOutput = z.object({
|
|
78
|
-
id: z.string(),
|
|
79
|
-
kind: z.string(),
|
|
80
|
-
scope: z.string(),
|
|
81
|
-
text: z.string().nullable(),
|
|
82
|
-
count: z.number().int(),
|
|
83
|
-
requiredCardinality: z.string().nullable(),
|
|
84
|
-
context: z.record(z.string(), z.unknown()).nullable(),
|
|
85
|
-
allowedActions: z.array(z.string()),
|
|
86
|
-
}).strict();
|
|
87
|
-
|
|
88
|
-
const migrationPublicChoiceOutput = migrationChoiceOutput.omit({ id: true }).extend({
|
|
89
|
-
ref: choiceReference,
|
|
90
|
-
}).strict();
|
|
91
|
-
|
|
92
|
-
const migrationAlternativeOutput = z.object({
|
|
93
|
-
ref: alternativeReference,
|
|
94
|
-
action: migrationQueryTargetAction,
|
|
95
|
-
target: migrationPublicChoiceOutput,
|
|
96
|
-
}).strict();
|
|
97
|
-
|
|
98
|
-
const migrationCatalog = z.object({
|
|
99
|
-
schema: z.string(),
|
|
100
|
-
pass: z.boolean(),
|
|
101
|
-
sourceSha256: z.string(),
|
|
102
|
-
baselineSha256: z.string(),
|
|
103
|
-
sources: z.array(migrationChoiceOutput),
|
|
104
|
-
targets: z.array(migrationChoiceOutput),
|
|
105
|
-
}).strict().superRefine((catalog, context) => {
|
|
106
|
-
for (const key of ['sources', 'targets']) {
|
|
107
|
-
const seen = new Set();
|
|
108
|
-
for (const [index, choice] of catalog[key].entries()) {
|
|
109
|
-
if (seen.has(choice.id)) {
|
|
110
|
-
context.addIssue({ code: 'custom', path: [key, index, 'id'], message: `duplicate ${key} choice id: ${choice.id}` });
|
|
111
|
-
}
|
|
112
|
-
seen.add(choice.id);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const migrationReceipt = z.object({
|
|
118
|
-
schema: z.string(),
|
|
119
|
-
toolVersion: z.string(),
|
|
120
|
-
status: z.enum(['pass', 'review-required', 'failed']),
|
|
121
|
-
pass: z.boolean(),
|
|
122
|
-
reviewRequired: z.boolean(),
|
|
123
|
-
outputVerified: z.boolean(),
|
|
124
|
-
output: z.string().nullable(),
|
|
125
|
-
plan: z.string().nullable(),
|
|
126
|
-
failures: z.array(z.unknown()),
|
|
127
|
-
}).passthrough();
|
|
128
|
-
|
|
129
45
|
const inputOnly = z.object({ input: pathInput }).strict();
|
|
130
46
|
const artifactInput = z.object({
|
|
131
47
|
input: pathInput,
|
|
@@ -137,39 +53,6 @@ const artifact = z.object({
|
|
|
137
53
|
bytes: z.number().int().nonnegative(),
|
|
138
54
|
}).strict();
|
|
139
55
|
|
|
140
|
-
const xlsxAppliedOperation = z.object({
|
|
141
|
-
type: z.string().min(1),
|
|
142
|
-
applied: z.boolean(),
|
|
143
|
-
detail: z.string(),
|
|
144
|
-
sheet: z.string().nullable().optional(),
|
|
145
|
-
changedRange: z.string().nullable().optional(),
|
|
146
|
-
warnings: z.array(z.string()).nullable().optional(),
|
|
147
|
-
}).strict();
|
|
148
|
-
const xlsxEditResult = z.object({
|
|
149
|
-
input: z.string().min(1),
|
|
150
|
-
output: z.string().min(1),
|
|
151
|
-
appliedOperations: z.array(xlsxAppliedOperation),
|
|
152
|
-
}).strict();
|
|
153
|
-
const xlsxApplyReceipt = z.object({
|
|
154
|
-
schema: z.literal('tiwater.office.xlsx-apply-receipt/v1'),
|
|
155
|
-
pass: z.boolean(),
|
|
156
|
-
input: artifact,
|
|
157
|
-
operations: artifact,
|
|
158
|
-
output: artifact.nullable(),
|
|
159
|
-
appliedOperations: z.array(xlsxAppliedOperation),
|
|
160
|
-
}).strict();
|
|
161
|
-
const xlsxApplyOutput = z.object({
|
|
162
|
-
tool: z.literal('xlsx_apply'),
|
|
163
|
-
runtime: runtimeIdentity,
|
|
164
|
-
receipt: artifact,
|
|
165
|
-
output: artifact.nullable(),
|
|
166
|
-
summary: z.object({
|
|
167
|
-
pass: z.boolean(),
|
|
168
|
-
operationCount: z.number().int().nonnegative(),
|
|
169
|
-
appliedCount: z.number().int().nonnegative(),
|
|
170
|
-
}).strict(),
|
|
171
|
-
}).strict();
|
|
172
|
-
|
|
173
56
|
const pptxTemplateApplyResult = z.object({
|
|
174
57
|
input: z.string().min(1),
|
|
175
58
|
template: z.string().min(1),
|
|
@@ -210,23 +93,6 @@ const pptxFormatApplyResult = z.object({
|
|
|
210
93
|
message: z.string(),
|
|
211
94
|
}).strict()),
|
|
212
95
|
}).strict();
|
|
213
|
-
const pptxTemplateApplyReceipt = z.object({
|
|
214
|
-
schema: z.literal('tiwater.office.pptx-template-apply-receipt/v1'),
|
|
215
|
-
pass: z.boolean(),
|
|
216
|
-
input: artifact,
|
|
217
|
-
template: artifact,
|
|
218
|
-
plan: artifact,
|
|
219
|
-
output: artifact.nullable(),
|
|
220
|
-
providerResult: pptxTemplateApplyResult,
|
|
221
|
-
}).strict();
|
|
222
|
-
const pptxFormatApplyReceipt = z.object({
|
|
223
|
-
schema: z.literal('tiwater.office.pptx-format-apply-receipt/v1'),
|
|
224
|
-
pass: z.boolean(),
|
|
225
|
-
input: artifact,
|
|
226
|
-
operations: artifact,
|
|
227
|
-
output: artifact.nullable(),
|
|
228
|
-
providerResult: pptxFormatApplyResult,
|
|
229
|
-
}).strict();
|
|
230
96
|
const pptxTemplateApplyOutput = z.object({
|
|
231
97
|
tool: z.literal('pptx_apply_template'),
|
|
232
98
|
runtime: runtimeIdentity,
|
|
@@ -287,99 +153,121 @@ const nativeRenderOutput = z.object({
|
|
|
287
153
|
}).strict(),
|
|
288
154
|
}).strict();
|
|
289
155
|
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
156
|
+
const index = z.number().int().nonnegative();
|
|
157
|
+
const positiveIndex = z.number().int().positive();
|
|
158
|
+
const optionalTextMatch = {
|
|
159
|
+
matchMode: z.string().optional(),
|
|
160
|
+
paragraphStyle: z.string().optional(),
|
|
161
|
+
};
|
|
162
|
+
const richTextSegment = z.object({
|
|
163
|
+
text: z.string(),
|
|
164
|
+
color: z.string().optional(),
|
|
165
|
+
underline: z.boolean().optional(),
|
|
166
|
+
bold: z.boolean().optional(),
|
|
167
|
+
fontName: z.string().optional(),
|
|
302
168
|
}).strict();
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
169
|
+
const tableCellInput = z.object({
|
|
170
|
+
text: z.string().optional(),
|
|
171
|
+
gridSpan: positiveIndex.optional(),
|
|
172
|
+
vMerge: z.string().optional(),
|
|
173
|
+
bold: z.boolean().optional(),
|
|
174
|
+
header: z.boolean().optional(),
|
|
175
|
+
shading: z.string().optional(),
|
|
176
|
+
alignment: z.string().optional(),
|
|
177
|
+
richText: z.array(richTextSegment).optional(),
|
|
309
178
|
}).strict();
|
|
310
179
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}).strict()
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
180
|
+
function editAction(name, operationType, description, changeSchema) {
|
|
181
|
+
return { name, operationType, description, changeSchema, batch: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function documentAction(name, operationType, description) {
|
|
185
|
+
return { name, operationType, description, batch: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const docxEditActions = [
|
|
189
|
+
editAction('docx_set_anchored_text', 'replaceAnchoredText', 'Set text at current DOCX comment anchors.', z.object({ commentId: pathInput, text: z.string() }).strict()),
|
|
190
|
+
editAction('docx_set_paragraph_text', 'replaceParagraphText', 'Set current body paragraph text.', z.object({ paragraphIndex: index, text: z.string() }).strict()),
|
|
191
|
+
editAction('docx_set_paragraph_run_text', 'replaceParagraphRunText', 'Set current body paragraph run text.', z.object({ paragraphIndex: index, runIndex: index, text: z.string() }).strict()),
|
|
192
|
+
editAction('docx_replace_body_text', 'replaceBodyText', 'Replace uniquely matched current body text.', z.object({ findText: pathInput, text: z.string() }).strict()),
|
|
193
|
+
editAction('docx_delete_body_paragraph', 'deleteBodyParagraph', 'Delete uniquely matched current body paragraphs.', z.object({ findText: pathInput, ...optionalTextMatch }).strict()),
|
|
194
|
+
editAction('docx_delete_body_drawing_before_paragraph', 'deleteBodyDrawingBeforeParagraph', 'Delete the drawing immediately before a uniquely matched current paragraph.', z.object({ findText: pathInput, ...optionalTextMatch }).strict()),
|
|
195
|
+
editAction('docx_delete_body_range', 'deleteBodyRange', 'Delete uniquely bounded current body ranges.', z.object({ findText: pathInput, endFindText: z.string().optional(), matchMode: z.string().optional(), endMatchMode: z.string().optional(), paragraphStyle: z.string().optional(), endParagraphStyle: z.string().optional(), deleteToBodyEnd: z.boolean().optional(), removePrecedingPageBreak: z.boolean().optional() }).strict()),
|
|
196
|
+
editAction('docx_start_section', 'startSectionBeforeParagraph', 'Start a section before a uniquely matched current paragraph.', z.object({ findText: pathInput, orientation: z.enum(['portrait', 'landscape']) }).strict()),
|
|
197
|
+
editAction('docx_set_header_paragraph_text', 'replaceHeaderParagraphText', 'Set current header paragraph text.', z.object({ headerIndex: index, paragraphIndex: index, text: z.string() }).strict()),
|
|
198
|
+
editAction('docx_set_header_run_text', 'replaceHeaderParagraphRunText', 'Set current header run text.', z.object({ headerIndex: index, paragraphIndex: index, runIndex: index, text: z.string() }).strict()),
|
|
199
|
+
editAction('docx_replace_header_text', 'replaceHeaderText', 'Replace uniquely matched current header text.', z.object({ findText: pathInput, text: z.string() }).strict()),
|
|
200
|
+
editAction('docx_set_footer_paragraph_text', 'replaceFooterParagraphText', 'Set current footer paragraph text.', z.object({ footerIndex: index, paragraphIndex: index, text: z.string() }).strict()),
|
|
201
|
+
editAction('docx_set_footer_run_text', 'replaceFooterParagraphRunText', 'Set current footer run text.', z.object({ footerIndex: index, paragraphIndex: index, runIndex: index, text: z.string() }).strict()),
|
|
202
|
+
editAction('docx_set_table_cell_text', 'replaceTableCellText', 'Set current body table cell text.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index, text: z.string(), alignment: z.string().optional() }).strict()),
|
|
203
|
+
editAction('docx_set_table_cell_run_text', 'replaceTableCellRunText', 'Set current body table cell run text.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index, paragraphIndex: index, runIndex: index, text: z.string() }).strict()),
|
|
204
|
+
editAction('docx_set_table_cell_choice_state', 'setTableCellChoiceState', 'Set the selected state represented by current table-cell content.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index, text: z.string() }).strict()),
|
|
205
|
+
editAction('docx_set_header_table_cell_text', 'replaceHeaderTableCellText', 'Set current header table cell text.', z.object({ headerIndex: index, tableIndex: index, rowIndex: index, cellIndex: index, text: z.string() }).strict()),
|
|
206
|
+
editAction('docx_set_header_table_cell_run_text', 'replaceHeaderTableCellRunText', 'Set current header table cell run text.', z.object({ headerIndex: index, tableIndex: index, rowIndex: index, cellIndex: index, paragraphIndex: index, runIndex: index, text: z.string() }).strict()),
|
|
207
|
+
editAction('docx_set_footer_table_cell_text', 'replaceFooterTableCellText', 'Set current footer table cell text.', z.object({ footerIndex: index, tableIndex: index, rowIndex: index, cellIndex: index, text: z.string() }).strict()),
|
|
208
|
+
editAction('docx_set_footer_table_cell_run_text', 'replaceFooterTableCellRunText', 'Set current footer table cell run text.', z.object({ footerIndex: index, tableIndex: index, rowIndex: index, cellIndex: index, paragraphIndex: index, runIndex: index, text: z.string() }).strict()),
|
|
209
|
+
editAction('docx_set_table_cell_rich_text', 'replaceTableCellRichText', 'Set current body table cell rich text.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index, richText: z.array(richTextSegment) }).strict()),
|
|
210
|
+
editAction('docx_insert_table_rows', 'insertTableRows', 'Insert rows into a current body table.', z.object({ tableIndex: index, rowIndex: index, templateRowIndex: index.optional(), rows: z.array(z.array(tableCellInput)) }).strict()),
|
|
211
|
+
editAction('docx_delete_table_rows', 'deleteTableRows', 'Delete current body table row ranges.', z.object({ tableIndex: index, startRowIndex: index, endRowIndex: index }).strict()),
|
|
212
|
+
editAction('docx_replace_table_rows', 'replaceTableRows', 'Replace current body table row ranges.', z.object({ tableIndex: index, startRowIndex: index, endRowIndex: index, templateRowIndex: index.optional(), rows: z.array(z.array(tableCellInput)) }).strict()),
|
|
213
|
+
editAction('docx_insert_table_columns', 'insertTableColumns', 'Insert columns into a current body table.', z.object({ tableIndex: index, columnIndex: index, columnCount: positiveIndex.optional(), templateColumnIndex: index.optional() }).strict()),
|
|
214
|
+
editAction('docx_set_table_width', 'setTableWidth', 'Set current body table widths.', z.object({ tableIndex: index, width: pathInput, widthType: z.enum(['pct', 'dxa', 'auto', 'nil']) }).strict()),
|
|
215
|
+
editAction('docx_set_table_cell_alignment', 'setTableCellAlignment', 'Set current body table cell alignment.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index, alignment: pathInput }).strict()),
|
|
216
|
+
editAction('docx_set_table_cell_no_wrap', 'setTableCellNoWrap', 'Set current body table cell no-wrap state.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index, noWrap: z.boolean() }).strict()),
|
|
217
|
+
editAction('docx_set_table_cell_font_size', 'setTableCellFontSize', 'Set current body table cell font size.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index, fontSize: pathInput }).strict()),
|
|
218
|
+
editAction('docx_apply_font_policy', 'applyDocumentFontPolicy', 'Apply an explicit font policy to current document text.', z.object({ fontPolicy: z.object({ schema: pathInput, body: z.record(z.string(), z.string()), table: z.record(z.string(), z.string()) }).strict() }).strict()),
|
|
219
|
+
editAction('docx_set_table_row_height', 'setTableRowHeight', 'Set current body table row height.', z.object({ tableIndex: index, rowIndex: index, height: pathInput, heightRule: z.string().optional() }).strict()),
|
|
220
|
+
editAction('docx_set_table_row_cant_split', 'setTableRowCantSplit', 'Set current body table row split behavior.', z.object({ tableIndex: index, rowIndex: index, cantSplit: z.boolean() }).strict()),
|
|
221
|
+
editAction('docx_set_table_row_keep_next', 'setTableRowKeepNext', 'Set keep-next behavior for current body table rows.', z.object({ tableIndex: index, rowIndex: index, keepNext: z.boolean() }).strict()),
|
|
222
|
+
editAction('docx_set_body_paragraph_keep_next', 'setBodyParagraphKeepNext', 'Set keep-next behavior for current body paragraphs.', z.object({ paragraphIndex: index, keepNext: z.boolean() }).strict()),
|
|
223
|
+
editAction('docx_set_body_paragraph_keep_lines', 'setBodyParagraphKeepLines', 'Set keep-lines behavior for current body paragraphs.', z.object({ paragraphIndex: index, keepLines: z.boolean() }).strict()),
|
|
224
|
+
editAction('docx_set_header_paragraph_font_size', 'setHeaderParagraphFontSize', 'Set current header paragraph font size.', z.object({ headerIndex: index, paragraphIndex: index, fontSize: pathInput }).strict()),
|
|
225
|
+
documentAction('docx_collapse_trailing_empty_section', 'collapseTrailingEmptySection', 'Collapse a current trailing empty section.'),
|
|
226
|
+
documentAction('docx_collapse_trailing_empty_paragraphs', 'collapseTrailingEmptyBodyParagraphs', 'Collapse current trailing empty body paragraphs.'),
|
|
227
|
+
editAction('docx_merge_table_cells', 'mergeTableCells', 'Merge current body table cells.', z.object({ tableIndex: index, rowIndex: index.optional(), startCellIndex: index.optional(), endCellIndex: index.optional(), startRowIndex: index.optional(), endRowIndex: index.optional(), cellIndex: index.optional(), gridColumn: index.optional() }).strict()),
|
|
228
|
+
editAction('docx_unmerge_table_row_cells', 'unmergeTableRowHorizontalCells', 'Unmerge current horizontal table cells.', z.object({ tableIndex: index, rowIndex: index, cellIndex: index }).strict()),
|
|
229
|
+
editAction('docx_unmerge_table_column_cells', 'unmergeTableColumnVerticalCells', 'Unmerge current vertical table cell ranges.', z.object({ tableIndex: index, cellIndex: index, startRowIndex: index, endRowIndex: index }).strict()),
|
|
230
|
+
editAction('docx_delete_comments', 'deleteComments', 'Delete explicit current DOCX comments.', z.object({ commentIds: z.array(pathInput).min(1) }).strict()),
|
|
231
|
+
documentAction('docx_mark_fields_dirty', 'markFieldsDirty', 'Mark current DOCX fields for native refresh.'),
|
|
232
|
+
documentAction('docx_sanitize_fields', 'sanitizeFields', 'Remove update prompts and dirty markers from current DOCX fields.'),
|
|
233
|
+
documentAction('docx_freeze_fields', 'freezeFields', 'Convert current visible DOCX field results to ordinary content.'),
|
|
234
|
+
];
|
|
329
235
|
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}).strict(),
|
|
337
|
-
z.object({
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
limit: boundedLimit,
|
|
345
|
-
}).strict(),
|
|
346
|
-
z.object({
|
|
347
|
-
...migrationQueryDocuments,
|
|
348
|
-
view: z.literal('cleanup'),
|
|
349
|
-
text: z.string().trim().min(1).optional().describe('Optional literal case-insensitive text to find in cleanup target visible text or context.'),
|
|
350
|
-
offset: boundedOffset,
|
|
351
|
-
limit: boundedLimit,
|
|
352
|
-
}).strict(),
|
|
353
|
-
]);
|
|
236
|
+
const scalar = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
237
|
+
const xlsxEditActions = [
|
|
238
|
+
editAction('xlsx_set_cell_value', 'setCellValue', 'Set current workbook cell values.', z.object({ sheet: pathInput, cell: pathInput, value: scalar, valueType: z.string().optional(), bold: z.boolean().optional(), shrinkToFit: z.boolean().optional(), wrapText: z.boolean().optional() }).strict()),
|
|
239
|
+
editAction('xlsx_set_cell_number_format', 'setCellNumberFormat', 'Set current workbook cell number formats.', z.object({ sheet: pathInput, cell: pathInput, numberFormat: pathInput }).strict()),
|
|
240
|
+
editAction('xlsx_set_rich_text_cell_value', 'setRichTextCellValue', 'Set current workbook rich-text cell values.', z.object({ sheet: pathInput, cell: pathInput, value: z.string(), bold: z.boolean() }).strict()),
|
|
241
|
+
editAction('xlsx_set_range_values', 'setRangeValues', 'Set rectangular values in a current workbook.', z.object({ sheet: pathInput, startCell: pathInput, values: z.array(z.array(scalar)), valueType: z.string().optional() }).strict()),
|
|
242
|
+
editAction('xlsx_insert_rows', 'insertRows', 'Insert rows into a current worksheet.', z.object({ sheet: pathInput, startRow: positiveIndex, count: positiveIndex, preserveHorizontalMergedRanges: z.boolean().optional(), expandAdjacentVerticalMergedRanges: z.boolean().optional() }).strict()),
|
|
243
|
+
editAction('xlsx_copy_row', 'copyRow', 'Copy current worksheet rows.', z.object({ sheet: pathInput, sourceRow: positiveIndex, targetRow: positiveIndex, translateFormulas: z.boolean().optional() }).strict()),
|
|
244
|
+
editAction('xlsx_expand_section_rows', 'expandSectionRows', 'Expand current worksheet row sections from visible anchors.', z.object({ sheet: pathInput, anchorText: pathInput, exampleRows: positiveIndex, targetRows: positiveIndex, preserveStyle: z.boolean().optional(), preserveFormulas: z.boolean().optional(), preserveMergedRanges: z.boolean().optional() }).strict()),
|
|
245
|
+
editAction('xlsx_set_print_area', 'setPrintArea', 'Set current worksheet print areas.', z.object({ sheet: pathInput, range: pathInput }).strict()),
|
|
246
|
+
editAction('xlsx_set_page_setup', 'setPageSetup', 'Set current worksheet page properties.', z.object({ sheet: pathInput, fitToPagesWide: positiveIndex.optional(), fitToPagesTall: positiveIndex.optional(), orientation: z.enum(['portrait', 'landscape']).optional(), paperSize: z.enum(['letter', 'legal', 'a3', 'a4']).optional(), repeatRowsStart: positiveIndex.optional(), repeatRowsEnd: positiveIndex.optional(), repeatColsStart: positiveIndex.optional(), repeatColsEnd: positiveIndex.optional() }).strict()),
|
|
247
|
+
editAction('xlsx_set_row_page_breaks', 'setRowPageBreaks', 'Set current worksheet row page breaks.', z.object({ sheet: pathInput, breakBeforeRows: z.array(positiveIndex) }).strict()),
|
|
248
|
+
editAction('xlsx_set_column_width', 'setColumnWidth', 'Set current worksheet column widths.', z.object({ sheet: pathInput, column: pathInput, width: z.number().positive().max(255) }).strict()),
|
|
249
|
+
];
|
|
354
250
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
251
|
+
function editToolDefinitions(actions) {
|
|
252
|
+
return actions.map(action => ({
|
|
253
|
+
name: action.name,
|
|
254
|
+
description: action.batch ? `${action.description} One call batches only this action kind.` : action.description,
|
|
255
|
+
inputSchema: z.object({
|
|
256
|
+
input: pathInput,
|
|
257
|
+
output: pathInput.describe('New document output path. Existing files are never overwritten.'),
|
|
258
|
+
receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
|
|
259
|
+
...(action.batch ? { changes: z.array(action.changeSchema).min(1) } : {}),
|
|
260
|
+
}).strict(),
|
|
261
|
+
outputSchema: fixedEditOutput(action.name),
|
|
262
|
+
annotations: action.batch ? undefined : { idempotentHint: true },
|
|
263
|
+
handler: args => fixedEdit(action, args),
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
366
266
|
|
|
367
|
-
function
|
|
267
|
+
function fixedEditOutput(tool) {
|
|
368
268
|
return z.object({
|
|
369
|
-
tool: z.literal(tool),
|
|
370
|
-
|
|
371
|
-
artifact,
|
|
372
|
-
summary: z.object({
|
|
373
|
-
schema: z.string(),
|
|
374
|
-
toolVersion: z.string(),
|
|
375
|
-
status: z.enum(['pass', 'review-required', 'failed']),
|
|
376
|
-
pass: z.boolean(),
|
|
377
|
-
reviewRequired: z.boolean(),
|
|
378
|
-
outputVerified: z.boolean(),
|
|
379
|
-
output: z.string().nullable(),
|
|
380
|
-
plan: z.string().nullable(),
|
|
381
|
-
failureCount: z.number().int().nonnegative(),
|
|
382
|
-
}).strict(),
|
|
269
|
+
tool: z.literal(tool), runtime: runtimeIdentity, receipt: artifact, output: artifact.nullable(),
|
|
270
|
+
summary: z.object({ pass: z.boolean(), operationCount: z.number().int().nonnegative(), appliedCount: z.number().int().nonnegative() }).strict(),
|
|
383
271
|
}).strict();
|
|
384
272
|
}
|
|
385
273
|
|
|
@@ -396,37 +284,12 @@ const tools = [
|
|
|
396
284
|
handler: docxInspect,
|
|
397
285
|
},
|
|
398
286
|
{
|
|
399
|
-
name: '
|
|
400
|
-
description: '
|
|
401
|
-
inputSchema:
|
|
402
|
-
|
|
403
|
-
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
404
|
-
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
405
|
-
}).strict(),
|
|
406
|
-
outputSchema: migrationCatalogOutput,
|
|
407
|
-
handler: docxListMigrationChoices,
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
name: 'docx_query_migration_choices',
|
|
411
|
-
description: 'Query current template-migration alternatives without reading the catalog artifact. place-content moves current content into a target content position; keep-template-label preserves the target label and structure while migrating a uniquely identified current field value; select-template-option marks a target option represented by the current source fact. Choose the action from scenario meaning and filter by it before paging compatible targets. Target results bind the action and target into one alternativeRef; result order helps discovery but never makes the business choice.',
|
|
412
|
-
inputSchema: migrationChoiceQueryInput,
|
|
413
|
-
outputSchema: migrationChoiceQueryOutput,
|
|
287
|
+
name: 'docx_inspect_tables',
|
|
288
|
+
description: 'Inspect current DOCX tables, cells, merges, paragraphs, runs, and formatting.',
|
|
289
|
+
inputSchema: artifactInput,
|
|
290
|
+
outputSchema: artifactOutput('docx_inspect_tables'),
|
|
414
291
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
415
|
-
handler:
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
name: 'docx_migrate_template',
|
|
419
|
-
description: 'Migrate a current DOCX into the selected baseline from one complete batch of business choices. For targeted choices, submit one alternativeRef returned by docx_query_migration_choices. Exclusion and local review use a sourceRef plus their terminal action. The tool derives all document values, coordinates, plans, and edits.',
|
|
420
|
-
inputSchema: templateMigrationInput,
|
|
421
|
-
outputSchema: migrationReceiptOutput('docx_migrate_template'),
|
|
422
|
-
handler: docxMigrateTemplate,
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
name: 'docx_verify_migration',
|
|
426
|
-
description: 'Independently re-resolve the same business choices and verify a migrated DOCX against the current source and baseline. This does not trust the migration receipt.',
|
|
427
|
-
inputSchema: templateMigrationInput,
|
|
428
|
-
outputSchema: migrationReceiptOutput('docx_verify_migration'),
|
|
429
|
-
handler: docxVerifyMigration,
|
|
292
|
+
handler: docxInspectTables,
|
|
430
293
|
},
|
|
431
294
|
{
|
|
432
295
|
name: 'docx_compare',
|
|
@@ -442,6 +305,39 @@ const tools = [
|
|
|
442
305
|
outputSchema: artifactOutput('docx_export_json'),
|
|
443
306
|
handler: docxExportJson,
|
|
444
307
|
},
|
|
308
|
+
{
|
|
309
|
+
name: 'docx_validate',
|
|
310
|
+
description: 'Validate a current DOCX package against the published OpenXML contract.',
|
|
311
|
+
inputSchema: inputOnly,
|
|
312
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
313
|
+
handler: docxValidate,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'docx_validate_font_policy',
|
|
317
|
+
description: 'Validate current DOCX text against an explicit font policy.',
|
|
318
|
+
inputSchema: z.object({ input: pathInput, policy: z.object({ schema: pathInput, body: z.record(z.string(), z.string()), table: z.record(z.string(), z.string()) }).strict() }).strict(),
|
|
319
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
320
|
+
handler: docxValidateFontPolicy,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'docx_strip_direct_formatting',
|
|
324
|
+
description: 'Remove direct paragraph and run formatting while preserving styles.',
|
|
325
|
+
inputSchema: z.object({ input: pathInput, output: pathInput }).strict(),
|
|
326
|
+
handler: docxStripDirectFormatting,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'docx_replace_style_ids',
|
|
330
|
+
description: 'Replace current DOCX style IDs from an explicit style map.',
|
|
331
|
+
inputSchema: z.object({ input: pathInput, output: pathInput, styleMap: z.record(z.string(), z.string()) }).strict(),
|
|
332
|
+
handler: docxReplaceStyleIds,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'docx_fill_template',
|
|
336
|
+
description: 'Fill current DOCX placeholders from an explicit data object.',
|
|
337
|
+
inputSchema: z.object({ template: pathInput, output: pathInput, data: z.record(z.string(), z.unknown()) }).strict(),
|
|
338
|
+
handler: docxFillTemplate,
|
|
339
|
+
},
|
|
340
|
+
...editToolDefinitions(docxEditActions),
|
|
445
341
|
{
|
|
446
342
|
name: 'office_render_pdf',
|
|
447
343
|
description: 'Render a current Office document to PDF with its required native WPS backend and write the complete provider receipt as evidence. The input extension selects Writer, Spreadsheets, or Presentation; fallback rendering is rejected.',
|
|
@@ -453,9 +349,15 @@ const tools = [
|
|
|
453
349
|
outputSchema: nativeRenderOutput,
|
|
454
350
|
handler: officeRenderPdf,
|
|
455
351
|
},
|
|
352
|
+
{
|
|
353
|
+
name: 'xlsx_convert_legacy',
|
|
354
|
+
description: 'Convert a current legacy XLS workbook to XLSX using the published native ET backend.',
|
|
355
|
+
inputSchema: z.object({ input: pathInput, output: pathInput, receiptOutput: pathInput }).strict(),
|
|
356
|
+
handler: xlsxConvertLegacy,
|
|
357
|
+
},
|
|
456
358
|
{
|
|
457
359
|
name: 'xlsx_inspect',
|
|
458
|
-
description: 'Inspect
|
|
360
|
+
description: 'Inspect a current XLSX workbook or legacy XLS workbook and write one JSON observation containing workbook structure, exported values, formulas, styles, merged ranges, and any published legacy-format conversion evidence.',
|
|
459
361
|
inputSchema: artifactInput,
|
|
460
362
|
outputSchema: artifactOutput('xlsx_inspect'),
|
|
461
363
|
handler: xlsxInspect,
|
|
@@ -472,17 +374,12 @@ const tools = [
|
|
|
472
374
|
handler: xlsxExportJson,
|
|
473
375
|
},
|
|
474
376
|
{
|
|
475
|
-
name: '
|
|
476
|
-
description: '
|
|
477
|
-
inputSchema: z.object({
|
|
478
|
-
|
|
479
|
-
operations: pathInput.describe('Path to the deterministic tiwater.xlsx-edit/v1 operations artifact.'),
|
|
480
|
-
output: pathInput.describe('New XLSX output path. Existing files are never overwritten.'),
|
|
481
|
-
receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
|
|
482
|
-
}).strict(),
|
|
483
|
-
outputSchema: xlsxApplyOutput,
|
|
484
|
-
handler: xlsxApply,
|
|
377
|
+
name: 'xlsx_fill_template',
|
|
378
|
+
description: 'Fill current XLSX placeholders from an explicit data object.',
|
|
379
|
+
inputSchema: z.object({ template: pathInput, output: pathInput, data: z.record(z.string(), z.unknown()) }).strict(),
|
|
380
|
+
handler: xlsxFillTemplate,
|
|
485
381
|
},
|
|
382
|
+
...editToolDefinitions(xlsxEditActions),
|
|
486
383
|
{
|
|
487
384
|
name: 'xlsx_validate',
|
|
488
385
|
description: 'Validate an XLSX workbook package and return Open XML validation evidence.',
|
|
@@ -504,13 +401,20 @@ const tools = [
|
|
|
504
401
|
outputSchema: artifactOutput('pptx_export_json'),
|
|
505
402
|
handler: pptxExportJson,
|
|
506
403
|
},
|
|
404
|
+
{
|
|
405
|
+
name: 'pptx_fill_template',
|
|
406
|
+
description: 'Fill current PPTX placeholders from an explicit data object.',
|
|
407
|
+
inputSchema: z.object({ template: pathInput, output: pathInput, data: z.record(z.string(), z.unknown()) }).strict(),
|
|
408
|
+
handler: pptxFillTemplate,
|
|
409
|
+
},
|
|
507
410
|
{
|
|
508
411
|
name: 'pptx_apply_template',
|
|
509
412
|
description: 'Apply one deterministic PPTX template-application plan to a current presentation. This tool executes the published plan; it does not select a template or derive business content, slide mappings, geometry, or formatting decisions.',
|
|
510
413
|
inputSchema: z.object({
|
|
511
414
|
input: pathInput.describe('Path to the current source PPTX.'),
|
|
512
415
|
template: pathInput.describe('Path to the selected current template PPTX.'),
|
|
513
|
-
|
|
416
|
+
targetMasterPath: pathInput,
|
|
417
|
+
slides: z.array(z.object({ slideNumber: positiveIndex, targetLayoutPath: pathInput, contentBounds: z.object({ x: index, y: index, cx: positiveIndex, cy: positiveIndex }).strict().optional(), contentShapeIds: z.array(positiveIndex).min(1).optional(), sourceLayoutShapeIdsToPreserve: z.array(positiveIndex).optional() }).strict()).min(1),
|
|
514
418
|
output: pathInput.describe('New PPTX output path. Existing files are never overwritten.'),
|
|
515
419
|
receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
|
|
516
420
|
}).strict(),
|
|
@@ -522,20 +426,27 @@ const tools = [
|
|
|
522
426
|
description: 'Apply one deterministic PPTX formatting plan to a current presentation. This tool executes published formatting operations; it does not derive values, coordinates, or business decisions.',
|
|
523
427
|
inputSchema: z.object({
|
|
524
428
|
input: pathInput.describe('Path to the current PPTX.'),
|
|
525
|
-
|
|
429
|
+
changes: z.array(z.object({ slideNumber: positiveIndex, shapeId: positiveIndex, runIndex: index, fontFamily: z.string().optional(), fontSize: z.number().positive().optional(), color: z.string().optional(), bold: z.boolean().optional(), paragraphAlignment: z.string().optional() }).strict()).min(1),
|
|
526
430
|
output: pathInput.describe('New PPTX output path. Existing files are never overwritten.'),
|
|
527
431
|
receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
|
|
528
432
|
}).strict(),
|
|
529
433
|
outputSchema: pptxFormatApplyOutput,
|
|
530
434
|
handler: pptxApplyFormat,
|
|
531
435
|
},
|
|
436
|
+
{
|
|
437
|
+
name: 'pptx_validate',
|
|
438
|
+
description: 'Validate a current PPTX package against the published OpenXML contract.',
|
|
439
|
+
inputSchema: inputOnly,
|
|
440
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
441
|
+
handler: pptxValidate,
|
|
442
|
+
},
|
|
532
443
|
];
|
|
533
444
|
|
|
534
445
|
function buildServer() {
|
|
535
446
|
const server = new McpServer(
|
|
536
447
|
{ name: 'tiwater-office', version: packageMetadata.version },
|
|
537
448
|
{
|
|
538
|
-
instructions: 'Use
|
|
449
|
+
instructions: 'Use these tools only for generic Office observation, conversion, editing, validation, and native rendering. Derive business meaning from the active scenario knowledge and current documents; the provider owns no scenario workflow.',
|
|
539
450
|
},
|
|
540
451
|
);
|
|
541
452
|
for (const tool of tools) {
|
|
@@ -563,452 +474,98 @@ async function docxInspect(args) {
|
|
|
563
474
|
};
|
|
564
475
|
}
|
|
565
476
|
|
|
566
|
-
async function
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
const result = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', source, baseline]);
|
|
570
|
-
const catalog = migrationCatalog.parse(result.json);
|
|
571
|
-
return {
|
|
572
|
-
tool: 'docx_list_migration_choices',
|
|
573
|
-
runtime: commandRuntime(result),
|
|
574
|
-
artifact: await writeJsonArtifact(requireString(args.output, 'output'), catalog),
|
|
575
|
-
summary: {
|
|
576
|
-
schema: catalog.schema,
|
|
577
|
-
pass: catalog.pass,
|
|
578
|
-
sourceSha256: catalog.sourceSha256,
|
|
579
|
-
baselineSha256: catalog.baselineSha256,
|
|
580
|
-
sourceCount: catalog.sources.length,
|
|
581
|
-
targetCount: catalog.targets.length,
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
async function docxQueryMigrationChoices(args) {
|
|
587
|
-
const sourcePath = requireString(args.source, 'source');
|
|
588
|
-
const baselinePath = requireString(args.baseline, 'baseline');
|
|
589
|
-
const offset = args.offset ?? 0;
|
|
590
|
-
const limit = args.limit ?? 10;
|
|
591
|
-
const catalogResult = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', sourcePath, baselinePath]);
|
|
592
|
-
const catalog = migrationCatalog.parse(catalogResult.json);
|
|
593
|
-
|
|
594
|
-
if (args.view === 'sources') {
|
|
595
|
-
return migrationQueryResult({
|
|
596
|
-
runtime: commandRuntime(catalogResult),
|
|
597
|
-
catalog,
|
|
598
|
-
view: 'sources',
|
|
599
|
-
action: null,
|
|
600
|
-
source: null,
|
|
601
|
-
items: catalog.sources.slice(offset, offset + limit),
|
|
602
|
-
offset,
|
|
603
|
-
total: catalog.sources.length,
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
let source = null;
|
|
608
|
-
let action = null;
|
|
609
|
-
if (args.view === 'cleanup') {
|
|
610
|
-
const targetResult = await loadMigrationTargets({
|
|
611
|
-
sourcePath,
|
|
612
|
-
baselinePath,
|
|
613
|
-
sourceChoiceId: '-',
|
|
614
|
-
branch: 'baseline-clear',
|
|
615
|
-
text: args.text,
|
|
616
|
-
});
|
|
617
|
-
const orderedTargets = validateAndOrderMigrationTargets(catalog, null, targetResult.targets);
|
|
618
|
-
return migrationQueryResult({
|
|
619
|
-
runtime: targetResult.runtime,
|
|
620
|
-
catalog,
|
|
621
|
-
view: 'cleanup',
|
|
622
|
-
action: null,
|
|
623
|
-
source: null,
|
|
624
|
-
items: orderedTargets.slice(offset, offset + limit),
|
|
625
|
-
offset,
|
|
626
|
-
total: orderedTargets.length,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const requestedSourceId = choiceIdFromRef(catalog, catalog.sources, args.sourceRef, 'S', 'source');
|
|
631
|
-
source = catalog.sources.find(item => item.id === requestedSourceId) ?? null;
|
|
632
|
-
if (!source) {
|
|
633
|
-
throw Object.assign(new Error(`Unknown migration source: ${args.sourceRef}`), { code: -32602 });
|
|
634
|
-
}
|
|
635
|
-
action = args.action ?? null;
|
|
636
|
-
const actions = action
|
|
637
|
-
? [action]
|
|
638
|
-
: [...targetActions].filter(candidate => source.allowedActions.includes(candidate));
|
|
639
|
-
const pages = await Promise.all(actions.map(async candidate => {
|
|
640
|
-
if (!source.allowedActions.includes(candidate)) {
|
|
641
|
-
throw Object.assign(new Error(`Action ${candidate} is not allowed for ${source.id}`), { code: -32602 });
|
|
642
|
-
}
|
|
643
|
-
const result = await loadMigrationTargets({
|
|
644
|
-
sourcePath,
|
|
645
|
-
baselinePath,
|
|
646
|
-
sourceChoiceId: source.id,
|
|
647
|
-
branch: migrationTargetBranch(candidate, source.kind),
|
|
648
|
-
text: args.text,
|
|
649
|
-
});
|
|
650
|
-
return {
|
|
651
|
-
action: candidate,
|
|
652
|
-
runtime: result.runtime,
|
|
653
|
-
targets: validateAndOrderMigrationTargets(catalog, source, result.targets),
|
|
654
|
-
};
|
|
655
|
-
}));
|
|
656
|
-
const catalogOrder = new Map(catalog.targets.map((item, index) => [item.id, index]));
|
|
657
|
-
const actionOrder = new Map([...targetActions].map((item, index) => [item, index]));
|
|
658
|
-
const alternatives = pages.flatMap(page => page.targets.map(target => ({ action: page.action, target })))
|
|
659
|
-
.sort((left, right) => {
|
|
660
|
-
const relevance = compareRelevance(
|
|
661
|
-
migrationChoiceRelevance(source, right.target),
|
|
662
|
-
migrationChoiceRelevance(source, left.target));
|
|
663
|
-
return relevance
|
|
664
|
-
|| catalogOrder.get(left.target.id) - catalogOrder.get(right.target.id)
|
|
665
|
-
|| actionOrder.get(left.action) - actionOrder.get(right.action);
|
|
666
|
-
})
|
|
667
|
-
.map(item => ({
|
|
668
|
-
ref: migrationAlternativeRef(catalog, source, item.action, item.target),
|
|
669
|
-
action: item.action,
|
|
670
|
-
target: publicMigrationChoice(catalog, item.target, 'T'),
|
|
671
|
-
}));
|
|
672
|
-
return migrationQueryResult({
|
|
673
|
-
runtime: pages[0]?.runtime ?? commandRuntime(catalogResult),
|
|
674
|
-
catalog,
|
|
675
|
-
view: 'targets',
|
|
676
|
-
action,
|
|
677
|
-
source,
|
|
678
|
-
items: alternatives.slice(offset, offset + limit),
|
|
679
|
-
offset,
|
|
680
|
-
total: alternatives.length,
|
|
681
|
-
alternatives: true,
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function validateAndOrderMigrationTargets(catalog, source, targets) {
|
|
686
|
-
const catalogTargets = new Map(catalog.targets.map(item => [item.id, item]));
|
|
687
|
-
for (const target of targets) {
|
|
688
|
-
const current = catalogTargets.get(target.id);
|
|
689
|
-
if (!current || !isDeepStrictEqual(current, target)) {
|
|
690
|
-
throw new Error(`Migration target ${target.id} is not bound to the current catalog`);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
const catalogOrder = new Map(catalog.targets.map((item, index) => [item.id, index]));
|
|
694
|
-
return source
|
|
695
|
-
? [...targets].sort((left, right) => {
|
|
696
|
-
const relevance = compareRelevance(
|
|
697
|
-
migrationChoiceRelevance(source, right),
|
|
698
|
-
migrationChoiceRelevance(source, left));
|
|
699
|
-
return relevance || catalogOrder.get(left.id) - catalogOrder.get(right.id);
|
|
700
|
-
})
|
|
701
|
-
: [...targets].sort((left, right) => catalogOrder.get(left.id) - catalogOrder.get(right.id));
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
async function loadMigrationTargets({ sourcePath, baselinePath, sourceChoiceId, branch, text }) {
|
|
705
|
-
const targets = [];
|
|
706
|
-
let offset = 0;
|
|
707
|
-
let total = null;
|
|
708
|
-
let runtime = null;
|
|
709
|
-
do {
|
|
710
|
-
const result = await runJsonCandidateChain(docxCandidates, [
|
|
711
|
-
'find-template-migration-targets',
|
|
712
|
-
sourcePath,
|
|
713
|
-
baselinePath,
|
|
714
|
-
sourceChoiceId,
|
|
715
|
-
branch,
|
|
716
|
-
text ?? '-',
|
|
717
|
-
String(offset),
|
|
718
|
-
'100',
|
|
719
|
-
]);
|
|
720
|
-
const page = migrationTargetPage.parse(result.json);
|
|
721
|
-
if (page.sourceChoiceId !== (sourceChoiceId === '-' ? null : sourceChoiceId) || page.branch !== branch) {
|
|
722
|
-
throw new Error('Migration target page identity does not match the requested current source and action');
|
|
723
|
-
}
|
|
724
|
-
if (total !== null && page.total !== total) {
|
|
725
|
-
throw new Error('Migration target page total changed while reading current alternatives');
|
|
726
|
-
}
|
|
727
|
-
if (page.offset !== offset || page.targets.length === 0 && offset < page.total) {
|
|
728
|
-
throw new Error('Migration target pagination did not advance');
|
|
729
|
-
}
|
|
730
|
-
runtime ??= commandRuntime(result);
|
|
731
|
-
total = page.total;
|
|
732
|
-
targets.push(...page.targets);
|
|
733
|
-
offset += page.targets.length;
|
|
734
|
-
} while (offset < total);
|
|
735
|
-
return { runtime, targets };
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function migrationChoiceRelevance(source, target) {
|
|
739
|
-
const sourceContext = source.context ?? {};
|
|
740
|
-
const targetContext = target.context ?? {};
|
|
741
|
-
const sourceRows = sourceContext.sameRowTexts ?? [];
|
|
742
|
-
const targetRows = targetContext.sameRowTexts ?? [];
|
|
743
|
-
const sourceHeaders = sourceContext.tableHeaderTexts ?? [];
|
|
744
|
-
const targetHeaders = targetContext.tableHeaderTexts ?? [];
|
|
745
|
-
const mainText = textSimilarity(source.text, target.text);
|
|
746
|
-
const tableIdentity = Math.max(
|
|
747
|
-
textSimilarity(sourceContext.columnHeaderText, targetContext.columnHeaderText),
|
|
748
|
-
maximumPairSimilarity(sourceHeaders, targetHeaders));
|
|
749
|
-
const neighborhood = Math.max(
|
|
750
|
-
textSimilarity(sourceContext.previousText, targetContext.previousText),
|
|
751
|
-
textSimilarity(sourceContext.nextText, targetContext.nextText),
|
|
752
|
-
maximumPairSimilarity(sourceRows, targetRows),
|
|
753
|
-
maximumPairSimilarity([source.text], targetRows),
|
|
754
|
-
maximumPairSimilarity(sourceRows, [target.text]));
|
|
755
|
-
return source.kind === 'table-cell' && target.kind === 'table-cell'
|
|
756
|
-
? [tableIdentity, mainText, neighborhood]
|
|
757
|
-
: [mainText, neighborhood, tableIdentity];
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function compareRelevance(left, right) {
|
|
761
|
-
for (let index = 0; index < left.length; index += 1) {
|
|
762
|
-
if (left[index] !== right[index]) return left[index] - right[index];
|
|
763
|
-
}
|
|
764
|
-
return 0;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function maximumPairSimilarity(leftValues, rightValues) {
|
|
768
|
-
let maximum = 0;
|
|
769
|
-
for (const left of leftValues ?? []) {
|
|
770
|
-
for (const right of rightValues ?? []) {
|
|
771
|
-
maximum = Math.max(maximum, textSimilarity(left, right));
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return maximum;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function textSimilarity(leftValue, rightValue) {
|
|
778
|
-
const left = normalizeSearchText(leftValue);
|
|
779
|
-
const right = normalizeSearchText(rightValue);
|
|
780
|
-
if (!left || !right) return 0;
|
|
781
|
-
if (left === right) return 1;
|
|
782
|
-
if (Math.min(left.length, right.length) >= 3 && (left.includes(right) || right.includes(left))) {
|
|
783
|
-
return Math.min(left.length, right.length) / Math.max(left.length, right.length);
|
|
784
|
-
}
|
|
785
|
-
const leftPairs = characterPairs(left);
|
|
786
|
-
const rightPairs = characterPairs(right);
|
|
787
|
-
if (leftPairs.size === 0 || rightPairs.size === 0) return 0;
|
|
788
|
-
let shared = 0;
|
|
789
|
-
for (const pair of leftPairs) if (rightPairs.has(pair)) shared += 1;
|
|
790
|
-
return 2 * shared / (leftPairs.size + rightPairs.size);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function normalizeSearchText(value) {
|
|
794
|
-
return typeof value === 'string'
|
|
795
|
-
? value.normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '')
|
|
796
|
-
: '';
|
|
477
|
+
async function docxInspectTables(args) {
|
|
478
|
+
const result = await runJsonCandidateChain(docxCandidates, ['inspect-tables', requireString(args.input, 'input'), '--json']);
|
|
479
|
+
return { tool: 'docx_inspect_tables', runtime: commandRuntime(result), artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json) };
|
|
797
480
|
}
|
|
798
481
|
|
|
799
|
-
function
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
const pairs = new Set();
|
|
803
|
-
for (let index = 0; index + 1 < characters.length; index += 1) {
|
|
804
|
-
pairs.add(characters[index] + characters[index + 1]);
|
|
805
|
-
}
|
|
806
|
-
return pairs;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function migrationTargetBranch(action, sourceKind) {
|
|
810
|
-
if (action === 'place-content') return sourceKind === 'media' ? 'copy-media' : 'copy-text';
|
|
811
|
-
if (action === 'keep-template-content') return 'retain-target';
|
|
812
|
-
if (action === 'keep-template-label') return 'retain-target-label';
|
|
813
|
-
if (action === 'select-template-option') return 'choice-selection';
|
|
814
|
-
throw new Error(`Unsupported migration target action: ${action}`);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function catalogReferenceToken(catalog) {
|
|
818
|
-
return createHash('sha256')
|
|
819
|
-
.update(`${catalog.sourceSha256}\n${catalog.baselineSha256}`)
|
|
820
|
-
.digest('hex')
|
|
821
|
-
.slice(0, 8);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
function choiceRef(catalog, items, id, prefix) {
|
|
825
|
-
const index = items.findIndex(item => item.id === id);
|
|
826
|
-
if (index < 0) throw new Error(`Migration choice is not bound to the current catalog: ${id}`);
|
|
827
|
-
return `${prefix}${index + 1}-${catalogReferenceToken(catalog)}`;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function choiceIdFromRef(catalog, items, ref, prefix, label) {
|
|
831
|
-
if (!ref.startsWith(prefix)) {
|
|
832
|
-
throw invalidMigrationInput(`invalid migration ${label} ref: ${ref}`);
|
|
833
|
-
}
|
|
834
|
-
const [ordinal, token] = ref.slice(1).split('-');
|
|
835
|
-
if (token !== catalogReferenceToken(catalog)) {
|
|
836
|
-
throw invalidMigrationInput(`stale migration ${label} ref: ${ref}`);
|
|
837
|
-
}
|
|
838
|
-
const item = items[Number(ordinal) - 1];
|
|
839
|
-
if (!item) throw invalidMigrationInput(`unknown migration ${label} ref: ${ref}`);
|
|
840
|
-
return item.id;
|
|
482
|
+
async function docxValidate(args) {
|
|
483
|
+
const result = await runJsonCandidateChain(docxCandidates, ['validate-openxml', requireString(args.input, 'input')], { allowedExitCodes: [0, 1] });
|
|
484
|
+
return { tool: 'docx_validate', runtime: commandRuntime(result), result: result.json };
|
|
841
485
|
}
|
|
842
486
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const migrationActionsByCode = new Map([...migrationActionCodes].map(([action, code]) => [code, action]));
|
|
849
|
-
|
|
850
|
-
function migrationAlternativeRef(catalog, source, action, target) {
|
|
851
|
-
const sourceOrdinal = catalog.sources.findIndex(item => item.id === source.id) + 1;
|
|
852
|
-
const targetOrdinal = catalog.targets.findIndex(item => item.id === target.id) + 1;
|
|
853
|
-
const code = migrationActionCodes.get(action);
|
|
854
|
-
if (sourceOrdinal < 1 || targetOrdinal < 1 || !code) {
|
|
855
|
-
throw new Error('Migration alternative is not bound to the current catalog');
|
|
856
|
-
}
|
|
857
|
-
return `S${sourceOrdinal}-${code}${targetOrdinal}-${migrationAlternativeToken(catalog, source, action, target)}`;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function migrationAlternativeToken(catalog, source, action, target) {
|
|
861
|
-
return createHash('sha256')
|
|
862
|
-
.update(`${catalog.sourceSha256}\n${catalog.baselineSha256}\n${source.id}\n${action}\n${target.id}`)
|
|
863
|
-
.digest('hex')
|
|
864
|
-
.slice(0, 8);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function migrationChoiceFromAlternativeRef(catalog, ref) {
|
|
868
|
-
const match = /^S([1-9][0-9]*)-([PLO])([1-9][0-9]*)-([0-9a-f]{8})$/.exec(ref);
|
|
869
|
-
if (!match) throw invalidMigrationInput(`invalid migration alternative ref: ${ref}`);
|
|
870
|
-
const source = catalog.sources[Number(match[1]) - 1];
|
|
871
|
-
const target = catalog.targets[Number(match[3]) - 1];
|
|
872
|
-
const action = migrationActionsByCode.get(match[2]);
|
|
873
|
-
if (!source || !target || !action) {
|
|
874
|
-
throw invalidMigrationInput(`unknown migration alternative ref: ${ref}`);
|
|
875
|
-
}
|
|
876
|
-
if (match[4] !== migrationAlternativeToken(catalog, source, action, target)) {
|
|
877
|
-
throw invalidMigrationInput(`stale or invalid migration alternative ref: ${ref}`);
|
|
878
|
-
}
|
|
879
|
-
return { sourceChoiceId: source.id, action, targetChoiceId: target.id };
|
|
487
|
+
async function docxValidateFontPolicy(args) {
|
|
488
|
+
return withTempJsonFile(args.policy, async policyPath => {
|
|
489
|
+
const result = await runJsonCandidateChain(docxCandidates, ['validate-font-policy', requireString(args.input, 'input'), policyPath], { allowedExitCodes: [0, 1] });
|
|
490
|
+
return { tool: 'docx_validate_font_policy', runtime: commandRuntime(result), result: result.json };
|
|
491
|
+
});
|
|
880
492
|
}
|
|
881
493
|
|
|
882
|
-
function
|
|
883
|
-
|
|
884
|
-
const items = prefix === 'S' ? catalog.sources : catalog.targets;
|
|
885
|
-
return {
|
|
886
|
-
...visible,
|
|
887
|
-
allowedActions: visible.allowedActions.filter(action => action !== 'keep-template-content'),
|
|
888
|
-
ref: choiceRef(catalog, items, id, prefix),
|
|
889
|
-
};
|
|
494
|
+
async function docxStripDirectFormatting(args) {
|
|
495
|
+
return copyTransform('docx_strip_direct_formatting', docxCandidates, ['strip-direct-formatting'], args);
|
|
890
496
|
}
|
|
891
497
|
|
|
892
|
-
function
|
|
893
|
-
return
|
|
894
|
-
tool: 'docx_query_migration_choices',
|
|
895
|
-
runtime,
|
|
896
|
-
sourceSha256: catalog.sourceSha256,
|
|
897
|
-
baselineSha256: catalog.baselineSha256,
|
|
898
|
-
view,
|
|
899
|
-
action,
|
|
900
|
-
source: source ? publicMigrationChoice(catalog, source, 'S') : null,
|
|
901
|
-
items: alternatives
|
|
902
|
-
? items
|
|
903
|
-
: items.map(item => publicMigrationChoice(catalog, item, view === 'sources' ? 'S' : 'T')),
|
|
904
|
-
page: {
|
|
905
|
-
offset,
|
|
906
|
-
returned: items.length,
|
|
907
|
-
total,
|
|
908
|
-
hasMore: offset + items.length < total,
|
|
909
|
-
},
|
|
910
|
-
};
|
|
498
|
+
async function docxReplaceStyleIds(args) {
|
|
499
|
+
return withTempJsonFile(args.styleMap, styleMapPath => copyTransform('docx_replace_style_ids', docxCandidates, ['replace-style-ids'], args, [styleMapPath]));
|
|
911
500
|
}
|
|
912
501
|
|
|
913
|
-
async function
|
|
914
|
-
return
|
|
502
|
+
async function docxFillTemplate(args) {
|
|
503
|
+
return withTempJsonFile(args.data, dataPath => templateFill('docx_fill_template', docxCandidates, args, dataPath));
|
|
915
504
|
}
|
|
916
505
|
|
|
917
|
-
async function
|
|
918
|
-
return
|
|
506
|
+
async function xlsxFillTemplate(args) {
|
|
507
|
+
return withTempJsonFile(args.data, dataPath => templateFill('xlsx_fill_template', xlsxCandidates, args, dataPath));
|
|
919
508
|
}
|
|
920
509
|
|
|
921
|
-
function
|
|
922
|
-
return
|
|
510
|
+
async function pptxFillTemplate(args) {
|
|
511
|
+
return withTempJsonFile(args.data, dataPath => templateFill('pptx_fill_template', pptxCandidates, args, dataPath));
|
|
923
512
|
}
|
|
924
513
|
|
|
925
|
-
function
|
|
926
|
-
const
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
: {
|
|
932
|
-
sourceChoiceId: choiceIdFromRef(catalog, catalog.sources, rawChoice.sourceRef, 'S', 'source'),
|
|
933
|
-
action: rawChoice.action,
|
|
934
|
-
};
|
|
935
|
-
const { sourceChoiceId } = choice;
|
|
936
|
-
const source = sources.get(sourceChoiceId);
|
|
937
|
-
if (!source) throw invalidMigrationInput(`unknown migration source id: ${sourceChoiceId}`);
|
|
938
|
-
if (seen.has(sourceChoiceId)) throw invalidMigrationInput(`duplicate migration source id: ${sourceChoiceId}`);
|
|
939
|
-
seen.add(sourceChoiceId);
|
|
940
|
-
if (!source.allowedActions.includes(choice.action)) {
|
|
941
|
-
throw invalidMigrationInput(`migration action ${choice.action} is not allowed for source id: ${sourceChoiceId}`);
|
|
942
|
-
}
|
|
943
|
-
return source.requiredCardinality === 'all'
|
|
944
|
-
? { ...choice, cardinality: 'all' }
|
|
945
|
-
: choice;
|
|
946
|
-
});
|
|
947
|
-
const missing = [...sources.keys()].filter(sourceChoiceId => !seen.has(sourceChoiceId));
|
|
948
|
-
if (missing.length > 0) {
|
|
949
|
-
throw invalidMigrationInput(`migration choices must cover every source id; missing ${missing.length}`);
|
|
950
|
-
}
|
|
951
|
-
return completed;
|
|
514
|
+
async function templateFill(tool, candidates, args, dataPath) {
|
|
515
|
+
const template = path.resolve(requireString(args.template, 'template'));
|
|
516
|
+
const output = path.resolve(requireString(args.output, 'output'));
|
|
517
|
+
await requireNewFile(output, 'output');
|
|
518
|
+
const result = await runJsonCandidateChain(candidates, ['fill-template', template, dataPath, output]);
|
|
519
|
+
return { tool, runtime: commandRuntime(result), output: await fileArtifact(output), result: result.json };
|
|
952
520
|
}
|
|
953
521
|
|
|
954
|
-
function
|
|
955
|
-
const
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
if (!seen.add(targetChoiceId)) throw invalidMigrationInput(`duplicate migration cleanup target id: ${targetChoiceId}`);
|
|
961
|
-
if (!targets.get(targetChoiceId)?.allowedActions.includes('template-cleanup')) {
|
|
962
|
-
throw invalidMigrationInput(`migration cleanup is not allowed for target id: ${targetChoiceId}`);
|
|
963
|
-
}
|
|
964
|
-
return { targetChoiceId, scope: rawCleanup.scope };
|
|
965
|
-
}).filter(cleanupChoice => !claimedTargets.has(cleanupChoice.targetChoiceId));
|
|
522
|
+
async function copyTransform(tool, candidates, command, args, suffix = []) {
|
|
523
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
524
|
+
const output = path.resolve(requireString(args.output, 'output'));
|
|
525
|
+
await requireNewFile(output, 'output');
|
|
526
|
+
const result = await runCandidateChain(candidates, [...command, input, output, ...suffix]);
|
|
527
|
+
return { tool, runtime: commandRuntime(result), output: await fileArtifact(output) };
|
|
966
528
|
}
|
|
967
529
|
|
|
968
|
-
async function
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
530
|
+
async function fixedEdit(action, args) {
|
|
531
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
532
|
+
const output = path.resolve(requireString(args.output, 'output'));
|
|
533
|
+
const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
|
|
534
|
+
await requireNewFile(output, 'output');
|
|
535
|
+
await requireNewFile(receiptOutput, 'receiptOutput');
|
|
536
|
+
const inputArtifact = await fileArtifact(input);
|
|
537
|
+
const operations = action.batch
|
|
538
|
+
? args.changes.map(change => ({ ...change, type: action.operationType }))
|
|
539
|
+
: [{ type: action.operationType }];
|
|
540
|
+
const candidates = action.name.startsWith('docx_') ? docxCandidates : xlsxCandidates;
|
|
541
|
+
return withTempJsonFile({ operations }, async operationsPath => {
|
|
542
|
+
try {
|
|
543
|
+
const result = await runJsonCandidateChain(candidates, ['edit', input, operationsPath, output], { allowedExitCodes: [0, 1] });
|
|
544
|
+
const appliedOperations = Array.isArray(result.json?.appliedOperations) ? result.json.appliedOperations : [];
|
|
545
|
+
const pass = appliedOperations.length === operations.length && appliedOperations.every(operation => operation.applied === true);
|
|
546
|
+
const outputArtifact = pass ? await fileArtifact(output) : null;
|
|
547
|
+
if (!pass) await rm(output, { force: true });
|
|
548
|
+
const receipt = {
|
|
549
|
+
schema: 'tiwater.office.fixed-edit-receipt/v1',
|
|
550
|
+
tool: action.name,
|
|
551
|
+
operationType: action.operationType,
|
|
552
|
+
pass,
|
|
553
|
+
input: inputArtifact,
|
|
554
|
+
output: outputArtifact,
|
|
555
|
+
operationCount: operations.length,
|
|
556
|
+
appliedOperations,
|
|
557
|
+
};
|
|
558
|
+
return {
|
|
559
|
+
tool: action.name,
|
|
560
|
+
runtime: commandRuntime(result),
|
|
561
|
+
receipt: await writeJsonArtifact(receiptOutput, receipt),
|
|
562
|
+
output: outputArtifact,
|
|
563
|
+
summary: { pass, operationCount: operations.length, appliedCount: appliedOperations.filter(operation => operation.applied).length },
|
|
564
|
+
};
|
|
565
|
+
} catch (error) {
|
|
566
|
+
await rm(output, { force: true });
|
|
567
|
+
throw error;
|
|
994
568
|
}
|
|
995
|
-
const receipt = migrationReceipt.parse(result.json);
|
|
996
|
-
return {
|
|
997
|
-
tool,
|
|
998
|
-
runtime: commandRuntime(result),
|
|
999
|
-
artifact: await writeJsonArtifact(requireString(args.receiptOutput, 'receiptOutput'), receipt),
|
|
1000
|
-
summary: {
|
|
1001
|
-
schema: receipt.schema,
|
|
1002
|
-
toolVersion: receipt.toolVersion,
|
|
1003
|
-
status: receipt.status,
|
|
1004
|
-
pass: receipt.pass,
|
|
1005
|
-
reviewRequired: receipt.reviewRequired,
|
|
1006
|
-
outputVerified: receipt.outputVerified,
|
|
1007
|
-
output: receipt.output,
|
|
1008
|
-
plan: receipt.plan,
|
|
1009
|
-
failureCount: receipt.failures.length,
|
|
1010
|
-
},
|
|
1011
|
-
};
|
|
1012
569
|
});
|
|
1013
570
|
}
|
|
1014
571
|
|
|
@@ -1080,6 +637,30 @@ async function officeRenderPdf(args) {
|
|
|
1080
637
|
}
|
|
1081
638
|
}
|
|
1082
639
|
|
|
640
|
+
async function xlsxConvertLegacy(args) {
|
|
641
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
642
|
+
const output = path.resolve(requireString(args.output, 'output'));
|
|
643
|
+
const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
|
|
644
|
+
if (path.extname(input).toLowerCase() !== '.xls' || path.extname(output).toLowerCase() !== '.xlsx') {
|
|
645
|
+
throw Object.assign(new Error('Legacy conversion requires .xls input and .xlsx output'), { code: -32602 });
|
|
646
|
+
}
|
|
647
|
+
await requireNewFile(output, 'output');
|
|
648
|
+
await requireNewFile(receiptOutput, 'receiptOutput');
|
|
649
|
+
const inputArtifact = await fileArtifact(input);
|
|
650
|
+
try {
|
|
651
|
+
const result = await runJsonCandidateChain(convertCandidates, ['xls-to-xlsx', input, output]);
|
|
652
|
+
const outputArtifact = await fileArtifact(output);
|
|
653
|
+
const receipt = {
|
|
654
|
+
schema: 'tiwater.office.xlsx-legacy-conversion-receipt/v1',
|
|
655
|
+
backend: 'et', input: inputArtifact, output: outputArtifact, provider: result.json,
|
|
656
|
+
};
|
|
657
|
+
return { tool: 'xlsx_convert_legacy', runtime: commandRuntime(result), output: outputArtifact, receipt: await writeJsonArtifact(receiptOutput, receipt) };
|
|
658
|
+
} catch (error) {
|
|
659
|
+
await rm(output, { force: true });
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
1083
664
|
function nativeRenderBackend(sourceFormat) {
|
|
1084
665
|
if (['doc', 'docx', 'odt', 'rtf'].includes(sourceFormat)) return 'wps';
|
|
1085
666
|
if (['xls', 'xlsx', 'ods'].includes(sourceFormat)) return 'et';
|
|
@@ -1111,56 +692,6 @@ async function xlsxExportJson(args) {
|
|
|
1111
692
|
};
|
|
1112
693
|
}
|
|
1113
694
|
|
|
1114
|
-
async function xlsxApply(args) {
|
|
1115
|
-
const input = path.resolve(requireString(args.input, 'input'));
|
|
1116
|
-
const operations = path.resolve(requireString(args.operations, 'operations'));
|
|
1117
|
-
const output = path.resolve(requireString(args.output, 'output'));
|
|
1118
|
-
const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
|
|
1119
|
-
if (path.extname(input).toLowerCase() !== '.xlsx' || path.extname(output).toLowerCase() !== '.xlsx') {
|
|
1120
|
-
throw Object.assign(new Error('XLSX apply input and output must use the .xlsx extension'), { code: -32602 });
|
|
1121
|
-
}
|
|
1122
|
-
await requireNewFile(output, 'output');
|
|
1123
|
-
await requireNewFile(receiptOutput, 'receiptOutput');
|
|
1124
|
-
const inputArtifact = await fileArtifact(input);
|
|
1125
|
-
const operationsArtifact = await fileArtifact(operations);
|
|
1126
|
-
await mkdir(path.dirname(output), { recursive: true });
|
|
1127
|
-
try {
|
|
1128
|
-
const result = await runJsonCandidateChain(
|
|
1129
|
-
xlsxCandidates,
|
|
1130
|
-
['edit', input, operations, output],
|
|
1131
|
-
{ allowedExitCodes: [0, 1] });
|
|
1132
|
-
const edit = xlsxEditResult.parse(result.json);
|
|
1133
|
-
if (path.resolve(edit.input) !== input || path.resolve(edit.output) !== output) {
|
|
1134
|
-
throw new Error('XLSX edit receipt is not bound to the current input and output');
|
|
1135
|
-
}
|
|
1136
|
-
const pass = edit.appliedOperations.every(operation => operation.applied);
|
|
1137
|
-
const outputArtifact = pass ? await fileArtifact(output) : null;
|
|
1138
|
-
if (!pass) await rm(output, { force: true });
|
|
1139
|
-
const receipt = xlsxApplyReceipt.parse({
|
|
1140
|
-
schema: 'tiwater.office.xlsx-apply-receipt/v1',
|
|
1141
|
-
pass,
|
|
1142
|
-
input: inputArtifact,
|
|
1143
|
-
operations: operationsArtifact,
|
|
1144
|
-
output: outputArtifact,
|
|
1145
|
-
appliedOperations: edit.appliedOperations,
|
|
1146
|
-
});
|
|
1147
|
-
return {
|
|
1148
|
-
tool: 'xlsx_apply',
|
|
1149
|
-
runtime: commandRuntime(result),
|
|
1150
|
-
receipt: await writeJsonArtifact(receiptOutput, receipt),
|
|
1151
|
-
output: outputArtifact,
|
|
1152
|
-
summary: {
|
|
1153
|
-
pass,
|
|
1154
|
-
operationCount: edit.appliedOperations.length,
|
|
1155
|
-
appliedCount: edit.appliedOperations.filter(operation => operation.applied).length,
|
|
1156
|
-
},
|
|
1157
|
-
};
|
|
1158
|
-
} catch (error) {
|
|
1159
|
-
await rm(output, { force: true });
|
|
1160
|
-
throw error;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
695
|
async function xlsxValidate(args) {
|
|
1165
696
|
const input = requireString(args.input, 'input');
|
|
1166
697
|
const result = await runJsonCandidateChain(xlsxCandidates, ['validate', input], { allowedExitCodes: [0, 1] });
|
|
@@ -1188,17 +719,16 @@ async function pptxExportJson(args) {
|
|
|
1188
719
|
}
|
|
1189
720
|
|
|
1190
721
|
async function pptxApplyTemplate(args) {
|
|
1191
|
-
return pptxApply('pptx_apply_template', args, true);
|
|
722
|
+
return withTempJsonFile({ targetMasterPath: args.targetMasterPath, slides: args.slides }, planPath => pptxApply('pptx_apply_template', args, true, planPath));
|
|
1192
723
|
}
|
|
1193
724
|
|
|
1194
725
|
async function pptxApplyFormat(args) {
|
|
1195
|
-
return pptxApply('pptx_apply_format', args, false);
|
|
726
|
+
return withTempJsonFile({ operations: args.changes }, planPath => pptxApply('pptx_apply_format', args, false, planPath));
|
|
1196
727
|
}
|
|
1197
728
|
|
|
1198
|
-
async function pptxApply(tool, args, templateMode) {
|
|
729
|
+
async function pptxApply(tool, args, templateMode, plan) {
|
|
1199
730
|
const input = path.resolve(requireString(args.input, 'input'));
|
|
1200
731
|
const template = templateMode ? path.resolve(requireString(args.template, 'template')) : null;
|
|
1201
|
-
const plan = path.resolve(requireString(templateMode ? args.plan : args.operations, templateMode ? 'plan' : 'operations'));
|
|
1202
732
|
const output = path.resolve(requireString(args.output, 'output'));
|
|
1203
733
|
const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
|
|
1204
734
|
for (const [label, candidate] of [['input', input], ['output', output], ...(template ? [['template', template]] : [])]) {
|
|
@@ -1230,18 +760,17 @@ async function pptxApply(tool, args, templateMode) {
|
|
|
1230
760
|
const pass = providerResult.issues.length === 0;
|
|
1231
761
|
const outputArtifact = pass ? await fileArtifact(output) : null;
|
|
1232
762
|
if (!pass) await rm(output, { force: true });
|
|
1233
|
-
const
|
|
1234
|
-
const receipt = receiptSchema.parse({
|
|
763
|
+
const receipt = {
|
|
1235
764
|
schema: templateMode
|
|
1236
765
|
? 'tiwater.office.pptx-template-apply-receipt/v1'
|
|
1237
766
|
: 'tiwater.office.pptx-format-apply-receipt/v1',
|
|
1238
767
|
pass,
|
|
1239
768
|
input: inputArtifact,
|
|
1240
769
|
...(templateMode ? { template: templateArtifact } : {}),
|
|
1241
|
-
|
|
770
|
+
requestSha256: planArtifact.sha256,
|
|
1242
771
|
output: outputArtifact,
|
|
1243
772
|
providerResult,
|
|
1244
|
-
}
|
|
773
|
+
};
|
|
1245
774
|
return {
|
|
1246
775
|
tool,
|
|
1247
776
|
runtime: commandRuntime(result),
|
|
@@ -1257,6 +786,11 @@ async function pptxApply(tool, args, templateMode) {
|
|
|
1257
786
|
}
|
|
1258
787
|
}
|
|
1259
788
|
|
|
789
|
+
async function pptxValidate(args) {
|
|
790
|
+
const result = await runJsonCandidateChain(pptxCandidates, ['validate', requireString(args.input, 'input')], { allowedExitCodes: [0, 1] });
|
|
791
|
+
return { tool: 'pptx_validate', runtime: commandRuntime(result), result: result.json };
|
|
792
|
+
}
|
|
793
|
+
|
|
1260
794
|
async function requireArtifactUnchanged(expected, label) {
|
|
1261
795
|
const current = await fileArtifact(expected.path);
|
|
1262
796
|
if (!isDeepStrictEqual(current, expected)) throw new Error(`${label} changed during provider execution`);
|