@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 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.2.0" : "dev-mode";
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(["RendererDependencyMissingError"]);
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 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.",
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: ["name", "extension", "label", "rendererIds"],
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 rendererIds = [];
803
+ let renderers = [];
722
804
  try {
723
- rendererIds = [...await adapter.rendererIds()];
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 valid .pptx: root, one slide, one title text.",
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
- rendererIds = [...await adapter.rendererIds()];
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: ["id", "default", "components", "unsupported"],
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, 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.",
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 all = validationDiagnostics(result.errors);
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: counts.error === 0,
2183
+ ok: !blocked,
1939
2184
  diagnostics: kept,
1940
- valid: counts.error === 0,
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
- truncated
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
- ...renderFailure("build", message(error)).diagnostics
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