@tiwater/office-mcp 0.6.0 → 0.7.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 +2 -1
- package/office/index.mjs +126 -18
- package/package.json +1 -1
package/office/README.md
CHANGED
|
@@ -34,4 +34,5 @@ artifact. MCP returns only the artifact path, hash, and byte count.
|
|
|
34
34
|
Template-migration choice artifacts are opaque evidence. Query the same current
|
|
35
35
|
source and baseline through `docx_query_migration_choices` to page unresolved
|
|
36
36
|
sources, request targets compatible with one business action, or inspect cleanup
|
|
37
|
-
targets.
|
|
37
|
+
targets. Compatible targets stay pageable and are shown in current text and
|
|
38
|
+
local document-context order; the tool does not choose the business mapping.
|
package/office/index.mjs
CHANGED
|
@@ -74,7 +74,7 @@ const templateMigrationInput = z.object({
|
|
|
74
74
|
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
75
75
|
output: pathInput.describe('Path to the migrated output DOCX.'),
|
|
76
76
|
receiptOutput: pathInput.describe('New JSON receipt artifact path. Existing files are never overwritten.'),
|
|
77
|
-
choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source id
|
|
77
|
+
choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source id: place-content writes the source fact; keep-template-content or keep-template-label preserves the matching baseline-owned content or label; select-template-option carries an option identity; exclude-source requires declared exclusion; review-source is limited to genuine local ambiguity.'),
|
|
78
78
|
templateCleanup: z.array(templateCleanupInput).optional().describe('Optional baseline-owned placeholders or example rows to clear.'),
|
|
79
79
|
}).strict();
|
|
80
80
|
|
|
@@ -194,7 +194,7 @@ const migrationChoiceQueryInput = z.discriminatedUnion('view', [
|
|
|
194
194
|
...migrationQueryDocuments,
|
|
195
195
|
view: z.literal('targets'),
|
|
196
196
|
sourceChoiceId: z.string().trim().min(1).describe('Opaque current source id returned by the sources view.'),
|
|
197
|
-
action: migrationTargetAction.describe('Business action
|
|
197
|
+
action: migrationTargetAction.describe('Business action: place-content writes the current source fact; keep-template-content keeps baseline-owned fixed content; keep-template-label keeps the baseline label for the same source meaning; select-template-option carries a selected source option into the matching baseline option.'),
|
|
198
198
|
text: z.string().trim().min(1).optional().describe('Optional literal case-insensitive text to find in target visible text or context.'),
|
|
199
199
|
offset: boundedOffset,
|
|
200
200
|
limit: boundedLimit,
|
|
@@ -264,7 +264,7 @@ const tools = [
|
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
name: 'docx_query_migration_choices',
|
|
267
|
-
description: 'Query current template-migration alternatives without reading the catalog artifact. Page unresolved sources, request provider-compatible targets for one source and business action, or inspect cleanup targets.
|
|
267
|
+
description: 'Query current template-migration alternatives without reading the catalog artifact. Page unresolved sources, request provider-compatible targets for one source and business action, or inspect cleanup targets. Target results are ordered by literal and local document-context relevance; that order helps discovery but does not make the business choice.',
|
|
268
268
|
inputSchema: migrationChoiceQueryInput,
|
|
269
269
|
outputSchema: migrationChoiceQueryOutput,
|
|
270
270
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
@@ -438,39 +438,147 @@ async function docxQueryMigrationChoices(args) {
|
|
|
438
438
|
branch = migrationTargetBranch(action, source.kind);
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
const targetResult = await
|
|
442
|
-
'find-template-migration-targets',
|
|
441
|
+
const targetResult = await loadMigrationTargets({
|
|
443
442
|
sourcePath,
|
|
444
443
|
baselinePath,
|
|
445
444
|
sourceChoiceId,
|
|
446
445
|
branch,
|
|
447
|
-
args.text
|
|
448
|
-
|
|
449
|
-
String(limit),
|
|
450
|
-
]);
|
|
451
|
-
const targetPage = migrationTargetPage.parse(targetResult.json);
|
|
452
|
-
if (targetPage.sourceChoiceId !== (args.view === 'cleanup' ? null : sourceChoiceId) || targetPage.branch !== branch) {
|
|
453
|
-
throw new Error('Migration target page identity does not match the requested current source and action');
|
|
454
|
-
}
|
|
446
|
+
text: args.text,
|
|
447
|
+
});
|
|
455
448
|
const catalogTargets = new Map(catalog.targets.map(item => [item.id, item]));
|
|
456
|
-
for (const target of
|
|
449
|
+
for (const target of targetResult.targets) {
|
|
457
450
|
const current = catalogTargets.get(target.id);
|
|
458
451
|
if (!current || !isDeepStrictEqual(current, target)) {
|
|
459
452
|
throw new Error(`Migration target ${target.id} is not bound to the current catalog`);
|
|
460
453
|
}
|
|
461
454
|
}
|
|
455
|
+
const catalogOrder = new Map(catalog.targets.map((item, index) => [item.id, index]));
|
|
456
|
+
const orderedTargets = source
|
|
457
|
+
? [...targetResult.targets].sort((left, right) => {
|
|
458
|
+
const relevance = compareRelevance(
|
|
459
|
+
migrationChoiceRelevance(source, right),
|
|
460
|
+
migrationChoiceRelevance(source, left));
|
|
461
|
+
return relevance || catalogOrder.get(left.id) - catalogOrder.get(right.id);
|
|
462
|
+
})
|
|
463
|
+
: [...targetResult.targets].sort((left, right) => catalogOrder.get(left.id) - catalogOrder.get(right.id));
|
|
464
|
+
const items = orderedTargets.slice(offset, offset + limit);
|
|
462
465
|
return migrationQueryResult({
|
|
463
|
-
runtime:
|
|
466
|
+
runtime: targetResult.runtime,
|
|
464
467
|
catalog,
|
|
465
468
|
view: args.view,
|
|
466
469
|
action,
|
|
467
470
|
source,
|
|
468
|
-
items
|
|
469
|
-
offset
|
|
470
|
-
total:
|
|
471
|
+
items,
|
|
472
|
+
offset,
|
|
473
|
+
total: orderedTargets.length,
|
|
471
474
|
});
|
|
472
475
|
}
|
|
473
476
|
|
|
477
|
+
async function loadMigrationTargets({ sourcePath, baselinePath, sourceChoiceId, branch, text }) {
|
|
478
|
+
const targets = [];
|
|
479
|
+
let offset = 0;
|
|
480
|
+
let total = null;
|
|
481
|
+
let runtime = null;
|
|
482
|
+
do {
|
|
483
|
+
const result = await runJsonCandidateChain(docxCandidates, [
|
|
484
|
+
'find-template-migration-targets',
|
|
485
|
+
sourcePath,
|
|
486
|
+
baselinePath,
|
|
487
|
+
sourceChoiceId,
|
|
488
|
+
branch,
|
|
489
|
+
text ?? '-',
|
|
490
|
+
String(offset),
|
|
491
|
+
'100',
|
|
492
|
+
]);
|
|
493
|
+
const page = migrationTargetPage.parse(result.json);
|
|
494
|
+
if (page.sourceChoiceId !== (sourceChoiceId === '-' ? null : sourceChoiceId) || page.branch !== branch) {
|
|
495
|
+
throw new Error('Migration target page identity does not match the requested current source and action');
|
|
496
|
+
}
|
|
497
|
+
if (total !== null && page.total !== total) {
|
|
498
|
+
throw new Error('Migration target page total changed while reading current alternatives');
|
|
499
|
+
}
|
|
500
|
+
if (page.offset !== offset || page.targets.length === 0 && offset < page.total) {
|
|
501
|
+
throw new Error('Migration target pagination did not advance');
|
|
502
|
+
}
|
|
503
|
+
runtime ??= commandRuntime(result);
|
|
504
|
+
total = page.total;
|
|
505
|
+
targets.push(...page.targets);
|
|
506
|
+
offset += page.targets.length;
|
|
507
|
+
} while (offset < total);
|
|
508
|
+
return { runtime, targets };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function migrationChoiceRelevance(source, target) {
|
|
512
|
+
const sourceContext = source.context ?? {};
|
|
513
|
+
const targetContext = target.context ?? {};
|
|
514
|
+
const sourceRows = sourceContext.sameRowTexts ?? [];
|
|
515
|
+
const targetRows = targetContext.sameRowTexts ?? [];
|
|
516
|
+
const sourceHeaders = sourceContext.tableHeaderTexts ?? [];
|
|
517
|
+
const targetHeaders = targetContext.tableHeaderTexts ?? [];
|
|
518
|
+
const mainText = textSimilarity(source.text, target.text);
|
|
519
|
+
const tableIdentity = Math.max(
|
|
520
|
+
textSimilarity(sourceContext.columnHeaderText, targetContext.columnHeaderText),
|
|
521
|
+
maximumPairSimilarity(sourceHeaders, targetHeaders));
|
|
522
|
+
const neighborhood = Math.max(
|
|
523
|
+
textSimilarity(sourceContext.previousText, targetContext.previousText),
|
|
524
|
+
textSimilarity(sourceContext.nextText, targetContext.nextText),
|
|
525
|
+
maximumPairSimilarity(sourceRows, targetRows),
|
|
526
|
+
maximumPairSimilarity([source.text], targetRows),
|
|
527
|
+
maximumPairSimilarity(sourceRows, [target.text]));
|
|
528
|
+
return source.kind === 'table-cell' && target.kind === 'table-cell'
|
|
529
|
+
? [tableIdentity, mainText, neighborhood]
|
|
530
|
+
: [mainText, neighborhood, tableIdentity];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function compareRelevance(left, right) {
|
|
534
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
535
|
+
if (left[index] !== right[index]) return left[index] - right[index];
|
|
536
|
+
}
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function maximumPairSimilarity(leftValues, rightValues) {
|
|
541
|
+
let maximum = 0;
|
|
542
|
+
for (const left of leftValues ?? []) {
|
|
543
|
+
for (const right of rightValues ?? []) {
|
|
544
|
+
maximum = Math.max(maximum, textSimilarity(left, right));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return maximum;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function textSimilarity(leftValue, rightValue) {
|
|
551
|
+
const left = normalizeSearchText(leftValue);
|
|
552
|
+
const right = normalizeSearchText(rightValue);
|
|
553
|
+
if (!left || !right) return 0;
|
|
554
|
+
if (left === right) return 1;
|
|
555
|
+
if (Math.min(left.length, right.length) >= 3 && (left.includes(right) || right.includes(left))) {
|
|
556
|
+
return Math.min(left.length, right.length) / Math.max(left.length, right.length);
|
|
557
|
+
}
|
|
558
|
+
const leftPairs = characterPairs(left);
|
|
559
|
+
const rightPairs = characterPairs(right);
|
|
560
|
+
if (leftPairs.size === 0 || rightPairs.size === 0) return 0;
|
|
561
|
+
let shared = 0;
|
|
562
|
+
for (const pair of leftPairs) if (rightPairs.has(pair)) shared += 1;
|
|
563
|
+
return 2 * shared / (leftPairs.size + rightPairs.size);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function normalizeSearchText(value) {
|
|
567
|
+
return typeof value === 'string'
|
|
568
|
+
? value.normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '')
|
|
569
|
+
: '';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function characterPairs(value) {
|
|
573
|
+
const characters = [...value];
|
|
574
|
+
if (characters.length < 2) return new Set(value ? [value] : []);
|
|
575
|
+
const pairs = new Set();
|
|
576
|
+
for (let index = 0; index + 1 < characters.length; index += 1) {
|
|
577
|
+
pairs.add(characters[index] + characters[index + 1]);
|
|
578
|
+
}
|
|
579
|
+
return pairs;
|
|
580
|
+
}
|
|
581
|
+
|
|
474
582
|
function migrationTargetBranch(action, sourceKind) {
|
|
475
583
|
if (action === 'place-content') return sourceKind === 'media' ? 'copy-media' : 'copy-text';
|
|
476
584
|
if (action === 'keep-template-content') return 'retain-target';
|