@tiwater/office-mcp 0.9.0 → 0.10.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/README.md +45 -5
- package/office/index.mjs +225 -87
- package/package.json +1 -1
package/office/README.md
CHANGED
|
@@ -31,8 +31,48 @@ The official MCP SDK derives the schemas advertised to clients and validates
|
|
|
31
31
|
tool arguments and structured results before they cross the protocol boundary.
|
|
32
32
|
Large observations and exports are written to a caller-selected new JSON
|
|
33
33
|
artifact. MCP returns only the artifact path, hash, and byte count.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
|
|
35
|
+
## Template migration
|
|
36
|
+
|
|
37
|
+
Template migration separates business choice from document mechanics:
|
|
38
|
+
|
|
39
|
+
1. `docx_list_migration_choices` records the complete current source and target
|
|
40
|
+
catalog in an opaque run-local artifact.
|
|
41
|
+
2. `docx_query_migration_choices` pages source items and returns bounded,
|
|
42
|
+
document-compatible alternatives for one source item.
|
|
43
|
+
3. `docx_migrate_template` accepts one complete batch and derives the plan,
|
|
44
|
+
edits, and readback receipt.
|
|
45
|
+
4. `docx_verify_migration` independently verifies the output from the same
|
|
46
|
+
source, baseline, and batch.
|
|
47
|
+
|
|
48
|
+
The scenario supplies the business meaning. The query tool exposes three
|
|
49
|
+
orthogonal target actions:
|
|
50
|
+
|
|
51
|
+
- `place-content` moves current content into a target content position.
|
|
52
|
+
- `keep-template-label` keeps the target label and structure while migrating a
|
|
53
|
+
uniquely identified current field value.
|
|
54
|
+
- `select-template-option` marks a target option represented by the current
|
|
55
|
+
source fact.
|
|
56
|
+
|
|
57
|
+
Choose the action first, then query targets with that action filter. Returned
|
|
58
|
+
`alternativeRef` values bind the action and target together. Source exclusion
|
|
59
|
+
and genuine local review are target-free terminal choices. The caller never
|
|
60
|
+
supplies document text, selectors, coordinates, plans, or edit operations.
|
|
61
|
+
|
|
62
|
+
Template-migration choice artifacts are opaque evidence. List the choices once,
|
|
63
|
+
then query the same current source and baseline to page sources, request targets
|
|
64
|
+
for a source, or inspect cleanup targets. Target queries return complete
|
|
65
|
+
provider-compatible action-and-target alternatives under short catalog-bound
|
|
66
|
+
references. Submit selected alternatives and target-free terminal choices as
|
|
67
|
+
one batch, then verify the output independently from the same inputs and batch.
|
|
68
|
+
The tool does not choose the business mapping.
|
|
69
|
+
|
|
70
|
+
## Version 0.10 migration
|
|
71
|
+
|
|
72
|
+
Version 0.10 replaces the 0.9 template-migration identity form. Targeted
|
|
73
|
+
choices now use one `alternativeRef` returned by
|
|
74
|
+
`docx_query_migration_choices`; terminal choices use `sourceRef` plus
|
|
75
|
+
`exclude-source` or `review-source`. The server rejects the old combination of
|
|
76
|
+
raw source id, action, and raw target id so an action cannot be paired with a
|
|
77
|
+
target from a different alternative. Other Office tools keep their existing
|
|
78
|
+
inputs and outputs.
|
package/office/index.mjs
CHANGED
|
@@ -36,37 +36,28 @@ const convertCandidates = [
|
|
|
36
36
|
];
|
|
37
37
|
|
|
38
38
|
const pathInput = z.string().trim().min(1);
|
|
39
|
-
const
|
|
39
|
+
const migrationQueryTargetAction = z.enum([
|
|
40
40
|
'place-content',
|
|
41
|
-
'keep-template-content',
|
|
42
|
-
'keep-template-label',
|
|
43
|
-
'select-template-option',
|
|
44
|
-
'exclude-source',
|
|
45
|
-
'review-source',
|
|
46
|
-
]);
|
|
47
|
-
const targetActions = new Set([
|
|
48
|
-
'place-content',
|
|
49
|
-
'keep-template-content',
|
|
50
41
|
'keep-template-label',
|
|
51
42
|
'select-template-option',
|
|
43
|
+
]).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.');
|
|
44
|
+
const targetActions = new Set(migrationQueryTargetAction.options);
|
|
45
|
+
|
|
46
|
+
const choiceReference = z.string().trim().regex(/^[ST][1-9][0-9]*-[0-9a-f]{8}$/);
|
|
47
|
+
const alternativeReference = z.string().trim().regex(/^S[1-9][0-9]*-[PLO][1-9][0-9]*-[0-9a-f]{8}$/);
|
|
48
|
+
|
|
49
|
+
const terminalMigrationChoiceInput = z.object({
|
|
50
|
+
sourceRef: choiceReference,
|
|
51
|
+
action: z.enum(['exclude-source', 'review-source']),
|
|
52
|
+
}).strict();
|
|
53
|
+
|
|
54
|
+
const migrationChoiceInput = z.union([
|
|
55
|
+
z.object({ alternativeRef: alternativeReference }).strict(),
|
|
56
|
+
terminalMigrationChoiceInput,
|
|
52
57
|
]);
|
|
53
|
-
const terminalActions = new Set(['exclude-source', 'review-source']);
|
|
54
|
-
|
|
55
|
-
const migrationChoiceInput = z.object({
|
|
56
|
-
sourceChoiceId: z.string().trim().min(1),
|
|
57
|
-
action: migrationAction,
|
|
58
|
-
targetChoiceId: z.string().trim().min(1).optional(),
|
|
59
|
-
}).strict().superRefine((choice, context) => {
|
|
60
|
-
if (targetActions.has(choice.action) && !choice.targetChoiceId) {
|
|
61
|
-
context.addIssue({ code: 'custom', path: ['targetChoiceId'], message: `${choice.action} requires targetChoiceId` });
|
|
62
|
-
}
|
|
63
|
-
if (terminalActions.has(choice.action) && choice.targetChoiceId) {
|
|
64
|
-
context.addIssue({ code: 'custom', path: ['targetChoiceId'], message: `${choice.action} forbids targetChoiceId` });
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
58
|
|
|
68
59
|
const templateCleanupInput = z.object({
|
|
69
|
-
|
|
60
|
+
targetRef: choiceReference,
|
|
70
61
|
scope: z.enum(['cell', 'row']),
|
|
71
62
|
}).strict();
|
|
72
63
|
|
|
@@ -75,7 +66,7 @@ const templateMigrationInput = z.object({
|
|
|
75
66
|
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
76
67
|
output: pathInput.describe('Path to the migrated output DOCX.'),
|
|
77
68
|
receiptOutput: pathInput.describe('New JSON receipt artifact path. Existing files are never overwritten.'),
|
|
78
|
-
choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source
|
|
69
|
+
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.'),
|
|
79
70
|
templateCleanup: z.array(templateCleanupInput).optional().describe('Optional baseline-owned placeholders or example rows to clear.'),
|
|
80
71
|
}).strict();
|
|
81
72
|
|
|
@@ -95,6 +86,16 @@ const migrationChoiceOutput = z.object({
|
|
|
95
86
|
allowedActions: z.array(z.string()),
|
|
96
87
|
}).strict();
|
|
97
88
|
|
|
89
|
+
const migrationPublicChoiceOutput = migrationChoiceOutput.omit({ id: true }).extend({
|
|
90
|
+
ref: choiceReference,
|
|
91
|
+
}).strict();
|
|
92
|
+
|
|
93
|
+
const migrationAlternativeOutput = z.object({
|
|
94
|
+
ref: alternativeReference,
|
|
95
|
+
action: migrationQueryTargetAction,
|
|
96
|
+
target: migrationPublicChoiceOutput,
|
|
97
|
+
}).strict();
|
|
98
|
+
|
|
98
99
|
const migrationCatalog = z.object({
|
|
99
100
|
schema: z.string(),
|
|
100
101
|
pass: z.boolean(),
|
|
@@ -205,13 +206,6 @@ const migrationTargetPage = z.object({
|
|
|
205
206
|
targets: z.array(migrationChoiceOutput),
|
|
206
207
|
}).strict();
|
|
207
208
|
|
|
208
|
-
const migrationTargetAction = z.enum([
|
|
209
|
-
'place-content',
|
|
210
|
-
'keep-template-content',
|
|
211
|
-
'keep-template-label',
|
|
212
|
-
'select-template-option',
|
|
213
|
-
]);
|
|
214
|
-
|
|
215
209
|
const migrationQueryDocuments = {
|
|
216
210
|
source: pathInput.describe('Path to the current source DOCX used by docx_list_migration_choices.'),
|
|
217
211
|
baseline: pathInput.describe('Path to the same selected current baseline DOCX used by docx_list_migration_choices.'),
|
|
@@ -220,7 +214,7 @@ const migrationQueryDocuments = {
|
|
|
220
214
|
const boundedOffset = z.number().int().nonnegative().optional().describe('Zero-based result offset. Defaults to 0.');
|
|
221
215
|
const boundedLimit = z.number().int().min(1).max(10).optional().describe('Maximum results to return. Defaults to 10 and cannot exceed 10.');
|
|
222
216
|
|
|
223
|
-
const migrationChoiceQueryInput = z.
|
|
217
|
+
const migrationChoiceQueryInput = z.union([
|
|
224
218
|
z.object({
|
|
225
219
|
...migrationQueryDocuments,
|
|
226
220
|
view: z.literal('sources'),
|
|
@@ -230,8 +224,8 @@ const migrationChoiceQueryInput = z.discriminatedUnion('view', [
|
|
|
230
224
|
z.object({
|
|
231
225
|
...migrationQueryDocuments,
|
|
232
226
|
view: z.literal('targets'),
|
|
233
|
-
|
|
234
|
-
action:
|
|
227
|
+
sourceRef: choiceReference.describe('Short source reference returned by the sources view.'),
|
|
228
|
+
action: migrationQueryTargetAction.optional().describe('Optional business-action filter. Choose the action from scenario meaning, then use this filter to inspect only compatible targets. Omit it only when comparing the three documented action meanings.'),
|
|
235
229
|
text: z.string().trim().min(1).optional().describe('Optional literal case-insensitive text to find in target visible text or context.'),
|
|
236
230
|
offset: boundedOffset,
|
|
237
231
|
limit: boundedLimit,
|
|
@@ -251,9 +245,9 @@ const migrationChoiceQueryOutput = z.object({
|
|
|
251
245
|
sourceSha256: z.string(),
|
|
252
246
|
baselineSha256: z.string(),
|
|
253
247
|
view: z.enum(['sources', 'targets', 'cleanup']),
|
|
254
|
-
action:
|
|
255
|
-
source:
|
|
256
|
-
items: z.array(
|
|
248
|
+
action: migrationQueryTargetAction.nullable(),
|
|
249
|
+
source: migrationPublicChoiceOutput.nullable(),
|
|
250
|
+
items: z.array(z.union([migrationPublicChoiceOutput, migrationAlternativeOutput])),
|
|
257
251
|
page: migrationQueryPage,
|
|
258
252
|
}).strict();
|
|
259
253
|
|
|
@@ -301,7 +295,7 @@ const tools = [
|
|
|
301
295
|
},
|
|
302
296
|
{
|
|
303
297
|
name: 'docx_query_migration_choices',
|
|
304
|
-
description: 'Query current template-migration alternatives without reading the catalog artifact.
|
|
298
|
+
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.',
|
|
305
299
|
inputSchema: migrationChoiceQueryInput,
|
|
306
300
|
outputSchema: migrationChoiceQueryOutput,
|
|
307
301
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
@@ -309,7 +303,7 @@ const tools = [
|
|
|
309
303
|
},
|
|
310
304
|
{
|
|
311
305
|
name: 'docx_migrate_template',
|
|
312
|
-
description: 'Migrate a current DOCX into the selected baseline from one complete batch of business choices.
|
|
306
|
+
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.',
|
|
313
307
|
inputSchema: templateMigrationInput,
|
|
314
308
|
outputSchema: migrationReceiptOutput('docx_migrate_template'),
|
|
315
309
|
handler: docxMigrateTemplate,
|
|
@@ -462,57 +456,99 @@ async function docxQueryMigrationChoices(args) {
|
|
|
462
456
|
|
|
463
457
|
let source = null;
|
|
464
458
|
let action = null;
|
|
465
|
-
let branch;
|
|
466
|
-
let sourceChoiceId = '-';
|
|
467
459
|
if (args.view === 'cleanup') {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
460
|
+
const targetResult = await loadMigrationTargets({
|
|
461
|
+
sourcePath,
|
|
462
|
+
baselinePath,
|
|
463
|
+
sourceChoiceId: '-',
|
|
464
|
+
branch: 'baseline-clear',
|
|
465
|
+
text: args.text,
|
|
466
|
+
});
|
|
467
|
+
const orderedTargets = validateAndOrderMigrationTargets(catalog, null, targetResult.targets);
|
|
468
|
+
return migrationQueryResult({
|
|
469
|
+
runtime: targetResult.runtime,
|
|
470
|
+
catalog,
|
|
471
|
+
view: 'cleanup',
|
|
472
|
+
action: null,
|
|
473
|
+
source: null,
|
|
474
|
+
items: orderedTargets.slice(offset, offset + limit),
|
|
475
|
+
offset,
|
|
476
|
+
total: orderedTargets.length,
|
|
477
|
+
});
|
|
480
478
|
}
|
|
481
479
|
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
480
|
+
const requestedSourceId = choiceIdFromRef(catalog, catalog.sources, args.sourceRef, 'S', 'source');
|
|
481
|
+
source = catalog.sources.find(item => item.id === requestedSourceId) ?? null;
|
|
482
|
+
if (!source) {
|
|
483
|
+
throw Object.assign(new Error(`Unknown migration source: ${args.sourceRef}`), { code: -32602 });
|
|
484
|
+
}
|
|
485
|
+
action = args.action ?? null;
|
|
486
|
+
const actions = action
|
|
487
|
+
? [action]
|
|
488
|
+
: [...targetActions].filter(candidate => source.allowedActions.includes(candidate));
|
|
489
|
+
const pages = await Promise.all(actions.map(async candidate => {
|
|
490
|
+
if (!source.allowedActions.includes(candidate)) {
|
|
491
|
+
throw Object.assign(new Error(`Action ${candidate} is not allowed for ${source.id}`), { code: -32602 });
|
|
492
|
+
}
|
|
493
|
+
const result = await loadMigrationTargets({
|
|
494
|
+
sourcePath,
|
|
495
|
+
baselinePath,
|
|
496
|
+
sourceChoiceId: source.id,
|
|
497
|
+
branch: migrationTargetBranch(candidate, source.kind),
|
|
498
|
+
text: args.text,
|
|
499
|
+
});
|
|
500
|
+
return {
|
|
501
|
+
action: candidate,
|
|
502
|
+
runtime: result.runtime,
|
|
503
|
+
targets: validateAndOrderMigrationTargets(catalog, source, result.targets),
|
|
504
|
+
};
|
|
505
|
+
}));
|
|
506
|
+
const catalogOrder = new Map(catalog.targets.map((item, index) => [item.id, index]));
|
|
507
|
+
const actionOrder = new Map([...targetActions].map((item, index) => [item, index]));
|
|
508
|
+
const alternatives = pages.flatMap(page => page.targets.map(target => ({ action: page.action, target })))
|
|
509
|
+
.sort((left, right) => {
|
|
510
|
+
const relevance = compareRelevance(
|
|
511
|
+
migrationChoiceRelevance(source, right.target),
|
|
512
|
+
migrationChoiceRelevance(source, left.target));
|
|
513
|
+
return relevance
|
|
514
|
+
|| catalogOrder.get(left.target.id) - catalogOrder.get(right.target.id)
|
|
515
|
+
|| actionOrder.get(left.action) - actionOrder.get(right.action);
|
|
516
|
+
})
|
|
517
|
+
.map(item => ({
|
|
518
|
+
ref: migrationAlternativeRef(catalog, source, item.action, item.target),
|
|
519
|
+
action: item.action,
|
|
520
|
+
target: publicMigrationChoice(catalog, item.target, 'T'),
|
|
521
|
+
}));
|
|
522
|
+
return migrationQueryResult({
|
|
523
|
+
runtime: pages[0]?.runtime ?? commandRuntime(catalogResult),
|
|
524
|
+
catalog,
|
|
525
|
+
view: 'targets',
|
|
526
|
+
action,
|
|
527
|
+
source,
|
|
528
|
+
items: alternatives.slice(offset, offset + limit),
|
|
529
|
+
offset,
|
|
530
|
+
total: alternatives.length,
|
|
531
|
+
alternatives: true,
|
|
488
532
|
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function validateAndOrderMigrationTargets(catalog, source, targets) {
|
|
489
536
|
const catalogTargets = new Map(catalog.targets.map(item => [item.id, item]));
|
|
490
|
-
for (const target of
|
|
537
|
+
for (const target of targets) {
|
|
491
538
|
const current = catalogTargets.get(target.id);
|
|
492
539
|
if (!current || !isDeepStrictEqual(current, target)) {
|
|
493
540
|
throw new Error(`Migration target ${target.id} is not bound to the current catalog`);
|
|
494
541
|
}
|
|
495
542
|
}
|
|
496
543
|
const catalogOrder = new Map(catalog.targets.map((item, index) => [item.id, index]));
|
|
497
|
-
|
|
498
|
-
? [...
|
|
544
|
+
return source
|
|
545
|
+
? [...targets].sort((left, right) => {
|
|
499
546
|
const relevance = compareRelevance(
|
|
500
547
|
migrationChoiceRelevance(source, right),
|
|
501
548
|
migrationChoiceRelevance(source, left));
|
|
502
549
|
return relevance || catalogOrder.get(left.id) - catalogOrder.get(right.id);
|
|
503
550
|
})
|
|
504
|
-
: [...
|
|
505
|
-
const items = orderedTargets.slice(offset, offset + limit);
|
|
506
|
-
return migrationQueryResult({
|
|
507
|
-
runtime: targetResult.runtime,
|
|
508
|
-
catalog,
|
|
509
|
-
view: args.view,
|
|
510
|
-
action,
|
|
511
|
-
source,
|
|
512
|
-
items,
|
|
513
|
-
offset,
|
|
514
|
-
total: orderedTargets.length,
|
|
515
|
-
});
|
|
551
|
+
: [...targets].sort((left, right) => catalogOrder.get(left.id) - catalogOrder.get(right.id));
|
|
516
552
|
}
|
|
517
553
|
|
|
518
554
|
async function loadMigrationTargets({ sourcePath, baselinePath, sourceChoiceId, branch, text }) {
|
|
@@ -628,7 +664,82 @@ function migrationTargetBranch(action, sourceKind) {
|
|
|
628
664
|
throw new Error(`Unsupported migration target action: ${action}`);
|
|
629
665
|
}
|
|
630
666
|
|
|
631
|
-
function
|
|
667
|
+
function catalogReferenceToken(catalog) {
|
|
668
|
+
return createHash('sha256')
|
|
669
|
+
.update(`${catalog.sourceSha256}\n${catalog.baselineSha256}`)
|
|
670
|
+
.digest('hex')
|
|
671
|
+
.slice(0, 8);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function choiceRef(catalog, items, id, prefix) {
|
|
675
|
+
const index = items.findIndex(item => item.id === id);
|
|
676
|
+
if (index < 0) throw new Error(`Migration choice is not bound to the current catalog: ${id}`);
|
|
677
|
+
return `${prefix}${index + 1}-${catalogReferenceToken(catalog)}`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function choiceIdFromRef(catalog, items, ref, prefix, label) {
|
|
681
|
+
if (!ref.startsWith(prefix)) {
|
|
682
|
+
throw invalidMigrationInput(`invalid migration ${label} ref: ${ref}`);
|
|
683
|
+
}
|
|
684
|
+
const [ordinal, token] = ref.slice(1).split('-');
|
|
685
|
+
if (token !== catalogReferenceToken(catalog)) {
|
|
686
|
+
throw invalidMigrationInput(`stale migration ${label} ref: ${ref}`);
|
|
687
|
+
}
|
|
688
|
+
const item = items[Number(ordinal) - 1];
|
|
689
|
+
if (!item) throw invalidMigrationInput(`unknown migration ${label} ref: ${ref}`);
|
|
690
|
+
return item.id;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const migrationActionCodes = new Map([
|
|
694
|
+
['place-content', 'P'],
|
|
695
|
+
['keep-template-label', 'L'],
|
|
696
|
+
['select-template-option', 'O'],
|
|
697
|
+
]);
|
|
698
|
+
const migrationActionsByCode = new Map([...migrationActionCodes].map(([action, code]) => [code, action]));
|
|
699
|
+
|
|
700
|
+
function migrationAlternativeRef(catalog, source, action, target) {
|
|
701
|
+
const sourceOrdinal = catalog.sources.findIndex(item => item.id === source.id) + 1;
|
|
702
|
+
const targetOrdinal = catalog.targets.findIndex(item => item.id === target.id) + 1;
|
|
703
|
+
const code = migrationActionCodes.get(action);
|
|
704
|
+
if (sourceOrdinal < 1 || targetOrdinal < 1 || !code) {
|
|
705
|
+
throw new Error('Migration alternative is not bound to the current catalog');
|
|
706
|
+
}
|
|
707
|
+
return `S${sourceOrdinal}-${code}${targetOrdinal}-${migrationAlternativeToken(catalog, source, action, target)}`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function migrationAlternativeToken(catalog, source, action, target) {
|
|
711
|
+
return createHash('sha256')
|
|
712
|
+
.update(`${catalog.sourceSha256}\n${catalog.baselineSha256}\n${source.id}\n${action}\n${target.id}`)
|
|
713
|
+
.digest('hex')
|
|
714
|
+
.slice(0, 8);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function migrationChoiceFromAlternativeRef(catalog, ref) {
|
|
718
|
+
const match = /^S([1-9][0-9]*)-([PLO])([1-9][0-9]*)-([0-9a-f]{8})$/.exec(ref);
|
|
719
|
+
if (!match) throw invalidMigrationInput(`invalid migration alternative ref: ${ref}`);
|
|
720
|
+
const source = catalog.sources[Number(match[1]) - 1];
|
|
721
|
+
const target = catalog.targets[Number(match[3]) - 1];
|
|
722
|
+
const action = migrationActionsByCode.get(match[2]);
|
|
723
|
+
if (!source || !target || !action) {
|
|
724
|
+
throw invalidMigrationInput(`unknown migration alternative ref: ${ref}`);
|
|
725
|
+
}
|
|
726
|
+
if (match[4] !== migrationAlternativeToken(catalog, source, action, target)) {
|
|
727
|
+
throw invalidMigrationInput(`stale or invalid migration alternative ref: ${ref}`);
|
|
728
|
+
}
|
|
729
|
+
return { sourceChoiceId: source.id, action, targetChoiceId: target.id };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function publicMigrationChoice(catalog, choice, prefix) {
|
|
733
|
+
const { id, ...visible } = choice;
|
|
734
|
+
const items = prefix === 'S' ? catalog.sources : catalog.targets;
|
|
735
|
+
return {
|
|
736
|
+
...visible,
|
|
737
|
+
allowedActions: visible.allowedActions.filter(action => action !== 'keep-template-content'),
|
|
738
|
+
ref: choiceRef(catalog, items, id, prefix),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function migrationQueryResult({ runtime, catalog, view, action, source, items, offset, total, alternatives = false }) {
|
|
632
743
|
return {
|
|
633
744
|
tool: 'docx_query_migration_choices',
|
|
634
745
|
runtime,
|
|
@@ -636,8 +747,10 @@ function migrationQueryResult({ runtime, catalog, view, action, source, items, o
|
|
|
636
747
|
baselineSha256: catalog.baselineSha256,
|
|
637
748
|
view,
|
|
638
749
|
action,
|
|
639
|
-
source,
|
|
640
|
-
items
|
|
750
|
+
source: source ? publicMigrationChoice(catalog, source, 'S') : null,
|
|
751
|
+
items: alternatives
|
|
752
|
+
? items
|
|
753
|
+
: items.map(item => publicMigrationChoice(catalog, item, view === 'sources' ? 'S' : 'T')),
|
|
641
754
|
page: {
|
|
642
755
|
offset,
|
|
643
756
|
returned: items.length,
|
|
@@ -662,13 +775,20 @@ function invalidMigrationInput(message) {
|
|
|
662
775
|
function completeMigrationChoices(catalog, choices) {
|
|
663
776
|
const sources = new Map(catalog.sources.map(source => [source.id, source]));
|
|
664
777
|
const seen = new Set();
|
|
665
|
-
const completed = choices.map(
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
778
|
+
const completed = choices.map(rawChoice => {
|
|
779
|
+
const choice = rawChoice.alternativeRef
|
|
780
|
+
? migrationChoiceFromAlternativeRef(catalog, rawChoice.alternativeRef)
|
|
781
|
+
: {
|
|
782
|
+
sourceChoiceId: choiceIdFromRef(catalog, catalog.sources, rawChoice.sourceRef, 'S', 'source'),
|
|
783
|
+
action: rawChoice.action,
|
|
784
|
+
};
|
|
785
|
+
const { sourceChoiceId } = choice;
|
|
786
|
+
const source = sources.get(sourceChoiceId);
|
|
787
|
+
if (!source) throw invalidMigrationInput(`unknown migration source id: ${sourceChoiceId}`);
|
|
788
|
+
if (seen.has(sourceChoiceId)) throw invalidMigrationInput(`duplicate migration source id: ${sourceChoiceId}`);
|
|
789
|
+
seen.add(sourceChoiceId);
|
|
670
790
|
if (!source.allowedActions.includes(choice.action)) {
|
|
671
|
-
throw invalidMigrationInput(`migration action ${choice.action} is not allowed for source id: ${
|
|
791
|
+
throw invalidMigrationInput(`migration action ${choice.action} is not allowed for source id: ${sourceChoiceId}`);
|
|
672
792
|
}
|
|
673
793
|
return source.requiredCardinality === 'all'
|
|
674
794
|
? { ...choice, cardinality: 'all' }
|
|
@@ -681,6 +801,20 @@ function completeMigrationChoices(catalog, choices) {
|
|
|
681
801
|
return completed;
|
|
682
802
|
}
|
|
683
803
|
|
|
804
|
+
function completeTemplateCleanup(catalog, cleanup, choices) {
|
|
805
|
+
const targets = new Map(catalog.targets.map(target => [target.id, target]));
|
|
806
|
+
const claimedTargets = new Set(choices.flatMap(choice => choice.targetChoiceId ? [choice.targetChoiceId] : []));
|
|
807
|
+
const seen = new Set();
|
|
808
|
+
return cleanup.map(rawCleanup => {
|
|
809
|
+
const targetChoiceId = choiceIdFromRef(catalog, catalog.targets, rawCleanup.targetRef, 'T', 'target');
|
|
810
|
+
if (!seen.add(targetChoiceId)) throw invalidMigrationInput(`duplicate migration cleanup target id: ${targetChoiceId}`);
|
|
811
|
+
if (!targets.get(targetChoiceId)?.allowedActions.includes('template-cleanup')) {
|
|
812
|
+
throw invalidMigrationInput(`migration cleanup is not allowed for target id: ${targetChoiceId}`);
|
|
813
|
+
}
|
|
814
|
+
return { targetChoiceId, scope: rawCleanup.scope };
|
|
815
|
+
}).filter(cleanupChoice => !claimedTargets.has(cleanupChoice.targetChoiceId));
|
|
816
|
+
}
|
|
817
|
+
|
|
684
818
|
async function runTemplateMigrationCommand(tool, command, args) {
|
|
685
819
|
const source = requireString(args.source, 'source');
|
|
686
820
|
const baseline = requireString(args.baseline, 'baseline');
|
|
@@ -690,10 +824,14 @@ async function runTemplateMigrationCommand(tool, command, args) {
|
|
|
690
824
|
}
|
|
691
825
|
const catalogResult = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', source, baseline]);
|
|
692
826
|
const catalog = migrationCatalog.parse(catalogResult.json);
|
|
827
|
+
const choices = completeMigrationChoices(catalog, args.choices);
|
|
828
|
+
const templateCleanup = Array.isArray(args.templateCleanup)
|
|
829
|
+
? completeTemplateCleanup(catalog, args.templateCleanup, choices)
|
|
830
|
+
: [];
|
|
693
831
|
const payload = {
|
|
694
832
|
schema: 'tiwater.docx.template-migration-business-choices/v1',
|
|
695
|
-
choices
|
|
696
|
-
...(
|
|
833
|
+
choices,
|
|
834
|
+
...(templateCleanup.length > 0 ? { templateCleanup } : {}),
|
|
697
835
|
};
|
|
698
836
|
return withTempJsonFile(payload, async choicesPath => {
|
|
699
837
|
const result = await runJsonCandidateChain(
|