@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/README.md
CHANGED
|
@@ -164,12 +164,14 @@ Nested components collapse to their names on purpose — describe those separate
|
|
|
164
164
|
|
|
165
165
|
### `jto_validate`
|
|
166
166
|
|
|
167
|
-
**In** — `format` (required); document source; `renderer` (validate against this profile instead of the document's own, for this check only); `maxDiagnostics` (1–1000, default 100 — errors are kept ahead of warnings when the cap bites).
|
|
167
|
+
**In** — `format` (required); document source; `renderer` (validate against this profile instead of the document's own, for this check only); `quality` `{profile?, policy?}`; `maxDiagnostics` (1–1000, default 100 — errors are kept ahead of warnings when the cap bites).
|
|
168
168
|
|
|
169
169
|
**Out** — `valid`, `format`, `renderer` (when one was requested), `source` `{origin, handle?, revision?}`, `counts` `{error, warning, info}` (before any cap), `truncated`.
|
|
170
170
|
|
|
171
171
|
`ok` mirrors the gate generation applies: schema and semantic errors block it, renderer-profile findings (`W_UNSUPPORTED_RENDERER_FEATURE`) come back as warnings, because the renderer has the last word on those.
|
|
172
172
|
|
|
173
|
+
Design-quality findings ride the same envelope as `W_QUALITY_*` warnings and infos: an undeclared slide canvas, estimated text overflow, overcrowding, unreadable type, table overflow, or a skipped heading. They carry category, certainty, evidence, suggestion, and optional fixes. They are advisory by default; `quality.policy.gate: "warning"` makes warning-or-higher findings set `ok: false` without turning the tool call into a protocol error.
|
|
174
|
+
|
|
173
175
|
The gate, not `jto://schema/{format}/document`. The two agree except on component nodes whose props are all optional, where the generated schema asks for a `props: {}` the validator and both renderers treat as omissible; validate follows the renderer, because that is what `jto_generate` runs.
|
|
174
176
|
|
|
175
177
|
### `jto_generate`
|
|
@@ -229,6 +231,7 @@ The same catalogues, for clients that read resources. URIs are stable.
|
|
|
229
231
|
| `jto://catalog` | The resource form of `jto_discover`: every format, in full. |
|
|
230
232
|
| `jto://renderers` | Renderer ids per format, which is default, what each profile can draw. |
|
|
231
233
|
| `jto://themes` | Built-in theme names per format. |
|
|
234
|
+
| `jto://themes/values` | What each built-in theme actually is: palette, fonts, style tables. |
|
|
232
235
|
| `jto://templates` | Every starter document. |
|
|
233
236
|
| `jto://schema/docx/document` | Generated JSON Schema for a complete `.docx` document, by renderer. |
|
|
234
237
|
| `jto://schema/pptx/document` | The same for `.pptx`. |
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
8
8
|
|
|
9
9
|
// src/lib/version.ts
|
|
10
|
-
var SERVER_VERSION = true ? "1.
|
|
10
|
+
var SERVER_VERSION = true ? "1.5.0" : "dev-mode";
|
|
11
11
|
var SERVER_NAME = "json-to-office";
|
|
12
12
|
var PACKAGE_NAME = "@json-to-office/mcp-server";
|
|
13
13
|
|
|
@@ -170,6 +170,9 @@ function outputSchema(properties, required = []) {
|
|
|
170
170
|
import {
|
|
171
171
|
runWithDiagnosticSink
|
|
172
172
|
} from "@json-to-office/jto-ops";
|
|
173
|
+
import {
|
|
174
|
+
RENDERER_DEPENDENCY_MISSING
|
|
175
|
+
} from "@json-to-office/shared";
|
|
173
176
|
import { ValueErrorType } from "@sinclair/typebox/errors";
|
|
174
177
|
var ERROR_CODES = {
|
|
175
178
|
/** An exception escaped a tool handler. Always a bug here, never the caller's. */
|
|
@@ -218,6 +221,8 @@ var ERROR_CODES = {
|
|
|
218
221
|
HOST_NOTE: "W_HOST_NOTE",
|
|
219
222
|
/** A generation warning the core raised without a code of its own. */
|
|
220
223
|
GENERATION: "W_GENERATION",
|
|
224
|
+
/** A design-quality rule threw, so its whole class of findings is missing. */
|
|
225
|
+
QUALITY_RULE_ERROR: "W_QUALITY_RULE_ERROR",
|
|
221
226
|
/** A required host binary (LibreOffice, poppler) is absent. */
|
|
222
227
|
DEPENDENCY_MISSING: "E_DEPENDENCY_MISSING",
|
|
223
228
|
/** The client cancelled the request. */
|
|
@@ -313,7 +318,30 @@ function toolResult(payload) {
|
|
|
313
318
|
structuredContent: payload
|
|
314
319
|
};
|
|
315
320
|
}
|
|
316
|
-
var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([
|
|
321
|
+
var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([RENDERER_DEPENDENCY_MISSING]);
|
|
322
|
+
function qualityOptionDiagnostic(error) {
|
|
323
|
+
const code = qualityCallerCode(error);
|
|
324
|
+
if (code === void 0 || code === ERROR_CODES.INVALID_DOCUMENT) {
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
return diagnostic(
|
|
328
|
+
code,
|
|
329
|
+
error instanceof Error ? error.message : String(error)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
function qualityCallerCode(error) {
|
|
333
|
+
const code = error?.code;
|
|
334
|
+
if (code === "QUALITY_PROFILE_INCOMPATIBLE")
|
|
335
|
+
return OPTION_ERROR_CODES.INVALID_QUALITY_PROFILE;
|
|
336
|
+
if (code === "QUALITY_POLICY_INVALID")
|
|
337
|
+
return OPTION_ERROR_CODES.INVALID_QUALITY_POLICY;
|
|
338
|
+
if (code === "QUALITY_GATE_FAILED") return ERROR_CODES.INVALID_DOCUMENT;
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
function stackAllowed() {
|
|
342
|
+
const flag = process.env.JTO_MCP_DEBUG_STACKS;
|
|
343
|
+
return flag === "1" || flag === "true";
|
|
344
|
+
}
|
|
317
345
|
function hostNote(text, tone = "muted") {
|
|
318
346
|
return {
|
|
319
347
|
// Never `error`. The body has already decided `ok` by the time a note
|
|
@@ -360,11 +388,11 @@ async function guarded(body) {
|
|
|
360
388
|
return withHostNotes(result, notes);
|
|
361
389
|
} catch (error) {
|
|
362
390
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
363
|
-
const code = error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL;
|
|
391
|
+
const code = qualityCallerCode(error) ?? (error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL);
|
|
364
392
|
return withHostNotes(
|
|
365
393
|
failure(code, message2, {
|
|
366
394
|
context: {
|
|
367
|
-
...error instanceof Error && error.stack !== void 0 && { stack: error.stack }
|
|
395
|
+
...stackAllowed() && error instanceof Error && error.stack !== void 0 && { stack: error.stack }
|
|
368
396
|
}
|
|
369
397
|
}),
|
|
370
398
|
notes
|
|
@@ -379,7 +407,11 @@ var OPTION_ERROR_CODES = {
|
|
|
379
407
|
/** `themePath` is not a data-only JSON theme path. */
|
|
380
408
|
INVALID_THEME_PATH: "E_INVALID_THEME_PATH",
|
|
381
409
|
/** The tool does not support the requested format. */
|
|
382
|
-
UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT"
|
|
410
|
+
UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT",
|
|
411
|
+
/** `quality.profile` does not cover the format or renderer of this run. */
|
|
412
|
+
INVALID_QUALITY_PROFILE: "E_INVALID_QUALITY_PROFILE",
|
|
413
|
+
/** `quality.policy` sets a gate, severity or budget that is not a legal value. */
|
|
414
|
+
INVALID_QUALITY_POLICY: "E_INVALID_QUALITY_POLICY"
|
|
383
415
|
};
|
|
384
416
|
var DEFERRED_TO_COMPILER = /* @__PURE__ */ new Set([
|
|
385
417
|
ERROR_CODES.UNSUPPORTED_RENDERER_FEATURE
|
|
@@ -419,6 +451,34 @@ function validationDiagnostics(errors) {
|
|
|
419
451
|
})
|
|
420
452
|
);
|
|
421
453
|
}
|
|
454
|
+
function qualityAnalysisDiagnostics(analysis) {
|
|
455
|
+
return analysis.diagnostics.map((diagnostic2) => ({
|
|
456
|
+
source: diagnostic2.source,
|
|
457
|
+
ruleId: diagnostic2.ruleId,
|
|
458
|
+
category: diagnostic2.category,
|
|
459
|
+
certainty: diagnostic2.certainty,
|
|
460
|
+
severity: diagnostic2.severity,
|
|
461
|
+
code: diagnostic2.code,
|
|
462
|
+
message: diagnostic2.message,
|
|
463
|
+
path: diagnostic2.path,
|
|
464
|
+
blocking: diagnostic2.blocking,
|
|
465
|
+
...diagnostic2.suggestion !== void 0 && {
|
|
466
|
+
suggestion: diagnostic2.suggestion
|
|
467
|
+
},
|
|
468
|
+
...diagnostic2.context !== void 0 && {
|
|
469
|
+
context: { ...diagnostic2.context }
|
|
470
|
+
},
|
|
471
|
+
...diagnostic2.relatedPaths !== void 0 && {
|
|
472
|
+
relatedPaths: diagnostic2.relatedPaths
|
|
473
|
+
},
|
|
474
|
+
...diagnostic2.evidence !== void 0 && {
|
|
475
|
+
evidence: { ...diagnostic2.evidence }
|
|
476
|
+
},
|
|
477
|
+
...diagnostic2.fixes !== void 0 && {
|
|
478
|
+
fixes: diagnostic2.fixes
|
|
479
|
+
}
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
422
482
|
function looksLikeValidationErrors(value) {
|
|
423
483
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
424
484
|
(entry) => typeof entry === "object" && entry !== null && typeof entry.message === "string"
|
|
@@ -607,7 +667,7 @@ function register(server, deps) {
|
|
|
607
667
|
"jto_info",
|
|
608
668
|
{
|
|
609
669
|
title: "Server info",
|
|
610
|
-
description: "Versions, supported formats
|
|
670
|
+
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.",
|
|
611
671
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
612
672
|
inputSchema: S({
|
|
613
673
|
type: "object",
|
|
@@ -659,10 +719,32 @@ function register(server, deps) {
|
|
|
659
719
|
rendererIds: {
|
|
660
720
|
type: "array",
|
|
661
721
|
items: { type: "string" },
|
|
662
|
-
description: "Defaults first."
|
|
722
|
+
description: "Defaults first. Registered, which is not the same as usable \u2014 read `renderers` before picking one."
|
|
723
|
+
},
|
|
724
|
+
renderers: {
|
|
725
|
+
type: "array",
|
|
726
|
+
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.",
|
|
727
|
+
items: {
|
|
728
|
+
type: "object",
|
|
729
|
+
properties: {
|
|
730
|
+
id: { type: "string" },
|
|
731
|
+
default: { type: "boolean" },
|
|
732
|
+
available: { type: "boolean" },
|
|
733
|
+
reason: { type: "string" },
|
|
734
|
+
installHint: { type: "string" }
|
|
735
|
+
},
|
|
736
|
+
required: ["id", "default", "available"],
|
|
737
|
+
additionalProperties: false
|
|
738
|
+
}
|
|
663
739
|
}
|
|
664
740
|
},
|
|
665
|
-
required: [
|
|
741
|
+
required: [
|
|
742
|
+
"name",
|
|
743
|
+
"extension",
|
|
744
|
+
"label",
|
|
745
|
+
"rendererIds",
|
|
746
|
+
"renderers"
|
|
747
|
+
],
|
|
666
748
|
additionalProperties: false
|
|
667
749
|
}
|
|
668
750
|
},
|
|
@@ -718,9 +800,21 @@ function register(server, deps) {
|
|
|
718
800
|
const formats = await Promise.all(
|
|
719
801
|
FORMAT_NAMES.map(async (name) => {
|
|
720
802
|
const adapter = deps.getAdapter(name);
|
|
721
|
-
let
|
|
803
|
+
let renderers = [];
|
|
722
804
|
try {
|
|
723
|
-
|
|
805
|
+
renderers = (await adapter.rendererStatuses()).map(
|
|
806
|
+
(status) => ({
|
|
807
|
+
id: status.id,
|
|
808
|
+
default: status.default,
|
|
809
|
+
available: status.available,
|
|
810
|
+
...status.reason !== void 0 && {
|
|
811
|
+
reason: status.reason
|
|
812
|
+
},
|
|
813
|
+
...status.installHint !== void 0 && {
|
|
814
|
+
installHint: status.installHint
|
|
815
|
+
}
|
|
816
|
+
})
|
|
817
|
+
);
|
|
724
818
|
} catch (error) {
|
|
725
819
|
diagnostics.push(
|
|
726
820
|
diagnostic(
|
|
@@ -730,11 +824,32 @@ function register(server, deps) {
|
|
|
730
824
|
)
|
|
731
825
|
);
|
|
732
826
|
}
|
|
827
|
+
for (const renderer of renderers) {
|
|
828
|
+
if (renderer.available) continue;
|
|
829
|
+
diagnostics.push(
|
|
830
|
+
diagnostic(
|
|
831
|
+
ERROR_CODES.DEPENDENCY_MISSING,
|
|
832
|
+
`The "${renderer.id}" ${name} renderer is registered but cannot load on this host, so every render through it will fail.`,
|
|
833
|
+
{
|
|
834
|
+
severity: "warning",
|
|
835
|
+
...renderer.installHint && {
|
|
836
|
+
suggestion: `Install its backend: ${renderer.installHint}. Until then use one of: ${renderers.filter((entry) => entry.available).map((entry) => `"${entry.id}"`).join(", ")}.`
|
|
837
|
+
},
|
|
838
|
+
context: {
|
|
839
|
+
format: name,
|
|
840
|
+
renderer: renderer.id,
|
|
841
|
+
...renderer.reason && { reason: renderer.reason }
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
}
|
|
733
847
|
return {
|
|
734
848
|
name: adapter.name,
|
|
735
849
|
extension: adapter.extension,
|
|
736
850
|
label: adapter.label,
|
|
737
|
-
rendererIds
|
|
851
|
+
rendererIds: renderers.map((renderer) => renderer.id),
|
|
852
|
+
renderers
|
|
738
853
|
};
|
|
739
854
|
})
|
|
740
855
|
);
|
|
@@ -1060,10 +1175,10 @@ var STARTERS = [
|
|
|
1060
1175
|
id: "pptx-minimal",
|
|
1061
1176
|
format: "pptx",
|
|
1062
1177
|
title: "Minimal presentation",
|
|
1063
|
-
description: "The smallest
|
|
1178
|
+
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.",
|
|
1064
1179
|
document: {
|
|
1065
1180
|
name: "pptx",
|
|
1066
|
-
props: { title: "Untitled deck" },
|
|
1181
|
+
props: { title: "Untitled deck", slideWidth: 13.333, slideHeight: 7.5 },
|
|
1067
1182
|
children: [
|
|
1068
1183
|
{
|
|
1069
1184
|
name: "slide",
|
|
@@ -1122,8 +1237,16 @@ async function catalogFormat(format, deps, diagnostics) {
|
|
|
1122
1237
|
const schemas = formatSchemas(format);
|
|
1123
1238
|
const adapter = deps.getAdapter(format);
|
|
1124
1239
|
let rendererIds = [];
|
|
1240
|
+
const availability = /* @__PURE__ */ new Map();
|
|
1241
|
+
const installHints = /* @__PURE__ */ new Map();
|
|
1242
|
+
let probed = false;
|
|
1125
1243
|
try {
|
|
1126
|
-
|
|
1244
|
+
for (const status of await adapter.rendererStatuses()) {
|
|
1245
|
+
rendererIds.push(status.id);
|
|
1246
|
+
availability.set(status.id, status.available);
|
|
1247
|
+
if (status.installHint) installHints.set(status.id, status.installHint);
|
|
1248
|
+
}
|
|
1249
|
+
probed = true;
|
|
1127
1250
|
} catch (error) {
|
|
1128
1251
|
diagnostics.push(
|
|
1129
1252
|
diagnostic(
|
|
@@ -1239,6 +1362,14 @@ async function catalogFormat(format, deps, diagnostics) {
|
|
|
1239
1362
|
renderers: orderedIds.map((id, index) => ({
|
|
1240
1363
|
id,
|
|
1241
1364
|
default: index === 0,
|
|
1365
|
+
// Three cases, and they are not the same. A probed renderer answers for
|
|
1366
|
+
// itself. A profile the cores never registered has no status but is
|
|
1367
|
+
// already reported as drift above, so calling it unavailable would be a
|
|
1368
|
+
// second, worse description of that. And a probe that threw knows
|
|
1369
|
+
// nothing about any of them — reporting those as usable would contradict
|
|
1370
|
+
// the diagnostic pushed beside them.
|
|
1371
|
+
available: availability.get(id) ?? probed,
|
|
1372
|
+
...installHints.has(id) && { installHint: installHints.get(id) },
|
|
1242
1373
|
components: [...byRenderer.get(id)?.components.keys() ?? []].sort(),
|
|
1243
1374
|
unsupported: allNames.filter((name) => !byRenderer.get(id)?.components.has(name)).sort()
|
|
1244
1375
|
})),
|
|
@@ -1308,6 +1439,14 @@ function register2(server, deps) {
|
|
|
1308
1439
|
properties: {
|
|
1309
1440
|
id: { type: "string" },
|
|
1310
1441
|
default: { type: "boolean" },
|
|
1442
|
+
available: {
|
|
1443
|
+
type: "boolean",
|
|
1444
|
+
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."
|
|
1445
|
+
},
|
|
1446
|
+
installHint: {
|
|
1447
|
+
type: "string",
|
|
1448
|
+
description: "The command that would make an unavailable renderer available."
|
|
1449
|
+
},
|
|
1311
1450
|
components: {
|
|
1312
1451
|
type: "array",
|
|
1313
1452
|
items: { type: "string" }
|
|
@@ -1318,7 +1457,13 @@ function register2(server, deps) {
|
|
|
1318
1457
|
description: "Components another renderer of this format accepts and this one does not."
|
|
1319
1458
|
}
|
|
1320
1459
|
},
|
|
1321
|
-
required: [
|
|
1460
|
+
required: [
|
|
1461
|
+
"id",
|
|
1462
|
+
"default",
|
|
1463
|
+
"available",
|
|
1464
|
+
"components",
|
|
1465
|
+
"unsupported"
|
|
1466
|
+
],
|
|
1322
1467
|
additionalProperties: false
|
|
1323
1468
|
}
|
|
1324
1469
|
},
|
|
@@ -1758,6 +1903,39 @@ function withRenderer(document, renderer) {
|
|
|
1758
1903
|
}
|
|
1759
1904
|
return { ...document, renderer };
|
|
1760
1905
|
}
|
|
1906
|
+
function effectiveRenderer(document, override2) {
|
|
1907
|
+
if (override2 !== void 0) return override2;
|
|
1908
|
+
if (typeof document === "object" && document !== null) {
|
|
1909
|
+
const declared = document.renderer;
|
|
1910
|
+
if (typeof declared === "string") return declared;
|
|
1911
|
+
}
|
|
1912
|
+
return void 0;
|
|
1913
|
+
}
|
|
1914
|
+
async function rendererAvailability(adapter, document, override2) {
|
|
1915
|
+
const wanted = effectiveRenderer(document, override2);
|
|
1916
|
+
let statuses;
|
|
1917
|
+
try {
|
|
1918
|
+
statuses = await adapter.rendererStatuses();
|
|
1919
|
+
} catch {
|
|
1920
|
+
return void 0;
|
|
1921
|
+
}
|
|
1922
|
+
const status = wanted ? statuses.find((entry) => entry.id === wanted) : statuses.find((entry) => entry.default);
|
|
1923
|
+
if (!status || status.available) return void 0;
|
|
1924
|
+
const usable = statuses.filter((entry) => entry.available).map((entry) => `"${entry.id}"`);
|
|
1925
|
+
return diagnostic(
|
|
1926
|
+
ERROR_CODES.DEPENDENCY_MISSING,
|
|
1927
|
+
`This document validates against the "${status.id}" ${adapter.name} renderer, but that renderer cannot load on this host \u2014 generating with it will fail.`,
|
|
1928
|
+
{
|
|
1929
|
+
severity: "warning",
|
|
1930
|
+
suggestion: status.installHint ? `Install its backend: ${status.installHint}.${usable.length > 0 ? ` Or render with ${usable.join(" or ")}.` : ""}` : `Render with ${usable.join(" or ")} instead.`,
|
|
1931
|
+
context: {
|
|
1932
|
+
format: adapter.name,
|
|
1933
|
+
renderer: status.id,
|
|
1934
|
+
...status.reason && { reason: status.reason }
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1761
1939
|
|
|
1762
1940
|
// src/lib/workspace-store.ts
|
|
1763
1941
|
var unavailable = () => failure(
|
|
@@ -1857,17 +2035,26 @@ function capDiagnostics(diagnostics, limit) {
|
|
|
1857
2035
|
if (diagnostics.length <= limit)
|
|
1858
2036
|
return { kept: diagnostics, truncated: false };
|
|
1859
2037
|
const ordered = [...diagnostics].sort(
|
|
1860
|
-
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
|
|
2038
|
+
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || Number(b.blocking === true) - Number(a.blocking === true)
|
|
1861
2039
|
);
|
|
1862
2040
|
return { kept: ordered.slice(0, limit), truncated: true };
|
|
1863
2041
|
}
|
|
2042
|
+
function ruleErrorDiagnostics(analysis) {
|
|
2043
|
+
return analysis.ruleErrors.map(
|
|
2044
|
+
(entry) => diagnostic(
|
|
2045
|
+
ERROR_CODES.QUALITY_RULE_ERROR,
|
|
2046
|
+
`Quality rule "${entry.ruleId}" failed: ${entry.message}`,
|
|
2047
|
+
{ severity: "warning", source: "quality", ruleId: entry.ruleId }
|
|
2048
|
+
)
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
1864
2051
|
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
1865
2052
|
function register4(server, deps) {
|
|
1866
2053
|
server.registerTool(
|
|
1867
2054
|
"jto_validate",
|
|
1868
2055
|
{
|
|
1869
2056
|
title: "Validate a document",
|
|
1870
|
-
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
|
|
2057
|
+
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.",
|
|
1871
2058
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
1872
2059
|
inputSchema: S({
|
|
1873
2060
|
type: "object",
|
|
@@ -1883,6 +2070,29 @@ function register4(server, deps) {
|
|
|
1883
2070
|
minimum: 1,
|
|
1884
2071
|
maximum: 1e3,
|
|
1885
2072
|
description: `Cap on returned diagnostics (default ${DEFAULT_MAX_DIAGNOSTICS}). Errors are kept ahead of warnings when the cap bites.`
|
|
2073
|
+
},
|
|
2074
|
+
quality: {
|
|
2075
|
+
type: "object",
|
|
2076
|
+
description: "Optional design profile plus per-run enforcement policy.",
|
|
2077
|
+
properties: {
|
|
2078
|
+
profile: {
|
|
2079
|
+
type: "object",
|
|
2080
|
+
properties: { id: { type: "string", minLength: 1 } },
|
|
2081
|
+
required: ["id"],
|
|
2082
|
+
additionalProperties: true
|
|
2083
|
+
},
|
|
2084
|
+
policy: {
|
|
2085
|
+
type: "object",
|
|
2086
|
+
properties: {
|
|
2087
|
+
gate: {
|
|
2088
|
+
type: "string",
|
|
2089
|
+
enum: ["none", "error", "warning", "info"]
|
|
2090
|
+
}
|
|
2091
|
+
},
|
|
2092
|
+
additionalProperties: true
|
|
2093
|
+
}
|
|
2094
|
+
},
|
|
2095
|
+
additionalProperties: false
|
|
1886
2096
|
}
|
|
1887
2097
|
},
|
|
1888
2098
|
required: ["format"],
|
|
@@ -1913,7 +2123,11 @@ function register4(server, deps) {
|
|
|
1913
2123
|
},
|
|
1914
2124
|
truncated: {
|
|
1915
2125
|
type: "boolean",
|
|
1916
|
-
description: "`diagnostics` was capped by `maxDiagnostics
|
|
2126
|
+
description: "`diagnostics` was capped, by `maxDiagnostics` or by the budget the quality policy set."
|
|
2127
|
+
},
|
|
2128
|
+
profileId: {
|
|
2129
|
+
type: "string",
|
|
2130
|
+
description: "The quality profile the design analysis ran under, when one applied."
|
|
1917
2131
|
}
|
|
1918
2132
|
})
|
|
1919
2133
|
)
|
|
@@ -1928,21 +2142,57 @@ function register4(server, deps) {
|
|
|
1928
2142
|
const result = adapter.validateDocument(
|
|
1929
2143
|
withRenderer(resolved.document, args.renderer)
|
|
1930
2144
|
);
|
|
1931
|
-
const
|
|
2145
|
+
const unavailable2 = await rendererAvailability(
|
|
2146
|
+
adapter,
|
|
2147
|
+
resolved.document,
|
|
2148
|
+
args.renderer
|
|
2149
|
+
);
|
|
2150
|
+
let analysis;
|
|
2151
|
+
let qualityOption;
|
|
2152
|
+
if (adapter.analyzeQuality) {
|
|
2153
|
+
try {
|
|
2154
|
+
analysis = await adapter.analyzeQuality(resolved.document, {
|
|
2155
|
+
renderer: args.renderer,
|
|
2156
|
+
quality: args.quality
|
|
2157
|
+
});
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
const option = result.valid ? void 0 : qualityOptionDiagnostic(error);
|
|
2160
|
+
if (!option) throw error;
|
|
2161
|
+
qualityOption = option;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const structural = [
|
|
2165
|
+
...validationDiagnostics(result.errors),
|
|
2166
|
+
...unavailable2 ? [unavailable2] : [],
|
|
2167
|
+
...qualityOption ? [qualityOption] : []
|
|
2168
|
+
];
|
|
2169
|
+
const all = [
|
|
2170
|
+
...structural,
|
|
2171
|
+
...analysis ? [
|
|
2172
|
+
...qualityAnalysisDiagnostics(analysis),
|
|
2173
|
+
...ruleErrorDiagnostics(analysis)
|
|
2174
|
+
] : []
|
|
2175
|
+
];
|
|
1932
2176
|
const counts = countDiagnostics(all);
|
|
2177
|
+
const blocked = countDiagnostics(structural).error > 0 || analysis?.blocked === true;
|
|
1933
2178
|
const { kept, truncated } = capDiagnostics(
|
|
1934
2179
|
all,
|
|
1935
2180
|
args.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS
|
|
1936
2181
|
);
|
|
1937
2182
|
return {
|
|
1938
|
-
ok:
|
|
2183
|
+
ok: !blocked,
|
|
1939
2184
|
diagnostics: kept,
|
|
1940
|
-
valid:
|
|
2185
|
+
valid: !blocked,
|
|
1941
2186
|
format: args.format,
|
|
1942
2187
|
...args.renderer !== void 0 && { renderer: args.renderer },
|
|
1943
2188
|
source: sourceSummary(resolved),
|
|
1944
2189
|
counts,
|
|
1945
|
-
|
|
2190
|
+
// The engine caps its own list under a policy budget, so a report
|
|
2191
|
+
// shortened there would otherwise come back reading complete.
|
|
2192
|
+
truncated: truncated || analysis?.truncated === true,
|
|
2193
|
+
...analysis?.profileId !== void 0 && {
|
|
2194
|
+
profileId: analysis.profileId
|
|
2195
|
+
}
|
|
1946
2196
|
};
|
|
1947
2197
|
})
|
|
1948
2198
|
)
|
|
@@ -2818,6 +3068,9 @@ import crypto2 from "crypto";
|
|
|
2818
3068
|
import { promises as fs6 } from "fs";
|
|
2819
3069
|
import os2 from "os";
|
|
2820
3070
|
import path5 from "path";
|
|
3071
|
+
import {
|
|
3072
|
+
RENDERER_DEPENDENCY_MISSING as RENDERER_DEPENDENCY_MISSING2
|
|
3073
|
+
} from "@json-to-office/shared";
|
|
2821
3074
|
import { getFontStager } from "@json-to-office/jto-ops";
|
|
2822
3075
|
|
|
2823
3076
|
// src/preview/cache-key.ts
|
|
@@ -3149,6 +3402,15 @@ function renderFailure(stage, detail, context = {}) {
|
|
|
3149
3402
|
{ suggestion, context: { stage, ...context } }
|
|
3150
3403
|
);
|
|
3151
3404
|
}
|
|
3405
|
+
function buildFailure(error) {
|
|
3406
|
+
if (error instanceof Error && error.name === RENDERER_DEPENDENCY_MISSING2) {
|
|
3407
|
+
return failure(ERROR_CODES.DEPENDENCY_MISSING, message(error), {
|
|
3408
|
+
suggestion: "Install the renderer's backend, or re-run with a renderer jto_info reports as available. The document is not at fault.",
|
|
3409
|
+
context: { stage: "build" }
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
return renderFailure("build", message(error));
|
|
3413
|
+
}
|
|
3152
3414
|
var PNG_SIGNATURE = Buffer.from([
|
|
3153
3415
|
137,
|
|
3154
3416
|
80,
|
|
@@ -3355,9 +3617,10 @@ async function renderPreview(options) {
|
|
|
3355
3617
|
converters = versions;
|
|
3356
3618
|
} catch (error) {
|
|
3357
3619
|
if (signal?.aborted) return cancelled2();
|
|
3620
|
+
const build = buildFailure(error);
|
|
3358
3621
|
return failureFrom([
|
|
3359
|
-
...validationDiagnostics2(options.getAdapter(format), document),
|
|
3360
|
-
...
|
|
3622
|
+
...build.diagnostics[0]?.code === ERROR_CODES.DEPENDENCY_MISSING ? [] : validationDiagnostics2(options.getAdapter(format), document),
|
|
3623
|
+
...build.diagnostics
|
|
3361
3624
|
]);
|
|
3362
3625
|
}
|
|
3363
3626
|
const generateMs = elapsed(generateStarted);
|
|
@@ -5559,6 +5822,7 @@ var RESOURCE_URIS = {
|
|
|
5559
5822
|
catalog: "jto://catalog",
|
|
5560
5823
|
renderers: "jto://renderers",
|
|
5561
5824
|
themes: "jto://themes",
|
|
5825
|
+
themeValues: "jto://themes/values",
|
|
5562
5826
|
templates: "jto://templates",
|
|
5563
5827
|
documentSchema: (format) => `jto://schema/${format}/document`,
|
|
5564
5828
|
themeSchema: (format) => `jto://schema/${format}/theme`
|
|
@@ -5610,7 +5874,7 @@ function register9(server, deps) {
|
|
|
5610
5874
|
RESOURCE_URIS.themes,
|
|
5611
5875
|
{
|
|
5612
5876
|
title: "Built-in themes",
|
|
5613
|
-
description: "Theme names shipped with each format, usable as a document\u2019s props.theme or as the tools\u2019 theme option.",
|
|
5877
|
+
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.",
|
|
5614
5878
|
mimeType: JSON_MIME
|
|
5615
5879
|
},
|
|
5616
5880
|
async (uri) => {
|
|
@@ -5623,6 +5887,33 @@ function register9(server, deps) {
|
|
|
5623
5887
|
});
|
|
5624
5888
|
}
|
|
5625
5889
|
);
|
|
5890
|
+
server.registerResource(
|
|
5891
|
+
"theme-values",
|
|
5892
|
+
RESOURCE_URIS.themeValues,
|
|
5893
|
+
{
|
|
5894
|
+
title: "Built-in theme values",
|
|
5895
|
+
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.",
|
|
5896
|
+
mimeType: JSON_MIME
|
|
5897
|
+
},
|
|
5898
|
+
async (uri) => {
|
|
5899
|
+
const formats = await Promise.all(
|
|
5900
|
+
FORMAT_NAMES.map(async (format) => {
|
|
5901
|
+
const adapter = deps.getAdapter(format);
|
|
5902
|
+
let themes;
|
|
5903
|
+
try {
|
|
5904
|
+
themes = adapter.getBuiltinThemeValues ? await adapter.getBuiltinThemeValues() : adapter.getBuiltinThemes();
|
|
5905
|
+
} catch {
|
|
5906
|
+
themes = adapter.getBuiltinThemes();
|
|
5907
|
+
}
|
|
5908
|
+
return {
|
|
5909
|
+
format,
|
|
5910
|
+
themes
|
|
5911
|
+
};
|
|
5912
|
+
})
|
|
5913
|
+
);
|
|
5914
|
+
return jsonContents(uri, { formats });
|
|
5915
|
+
}
|
|
5916
|
+
);
|
|
5626
5917
|
server.registerResource(
|
|
5627
5918
|
"templates",
|
|
5628
5919
|
RESOURCE_URIS.templates,
|
|
@@ -5671,6 +5962,7 @@ Working rules:
|
|
|
5671
5962
|
- 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.
|
|
5672
5963
|
- 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.
|
|
5673
5964
|
- 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.
|
|
5965
|
+
- 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.
|
|
5674
5966
|
- 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.
|
|
5675
5967
|
- Snapshot before risky changes. jto_workspace_snapshot pins the current revision so a restructuring you cannot cleanly undo is still recoverable.
|
|
5676
5968
|
|