@json-to-office/mcp-server 1.2.0 → 1.5.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/README.md +4 -1
- package/dist/cli.js +317 -25
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +317 -25
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer, McpServerFactory, JsonSchemaType, StandardSchemaWithJSON } f
|
|
|
2
2
|
import { FormatAdapter, FormatName } from '@json-to-office/jto-ops';
|
|
3
3
|
export { FormatAdapter, FormatName } from '@json-to-office/jto-ops';
|
|
4
4
|
import { ValidationError } from '@json-to-office/shared';
|
|
5
|
+
import { QualityCategory, QualityCertainty, QualityEvidence, JsonPatchOperation as JsonPatchOperation$1 } from '@json-to-office/quality';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Structured results, not protocol errors.
|
|
@@ -32,6 +33,14 @@ interface Diagnostic {
|
|
|
32
33
|
suggestion?: string;
|
|
33
34
|
/** Free-form extras (offending value, component name, renderer id, …). */
|
|
34
35
|
context?: Record<string, unknown>;
|
|
36
|
+
source?: 'schema' | 'semantic' | 'renderer' | 'quality';
|
|
37
|
+
ruleId?: string;
|
|
38
|
+
category?: QualityCategory;
|
|
39
|
+
certainty?: QualityCertainty;
|
|
40
|
+
relatedPaths?: readonly string[];
|
|
41
|
+
evidence?: QualityEvidence;
|
|
42
|
+
fixes?: readonly JsonPatchOperation$1[];
|
|
43
|
+
blocking?: boolean;
|
|
35
44
|
}
|
|
36
45
|
/**
|
|
37
46
|
* The envelope every tool's `structuredContent` starts from.
|
|
@@ -100,6 +109,8 @@ declare const ERROR_CODES: {
|
|
|
100
109
|
readonly HOST_NOTE: "W_HOST_NOTE";
|
|
101
110
|
/** A generation warning the core raised without a code of its own. */
|
|
102
111
|
readonly GENERATION: "W_GENERATION";
|
|
112
|
+
/** A design-quality rule threw, so its whole class of findings is missing. */
|
|
113
|
+
readonly QUALITY_RULE_ERROR: "W_QUALITY_RULE_ERROR";
|
|
103
114
|
/** A required host binary (LibreOffice, poppler) is absent. */
|
|
104
115
|
readonly DEPENDENCY_MISSING: "E_DEPENDENCY_MISSING";
|
|
105
116
|
/** The client cancelled the request. */
|
|
@@ -177,6 +188,10 @@ declare const OPTION_ERROR_CODES: {
|
|
|
177
188
|
readonly INVALID_THEME_PATH: "E_INVALID_THEME_PATH";
|
|
178
189
|
/** The tool does not support the requested format. */
|
|
179
190
|
readonly UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT";
|
|
191
|
+
/** `quality.profile` does not cover the format or renderer of this run. */
|
|
192
|
+
readonly INVALID_QUALITY_PROFILE: "E_INVALID_QUALITY_PROFILE";
|
|
193
|
+
/** `quality.policy` sets a gate, severity or budget that is not a legal value. */
|
|
194
|
+
readonly INVALID_QUALITY_POLICY: "E_INVALID_QUALITY_POLICY";
|
|
180
195
|
};
|
|
181
196
|
/**
|
|
182
197
|
* Normalize a validator path to an RFC 6901 JSON Pointer.
|
|
@@ -492,7 +507,7 @@ declare function createToolDeps(options?: CreateToolDepsOptions): ToolDeps;
|
|
|
492
507
|
* lose more than they fix, and that looking at a rendered page is cheaper than
|
|
493
508
|
* reasoning about whether a layout worked (#271).
|
|
494
509
|
*/
|
|
495
|
-
declare const SERVER_INSTRUCTIONS = "Author Microsoft Word (.docx) and PowerPoint (.pptx) documents as JSON.\n\nThe JSON is authoritative. A generated file is a build product of the document JSON plus a renderer, a theme, fonts, assets and options \u2014 edit the JSON and regenerate; never treat the binary as the source.\n\nWorking rules:\n- Discover before authoring. Call jto_info first, then jto_discover and jto_describe_component (or read the jto:// resources) for the components and renderer ids a format actually supports.\n- Make small edits. With a workspace handle, patch precisely (RFC 6902 over RFC 6901 paths) instead of resending the whole document; without one, change one region at a time.\n- Validate often. Run jto_validate after each edit rather than once at the end; diagnostics are path-addressed, so they map straight back onto the JSON you just changed.\n- Preview when the answer is visual. jto_preview renders pages to PNG; use it whenever layout, overflow or fit is in question, not only before finishing.\n- Snapshot before risky changes. jto_workspace_snapshot pins the current revision so a restructuring you cannot cleanly undo is still recoverable.\n\nDocument defects come back as structured diagnostics with ok: false, not as errors \u2014 read them and repair. Generated files are written under the server's output root and returned as paths; ask for base64 only for small artifacts.";
|
|
510
|
+
declare const SERVER_INSTRUCTIONS = "Author Microsoft Word (.docx) and PowerPoint (.pptx) documents as JSON.\n\nThe JSON is authoritative. A generated file is a build product of the document JSON plus a renderer, a theme, fonts, assets and options \u2014 edit the JSON and regenerate; never treat the binary as the source.\n\nWorking rules:\n- Discover before authoring. Call jto_info first, then jto_discover and jto_describe_component (or read the jto:// resources) for the components and renderer ids a format actually supports.\n- Make small edits. With a workspace handle, patch precisely (RFC 6902 over RFC 6901 paths) instead of resending the whole document; without one, change one region at a time.\n- Validate often. Run jto_validate after each edit rather than once at the end; diagnostics are path-addressed, so they map straight back onto the JSON you just changed.\n- Treat design findings as defects. Schema-valid is not well-designed: jto_validate also lints layout and legibility (W_QUALITY_* \u2014 undeclared slide canvas, text overflowing its box, overcrowded slides, table widths exceeding their section). These never block generation, but they almost always show in the rendered result \u2014 repair them like errors.\n- Preview when the answer is visual. jto_preview renders pages to PNG; use it whenever layout, overflow or fit is in question, not only before finishing.\n- Snapshot before risky changes. jto_workspace_snapshot pins the current revision so a restructuring you cannot cleanly undo is still recoverable.\n\nDocument defects come back as structured diagnostics with ok: false, not as errors \u2014 read them and repair. Generated files are written under the server's output root and returned as paths; ask for base64 only for small artifacts.";
|
|
496
511
|
/** Build a server with every tool and resource registered. */
|
|
497
512
|
declare function createServer(deps: ToolDeps): McpServer;
|
|
498
513
|
/**
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
3
|
|
|
4
4
|
// src/lib/version.ts
|
|
5
|
-
var SERVER_VERSION = true ? "1.
|
|
5
|
+
var SERVER_VERSION = true ? "1.5.0" : "dev-mode";
|
|
6
6
|
var SERVER_NAME = "json-to-office";
|
|
7
7
|
var PACKAGE_NAME = "@json-to-office/mcp-server";
|
|
8
8
|
|
|
@@ -165,6 +165,9 @@ function outputSchema(properties, required = []) {
|
|
|
165
165
|
import {
|
|
166
166
|
runWithDiagnosticSink
|
|
167
167
|
} from "@json-to-office/jto-ops";
|
|
168
|
+
import {
|
|
169
|
+
RENDERER_DEPENDENCY_MISSING
|
|
170
|
+
} from "@json-to-office/shared";
|
|
168
171
|
import { ValueErrorType } from "@sinclair/typebox/errors";
|
|
169
172
|
var ERROR_CODES = {
|
|
170
173
|
/** An exception escaped a tool handler. Always a bug here, never the caller's. */
|
|
@@ -213,6 +216,8 @@ var ERROR_CODES = {
|
|
|
213
216
|
HOST_NOTE: "W_HOST_NOTE",
|
|
214
217
|
/** A generation warning the core raised without a code of its own. */
|
|
215
218
|
GENERATION: "W_GENERATION",
|
|
219
|
+
/** A design-quality rule threw, so its whole class of findings is missing. */
|
|
220
|
+
QUALITY_RULE_ERROR: "W_QUALITY_RULE_ERROR",
|
|
216
221
|
/** A required host binary (LibreOffice, poppler) is absent. */
|
|
217
222
|
DEPENDENCY_MISSING: "E_DEPENDENCY_MISSING",
|
|
218
223
|
/** The client cancelled the request. */
|
|
@@ -308,7 +313,30 @@ function toolResult(payload) {
|
|
|
308
313
|
structuredContent: payload
|
|
309
314
|
};
|
|
310
315
|
}
|
|
311
|
-
var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([
|
|
316
|
+
var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([RENDERER_DEPENDENCY_MISSING]);
|
|
317
|
+
function qualityOptionDiagnostic(error) {
|
|
318
|
+
const code = qualityCallerCode(error);
|
|
319
|
+
if (code === void 0 || code === ERROR_CODES.INVALID_DOCUMENT) {
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
return diagnostic(
|
|
323
|
+
code,
|
|
324
|
+
error instanceof Error ? error.message : String(error)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
function qualityCallerCode(error) {
|
|
328
|
+
const code = error?.code;
|
|
329
|
+
if (code === "QUALITY_PROFILE_INCOMPATIBLE")
|
|
330
|
+
return OPTION_ERROR_CODES.INVALID_QUALITY_PROFILE;
|
|
331
|
+
if (code === "QUALITY_POLICY_INVALID")
|
|
332
|
+
return OPTION_ERROR_CODES.INVALID_QUALITY_POLICY;
|
|
333
|
+
if (code === "QUALITY_GATE_FAILED") return ERROR_CODES.INVALID_DOCUMENT;
|
|
334
|
+
return void 0;
|
|
335
|
+
}
|
|
336
|
+
function stackAllowed() {
|
|
337
|
+
const flag = process.env.JTO_MCP_DEBUG_STACKS;
|
|
338
|
+
return flag === "1" || flag === "true";
|
|
339
|
+
}
|
|
312
340
|
function hostNote(text, tone = "muted") {
|
|
313
341
|
return {
|
|
314
342
|
// Never `error`. The body has already decided `ok` by the time a note
|
|
@@ -355,11 +383,11 @@ async function guarded(body) {
|
|
|
355
383
|
return withHostNotes(result, notes);
|
|
356
384
|
} catch (error) {
|
|
357
385
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
358
|
-
const code = error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL;
|
|
386
|
+
const code = qualityCallerCode(error) ?? (error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL);
|
|
359
387
|
return withHostNotes(
|
|
360
388
|
failure(code, message2, {
|
|
361
389
|
context: {
|
|
362
|
-
...error instanceof Error && error.stack !== void 0 && { stack: error.stack }
|
|
390
|
+
...stackAllowed() && error instanceof Error && error.stack !== void 0 && { stack: error.stack }
|
|
363
391
|
}
|
|
364
392
|
}),
|
|
365
393
|
notes
|
|
@@ -374,7 +402,11 @@ var OPTION_ERROR_CODES = {
|
|
|
374
402
|
/** `themePath` is not a data-only JSON theme path. */
|
|
375
403
|
INVALID_THEME_PATH: "E_INVALID_THEME_PATH",
|
|
376
404
|
/** The tool does not support the requested format. */
|
|
377
|
-
UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT"
|
|
405
|
+
UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT",
|
|
406
|
+
/** `quality.profile` does not cover the format or renderer of this run. */
|
|
407
|
+
INVALID_QUALITY_PROFILE: "E_INVALID_QUALITY_PROFILE",
|
|
408
|
+
/** `quality.policy` sets a gate, severity or budget that is not a legal value. */
|
|
409
|
+
INVALID_QUALITY_POLICY: "E_INVALID_QUALITY_POLICY"
|
|
378
410
|
};
|
|
379
411
|
var DEFERRED_TO_COMPILER = /* @__PURE__ */ new Set([
|
|
380
412
|
ERROR_CODES.UNSUPPORTED_RENDERER_FEATURE
|
|
@@ -414,6 +446,34 @@ function validationDiagnostics(errors) {
|
|
|
414
446
|
})
|
|
415
447
|
);
|
|
416
448
|
}
|
|
449
|
+
function qualityAnalysisDiagnostics(analysis) {
|
|
450
|
+
return analysis.diagnostics.map((diagnostic2) => ({
|
|
451
|
+
source: diagnostic2.source,
|
|
452
|
+
ruleId: diagnostic2.ruleId,
|
|
453
|
+
category: diagnostic2.category,
|
|
454
|
+
certainty: diagnostic2.certainty,
|
|
455
|
+
severity: diagnostic2.severity,
|
|
456
|
+
code: diagnostic2.code,
|
|
457
|
+
message: diagnostic2.message,
|
|
458
|
+
path: diagnostic2.path,
|
|
459
|
+
blocking: diagnostic2.blocking,
|
|
460
|
+
...diagnostic2.suggestion !== void 0 && {
|
|
461
|
+
suggestion: diagnostic2.suggestion
|
|
462
|
+
},
|
|
463
|
+
...diagnostic2.context !== void 0 && {
|
|
464
|
+
context: { ...diagnostic2.context }
|
|
465
|
+
},
|
|
466
|
+
...diagnostic2.relatedPaths !== void 0 && {
|
|
467
|
+
relatedPaths: diagnostic2.relatedPaths
|
|
468
|
+
},
|
|
469
|
+
...diagnostic2.evidence !== void 0 && {
|
|
470
|
+
evidence: { ...diagnostic2.evidence }
|
|
471
|
+
},
|
|
472
|
+
...diagnostic2.fixes !== void 0 && {
|
|
473
|
+
fixes: diagnostic2.fixes
|
|
474
|
+
}
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
417
477
|
function looksLikeValidationErrors(value) {
|
|
418
478
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
419
479
|
(entry) => typeof entry === "object" && entry !== null && typeof entry.message === "string"
|
|
@@ -602,7 +662,7 @@ function register(server, deps) {
|
|
|
602
662
|
"jto_info",
|
|
603
663
|
{
|
|
604
664
|
title: "Server info",
|
|
605
|
-
description: "Versions, supported formats
|
|
665
|
+
description: "Versions, supported formats with each renderer and whether its backend loads here, workspace availability, output-root and size limits, and whether the optional host dependencies (LibreOffice and poppler for jto_preview, a Highcharts export server for the DOCX `highcharts` component) are present on this host. Call this first.",
|
|
606
666
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
607
667
|
inputSchema: S({
|
|
608
668
|
type: "object",
|
|
@@ -654,10 +714,32 @@ function register(server, deps) {
|
|
|
654
714
|
rendererIds: {
|
|
655
715
|
type: "array",
|
|
656
716
|
items: { type: "string" },
|
|
657
|
-
description: "Defaults first."
|
|
717
|
+
description: "Defaults first. Registered, which is not the same as usable \u2014 read `renderers` before picking one."
|
|
718
|
+
},
|
|
719
|
+
renderers: {
|
|
720
|
+
type: "array",
|
|
721
|
+
description: "Every registered renderer with whether its backend loads on this host. A renderer with `available: false` will fail every render until `installHint` is run.",
|
|
722
|
+
items: {
|
|
723
|
+
type: "object",
|
|
724
|
+
properties: {
|
|
725
|
+
id: { type: "string" },
|
|
726
|
+
default: { type: "boolean" },
|
|
727
|
+
available: { type: "boolean" },
|
|
728
|
+
reason: { type: "string" },
|
|
729
|
+
installHint: { type: "string" }
|
|
730
|
+
},
|
|
731
|
+
required: ["id", "default", "available"],
|
|
732
|
+
additionalProperties: false
|
|
733
|
+
}
|
|
658
734
|
}
|
|
659
735
|
},
|
|
660
|
-
required: [
|
|
736
|
+
required: [
|
|
737
|
+
"name",
|
|
738
|
+
"extension",
|
|
739
|
+
"label",
|
|
740
|
+
"rendererIds",
|
|
741
|
+
"renderers"
|
|
742
|
+
],
|
|
661
743
|
additionalProperties: false
|
|
662
744
|
}
|
|
663
745
|
},
|
|
@@ -713,9 +795,21 @@ function register(server, deps) {
|
|
|
713
795
|
const formats = await Promise.all(
|
|
714
796
|
FORMAT_NAMES.map(async (name) => {
|
|
715
797
|
const adapter = deps.getAdapter(name);
|
|
716
|
-
let
|
|
798
|
+
let renderers = [];
|
|
717
799
|
try {
|
|
718
|
-
|
|
800
|
+
renderers = (await adapter.rendererStatuses()).map(
|
|
801
|
+
(status) => ({
|
|
802
|
+
id: status.id,
|
|
803
|
+
default: status.default,
|
|
804
|
+
available: status.available,
|
|
805
|
+
...status.reason !== void 0 && {
|
|
806
|
+
reason: status.reason
|
|
807
|
+
},
|
|
808
|
+
...status.installHint !== void 0 && {
|
|
809
|
+
installHint: status.installHint
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
);
|
|
719
813
|
} catch (error) {
|
|
720
814
|
diagnostics.push(
|
|
721
815
|
diagnostic(
|
|
@@ -725,11 +819,32 @@ function register(server, deps) {
|
|
|
725
819
|
)
|
|
726
820
|
);
|
|
727
821
|
}
|
|
822
|
+
for (const renderer of renderers) {
|
|
823
|
+
if (renderer.available) continue;
|
|
824
|
+
diagnostics.push(
|
|
825
|
+
diagnostic(
|
|
826
|
+
ERROR_CODES.DEPENDENCY_MISSING,
|
|
827
|
+
`The "${renderer.id}" ${name} renderer is registered but cannot load on this host, so every render through it will fail.`,
|
|
828
|
+
{
|
|
829
|
+
severity: "warning",
|
|
830
|
+
...renderer.installHint && {
|
|
831
|
+
suggestion: `Install its backend: ${renderer.installHint}. Until then use one of: ${renderers.filter((entry) => entry.available).map((entry) => `"${entry.id}"`).join(", ")}.`
|
|
832
|
+
},
|
|
833
|
+
context: {
|
|
834
|
+
format: name,
|
|
835
|
+
renderer: renderer.id,
|
|
836
|
+
...renderer.reason && { reason: renderer.reason }
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
728
842
|
return {
|
|
729
843
|
name: adapter.name,
|
|
730
844
|
extension: adapter.extension,
|
|
731
845
|
label: adapter.label,
|
|
732
|
-
rendererIds
|
|
846
|
+
rendererIds: renderers.map((renderer) => renderer.id),
|
|
847
|
+
renderers
|
|
733
848
|
};
|
|
734
849
|
})
|
|
735
850
|
);
|
|
@@ -1055,10 +1170,10 @@ var STARTERS = [
|
|
|
1055
1170
|
id: "pptx-minimal",
|
|
1056
1171
|
format: "pptx",
|
|
1057
1172
|
title: "Minimal presentation",
|
|
1058
|
-
description: "The smallest
|
|
1173
|
+
description: "The smallest well-formed .pptx: root with a declared 16:9 canvas, one slide, one title text. The canvas stays: without it the renderer silently falls back to 4:3.",
|
|
1059
1174
|
document: {
|
|
1060
1175
|
name: "pptx",
|
|
1061
|
-
props: { title: "Untitled deck" },
|
|
1176
|
+
props: { title: "Untitled deck", slideWidth: 13.333, slideHeight: 7.5 },
|
|
1062
1177
|
children: [
|
|
1063
1178
|
{
|
|
1064
1179
|
name: "slide",
|
|
@@ -1117,8 +1232,16 @@ async function catalogFormat(format, deps, diagnostics) {
|
|
|
1117
1232
|
const schemas = formatSchemas(format);
|
|
1118
1233
|
const adapter = deps.getAdapter(format);
|
|
1119
1234
|
let rendererIds = [];
|
|
1235
|
+
const availability = /* @__PURE__ */ new Map();
|
|
1236
|
+
const installHints = /* @__PURE__ */ new Map();
|
|
1237
|
+
let probed = false;
|
|
1120
1238
|
try {
|
|
1121
|
-
|
|
1239
|
+
for (const status of await adapter.rendererStatuses()) {
|
|
1240
|
+
rendererIds.push(status.id);
|
|
1241
|
+
availability.set(status.id, status.available);
|
|
1242
|
+
if (status.installHint) installHints.set(status.id, status.installHint);
|
|
1243
|
+
}
|
|
1244
|
+
probed = true;
|
|
1122
1245
|
} catch (error) {
|
|
1123
1246
|
diagnostics.push(
|
|
1124
1247
|
diagnostic(
|
|
@@ -1234,6 +1357,14 @@ async function catalogFormat(format, deps, diagnostics) {
|
|
|
1234
1357
|
renderers: orderedIds.map((id, index) => ({
|
|
1235
1358
|
id,
|
|
1236
1359
|
default: index === 0,
|
|
1360
|
+
// Three cases, and they are not the same. A probed renderer answers for
|
|
1361
|
+
// itself. A profile the cores never registered has no status but is
|
|
1362
|
+
// already reported as drift above, so calling it unavailable would be a
|
|
1363
|
+
// second, worse description of that. And a probe that threw knows
|
|
1364
|
+
// nothing about any of them — reporting those as usable would contradict
|
|
1365
|
+
// the diagnostic pushed beside them.
|
|
1366
|
+
available: availability.get(id) ?? probed,
|
|
1367
|
+
...installHints.has(id) && { installHint: installHints.get(id) },
|
|
1237
1368
|
components: [...byRenderer.get(id)?.components.keys() ?? []].sort(),
|
|
1238
1369
|
unsupported: allNames.filter((name) => !byRenderer.get(id)?.components.has(name)).sort()
|
|
1239
1370
|
})),
|
|
@@ -1303,6 +1434,14 @@ function register2(server, deps) {
|
|
|
1303
1434
|
properties: {
|
|
1304
1435
|
id: { type: "string" },
|
|
1305
1436
|
default: { type: "boolean" },
|
|
1437
|
+
available: {
|
|
1438
|
+
type: "boolean",
|
|
1439
|
+
description: "Whether this renderer's backend loads on this host. A renderer that is registered but unavailable accepts the components below and then fails every render."
|
|
1440
|
+
},
|
|
1441
|
+
installHint: {
|
|
1442
|
+
type: "string",
|
|
1443
|
+
description: "The command that would make an unavailable renderer available."
|
|
1444
|
+
},
|
|
1306
1445
|
components: {
|
|
1307
1446
|
type: "array",
|
|
1308
1447
|
items: { type: "string" }
|
|
@@ -1313,7 +1452,13 @@ function register2(server, deps) {
|
|
|
1313
1452
|
description: "Components another renderer of this format accepts and this one does not."
|
|
1314
1453
|
}
|
|
1315
1454
|
},
|
|
1316
|
-
required: [
|
|
1455
|
+
required: [
|
|
1456
|
+
"id",
|
|
1457
|
+
"default",
|
|
1458
|
+
"available",
|
|
1459
|
+
"components",
|
|
1460
|
+
"unsupported"
|
|
1461
|
+
],
|
|
1317
1462
|
additionalProperties: false
|
|
1318
1463
|
}
|
|
1319
1464
|
},
|
|
@@ -1756,6 +1901,39 @@ function withRenderer(document, renderer) {
|
|
|
1756
1901
|
}
|
|
1757
1902
|
return { ...document, renderer };
|
|
1758
1903
|
}
|
|
1904
|
+
function effectiveRenderer(document, override2) {
|
|
1905
|
+
if (override2 !== void 0) return override2;
|
|
1906
|
+
if (typeof document === "object" && document !== null) {
|
|
1907
|
+
const declared = document.renderer;
|
|
1908
|
+
if (typeof declared === "string") return declared;
|
|
1909
|
+
}
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1912
|
+
async function rendererAvailability(adapter, document, override2) {
|
|
1913
|
+
const wanted = effectiveRenderer(document, override2);
|
|
1914
|
+
let statuses;
|
|
1915
|
+
try {
|
|
1916
|
+
statuses = await adapter.rendererStatuses();
|
|
1917
|
+
} catch {
|
|
1918
|
+
return void 0;
|
|
1919
|
+
}
|
|
1920
|
+
const status = wanted ? statuses.find((entry) => entry.id === wanted) : statuses.find((entry) => entry.default);
|
|
1921
|
+
if (!status || status.available) return void 0;
|
|
1922
|
+
const usable = statuses.filter((entry) => entry.available).map((entry) => `"${entry.id}"`);
|
|
1923
|
+
return diagnostic(
|
|
1924
|
+
ERROR_CODES.DEPENDENCY_MISSING,
|
|
1925
|
+
`This document validates against the "${status.id}" ${adapter.name} renderer, but that renderer cannot load on this host \u2014 generating with it will fail.`,
|
|
1926
|
+
{
|
|
1927
|
+
severity: "warning",
|
|
1928
|
+
suggestion: status.installHint ? `Install its backend: ${status.installHint}.${usable.length > 0 ? ` Or render with ${usable.join(" or ")}.` : ""}` : `Render with ${usable.join(" or ")} instead.`,
|
|
1929
|
+
context: {
|
|
1930
|
+
format: adapter.name,
|
|
1931
|
+
renderer: status.id,
|
|
1932
|
+
...status.reason && { reason: status.reason }
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1759
1937
|
|
|
1760
1938
|
// src/lib/workspace-store.ts
|
|
1761
1939
|
var unavailable = () => failure(
|
|
@@ -1869,17 +2047,26 @@ function capDiagnostics(diagnostics, limit) {
|
|
|
1869
2047
|
if (diagnostics.length <= limit)
|
|
1870
2048
|
return { kept: diagnostics, truncated: false };
|
|
1871
2049
|
const ordered = [...diagnostics].sort(
|
|
1872
|
-
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
|
|
2050
|
+
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || Number(b.blocking === true) - Number(a.blocking === true)
|
|
1873
2051
|
);
|
|
1874
2052
|
return { kept: ordered.slice(0, limit), truncated: true };
|
|
1875
2053
|
}
|
|
2054
|
+
function ruleErrorDiagnostics(analysis) {
|
|
2055
|
+
return analysis.ruleErrors.map(
|
|
2056
|
+
(entry) => diagnostic(
|
|
2057
|
+
ERROR_CODES.QUALITY_RULE_ERROR,
|
|
2058
|
+
`Quality rule "${entry.ruleId}" failed: ${entry.message}`,
|
|
2059
|
+
{ severity: "warning", source: "quality", ruleId: entry.ruleId }
|
|
2060
|
+
)
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
1876
2063
|
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
1877
2064
|
function register4(server, deps) {
|
|
1878
2065
|
server.registerTool(
|
|
1879
2066
|
"jto_validate",
|
|
1880
2067
|
{
|
|
1881
2068
|
title: "Validate a document",
|
|
1882
|
-
description: "Check a document against its format schema and report every defect as a path-addressed diagnostic. Paths are RFC 6901 JSON Pointers into the document you passed, so they can be used directly as patch targets; codes are the stable `E_`/`W_` vocabulary
|
|
2069
|
+
description: "Check a document against its format schema and report every defect as a path-addressed diagnostic. Paths are RFC 6901 JSON Pointers into the document you passed, so they can be used directly as patch targets; codes are the stable `E_`/`W_` vocabulary. `ok` mirrors generation: schema and semantic errors block; design-quality `W_QUALITY_*` findings advise by default and block only when `quality.policy.gate` requests it. A broken document is a normal result with `ok: false`, never a protocol error.",
|
|
1883
2070
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
1884
2071
|
inputSchema: S({
|
|
1885
2072
|
type: "object",
|
|
@@ -1895,6 +2082,29 @@ function register4(server, deps) {
|
|
|
1895
2082
|
minimum: 1,
|
|
1896
2083
|
maximum: 1e3,
|
|
1897
2084
|
description: `Cap on returned diagnostics (default ${DEFAULT_MAX_DIAGNOSTICS}). Errors are kept ahead of warnings when the cap bites.`
|
|
2085
|
+
},
|
|
2086
|
+
quality: {
|
|
2087
|
+
type: "object",
|
|
2088
|
+
description: "Optional design profile plus per-run enforcement policy.",
|
|
2089
|
+
properties: {
|
|
2090
|
+
profile: {
|
|
2091
|
+
type: "object",
|
|
2092
|
+
properties: { id: { type: "string", minLength: 1 } },
|
|
2093
|
+
required: ["id"],
|
|
2094
|
+
additionalProperties: true
|
|
2095
|
+
},
|
|
2096
|
+
policy: {
|
|
2097
|
+
type: "object",
|
|
2098
|
+
properties: {
|
|
2099
|
+
gate: {
|
|
2100
|
+
type: "string",
|
|
2101
|
+
enum: ["none", "error", "warning", "info"]
|
|
2102
|
+
}
|
|
2103
|
+
},
|
|
2104
|
+
additionalProperties: true
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
additionalProperties: false
|
|
1898
2108
|
}
|
|
1899
2109
|
},
|
|
1900
2110
|
required: ["format"],
|
|
@@ -1925,7 +2135,11 @@ function register4(server, deps) {
|
|
|
1925
2135
|
},
|
|
1926
2136
|
truncated: {
|
|
1927
2137
|
type: "boolean",
|
|
1928
|
-
description: "`diagnostics` was capped by `maxDiagnostics
|
|
2138
|
+
description: "`diagnostics` was capped, by `maxDiagnostics` or by the budget the quality policy set."
|
|
2139
|
+
},
|
|
2140
|
+
profileId: {
|
|
2141
|
+
type: "string",
|
|
2142
|
+
description: "The quality profile the design analysis ran under, when one applied."
|
|
1929
2143
|
}
|
|
1930
2144
|
})
|
|
1931
2145
|
)
|
|
@@ -1940,21 +2154,57 @@ function register4(server, deps) {
|
|
|
1940
2154
|
const result = adapter.validateDocument(
|
|
1941
2155
|
withRenderer(resolved.document, args.renderer)
|
|
1942
2156
|
);
|
|
1943
|
-
const
|
|
2157
|
+
const unavailable2 = await rendererAvailability(
|
|
2158
|
+
adapter,
|
|
2159
|
+
resolved.document,
|
|
2160
|
+
args.renderer
|
|
2161
|
+
);
|
|
2162
|
+
let analysis;
|
|
2163
|
+
let qualityOption;
|
|
2164
|
+
if (adapter.analyzeQuality) {
|
|
2165
|
+
try {
|
|
2166
|
+
analysis = await adapter.analyzeQuality(resolved.document, {
|
|
2167
|
+
renderer: args.renderer,
|
|
2168
|
+
quality: args.quality
|
|
2169
|
+
});
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
const option = result.valid ? void 0 : qualityOptionDiagnostic(error);
|
|
2172
|
+
if (!option) throw error;
|
|
2173
|
+
qualityOption = option;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
const structural = [
|
|
2177
|
+
...validationDiagnostics(result.errors),
|
|
2178
|
+
...unavailable2 ? [unavailable2] : [],
|
|
2179
|
+
...qualityOption ? [qualityOption] : []
|
|
2180
|
+
];
|
|
2181
|
+
const all = [
|
|
2182
|
+
...structural,
|
|
2183
|
+
...analysis ? [
|
|
2184
|
+
...qualityAnalysisDiagnostics(analysis),
|
|
2185
|
+
...ruleErrorDiagnostics(analysis)
|
|
2186
|
+
] : []
|
|
2187
|
+
];
|
|
1944
2188
|
const counts = countDiagnostics(all);
|
|
2189
|
+
const blocked = countDiagnostics(structural).error > 0 || analysis?.blocked === true;
|
|
1945
2190
|
const { kept, truncated } = capDiagnostics(
|
|
1946
2191
|
all,
|
|
1947
2192
|
args.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS
|
|
1948
2193
|
);
|
|
1949
2194
|
return {
|
|
1950
|
-
ok:
|
|
2195
|
+
ok: !blocked,
|
|
1951
2196
|
diagnostics: kept,
|
|
1952
|
-
valid:
|
|
2197
|
+
valid: !blocked,
|
|
1953
2198
|
format: args.format,
|
|
1954
2199
|
...args.renderer !== void 0 && { renderer: args.renderer },
|
|
1955
2200
|
source: sourceSummary(resolved),
|
|
1956
2201
|
counts,
|
|
1957
|
-
|
|
2202
|
+
// The engine caps its own list under a policy budget, so a report
|
|
2203
|
+
// shortened there would otherwise come back reading complete.
|
|
2204
|
+
truncated: truncated || analysis?.truncated === true,
|
|
2205
|
+
...analysis?.profileId !== void 0 && {
|
|
2206
|
+
profileId: analysis.profileId
|
|
2207
|
+
}
|
|
1958
2208
|
};
|
|
1959
2209
|
})
|
|
1960
2210
|
)
|
|
@@ -2830,6 +3080,9 @@ import crypto2 from "crypto";
|
|
|
2830
3080
|
import { promises as fs6 } from "fs";
|
|
2831
3081
|
import os2 from "os";
|
|
2832
3082
|
import path5 from "path";
|
|
3083
|
+
import {
|
|
3084
|
+
RENDERER_DEPENDENCY_MISSING as RENDERER_DEPENDENCY_MISSING2
|
|
3085
|
+
} from "@json-to-office/shared";
|
|
2833
3086
|
import { getFontStager } from "@json-to-office/jto-ops";
|
|
2834
3087
|
|
|
2835
3088
|
// src/preview/cache-key.ts
|
|
@@ -3161,6 +3414,15 @@ function renderFailure(stage, detail, context = {}) {
|
|
|
3161
3414
|
{ suggestion, context: { stage, ...context } }
|
|
3162
3415
|
);
|
|
3163
3416
|
}
|
|
3417
|
+
function buildFailure(error) {
|
|
3418
|
+
if (error instanceof Error && error.name === RENDERER_DEPENDENCY_MISSING2) {
|
|
3419
|
+
return failure(ERROR_CODES.DEPENDENCY_MISSING, message(error), {
|
|
3420
|
+
suggestion: "Install the renderer's backend, or re-run with a renderer jto_info reports as available. The document is not at fault.",
|
|
3421
|
+
context: { stage: "build" }
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
return renderFailure("build", message(error));
|
|
3425
|
+
}
|
|
3164
3426
|
var PNG_SIGNATURE = Buffer.from([
|
|
3165
3427
|
137,
|
|
3166
3428
|
80,
|
|
@@ -3367,9 +3629,10 @@ async function renderPreview(options) {
|
|
|
3367
3629
|
converters = versions;
|
|
3368
3630
|
} catch (error) {
|
|
3369
3631
|
if (signal?.aborted) return cancelled2();
|
|
3632
|
+
const build = buildFailure(error);
|
|
3370
3633
|
return failureFrom([
|
|
3371
|
-
...validationDiagnostics2(options.getAdapter(format), document),
|
|
3372
|
-
...
|
|
3634
|
+
...build.diagnostics[0]?.code === ERROR_CODES.DEPENDENCY_MISSING ? [] : validationDiagnostics2(options.getAdapter(format), document),
|
|
3635
|
+
...build.diagnostics
|
|
3373
3636
|
]);
|
|
3374
3637
|
}
|
|
3375
3638
|
const generateMs = elapsed(generateStarted);
|
|
@@ -5571,6 +5834,7 @@ var RESOURCE_URIS = {
|
|
|
5571
5834
|
catalog: "jto://catalog",
|
|
5572
5835
|
renderers: "jto://renderers",
|
|
5573
5836
|
themes: "jto://themes",
|
|
5837
|
+
themeValues: "jto://themes/values",
|
|
5574
5838
|
templates: "jto://templates",
|
|
5575
5839
|
documentSchema: (format) => `jto://schema/${format}/document`,
|
|
5576
5840
|
themeSchema: (format) => `jto://schema/${format}/theme`
|
|
@@ -5622,7 +5886,7 @@ function register9(server, deps) {
|
|
|
5622
5886
|
RESOURCE_URIS.themes,
|
|
5623
5887
|
{
|
|
5624
5888
|
title: "Built-in themes",
|
|
5625
|
-
description: "Theme names shipped with each format, usable as a document\u2019s props.theme or as the tools\u2019 theme option.",
|
|
5889
|
+
description: "Theme names shipped with each format, usable as a document\u2019s props.theme or as the tools\u2019 theme option. jto://themes/values carries what each name actually looks like.",
|
|
5626
5890
|
mimeType: JSON_MIME
|
|
5627
5891
|
},
|
|
5628
5892
|
async (uri) => {
|
|
@@ -5635,6 +5899,33 @@ function register9(server, deps) {
|
|
|
5635
5899
|
});
|
|
5636
5900
|
}
|
|
5637
5901
|
);
|
|
5902
|
+
server.registerResource(
|
|
5903
|
+
"theme-values",
|
|
5904
|
+
RESOURCE_URIS.themeValues,
|
|
5905
|
+
{
|
|
5906
|
+
title: "Built-in theme values",
|
|
5907
|
+
description: "The palette, fonts, style tables and component defaults behind every built-in theme name \u2014 what a document actually opts into with props.theme. A name alone cannot tell you whether a theme fits the brief; this can.",
|
|
5908
|
+
mimeType: JSON_MIME
|
|
5909
|
+
},
|
|
5910
|
+
async (uri) => {
|
|
5911
|
+
const formats = await Promise.all(
|
|
5912
|
+
FORMAT_NAMES.map(async (format) => {
|
|
5913
|
+
const adapter = deps.getAdapter(format);
|
|
5914
|
+
let themes;
|
|
5915
|
+
try {
|
|
5916
|
+
themes = adapter.getBuiltinThemeValues ? await adapter.getBuiltinThemeValues() : adapter.getBuiltinThemes();
|
|
5917
|
+
} catch {
|
|
5918
|
+
themes = adapter.getBuiltinThemes();
|
|
5919
|
+
}
|
|
5920
|
+
return {
|
|
5921
|
+
format,
|
|
5922
|
+
themes
|
|
5923
|
+
};
|
|
5924
|
+
})
|
|
5925
|
+
);
|
|
5926
|
+
return jsonContents(uri, { formats });
|
|
5927
|
+
}
|
|
5928
|
+
);
|
|
5638
5929
|
server.registerResource(
|
|
5639
5930
|
"templates",
|
|
5640
5931
|
RESOURCE_URIS.templates,
|
|
@@ -5683,6 +5974,7 @@ Working rules:
|
|
|
5683
5974
|
- Discover before authoring. Call jto_info first, then jto_discover and jto_describe_component (or read the jto:// resources) for the components and renderer ids a format actually supports.
|
|
5684
5975
|
- Make small edits. With a workspace handle, patch precisely (RFC 6902 over RFC 6901 paths) instead of resending the whole document; without one, change one region at a time.
|
|
5685
5976
|
- Validate often. Run jto_validate after each edit rather than once at the end; diagnostics are path-addressed, so they map straight back onto the JSON you just changed.
|
|
5977
|
+
- Treat design findings as defects. Schema-valid is not well-designed: jto_validate also lints layout and legibility (W_QUALITY_* \u2014 undeclared slide canvas, text overflowing its box, overcrowded slides, table widths exceeding their section). These never block generation, but they almost always show in the rendered result \u2014 repair them like errors.
|
|
5686
5978
|
- Preview when the answer is visual. jto_preview renders pages to PNG; use it whenever layout, overflow or fit is in question, not only before finishing.
|
|
5687
5979
|
- Snapshot before risky changes. jto_workspace_snapshot pins the current revision so a restructuring you cannot cleanly undo is still recoverable.
|
|
5688
5980
|
|