slidemind 0.3.16 → 0.3.17

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Connect Claude Code to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design, from the terminal.",
5
5
  "author": {
6
6
  "name": "statworx",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Connect Codex to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design. Requires the slidemind CLI (npm install -g slidemind); the skills explain the install when it is missing.",
5
5
  "author": {
6
6
  "name": "statworx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -652,7 +652,9 @@ From here:
652
652
  the gate will open). Two things come back, and both bind:
653
653
  - **Measured lines** under each slide — `[fill:warn]` (a stretched region whose organisms pool
654
654
  at its top, with the region and the dead band) and `[inventory:warn]` (connectors listed in
655
- `asset-plan.md` but not drawn, an empty inventory, a fetched asset no slide references) —
655
+ `asset-plan.md` but not drawn — counted per route kind too, so five `curved` rows drawn as
656
+ elbows warn even when the total matches; an empty inventory, a fetched asset no slide
657
+ references) —
656
658
  and a closing `DRAFT CHECK: N measured defects` line. Fix every one in the JSX and re-run;
657
659
  the review gate refuses to open while they stand for an unchanged slide. A fill band is
658
660
  fixed by distributing the organisms (`grid-template-rows` with one track per organism,
@@ -621,7 +621,10 @@ export async function viewDraft(
621
621
 
622
622
  let captured;
623
623
  try {
624
- captured = await captureDeckLocally({ deckDir, bundleDir, onProgress, only });
624
+ // Converted slides never reach the local engine — it would lint each one
625
+ // as an unregistered layout and the noise would ride the translate gate.
626
+ const capturable = all.filter((f) => f.endsWith(".jsx") && !conversions[f]);
627
+ captured = await captureDeckLocally({ deckDir, bundleDir, onProgress, only: capturable });
625
628
  } catch (err) {
626
629
  throw new Error(
627
630
  `local draft capture failed (${err instanceof Error ? err.message : String(err)}). ` +
@@ -38,6 +38,7 @@ import { loadChartPreview, loadConnectorPreview } from "../bridge/capture.mjs";
38
38
  import { FAVICON_LINK } from "./gate-server.mjs";
39
39
  import { lintSlidesOrThrow } from "./lint.mjs";
40
40
  import { resolveSkillForDeck } from "./resolve-skill.mjs";
41
+ import { loadConversions } from "../lib/conversions.mjs";
41
42
  import { slideOrderWarning, sortSlideFilenames } from "../lib/slide-order.mjs";
42
43
 
43
44
  const __filename = fileURLToPath(import.meta.url);
@@ -87,6 +88,29 @@ function inlineLayout(file) {
87
88
  );
88
89
  }
89
90
 
91
+ // Preview stand-ins for the ConvertedHost layouts converted slides invoke. The
92
+ // host's name carries its canvas (`ConvertedHost<cx>x<cy>`, EMU), so the stub
93
+ // sizes itself the way the minted host does (EMU / 9525, 3 decimals) instead of
94
+ // assuming 16:9 — a 4:3 source previews at 960x720, not stretched to 1280.
95
+ // Real geometry still comes from the server render.
96
+ const HOST_NAME_RE = /<(ConvertedHost(\d+)x(\d+))/;
97
+ const emuToCssPx = (emu) => `${Math.round((Number(emu) / 9525) * 1000) / 1000}px`;
98
+
99
+ function convertedHostStubs(slideFiles, conversions) {
100
+ const hosts = new Map();
101
+ for (const file of slideFiles) {
102
+ if (!conversions[path.basename(file)]) continue;
103
+ const m = HOST_NAME_RE.exec(fs.readFileSync(file, "utf8"));
104
+ if (m) hosts.set(m[1], { w: emuToCssPx(m[2]), h: emuToCssPx(m[3]) });
105
+ }
106
+ return [...hosts].map(
107
+ ([name, { w, h }]) =>
108
+ `<script type="text/jsx-template" data-kind="layout" data-name="${name}" data-binding="${name}">\n` +
109
+ `function ${name}({ background, children }) { return <div style={{ position: "relative", width: "${w}", height: "${h}", overflow: "hidden", background: background || "#FFFFFF" }}>{children}</div>; }\n` +
110
+ `</script>`,
111
+ );
112
+ }
113
+
90
114
  function layoutBindingNameForStem(stem) {
91
115
  return "Slide" + stem.split("-").map((p) => (
92
116
  p ? p[0].toUpperCase() + p.slice(1) : ""
@@ -354,7 +378,15 @@ export async function buildDeck(deckDir, opts = {}) {
354
378
  fs.readFileSync(file, "utf8"),
355
379
  ]),
356
380
  );
357
- await lintSlidesOrThrow({ deckDir, slideFiles, layoutSources });
381
+ // Converted slides (`slidemind adopt`) invoke a synthetic ConvertedHost
382
+ // layout the server mints per source canvas; no bundle carries it, so the
383
+ // local engine would fail every one of them as an unregistered layout. They
384
+ // are linted server-side on render — skip them here, lint the authored rest.
385
+ const conversions = loadConversions(deckDir, { includeSource: false });
386
+ const authoredFiles = slideFiles.filter((f) => !conversions[path.basename(f)]);
387
+ if (authoredFiles.length > 0) {
388
+ await lintSlidesOrThrow({ deckDir, slideFiles: authoredFiles, layoutSources });
389
+ }
358
390
 
359
391
  // The <Chart> stand-in runtime rides in the same bundle cache (see
360
392
  // loadChartPreview) — resolved after lint, which already proved the bundle
@@ -364,7 +396,10 @@ export async function buildDeck(deckDir, opts = {}) {
364
396
 
365
397
  copyDesignSystem(deckDir, skill);
366
398
 
367
- const layoutsBlock = layoutFiles.map(inlineLayout).join("\n");
399
+ const layoutsBlock = layoutFiles
400
+ .map(inlineLayout)
401
+ .concat(convertedHostStubs(slideFiles, conversions))
402
+ .join("\n");
368
403
  const slidesBlock = slideFiles
369
404
  .map((file, i) => inlineSlide(file, i))
370
405
  .join("\n");
@@ -30,6 +30,20 @@ const NONE_RE = /^\(?\s*(none|n\/a|no\b)/i;
30
30
 
31
31
  const pad2 = (n) => String(n).padStart(2, "0");
32
32
 
33
+ // Route kind per connector row / directive — part of the mockup contract: a
34
+ // loop the concept draws as curves that lands as elbows is a different figure,
35
+ // and the count alone (5 = 5) never fires. Port of inventory.ts (gotcha 84).
36
+ const ROUTE_KINDS = ["straight", "elbow", "curved"];
37
+ const emptyRoutes = () => ({ straight: 0, elbow: 0, curved: 0 });
38
+ function routeKindOf(text) {
39
+ for (const token of text.split(",")) {
40
+ const t = token.trim().toLowerCase();
41
+ if (ROUTE_KINDS.includes(t)) return t;
42
+ }
43
+ return null;
44
+ }
45
+ const describeRoutes = (r) => ROUTE_KINDS.map((k) => `${r[k]} ${k}`).join(", ");
46
+
33
47
  /** Seed the inventory form once (never overwrites a filled one). `images`: [{slideNumber,label}]. */
34
48
  export function seedAssetPlan(deckDir, images) {
35
49
  const file = path.join(deckDir, ASSET_PLAN_FILE);
@@ -91,7 +105,7 @@ function parseAssetPlan(text) {
91
105
  const line = raw.trim();
92
106
  const heading = SLIDE_HEADING_RE.exec(line);
93
107
  if (heading) {
94
- current = { filled: false, connectors: 0, organisms: 0, icons: 0, iconsResolved: 0, verdict: null, ticked: 0, unticked: 0 };
108
+ current = { filled: false, connectors: 0, routes: emptyRoutes(), organisms: 0, icons: 0, iconsResolved: 0, verdict: null, ticked: 0, unticked: 0 };
95
109
  out.set(Number(heading[1]), current);
96
110
  section = "";
97
111
  continue;
@@ -119,7 +133,12 @@ function parseAssetPlan(text) {
119
133
  if (inline && !NONE_RE.test(inline)) {
120
134
  if (section === "organisms" || section === "regions") current.filled = true;
121
135
  if (section === "connectors") {
122
- current.connectors += inline.split(";").filter((x) => x.trim()).length;
136
+ for (const row of inline.split(";")) {
137
+ if (!row.trim()) continue;
138
+ current.connectors += 1;
139
+ const kind = routeKindOf(row);
140
+ if (kind) current.routes[kind] += 1;
141
+ }
123
142
  }
124
143
  }
125
144
  continue;
@@ -129,7 +148,11 @@ function parseAssetPlan(text) {
129
148
  if (!body || NONE_RE.test(body)) continue;
130
149
  if (section === "organisms" || section === "regions") current.filled = true;
131
150
  if (section === "organisms") current.organisms += 1;
132
- if (section === "connectors") current.connectors += 1;
151
+ if (section === "connectors") {
152
+ current.connectors += 1;
153
+ const kind = routeKindOf(body);
154
+ if (kind) current.routes[kind] += 1;
155
+ }
133
156
  if (section === "icons") {
134
157
  current.icons += 1;
135
158
  // Resolved = a fetched file appended, or an explicit "(no result)".
@@ -167,6 +190,19 @@ function countConnectors(jsx) {
167
190
  return attached + markers;
168
191
  }
169
192
 
193
+ // Route kind per `data-connector` directive; a missing data-route is straight.
194
+ // Imported native connectors carry their own preset and are not authored
195
+ // against the mockup, so they are left out of the kind tally.
196
+ function countRoutes(jsx) {
197
+ const routes = emptyRoutes();
198
+ for (const tag of jsx.match(/<[^<>]*\bdata-connector\b[^<>]*>/g) ?? []) {
199
+ if (/\bdata-native-connector\b/.test(tag)) continue;
200
+ const kind = /\bdata-route\s*=\s*["']([a-z]+)["']/.exec(tag)?.[1] ?? "straight";
201
+ if (ROUTE_KINDS.includes(kind)) routes[kind] += 1;
202
+ }
203
+ return routes;
204
+ }
205
+
170
206
  /**
171
207
  * `[inventory:warn]` lines for the deck: per authored slide (inventory empty,
172
208
  * connectors listed > drawn) plus fetched assets no slide references. Empty
@@ -212,6 +248,14 @@ export function inventoryWarnings(deckDir, jsxFiles = null) {
212
248
  `[inventory:warn] ${jsxPath} — asset-plan.md lists ${inv.connectors} connector${inv.connectors === 1 ? "" : "s"} for this slide, the JSX draws ${drawn} (data-connector / marker-end). Draw the missing ones per workflow/authoring.md → Arrow contract; a mockup connector is never dropped.`,
213
249
  );
214
250
  }
251
+ const drawnRoutes = countRoutes(jsx);
252
+ for (const kind of ROUTE_KINDS) {
253
+ if (inv.routes[kind] > drawnRoutes[kind]) {
254
+ lines.push(
255
+ `[inventory:warn] ${jsxPath} — asset-plan.md lists ${inv.routes[kind]} ${kind} connector${inv.routes[kind] === 1 ? "" : "s"} for this slide, the JSX draws ${drawnRoutes[kind]} (${describeRoutes(drawnRoutes)}). The mockup's route shape is the contract: keep data-route="${kind}" and fix the endpoint sides or node positions instead of changing the route kind.`,
256
+ );
257
+ }
258
+ }
215
259
  }
216
260
  // Fetched assets nobody references: the mockup staged them, the deck lost them.
217
261
  const referenced = allJsx
@@ -166,8 +166,34 @@ export async function collectLiveReconcileInputs({
166
166
  const before = Object.keys(exported).length;
167
167
  const ack = await exportRound(exportSlides, pending);
168
168
  deck ??= ack?.deck ?? null;
169
+ // PowerPoint web (package fallback on) may hand back the SAVED file's bytes
170
+ // (`provenance.source` graph/saved) when the host export fails. Saved bytes
171
+ // cannot show unsaved manual edits, so projecting them would read "no
172
+ // drift" for a slide the user is editing right now — treat them as not
173
+ // exported, same failure mode as a slide that never arrived.
174
+ const stale = [];
169
175
  for (const [guid, slide] of Object.entries(ack?.slides ?? {})) {
170
- if (slide?.base64 && slide?.tags) exported[guid] = slide;
176
+ if (!slide?.base64 || !slide?.tags) continue;
177
+ if (slide.provenance && slide.provenance.source !== "host") stale.push(guid);
178
+ else exported[guid] = slide;
179
+ }
180
+ // `failed` names slides neither the host nor the stored file could supply
181
+ // (web fallback); they never come back, so they fail closed at once.
182
+ const failed = Object.keys(ack?.failed ?? {}).filter(
183
+ (guid) => !exported[guid] && requested.includes(guid),
184
+ );
185
+ const unverifiable = [...new Set([...failed, ...stale])];
186
+ if (unverifiable.length > 0) {
187
+ const err = new Error(
188
+ `the pane could not export ${unverifiable.length} live slide(s) from the ` +
189
+ `open presentation (${failed.length} unavailable, ${stale.length} only as ` +
190
+ `the saved file), so their manual edits cannot be checked. Save the ` +
191
+ `presentation and retry, or re-run with --force to push anyway and ` +
192
+ `discard any manual edits.`,
193
+ );
194
+ err.code = "export_incomplete";
195
+ err.failed = ack?.failed ?? {};
196
+ throw err;
171
197
  }
172
198
  const next = (Array.isArray(ack?.truncated) ? ack.truncated : []).filter(
173
199
  (guid) => !exported[guid] && requested.includes(guid),