slidemind 0.3.20 → 0.4.1

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.
Files changed (187) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +592 -16
  4. package/LICENSE +4 -0
  5. package/README.md +203 -8
  6. package/THIRD-PARTY-NOTICES.md +201 -0
  7. package/bin/slidemind.mjs +50 -2
  8. package/package.json +5 -3
  9. package/skills/create-slides/SKILL.md +484 -886
  10. package/skills/search/SKILL.md +46 -23
  11. package/src/bridge/build-and-push.mjs +548 -73
  12. package/src/bridge/bundle.mjs +100 -31
  13. package/src/bridge/capture.mjs +14 -17
  14. package/src/bridge/client/brands.mjs +157 -18
  15. package/src/bridge/client/engine-routing.mjs +48 -13
  16. package/src/bridge/client/engine-search.mjs +24 -1
  17. package/src/bridge/client/render.mjs +27 -1
  18. package/src/bridge/client/transport.mjs +499 -23
  19. package/src/bridge/client.mjs +10 -0
  20. package/src/bridge/config.mjs +67 -6
  21. package/src/bridge/engine-download.mjs +0 -10
  22. package/src/bridge/graph-auth.mjs +55 -3
  23. package/src/bridge/image-intent-envelope.mjs +239 -0
  24. package/src/bridge/package-preview.mjs +67 -0
  25. package/src/bridge/sharepoint.mjs +914 -0
  26. package/src/builders/asset-fetch.mjs +203 -0
  27. package/src/builders/block-dividers.mjs +83 -0
  28. package/src/builders/build.mjs +171 -87
  29. package/src/builders/card-height.mjs +294 -0
  30. package/src/builders/chart-hole.mjs +183 -0
  31. package/src/builders/chart-jsx.mjs +410 -0
  32. package/src/builders/chart-labels.mjs +326 -0
  33. package/src/builders/concept-geometry.mjs +370 -0
  34. package/src/builders/concept-intent.mjs +200 -0
  35. package/src/builders/concept-layout.mjs +79 -0
  36. package/src/builders/concept-lines.mjs +509 -0
  37. package/src/builders/concept-lists.mjs +270 -0
  38. package/src/builders/concept-marks.mjs +84 -0
  39. package/src/builders/concept-relationships.mjs +12 -0
  40. package/src/builders/concept-spec.mjs +645 -0
  41. package/src/builders/concept-text.mjs +140 -0
  42. package/src/builders/concept-waves.mjs +57 -0
  43. package/src/builders/copy-fit.mjs +139 -0
  44. package/src/builders/design-tokens.mjs +347 -0
  45. package/src/builders/draft-check.mjs +552 -340
  46. package/src/builders/draft-lines.mjs +369 -0
  47. package/src/builders/edit-route.mjs +29 -0
  48. package/src/builders/figure-sketch.mjs +375 -0
  49. package/src/builders/fold-sets.mjs +639 -0
  50. package/src/builders/gate-server.mjs +9 -4
  51. package/src/builders/gate-session.js +273 -12
  52. package/src/builders/gate-shell.js +1494 -0
  53. package/src/builders/image-intent-check.mjs +308 -0
  54. package/src/builders/image-use-reconcile.mjs +107 -0
  55. package/src/builders/kit-helpers.mjs +121 -0
  56. package/src/builders/layout-names.mjs +32 -0
  57. package/src/builders/legacy-reference-uses.mjs +179 -0
  58. package/src/builders/line-fit.mjs +224 -0
  59. package/src/builders/line-native.mjs +895 -0
  60. package/src/builders/line-relations.mjs +230 -0
  61. package/src/builders/lint-fix.mjs +117 -9
  62. package/src/builders/lint.mjs +25 -20
  63. package/src/builders/list-jsx.mjs +88 -0
  64. package/src/builders/placement.mjs +133 -0
  65. package/src/builders/plan-checks.mjs +135 -0
  66. package/src/builders/plan-comments.mjs +19 -0
  67. package/src/builders/plan-handlers.mjs +95 -0
  68. package/src/builders/plan-markdown-edit.mjs +3 -3
  69. package/src/builders/plan-overlay.js +1530 -1608
  70. package/src/builders/plan.mjs +689 -186
  71. package/src/builders/preset-definitions.json +1 -0
  72. package/src/builders/preset-eval.mjs +244 -0
  73. package/src/builders/preview.html +51 -120
  74. package/src/builders/red-thread-backbone.mjs +135 -36
  75. package/src/builders/repair.mjs +205 -0
  76. package/src/builders/repeat-groups.mjs +540 -0
  77. package/src/builders/resolve-skill.mjs +5 -4
  78. package/src/builders/review-overlay.js +296 -293
  79. package/src/builders/review.mjs +305 -84
  80. package/src/builders/rule-shift.mjs +96 -0
  81. package/src/builders/seat.mjs +354 -0
  82. package/src/builders/settle-driver.mjs +236 -0
  83. package/src/builders/settle.mjs +1448 -0
  84. package/src/builders/slide-prep.mjs +67 -0
  85. package/src/builders/spec-assets.mjs +236 -0
  86. package/src/builders/spec-match.mjs +56 -0
  87. package/src/builders/stack.mjs +413 -0
  88. package/src/builders/storyboard-markdown.mjs +70 -27
  89. package/src/builders/storyboard-schema.mjs +54 -114
  90. package/src/builders/text-blocks.mjs +212 -0
  91. package/src/builders/text-card-fit.mjs +552 -0
  92. package/src/builders/text-stack.mjs +276 -0
  93. package/src/builders/transcribe.mjs +1069 -0
  94. package/src/cli/assets.mjs +164 -0
  95. package/src/cli/common.mjs +173 -12
  96. package/src/cli/concept-spec-support.mjs +76 -0
  97. package/src/cli/concepts.mjs +1002 -103
  98. package/src/cli/deck.mjs +401 -65
  99. package/src/cli/diagnose.mjs +4 -2
  100. package/src/cli/engine-helpers.mjs +17 -10
  101. package/src/cli/engine-results.mjs +76 -3
  102. package/src/cli/help.mjs +232 -28
  103. package/src/cli/install.mjs +9 -0
  104. package/src/cli/maintenance.mjs +104 -0
  105. package/src/cli/read.mjs +2 -1
  106. package/src/cli/ready.mjs +6 -3
  107. package/src/cli/record.mjs +522 -0
  108. package/src/cli/remote.mjs +818 -0
  109. package/src/cli/revision.mjs +686 -0
  110. package/src/cli/search.mjs +30 -11
  111. package/src/cli/session.mjs +56 -13
  112. package/src/cli/setup.mjs +46 -2
  113. package/src/cli/status.mjs +142 -33
  114. package/src/cli/tasks.mjs +246 -0
  115. package/src/cli/transcribe.mjs +854 -0
  116. package/src/cli/workspace-ui.mjs +260 -0
  117. package/src/cli/workspace.mjs +212 -34
  118. package/src/install-channel.mjs +64 -7
  119. package/src/lib/adopt.mjs +346 -25
  120. package/src/lib/concept-spec-binding.mjs +96 -0
  121. package/src/lib/conversions.mjs +70 -6
  122. package/src/lib/convert-folded.mjs +107 -0
  123. package/src/lib/deck-paths.mjs +418 -0
  124. package/src/lib/durable-ids.mjs +47 -0
  125. package/src/lib/edit-operations.mjs +316 -0
  126. package/src/lib/edit-state.mjs +1244 -0
  127. package/src/lib/edit-transitions.mjs +120 -0
  128. package/src/lib/extensions.mjs +501 -0
  129. package/src/lib/image-assets.mjs +465 -0
  130. package/src/lib/image-derivatives.mjs +234 -0
  131. package/src/lib/image-intent-fingerprint.mjs +70 -0
  132. package/src/lib/image-intent-tags.mjs +176 -0
  133. package/src/lib/image-intent.mjs +802 -0
  134. package/src/lib/image-use-overrides.mjs +279 -0
  135. package/src/lib/inline-components.mjs +613 -0
  136. package/src/lib/lint-converted.mjs +68 -4
  137. package/src/lib/merge-json.mjs +163 -0
  138. package/src/lib/overwrite-guard.mjs +3 -3
  139. package/src/lib/presentation-context.mjs +531 -0
  140. package/src/lib/preservation-tags.mjs +157 -0
  141. package/src/lib/preservation-verdict.mjs +1342 -0
  142. package/src/lib/publish.mjs +1557 -0
  143. package/src/lib/rebase.mjs +945 -0
  144. package/src/lib/reconcile-ack.mjs +4 -6
  145. package/src/lib/reconcile.mjs +62 -24
  146. package/src/lib/record-client.mjs +175 -0
  147. package/src/lib/record-state.mjs +151 -0
  148. package/src/lib/remote.mjs +661 -0
  149. package/src/lib/revision.mjs +1210 -0
  150. package/src/lib/slide-guids.mjs +4 -6
  151. package/src/lib/slide-preservation.mjs +423 -0
  152. package/src/lib/slide-selection.mjs +9 -0
  153. package/src/lib/workspace-transaction.mjs +117 -0
  154. package/src/pptx/content-digest.mjs +334 -0
  155. package/src/pptx/deck-assemble.mjs +1278 -0
  156. package/src/pptx/fill/engine.mjs +2 -3
  157. package/src/pptx/fill/freeform.mjs +6 -3
  158. package/src/pptx/ooxml.mjs +20 -0
  159. package/src/pptx/package-evidence.mjs +383 -0
  160. package/src/pptx/tags.mjs +163 -11
  161. package/src/update-check.mjs +22 -4
  162. package/src/workspace/adapters.mjs +222 -0
  163. package/src/workspace/auth.mjs +256 -0
  164. package/src/workspace/autostart.mjs +204 -0
  165. package/src/workspace/daemon.mjs +360 -0
  166. package/src/workspace/discovery.mjs +180 -0
  167. package/src/workspace/edit.mjs +167 -0
  168. package/src/workspace/events.mjs +52 -0
  169. package/src/workspace/gate-client.mjs +290 -0
  170. package/src/workspace/handback.mjs +73 -0
  171. package/src/workspace/lifecycle.mjs +635 -0
  172. package/src/workspace/operations.mjs +200 -0
  173. package/src/workspace/pick-folder.mjs +152 -0
  174. package/src/workspace/registry.mjs +359 -0
  175. package/src/workspace/remote-links.mjs +397 -0
  176. package/src/workspace/remote-status.mjs +359 -0
  177. package/src/workspace/routes.mjs +1331 -0
  178. package/src/workspace/server.mjs +159 -0
  179. package/src/workspace/sync.mjs +1119 -0
  180. package/src/workspace/task-actions.mjs +215 -0
  181. package/src/workspace/tasks.mjs +507 -0
  182. package/src/workspace/ui/app.js +1886 -0
  183. package/src/workspace/ui/index.html +141 -0
  184. package/src/workspace/ui/plan-bridge.js +120 -0
  185. package/src/workspace/ui/sharepoint.svg +59 -0
  186. package/src/workspace/ui/styles.css +423 -0
  187. package/src/builders/visual-families.mjs +0 -46
package/src/cli/deck.mjs CHANGED
@@ -1,17 +1,92 @@
1
+ import { draftLineTier, imageCountLines, lineShapeLines, listLines, missingAssetLines, repeatLines } from "../builders/draft-lines.mjs";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
4
+ import crypto from "node:crypto";
3
5
  import { pathToFileURL } from "node:url";
4
- import { fail, requirePlanApproved, requireReviewApproved, resolveDeckArg, runNode, splitDeckArg, renderProgressPrinter, dataUriToPng, freshenDeckBundle, withUnboundHint, SRC_DIR, BUILDERS_DIR } from "./common.mjs";
6
+ import { requireAuthorizedEdits, seedRenderedSlides } from "./revision.mjs";
7
+ import { scopedSlideFiles } from "../lib/edit-transitions.mjs";
8
+ import { fail, loadBridgeModule, requirePlanApproved, requireReviewApproved, requireSlidesDraftChecked, requireSlidesPreserved, resolveDeckArg, runNode, splitDeckArg, renderProgressPrinter, dataUriToPng, freshenDeckBundle, withUnboundHint, SRC_DIR, BUILDERS_DIR } from "./common.mjs";
9
+ import { readDesignName } from "./designs.mjs";
10
+ import { deckSlug, previewPath, readPlanOwner, statePath } from "../lib/deck-paths.mjs";
11
+ import { currentSpec } from "../lib/concept-spec-binding.mjs";
5
12
 
6
13
  export async function cmdPlan(rest) {
7
14
  const { deck, passthrough } = splitDeckArg(rest, "plan");
15
+ const deckDir = resolveDeckArg(deck);
16
+ // A plan written one directory up (the deck folder instead of its
17
+ // workspace) left the gate approving an empty record and `concepts`
18
+ // generating nothing, four rounds in a row (x51 cloud). Say where it goes.
19
+ const record = path.join(deckDir, "plan.md");
20
+ const slides = (f) => { try { return /```storyboard-canvas/.test(fs.readFileSync(f, "utf8")); } catch { return false; } };
21
+ const stray = path.join(path.dirname(deckDir), "plan.md");
22
+ if (!slides(record) && stray !== record && slides(stray)) {
23
+ fail(`plan: the deck's plan record is ${record}, which has no storyboard-canvas slides; ${stray} has them. Move it (mv "${stray}" "${record}") and re-run \`slidemind plan ${deckSlug(deckDir)}\`.`);
24
+ }
25
+ // The agent writes the plan; the person reviews it. A record with no slides is
26
+ // never asked about: a gate that approves itself then made `concepts` generate
27
+ // nothing (7 of 9 x44–x52 batches), and with a person it handed them the
28
+ // scaffold's placeholder to review (a Codex agent chained `new && plan`,
29
+ // 2026-10-11). An extension-owned plan (a proposal pack) keeps its record
30
+ // elsewhere and its plan.md is deck metadata only: there are no storyboards.
31
+ const owner = readPlanOwner(deckDir)?.owner;
32
+ if (!slides(record) && (!owner || owner === "core")) {
33
+ fail(`plan: ${record} has no slides yet — write \`## Red thread\` and \`## Per-slide storyboards\` with one \`\`\`storyboard-canvas JSON block per slide (the design bundle's workflow/plan.md has the exact shape), then re-run \`slidemind plan ${deckSlug(deckDir)}\`.`);
34
+ }
35
+ if (!owner || owner === "core") {
36
+ if ((await ensurePlanDesign(deckDir)) === "blocked") return;
37
+ }
8
38
  await runNode(path.join(BUILDERS_DIR, "plan.mjs"), [
9
39
  "--deck",
10
- resolveDeckArg(deck),
40
+ deckDir,
11
41
  ...passthrough,
12
42
  ]);
13
43
  }
14
44
 
45
+ /**
46
+ * The plan page opens on the deck's design, so the deck must have one before it
47
+ * is asked about. A deck nobody bound (the agent wrote the plan without `new
48
+ * --brand`) is bound here the way `new` binds: the one design open in
49
+ * PowerPoint, else the one design cached on this machine. With several at hand
50
+ * the user named one — the agent binds it (`ready <deck> --brand`) and asks
51
+ * again: BLOCKED, exit 2. Nothing at hand (offline, nothing downloaded) leaves
52
+ * the choice to the page's own menu.
53
+ */
54
+ async function ensurePlanDesign(deckDir) {
55
+ const bundle = await loadBridgeModule("bundle");
56
+ if (bundle.readDeckBundleRef(deckDir)?.presFP) return "bound";
57
+ const cfg = await loadBridgeModule("config");
58
+ if (cfg.readSessions().length === 0) return "none";
59
+ const cached = bundle.listCachedBundles();
60
+ let presFP = null;
61
+ try {
62
+ const client = await loadBridgeModule("client");
63
+ const { presentations } = await client.listOpenPresentationsDetailed();
64
+ const resolution = client.resolveDesignSource(presentations, {});
65
+ if (resolution.status === "resolved") presFP = resolution.entry.brandId;
66
+ } catch {
67
+ // no window to ask: the cache decides
68
+ }
69
+ if (!presFP && cached.length === 1) presFP = cached[0].presFP;
70
+ const meta = presFP ? bundle.readMaterializedBundleMeta(presFP) : null;
71
+ if (meta?.presFP) {
72
+ bundle.writeDeckBundleRef(deckDir, { presFP: meta.presFP, etag: meta.etag ?? null });
73
+ process.stderr.write(`slidemind: bound ${deckSlug(deckDir)} to design ${readDesignName(bundle.bundleDirForPresFP(meta.presFP)) ?? meta.presFP}\n`);
74
+ return "bound";
75
+ }
76
+ if (cached.length < 2) return "none";
77
+ const slug = deckSlug(deckDir);
78
+ const list = cached.map((b) => `${readDesignName(b.dir) ?? b.presFP} (${b.presFP})`).join(", ");
79
+ process.stdout.write(
80
+ "BLOCKED: deck_unbound\n" +
81
+ `${slug} has no corporate design yet and several are at hand, so the plan page could not open on one. ` +
82
+ "Bind it to the design the user asked for, then run this again:\n" +
83
+ ` slidemind ready ${slug} --brand <presFP>\n` +
84
+ `Designs on this machine: ${list}. The user named none? Ask which one — \`slidemind ls designs\` lists every design the backend serves.\n`,
85
+ );
86
+ process.exitCode = 2;
87
+ return "blocked";
88
+ }
89
+
15
90
  export async function cmdReview(rest) {
16
91
  const { deck, passthrough } = splitDeckArg(rest, "review");
17
92
  const deckDir = resolveDeckArg(deck);
@@ -86,7 +161,7 @@ export async function cmdImagen(rest) {
86
161
  const ref = bundleMod.readDeckBundleRef(deckDir);
87
162
  if (!ref?.presFP) {
88
163
  fail(
89
- `deck ${path.basename(deckDir)} is not bound to a design yet — run ` +
164
+ `deck ${deckSlug(deckDir)} is not bound to a design yet — run ` +
90
165
  `'slidemind ready ${deck}' first (imagen routes by the deck's bound design).`,
91
166
  );
92
167
  }
@@ -231,11 +306,17 @@ export async function cmdBuildPptx(rest) {
231
306
  let outPath = null;
232
307
  let packDir = null;
233
308
  let force = false;
309
+ let refreshDesign = false;
234
310
  let asJson = false;
311
+ let publish = true;
235
312
  for (let i = 0; i < passthrough.length; i++) {
236
313
  const a = passthrough[i];
237
314
  if (a === "--force") {
238
315
  force = true;
316
+ } else if (a === "--no-publish") {
317
+ publish = false;
318
+ } else if (a === "--refresh-design") {
319
+ refreshDesign = true;
239
320
  } else if (a === "--json") {
240
321
  asJson = true;
241
322
  } else if (a === "--to") {
@@ -272,9 +353,16 @@ export async function cmdBuildPptx(rest) {
272
353
  fail(`explicit --pack ${path.resolve(packDir)} is mode ${deckMode.mode}, not fill-template`);
273
354
  }
274
355
  // Template packs have no HTML preview to review and carry their own plan
275
- // sign-off; compose decks ship only from an approved, unchanged preview.
356
+ // sign-off; compose decks ship only once their preview has been approved
357
+ // (one-time per deck — a change after the approval is not re-reviewed).
276
358
  if (deckMode.mode !== "fill-template") {
277
359
  await requireReviewApproved(deckDir);
360
+ requireAuthorizedEdits(deckDir);
361
+ await requireSlidesPreserved(deckDir);
362
+ const { deliveredSlideFiles } = await import(
363
+ pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
364
+ );
365
+ await requireSlidesDraftChecked(deckDir, { files: deliveredSlideFiles(deckDir, { refreshDesign }) });
278
366
  await freshenDeckBundle(deckDir);
279
367
  }
280
368
  if (deckMode.mode === "fill-template") {
@@ -342,35 +430,78 @@ export async function cmdBuildPptx(rest) {
342
430
  presentationId: to,
343
431
  outPath,
344
432
  force,
433
+ refreshDesign,
345
434
  onProgress: renderProgressPrinter(),
346
435
  })
347
436
  .catch((err) => fail(withUnboundHint(deckDir, err)));
348
- if (asJson) {
349
- process.stdout.write(
350
- `${JSON.stringify({ outPath: result.outPath, slideCount: result.slideCount ?? null, presId: result.presId ?? null, windowless: Boolean(result.windowless), warnings: result.warnings ?? [], skippedSlides: result.skippedSlides ?? [], preview: result.preview ?? null }, null, 2)}\n`,
351
- );
352
- } else {
437
+ // The package's measured lines, classed by the shared table as the add-in's
438
+ // build classes them.
439
+ const { splitPackageLines, packageDefectsText } = await import(
440
+ pathToFileURL(path.join(BUILDERS_DIR, "draft-check.mjs")).href
441
+ );
442
+ const { defects: packageDefects, notes: packageNotes } = splitPackageLines(result.warnings);
443
+ if (packageDefects.length > 0) process.exitCode = 1;
444
+ if (!asJson) {
353
445
  process.stdout.write(
354
446
  `Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in` +
355
447
  `${result.windowless ? " (no PowerPoint window needed)" : ""}\n`,
356
448
  );
357
- // LOOK at what shipped — the .pptx is opaque from here.
358
- if (result.preview?.dir) {
449
+ const preview = result.packagePreview?.status === "rendered" ? result.packagePreview : result.preview;
450
+ if (preview?.dir) {
359
451
  process.stdout.write(
360
- ` ${result.preview.files.length} slide png(s) in ${path.relative(process.cwd(), result.preview.dir)}/ — read them before handing the deck over\n`,
452
+ ` ${preview.files.length} ${preview.kind === "pptx" ? "exported PPTX" : "browser source"} slide png(s) in ${path.relative(process.cwd(), preview.dir)}/ — judge a <Chart> here (view-draft paints a stand-in); the concept comparison was the draft check's\n`,
361
453
  );
362
- } else if (result.preview?.note) {
363
- process.stdout.write(` ${result.preview.note}\n`);
364
454
  }
455
+ if (result.packagePreview?.note) process.stderr.write(` ${result.packagePreview.note}\n`);
456
+ if (packageDefects.length > 0) process.stdout.write(packageDefectsText(packageDefects, deckSlug(deckDir)));
365
457
  }
366
- for (const w of result.warnings ?? []) {
458
+ for (const w of packageNotes) {
367
459
  process.stderr.write(` warning: ${w}\n`);
368
460
  }
369
461
  for (const s of result.skippedSlides ?? []) {
370
462
  process.stderr.write(` skipped ${s.stem}: ${s.reason}\n`);
371
463
  }
464
+ // A deck connected to SharePoint — the file it was opened from, or a folder
465
+ // the user connected in the workspace — is delivered THERE, so the build
466
+ // publishes the candidate it just wrote. The connection is read from the
467
+ // deck's own state at every build: a folder connected mid-session is honoured
468
+ // by the next build without the agent having to know. `--no-publish` keeps a
469
+ // build local; `slidemind publish` retries one whose publish did not save.
470
+ // A build that ends on package defects is not ready for the user, so it does
471
+ // not go to SharePoint either: fixed and rebuilt, that build publishes.
472
+ const published = publish && result.candidate
473
+ ? await (await import("./remote.mjs")).publishAfterBuild(deckDir, {
474
+ candidateId: result.candidate.candidateId,
475
+ withheld: packageDefects.length > 0
476
+ ? { reason: "package_defects", message: `held back: ${packageDefects.length} package defect(s) — fix them and build again` }
477
+ : null,
478
+ })
479
+ : null;
480
+ if (published) process.stderr.write(published.text);
481
+ if (asJson) {
482
+ process.stdout.write(
483
+ `${JSON.stringify({ outPath: result.outPath, slideCount: result.slideCount ?? null, presId: result.presId ?? null, windowless: Boolean(result.windowless), packageDefects, warnings: result.warnings ?? [], skippedSlides: result.skippedSlides ?? [], targeted: result.targeted ?? null, preview: result.preview ?? null, packagePreview: result.packagePreview ?? null, publish: published?.json ?? null }, null, 2)}\n`,
484
+ );
485
+ }
486
+ if (published?.exitCode) process.exit(published.exitCode);
372
487
  }
373
488
 
489
+ // `transcribe` names every asset it fetches `NN-<element id>` for the slide it
490
+ // fetched it FOR, and recolours it for THAT slide's background. So a slide that
491
+ // points at another slide's numbered asset is not reusing imagery, it is
492
+ // clearing an `[asset:error]` with the nearest file on disk — x44: nine such
493
+ // refs, four white glyphs landed on white cards and one blue glyph on a blue
494
+ // disc, all invisible, on the two worst-scoring slides of the deck. Existence
495
+ // is not evidence that the file is the right glyph, or legible where it now
496
+ // sits. Returns the owning slide number, or null when the reference is fine:
497
+ // an unnumbered asset (a deck image used on several slides) is never borrowed,
498
+ // and neither is a slide file that carries no number of its own.
499
+ export { borrowedAssetOwner } from "../builders/draft-lines.mjs";
500
+
501
+ // What a concept is to the draft check, in both outputs (text and --json).
502
+ // Same words as the add-in's see_draft caption (build-and-push.ts).
503
+ const CONCEPT_GUIDANCE = "the blueprint, not the target: every element it shows inside the content slot is on the render as that kind of thing (the same hero, grouping, order, hierarchy and focal element), every connector present with its line shape (straight, elbow, curve or arc) and one shape per cycle or fan. Build it cleaner than the painting: siblings the same size, even gaps, straight edges straight, round forms round, token colours; a cleaner, aligned build of the same elements needs no change. Delete what the painting drew by accident or in the template's chrome (a footer mark, a guide box, a stray stroke, a second wordmark) (workflow/create.md, Execute, don't trace).";
504
+
374
505
  export async function cmdViewDraft(rest) {
375
506
  const { deck, passthrough } = splitDeckArg(rest, "view-draft");
376
507
  let to = null;
@@ -382,7 +513,11 @@ export async function cmdViewDraft(rest) {
382
513
  // with a note when the backend is unreachable; --translate insists, and
383
514
  // --no-translate keeps the check purely local.
384
515
  let translate = "auto";
385
- let fix = false;
516
+ // Autofix is the default: the deterministic token conversions (raw px →
517
+ // --sp-*/--fs-*, em → px) were the largest lint class agents fought by hand
518
+ // (137 raw-spacing warnings on one 8-slide deck, x20 2026-09-18) and carry
519
+ // no design decision. --no-fix keeps a file byte-exact for inspection.
520
+ let fix = true;
386
521
  let lintOnly = false;
387
522
  let asJson = false;
388
523
  // Converted slides only: promote the dialect lint's warnings (an empty
@@ -392,6 +527,8 @@ export async function cmdViewDraft(rest) {
392
527
  const a = passthrough[i];
393
528
  if (a === "--fix") {
394
529
  fix = true;
530
+ } else if (a === "--no-fix") {
531
+ fix = false;
395
532
  } else if (a === "--strict") {
396
533
  strict = true;
397
534
  } else if (a === "--lint-only") {
@@ -431,15 +568,22 @@ export async function cmdViewDraft(rest) {
431
568
  only = onlySpec.split(",").map((t) => t.trim()).filter(Boolean).map((t) => {
432
569
  if (files.includes(t)) return t;
433
570
  if (/^\d{1,3}$/.test(t)) {
434
- const hit = files.find((f) => Number(/^(\d{1,3})[-_.]/.exec(f)?.[1]) === Number(t));
571
+ // Numeric prefix first (`03-loop.jsx`); then any file whose first
572
+ // number is that slide (`slide-03.jsx`, `s03-loop.jsx`) — three sim
573
+ // sessions named files that way and burned 6–13 failed `--only`
574
+ // calls each on "no slide file matches".
575
+ const hit =
576
+ files.find((f) => Number(/^(\d{1,3})[-_.]/.exec(f)?.[1]) === Number(t)) ??
577
+ files.find((f) => Number(/(\d{1,3})/.exec(f)?.[1]) === Number(t));
435
578
  if (hit) return hit;
436
579
  }
437
580
  fail(`--only: no slide file matches '${t}' under ${path.join(deckDir, "slides")}`);
438
581
  });
439
582
  }
583
+ requireAuthorizedEdits(deckDir);
440
584
  await requirePlanApproved(deckDir);
441
585
  await freshenDeckBundle(deckDir);
442
- // The HTML build runs FIRST — it is what lints every slide against the
586
+ // The HTML build runs FIRST — it lints the selected slides against the
443
587
  // bundle's capability table and copies the bundle's corporate-design.css +
444
588
  // assets into the deck, which the type-ramp check below reads. (This is the
445
589
  // work the retired `slidemind build` command used to do in a separate call;
@@ -447,22 +591,35 @@ export async function cmdViewDraft(rest) {
447
591
  const { buildDeck } = await import(
448
592
  pathToFileURL(path.join(BUILDERS_DIR, "build.mjs")).href
449
593
  );
450
- if (fix) {
594
+ // An open edit scope bounds slidemind's own rewrites too: the autofix of a
595
+ // slide outside it would be the out-of-scope change the gate refuses.
596
+ const fixOnly = only ?? scopedSlideFiles(deckDir);
597
+ if (fix && fixOnly?.length !== 0) {
451
598
  const { fixDeckSlides } = await import(
452
599
  pathToFileURL(path.join(BUILDERS_DIR, "lint-fix.mjs")).href
453
600
  );
454
- const fixed = fixDeckSlides(deckDir);
601
+ const fixed = fixDeckSlides(deckDir, { only: fixOnly });
455
602
  if (fixed.fixes > 0) {
456
- process.stdout.write(
603
+ // stderr, like the `[fix:withheld]` lines below it: `--json` must parse
604
+ // as ONE document, and this sentence used to sit in front of the payload.
605
+ process.stderr.write(
457
606
  `Autofixed ${fixed.fixes} lint issue(s) in ${fixed.files} slide file(s) — re-read any file before patching it.\n`,
458
607
  );
459
608
  }
609
+ // Source someone else owns is reported, never rewritten — otherwise the
610
+ // next preview quietly undoes an edit. Stderr, so --json stays one payload.
611
+ for (const w of fixed.withheld ?? []) {
612
+ process.stderr.write(
613
+ `[fix:withheld] ${w.file} — autofix (${w.fixes.join(", ")}) NOT applied: ${w.reason}. ` +
614
+ `Its lint findings below still stand; apply them by hand if you want them.\n`,
615
+ );
616
+ }
460
617
  }
461
618
  // Advisory here: the converted rules print as warnings while slides are
462
619
  // still being edited, and block at `review` / `build-pptx`. --strict gates now.
463
- await buildDeck(deckDir, { strict, enforceConverted: strict });
620
+ await buildDeck(deckDir, { strict, enforceConverted: strict, only });
464
621
  if (lintOnly) {
465
- if (asJson) process.stdout.write(`${JSON.stringify({ deck: path.basename(deckDir), lint: "clean" })}\n`);
622
+ if (asJson) process.stdout.write(`${JSON.stringify({ deck: deckSlug(deckDir), lint: "clean" })}\n`);
466
623
  else process.stdout.write("Lint clean (no render — drop --lint-only for the draft previews).\n");
467
624
  return;
468
625
  }
@@ -471,6 +628,9 @@ export async function cmdViewDraft(rest) {
471
628
  const mod = await import(
472
629
  pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
473
630
  );
631
+ // A concurrent edit during capture cannot inherit a passing preview hash.
632
+ const capturedSources = new Map((only ?? fs.readdirSync(path.join(deckDir, "slides")).filter(f => f.endsWith(".jsx")))
633
+ .map(file => [file, fs.readFileSync(path.join(deckDir, "slides", file), "utf8")]));
474
634
  // The capture engine lints every slide again and prints the same lines the
475
635
  // build just did; mute that second copy (its warnings still ride the result).
476
636
  const stderrWrite = process.stderr.write.bind(process.stderr);
@@ -482,8 +642,8 @@ export async function cmdViewDraft(rest) {
482
642
  presentationId: to,
483
643
  onProgress: renderProgressPrinter(),
484
644
  only,
485
- // The server's export gate (slot-fit, diagram geometry, connector
486
- // endpoints) on the captured slides, verdict only, no .pptx, no review
645
+ // The server's export gate (slot-fit, diagram geometry, text escape,
646
+ // chrome) on the captured slides, verdict only, no .pptx, no review
487
647
  // gate — so a slide is proven buildable before hand-over.
488
648
  translate,
489
649
  });
@@ -499,7 +659,7 @@ export async function cmdViewDraft(rest) {
499
659
 
500
660
  const previewDir = outDir
501
661
  ? path.resolve(process.cwd(), outDir)
502
- : path.join(deckDir, ".draft-preview");
662
+ : previewPath(deckDir, "draft");
503
663
  fs.mkdirSync(previewDir, { recursive: true });
504
664
 
505
665
  // The measured half of the draft check — the same mechanics the add-in's
@@ -518,11 +678,6 @@ export async function cmdViewDraft(rest) {
518
678
  const file = path.join(conceptsDir, `concept-slide-${String(Number(m[1])).padStart(2, "0")}.png`);
519
679
  return fs.existsSync(file) ? file : null;
520
680
  };
521
- const inventory = check.inventoryWarnings(
522
- deckDir,
523
- (result.slides ?? []).map((s) => s.fileName).filter((f) => f.endsWith(".jsx")),
524
- );
525
- const deckLevel = inventory.filter((l) => l.includes("] assets/"));
526
681
  // The measured rules come from the add-in's capture engine in the bundle —
527
682
  // the same fill / type-scale / overlap checks see_draft runs in-app.
528
683
  const { loadDraftChecks } = await import(
@@ -537,24 +692,168 @@ export async function cmdViewDraft(rest) {
537
692
  "the design bundle's capture engine exports no draft checks — the connected add-in predates them. Update the add-in deployment, then run `slidemind download-bundle <deck>` and retry.",
538
693
  );
539
694
  }
540
- // Type ramp from the deck's own copy of the bundle css (build copies it in).
695
+ // Type ramp from the bundle's css — the deck carries no copy of the design.
541
696
  let typeTokens = null;
542
697
  try {
698
+ const { resolveSkillForDeck } = await import(
699
+ pathToFileURL(path.join(BUILDERS_DIR, "resolve-skill.mjs")).href
700
+ );
543
701
  typeTokens = rules.typeScaleTokens(
544
- fs.readFileSync(path.join(deckDir, "corporate-design.css"), "utf8"),
702
+ fs.readFileSync(resolveSkillForDeck(deckDir).cssFile, "utf8"),
545
703
  );
546
704
  } catch {
547
705
  typeTokens = null;
548
706
  }
707
+ // The tiers are the shared table's (draft-lines.mjs → draftLineTier). The
708
+ // measured rules below run on authored slides only, as in the add-in: a
709
+ // converted slide keeps its source deck's geometry. Fill and type (and the
710
+ // split advice) print on the deck-wide pass only — a stretched region is
711
+ // information about density, not a defect; body sizes are the agent's call.
549
712
  const measuredLines = (stem, render) =>
550
713
  [
551
- ...rules.fill([{ stem, render }]),
552
- ...rules.typeScale([{ stem, render }], typeTokens),
553
714
  ...rules.overlap([{ stem, render }]),
554
715
  // Text leaving its painted card — engines from 2026-09-06 on; older
555
716
  // ones simply have no rule here.
556
717
  ...(typeof rules.escape === "function" ? rules.escape([{ stem, render }]) : []),
718
+ ...(typeof rules.chrome === "function" ? rules.chrome([{ stem, render }]) : []),
719
+ // A word PowerPoint would break inside its frame — engines from
720
+ // 2026-09-27 on.
721
+ ...(typeof rules.wordFit === "function" ? rules.wordFit([{ stem, render }]) : []),
722
+ // One text set as separate boxes (a declared block split, a sentence
723
+ // run on across two boxes) — engines from 2026-10-08 on.
724
+ ...(typeof rules.splitText === "function" ? rules.splitText([{ stem, render }]) : []),
557
725
  ].map((l) => l.trim());
726
+ const advisoryLines = (stem, render) =>
727
+ [
728
+ ...rules.fill([{ stem, render }]),
729
+ ...rules.typeScale([{ stem, render }], typeTokens),
730
+ // Pieces set close on one row that only the agent can tell apart from
731
+ // two columns — engines from 2026-10-08 on (`splitText` carries the
732
+ // blocking ones: one declared block in several boxes, a sentence run on).
733
+ ...(typeof rules.splitTextAdvice === "function" ? rules.splitTextAdvice([{ stem, render }]) : []),
734
+ ].map((l) => l.trim());
735
+
736
+ // The image line — the slide's own count contract, written by the agent
737
+ // right after it looked at the concept: `// image: nodes=5 arrows=5 charts=0
738
+ // icons=5`. Arrows are compared against authored data-connector directives,
739
+ // charts against <Chart> elements, icons against <img> tags. FEWER in the
740
+ // JSX than the line says is a recorded defect; the omission this catches
741
+ // (sol@low shipped a loop with zero arrows and a scorecard with zero charts
742
+ // on x22, 2026-09-19) is invisible to every other check. No line, no check.
743
+ // The first count the agent wrote is the one that binds: x25 showed sol
744
+ // resolving `icons=8, JSX has 0` by editing the line down to icons=0. A
745
+ // lowered count is itself a defect; raising one (a miscount upward) is fine.
746
+ const imageLinesFile = statePath(deckDir, "image-lines.json");
747
+ let imageLines = {};
748
+ try {
749
+ imageLines = JSON.parse(fs.readFileSync(imageLinesFile, "utf8"));
750
+ } catch {}
751
+ // Counts come from the RENDER, not the source: a `.map()` over one <img>
752
+ // renders eleven icons but greps as one, and that false shortfall is what
753
+ // taught sol to lower its counts (x24). The capture boxes carry `imgSrc`,
754
+ // a non-native `connector` payload and a `chart` payload per element.
755
+ // When the add-in extracted a concept spec (concepts/concept-slide-NN.spec.json),
756
+ // ITS counts are the expectation and the agent's own line is ignored: the
757
+ // spec is machine-read from the mockup and cannot be edited down.
758
+ const specMod = await import(pathToFileURL(path.join(BUILDERS_DIR, "concept-spec.mjs")).href);
759
+ // What each slide still OWES, by element identity. This is what the old
760
+ // `ownership()` gate has been replaced by: the spec's counts used to switch
761
+ // themselves OFF as soon as a slide's bytes stopped matching
762
+ // state/transcribed.json, which is precisely when an edit is most likely to
763
+ // have dropped something (E09). A preservation record never lapses on a
764
+ // rewrite, so a hand-edited slide is now checked, not excused.
765
+ // Seed first, as the add-in's render does: a slide without a record gets one
766
+ // from its current evidence before its verdict is computed.
767
+ if (!outDir) seedRenderedSlides(deckDir, (result.slides ?? []).map((s) => s.fileName));
768
+ const preserve = await check.preservationDiagnostics(
769
+ deckDir,
770
+ (result.slides ?? []).map((s) => ({ fileName: s.fileName, render: s.render })),
771
+ { persist: !outDir },
772
+ );
773
+ // Counting elements is the weaker check of the two: where a record stands,
774
+ // every element is checked by its own id instead, and an intentional removal
775
+ // has a legal exit (a scoped amendment) that a count never had.
776
+ // What the reader flagged on the concept, and the template's own marks the
777
+ // mapper left out: the agent judges the pair with these in hand (the add-in's
778
+ // see_draft prints the same `[concept:flaw]` lines).
779
+ const conceptNotesFor = (fileName) => {
780
+ const m = /^(\d{1,3})[-_.]/.exec(fileName);
781
+ const notes = m ? currentSpec(deckDir, Number(m[1]))?.notes ?? [] : [];
782
+ return notes
783
+ .filter((n) => /^concept flaw —|^left out as the template's own marks/.test(n))
784
+ .map((n) => `[concept:flaw] ${n}`);
785
+ };
786
+ const specCountsFor = (fileName) => {
787
+ const m = /^(\d{1,3})[-_.]/.exec(fileName);
788
+ if (!m || preserve.covered.has(fileName)) return null;
789
+ return specMod.expectedCountsFromSpec(currentSpec(deckDir, Number(m[1])));
790
+ };
791
+ const imageLineDefects = (fileName, render) => {
792
+ let src = "";
793
+ try {
794
+ src = fs.readFileSync(path.join(deckDir, "slides", fileName), "utf8");
795
+ } catch {
796
+ return [];
797
+ }
798
+ const fromSpec = specCountsFor(fileName);
799
+ const m = /^\s*\/\/\s*image:\s*([^\n]*)$/m.exec(src);
800
+ if (!fromSpec && !m) return [];
801
+ const want = {};
802
+ const lowered = [];
803
+ if (fromSpec) {
804
+ Object.assign(want, fromSpec);
805
+ } else {
806
+ for (const [, k, v] of m[1].matchAll(/\b(arrows|charts|icons)\s*=\s*(\d+)/g)) want[k] = Number(v);
807
+ const first = imageLines[fileName] ?? {};
808
+ for (const k of ["arrows", "charts", "icons"]) {
809
+ if (first[k] != null && want[k] != null && want[k] < first[k]) {
810
+ lowered.push(`${k} ${first[k]}→${want[k]}`);
811
+ want[k] = first[k];
812
+ }
813
+ }
814
+ if (!imageLines[fileName]) {
815
+ imageLines[fileName] = { ...want };
816
+ fs.mkdirSync(path.dirname(imageLinesFile), { recursive: true });
817
+ fs.writeFileSync(imageLinesFile, JSON.stringify(imageLines, null, 2) + "\n");
818
+ }
819
+ }
820
+ return imageCountLines({ fileName, src, render, want, fromSpec, lowered });
821
+ };
822
+ // Every rendered line keeps the concept's shape, hand-edited slides included.
823
+ const lineShapeDefects = (fileName, render) => {
824
+ const m = /^(\d{1,3})[-_.]/.exec(fileName);
825
+ if (!m) return [];
826
+ let spec = null;
827
+ try { spec = currentSpec(deckDir, Number(m[1])); } catch { return []; }
828
+ // Declared repeated sets keep one size, one inner layout and their arrangement.
829
+ return [...lineShapeLines({ fileName, spec, src: capturedSources.get(fileName) ?? "", render }), ...repeatLines({ fileName, spec, render }), ...listLines({ fileName, spec, src: capturedSources.get(fileName) ?? "" })];
830
+ };
831
+
832
+ // A slide that references an asset file that is not on disk renders a
833
+ // broken image here and a 422 at build-pptx (x28: one icon the fetcher never
834
+ // delivered failed the whole deck AFTER review). Deterministic, so it is a
835
+ // blocking defect the review gate holds on.
836
+ let bundleAssetDir;
837
+ const bundleHasAsset = (file) => {
838
+ if (bundleAssetDir === undefined) {
839
+ try {
840
+ bundleAssetDir = path.join(resolveBundleDirForDeck(deckDir), "assets");
841
+ } catch {
842
+ bundleAssetDir = null;
843
+ }
844
+ }
845
+ return bundleAssetDir ? fs.existsSync(path.join(bundleAssetDir, file)) : false;
846
+ };
847
+
848
+ const missingAssetDefects = (fileName) => {
849
+ let src = "";
850
+ try {
851
+ src = fs.readFileSync(path.join(deckDir, "slides", fileName), "utf8");
852
+ } catch {
853
+ return [];
854
+ }
855
+ return missingAssetLines({ fileName, src, hasAsset: (file) => fs.existsSync(path.join(deckDir, "assets", file)), hasBundleAsset: bundleHasAsset });
856
+ };
558
857
 
559
858
  const written = [];
560
859
  const record = [];
@@ -565,59 +864,94 @@ export async function cmdViewDraft(rest) {
565
864
  const stem = slide.fileName.replace(/\.[^.]+$/, "");
566
865
  const file = path.join(previewDir, `${stem}.png`);
567
866
  fs.writeFileSync(file, png);
867
+ if (!outDir) {
868
+ const first = path.join(statePath(deckDir, "first-draft"), stem);
869
+ if (!fs.existsSync(path.join(first, "preview.png"))) {
870
+ fs.mkdirSync(first, { recursive: true });
871
+ fs.writeFileSync(path.join(first, "slide.jsx"), capturedSources.get(slide.fileName) ?? "");
872
+ fs.writeFileSync(path.join(first, "preview.png"), png);
873
+ }
874
+ }
568
875
  written.push(file);
569
876
  const flag = slide.status === "degraded" ? " (DEGRADED)" : "";
570
877
  out(` ${slide.fileName}${flag} → ${file}\n`);
571
878
  if (slide.note) process.stderr.write(` note: ${slide.note}\n`);
572
879
  const concept = conceptFor(slide.fileName);
573
- const defects =
880
+ // A Set: the chrome rule runs locally AND in the export gate over the
881
+ // same capture, so its verdict arrives twice — it is one defect.
882
+ const allLines =
574
883
  slide.status === "ok"
575
- ? [
576
- ...measuredLines(stem, slide.render),
577
- ...inventory.filter((l) => l.includes(` slides/${slide.fileName} `)),
884
+ ? [...new Set([
885
+ ...(slide.converted ? [] : measuredLines(stem, slide.render)),
886
+ ...imageLineDefects(slide.fileName, slide.render),
887
+ ...lineShapeDefects(slide.fileName, slide.render),
888
+ ...missingAssetDefects(slide.fileName),
578
889
  ...(slide.translateErrors ?? []),
579
- ]
890
+ // Approved image obligations: a source that is gone or changed,
891
+ // and an original the render places nowhere (or twice). Blocking
892
+ // — `build-pptx` refuses the same deck, so the draft is where it
893
+ // gets repaired, not after review.
894
+ ...(slide.imageIntentIssues ?? []),
895
+ // Preservation: named element obligations, reported here and
896
+ // enforced at delivery (build-pptx / push).
897
+ ...(preserve.byFile[slide.fileName] ?? []),
898
+ ])]
580
899
  : [];
581
- if (concept) {
582
- out(
583
- ` concept: ${concept} — compare against the render above: every organism present at the same relative position and share, every connector present, no region emptier than in the concept.\n`,
584
- );
585
- }
900
+ // Everything the shared table does not class as advisory is recorded and
901
+ // holds review and delivery: its defects, every gate refusal, and a
902
+ // refusal's plain-text continuation lines.
903
+ const defects = allLines.filter((l) => draftLineTier(l) !== "advisory");
904
+ const capturedSource = capturedSources.get(slide.fileName);
905
+ const capturedHash = crypto.createHash("sha256").update(capturedSource ?? "").digest("hex");
906
+ const currentSourceFile = path.join(deckDir, "slides", slide.fileName);
907
+ if (!fs.existsSync(currentSourceFile) || check.hashFile(currentSourceFile) !== capturedHash)
908
+ defects.push(`[draft:stale] ${slide.fileName} changed during capture. Render and inspect it again.`);
909
+ const notes = allLines.filter((l) => draftLineTier(l) === "advisory");
910
+ const flaws = concept ? conceptNotesFor(slide.fileName) : [];
911
+ if (concept) out(` concept: ${concept} — ${CONCEPT_GUIDANCE}\n`);
912
+ for (const line of flaws) out(` ${line}\n`);
586
913
  for (const line of defects) out(` ${line}\n`);
914
+ for (const line of notes) out(` ${line}\n`);
915
+ // Advisory lines only on the deck-wide pass: in a per-slide `--only`
916
+ // round they are noise next to the defects the agent is fixing (x23:
917
+ // sol spent rounds on 48 fill and 26 nested-paint lines that block
918
+ // nothing).
919
+ if (slide.status === "ok" && !slide.converted && !only) {
920
+ for (const line of advisoryLines(stem, slide.render)) {
921
+ out(` ${line} (advisory)\n`);
922
+ }
923
+ }
587
924
  measured += defects.length;
588
- slidesOut.push({ file: slide.fileName, status: slide.status, png: file, concept, defects, note: slide.note ?? null });
925
+ // `notes` carries the advisory lines — without it an agent on --json
926
+ // never sees them at all.
927
+ slidesOut.push({ file: slide.fileName, status: slide.status, png: file, concept, flaws, defects, notes, note: slide.note ?? null });
589
928
  const jsxFile = path.join(deckDir, "slides", slide.fileName);
590
929
  if (slide.status === "ok" && fs.existsSync(jsxFile)) {
591
- record.push({ fileName: slide.fileName, hash: check.hashFile(jsxFile), defects });
930
+ record.push({ fileName: slide.fileName, hash: capturedHash, defects });
592
931
  }
593
932
  } else {
594
933
  out(` ${slide.fileName} — ${slide.note ?? slide.status}\n`);
934
+ // A purpose-built layout has no freeform region: checked, nothing to
935
+ // measure. Delivery asks every shipped slide for a current check.
936
+ const jsxFile = path.join(deckDir, "slides", slide.fileName);
937
+ if (slide.status === "no-preview" && fs.existsSync(jsxFile)) {
938
+ const capturedHash = crypto.createHash("sha256").update(capturedSources.get(slide.fileName) ?? "").digest("hex");
939
+ if (check.hashFile(jsxFile) === capturedHash) record.push({ fileName: slide.fileName, hash: capturedHash, defects: [] });
940
+ }
595
941
  slidesOut.push({ file: slide.fileName, status: slide.status, png: null, concept: null, defects: [], note: slide.note ?? null });
596
942
  }
597
943
  }
598
- for (const line of deckLevel) out(` ${line}\n`);
599
- measured += deckLevel.length;
600
944
  const translateRan = translate && result.translateRan !== false;
601
945
  if (translateRan) {
602
946
  const failed = (result.slides ?? []).filter((s) => s.translateErrors?.length).length;
603
947
  out(
604
948
  failed
605
949
  ? ` TRANSLATE GATE: ${failed} slide${failed === 1 ? "" : "s"} refused by the export gate (lines above) — these are the errors build-pptx would stop on.\n`
606
- : " TRANSLATE GATE: every rendered slide passed the export gate (slot-fit, diagram geometry, connector endpoints).\n",
607
- );
608
- for (const w of result.translateWarnings ?? []) process.stderr.write(` note: ${w}\n`);
609
- }
610
- const verdict = check.verdictBlocker(deckDir);
611
- if (verdict) {
612
- out(
613
- ` self-check: ${verdict.replace(/\n\s+/g, "; ")}\n` +
614
- " (under each slide in asset-plan.md: `draft-check:` then `- [x] <organism>` per organism found in the render, `- [ ] <organism> — <what differs>` otherwise; `review` refuses until every organism is ticked [x])\n",
950
+ : " TRANSLATE GATE: every rendered slide passed the export gate (slot-fit, diagram geometry, text escape, chrome).\n",
615
951
  );
952
+ if (!only) for (const w of result.translateWarnings ?? []) process.stderr.write(` note: ${w}\n`);
616
953
  }
617
954
  if (record.length > 0 && !outDir) {
618
- // Deck-level asset lines are recorded on the first slide so they stand
619
- // until that slide (or the asset plan) changes.
620
- if (deckLevel.length > 0) record[0].defects.push(...deckLevel);
621
955
  // A partial run (--only) updates its slides and keeps the others' record.
622
956
  const prior = only ? check.loadDraftCheck(deckDir)?.slides ?? {} : {};
623
957
  const rendered = new Set(record.map((r) => r.fileName));
@@ -633,7 +967,7 @@ export async function cmdViewDraft(rest) {
633
967
  } else if (measured > 0) {
634
968
  out(
635
969
  `DRAFT CHECK: ${measured} measured defect${measured === 1 ? "" : "s"} (above) — fix them in the JSX and re-run ` +
636
- "`slidemind view-draft`; `slidemind review` does not open while they stand. " +
970
+ "`slidemind view-draft`; `review` and delivery refuse while they stand. " +
637
971
  `Previews in ${previewDir} — NOT pushed.\n`,
638
972
  );
639
973
  } else {
@@ -649,13 +983,15 @@ export async function cmdViewDraft(rest) {
649
983
  process.stdout.write(
650
984
  `${JSON.stringify(
651
985
  {
652
- deck: path.basename(deckDir),
986
+ deck: deckSlug(deckDir),
653
987
  previewDir,
988
+ // The pair rule the text output prints per slide: an agent on
989
+ // --json must read it too.
990
+ conceptGuidance: `Open each slide's concept and png together, in one look. The concept is ${CONCEPT_GUIDANCE}`,
654
991
  slides: slidesOut,
655
992
  measuredDefects: measured,
656
993
  translateGate: translateRan ? (slidesOut.some((s) => s.defects.some((d) => /:error\]/.test(d))) ? "failed" : "passed") : "skipped",
657
- selfCheck: verdict ? "incomplete" : "ok",
658
- reviewCanOpen: measured === 0 && !verdict,
994
+ reviewCanOpen: measured === 0,
659
995
  warnings,
660
996
  },
661
997
  null,