@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/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.2.0" : "dev-mode";
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(["RendererDependencyMissingError"]);
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 and renderer ids, 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.",
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: ["name", "extension", "label", "rendererIds"],
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 rendererIds = [];
798
+ let renderers = [];
717
799
  try {
718
- rendererIds = [...await adapter.rendererIds()];
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 valid .pptx: root, one slide, one title text.",
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
- rendererIds = [...await adapter.rendererIds()];
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: ["id", "default", "components", "unsupported"],
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, e.g. `E_REQUIRED_PROPERTY`, `E_UNEXPECTED_PROPERTY`, `E_TYPE_MISMATCH`, `E_UNKNOWN_COMPONENT`. `ok` mirrors the gate generation applies: schema and semantic errors block it \u2014 the semantic rules the published JSON Schema cannot state, such as a text component needing one of `text`/`runs`, are checked here and only here \u2014 while renderer-profile findings (code `W_UNSUPPORTED_RENDERER_FEATURE`) come back as warnings because the renderer, not the schema, has the last word on those. A broken document is a normal result with `ok: false`, never an error.",
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 all = validationDiagnostics(result.errors);
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: counts.error === 0,
2195
+ ok: !blocked,
1951
2196
  diagnostics: kept,
1952
- valid: counts.error === 0,
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
- truncated
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
- ...renderFailure("build", message(error)).diagnostics
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