slidemind 0.3.19 → 0.4.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.
Files changed (186) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/engine-slides.mjs +1 -1
  16. package/src/bridge/client/render.mjs +27 -1
  17. package/src/bridge/client/transport.mjs +499 -23
  18. package/src/bridge/client.mjs +10 -0
  19. package/src/bridge/config.mjs +67 -6
  20. package/src/bridge/engine-download.mjs +0 -10
  21. package/src/bridge/graph-auth.mjs +55 -3
  22. package/src/bridge/image-intent-envelope.mjs +239 -0
  23. package/src/bridge/package-preview.mjs +67 -0
  24. package/src/bridge/sharepoint.mjs +914 -0
  25. package/src/builders/asset-fetch.mjs +203 -0
  26. package/src/builders/block-dividers.mjs +83 -0
  27. package/src/builders/build.mjs +171 -87
  28. package/src/builders/card-height.mjs +294 -0
  29. package/src/builders/chart-hole.mjs +183 -0
  30. package/src/builders/chart-jsx.mjs +410 -0
  31. package/src/builders/chart-labels.mjs +326 -0
  32. package/src/builders/concept-geometry.mjs +370 -0
  33. package/src/builders/concept-intent.mjs +200 -0
  34. package/src/builders/concept-layout.mjs +79 -0
  35. package/src/builders/concept-lines.mjs +509 -0
  36. package/src/builders/concept-lists.mjs +270 -0
  37. package/src/builders/concept-marks.mjs +84 -0
  38. package/src/builders/concept-relationships.mjs +12 -0
  39. package/src/builders/concept-spec.mjs +645 -0
  40. package/src/builders/concept-text.mjs +140 -0
  41. package/src/builders/concept-waves.mjs +57 -0
  42. package/src/builders/copy-fit.mjs +139 -0
  43. package/src/builders/design-tokens.mjs +347 -0
  44. package/src/builders/draft-check.mjs +552 -340
  45. package/src/builders/draft-lines.mjs +369 -0
  46. package/src/builders/edit-route.mjs +29 -0
  47. package/src/builders/figure-sketch.mjs +375 -0
  48. package/src/builders/fold-sets.mjs +639 -0
  49. package/src/builders/gate-server.mjs +9 -4
  50. package/src/builders/gate-session.js +273 -12
  51. package/src/builders/gate-shell.js +1494 -0
  52. package/src/builders/image-intent-check.mjs +308 -0
  53. package/src/builders/image-use-reconcile.mjs +107 -0
  54. package/src/builders/kit-helpers.mjs +121 -0
  55. package/src/builders/layout-names.mjs +32 -0
  56. package/src/builders/legacy-reference-uses.mjs +179 -0
  57. package/src/builders/line-fit.mjs +224 -0
  58. package/src/builders/line-native.mjs +895 -0
  59. package/src/builders/line-relations.mjs +230 -0
  60. package/src/builders/lint-fix.mjs +117 -9
  61. package/src/builders/lint.mjs +25 -20
  62. package/src/builders/list-jsx.mjs +88 -0
  63. package/src/builders/placement.mjs +133 -0
  64. package/src/builders/plan-checks.mjs +135 -0
  65. package/src/builders/plan-comments.mjs +19 -0
  66. package/src/builders/plan-handlers.mjs +95 -0
  67. package/src/builders/plan-markdown-edit.mjs +3 -3
  68. package/src/builders/plan-overlay.js +1530 -1608
  69. package/src/builders/plan.mjs +689 -186
  70. package/src/builders/preset-definitions.json +1 -0
  71. package/src/builders/preset-eval.mjs +244 -0
  72. package/src/builders/preview.html +51 -120
  73. package/src/builders/red-thread-backbone.mjs +135 -36
  74. package/src/builders/repair.mjs +205 -0
  75. package/src/builders/repeat-groups.mjs +540 -0
  76. package/src/builders/resolve-skill.mjs +5 -4
  77. package/src/builders/review-overlay.js +296 -293
  78. package/src/builders/review.mjs +305 -84
  79. package/src/builders/rule-shift.mjs +96 -0
  80. package/src/builders/seat.mjs +354 -0
  81. package/src/builders/settle-driver.mjs +236 -0
  82. package/src/builders/settle.mjs +1448 -0
  83. package/src/builders/slide-prep.mjs +67 -0
  84. package/src/builders/spec-assets.mjs +236 -0
  85. package/src/builders/spec-match.mjs +56 -0
  86. package/src/builders/stack.mjs +413 -0
  87. package/src/builders/storyboard-markdown.mjs +70 -27
  88. package/src/builders/storyboard-schema.mjs +54 -114
  89. package/src/builders/text-blocks.mjs +212 -0
  90. package/src/builders/text-card-fit.mjs +552 -0
  91. package/src/builders/text-stack.mjs +276 -0
  92. package/src/builders/transcribe.mjs +1069 -0
  93. package/src/cli/assets.mjs +164 -0
  94. package/src/cli/common.mjs +173 -12
  95. package/src/cli/concept-spec-support.mjs +76 -0
  96. package/src/cli/concepts.mjs +1002 -103
  97. package/src/cli/deck.mjs +401 -65
  98. package/src/cli/diagnose.mjs +4 -2
  99. package/src/cli/engine-helpers.mjs +17 -10
  100. package/src/cli/engine-results.mjs +76 -3
  101. package/src/cli/help.mjs +232 -28
  102. package/src/cli/install.mjs +9 -0
  103. package/src/cli/maintenance.mjs +104 -0
  104. package/src/cli/read.mjs +2 -1
  105. package/src/cli/ready.mjs +6 -3
  106. package/src/cli/record.mjs +522 -0
  107. package/src/cli/remote.mjs +818 -0
  108. package/src/cli/revision.mjs +686 -0
  109. package/src/cli/search.mjs +30 -11
  110. package/src/cli/session.mjs +56 -13
  111. package/src/cli/setup.mjs +46 -2
  112. package/src/cli/status.mjs +142 -33
  113. package/src/cli/tasks.mjs +246 -0
  114. package/src/cli/transcribe.mjs +854 -0
  115. package/src/cli/workspace-ui.mjs +260 -0
  116. package/src/cli/workspace.mjs +212 -34
  117. package/src/install-channel.mjs +64 -7
  118. package/src/lib/adopt.mjs +346 -25
  119. package/src/lib/concept-spec-binding.mjs +96 -0
  120. package/src/lib/conversions.mjs +70 -6
  121. package/src/lib/convert-folded.mjs +107 -0
  122. package/src/lib/deck-paths.mjs +418 -0
  123. package/src/lib/durable-ids.mjs +47 -0
  124. package/src/lib/edit-operations.mjs +316 -0
  125. package/src/lib/edit-state.mjs +1244 -0
  126. package/src/lib/edit-transitions.mjs +120 -0
  127. package/src/lib/extensions.mjs +501 -0
  128. package/src/lib/image-assets.mjs +465 -0
  129. package/src/lib/image-derivatives.mjs +234 -0
  130. package/src/lib/image-intent-fingerprint.mjs +70 -0
  131. package/src/lib/image-intent-tags.mjs +176 -0
  132. package/src/lib/image-intent.mjs +802 -0
  133. package/src/lib/image-use-overrides.mjs +279 -0
  134. package/src/lib/inline-components.mjs +613 -0
  135. package/src/lib/lint-converted.mjs +68 -4
  136. package/src/lib/merge-json.mjs +163 -0
  137. package/src/lib/overwrite-guard.mjs +3 -3
  138. package/src/lib/presentation-context.mjs +531 -0
  139. package/src/lib/preservation-tags.mjs +157 -0
  140. package/src/lib/preservation-verdict.mjs +1342 -0
  141. package/src/lib/publish.mjs +1557 -0
  142. package/src/lib/rebase.mjs +945 -0
  143. package/src/lib/reconcile-ack.mjs +4 -6
  144. package/src/lib/reconcile.mjs +62 -24
  145. package/src/lib/record-client.mjs +175 -0
  146. package/src/lib/record-state.mjs +151 -0
  147. package/src/lib/remote.mjs +661 -0
  148. package/src/lib/revision.mjs +1210 -0
  149. package/src/lib/slide-guids.mjs +4 -6
  150. package/src/lib/slide-preservation.mjs +423 -0
  151. package/src/lib/slide-selection.mjs +9 -0
  152. package/src/lib/workspace-transaction.mjs +117 -0
  153. package/src/pptx/content-digest.mjs +334 -0
  154. package/src/pptx/deck-assemble.mjs +1278 -0
  155. package/src/pptx/fill/engine.mjs +2 -3
  156. package/src/pptx/fill/freeform.mjs +6 -3
  157. package/src/pptx/ooxml.mjs +20 -0
  158. package/src/pptx/package-evidence.mjs +383 -0
  159. package/src/pptx/tags.mjs +163 -11
  160. package/src/update-check.mjs +22 -4
  161. package/src/workspace/adapters.mjs +222 -0
  162. package/src/workspace/auth.mjs +256 -0
  163. package/src/workspace/autostart.mjs +204 -0
  164. package/src/workspace/daemon.mjs +360 -0
  165. package/src/workspace/discovery.mjs +180 -0
  166. package/src/workspace/edit.mjs +167 -0
  167. package/src/workspace/events.mjs +52 -0
  168. package/src/workspace/gate-client.mjs +290 -0
  169. package/src/workspace/handback.mjs +73 -0
  170. package/src/workspace/lifecycle.mjs +635 -0
  171. package/src/workspace/operations.mjs +200 -0
  172. package/src/workspace/pick-folder.mjs +152 -0
  173. package/src/workspace/registry.mjs +359 -0
  174. package/src/workspace/remote-links.mjs +397 -0
  175. package/src/workspace/remote-status.mjs +359 -0
  176. package/src/workspace/routes.mjs +1331 -0
  177. package/src/workspace/server.mjs +159 -0
  178. package/src/workspace/sync.mjs +1119 -0
  179. package/src/workspace/task-actions.mjs +215 -0
  180. package/src/workspace/tasks.mjs +507 -0
  181. package/src/workspace/ui/app.js +1886 -0
  182. package/src/workspace/ui/index.html +141 -0
  183. package/src/workspace/ui/plan-bridge.js +120 -0
  184. package/src/workspace/ui/sharepoint.svg +59 -0
  185. package/src/workspace/ui/styles.css +423 -0
  186. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,230 @@
1
+ // How a line relates to the painted shapes it meets — one relation, read by
2
+ // the transcriber that orders a free line among the shapes (placement.mjs) and
3
+ // by the draft check that judges the render (the add-in's
4
+ // connector-occlusion.ts). Two readings of it disagreed: the transcriber took
5
+ // as ground only a panel holding ALL of the line and put it under everything
6
+ // else it crossed, the check takes the panel under most of it — a divider
7
+ // 13px past its card's edge was written under that card and the check named
8
+ // it hidden (x114 QBR 6).
9
+ //
10
+ // - Ground: the painted shape under most of a free line's length. The line
11
+ // lies on it and paints after it (a divider on its card, an axis on its
12
+ // panel). A line glued at both ends relates its nodes; only a shape holding
13
+ // all of it is its ground.
14
+ // - Station: an opaque node the line threads near its centre (a timeline's
15
+ // stop, the disc on a divider). It sits on the line and paints after it.
16
+ // A bead (≤16px) reads as a dot on its line whichever paints first.
17
+ //
18
+ // A shape is `{ rect, outline, content, alpha, lines }`: `outline` is
19
+ // `{ ellipse, poly }` when anything paints it (null otherwise), `content` a
20
+ // picture, icon or chart (its pixels are unknown), `alpha` how opaque its
21
+ // paint is, `lines` its text lines. Points and rects are px in one frame.
22
+ //
23
+ // Shared by both clients; mirrored by scripts/sync-transcription-runtime.mjs —
24
+ // no imports here.
25
+
26
+ export const STATION_MAX_PX = 16;
27
+ export const STATION_CENTRE_FRAC = 0.75;
28
+ export const NODE_CENTRE_FRAC = 0.4;
29
+ export const OPAQUE = 0.99;
30
+
31
+ // ---- Points and segments against rects, ellipses and clip polygons ----------
32
+
33
+ export function pointInsideRect(point, rect) {
34
+ return point.x >= rect.x && point.x <= rect.x + rect.w && point.y >= rect.y && point.y <= rect.y + rect.h;
35
+ }
36
+
37
+ export function pointInsideShape(point, rect, ellipse, grow) {
38
+ const r = { x: rect.x - grow, y: rect.y - grow, w: rect.w + grow * 2, h: rect.h + grow * 2 };
39
+ if (r.w <= 0 || r.h <= 0) return false;
40
+ if (!ellipse) return pointInsideRect(point, r);
41
+ const nx = (point.x - (r.x + r.w / 2)) / (r.w / 2);
42
+ const ny = (point.y - (r.y + r.h / 2)) / (r.h / 2);
43
+ return nx * nx + ny * ny <= 1;
44
+ }
45
+
46
+ /** The point sits ON the shape's outline (within `band` px either side), not deep inside it. */
47
+ export function pointOnOutline(point, rect, ellipse, band) {
48
+ return pointInsideShape(point, rect, ellipse, band) && !pointInsideShape(point, rect, ellipse, -band);
49
+ }
50
+
51
+ export function rectContainsRect(outer, inner, eps = 0.5) {
52
+ return inner.x >= outer.x - eps && inner.y >= outer.y - eps && inner.x + inner.w <= outer.x + outer.w + eps && inner.y + inner.h <= outer.y + outer.h + eps;
53
+ }
54
+
55
+ export function insidePolygon(p, poly) {
56
+ let inside = false;
57
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
58
+ const a = poly[i];
59
+ const b = poly[j];
60
+ if (a.y > p.y !== b.y > p.y && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;
61
+ }
62
+ return inside;
63
+ }
64
+
65
+ function orientation(a, b, c) {
66
+ const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
67
+ if (Math.abs(value) < 0.0001) return 0;
68
+ return value > 0 ? 1 : 2;
69
+ }
70
+
71
+ function onSegment(a, b, c) {
72
+ return b.x <= Math.max(a.x, c.x) + 0.0001 && b.x + 0.0001 >= Math.min(a.x, c.x) && b.y <= Math.max(a.y, c.y) + 0.0001 && b.y + 0.0001 >= Math.min(a.y, c.y);
73
+ }
74
+
75
+ function segmentsIntersect(a, b, c, d) {
76
+ const o1 = orientation(a, b, c);
77
+ const o2 = orientation(a, b, d);
78
+ const o3 = orientation(c, d, a);
79
+ const o4 = orientation(c, d, b);
80
+ if (o1 !== o2 && o3 !== o4) return true;
81
+ if (o1 === 0 && onSegment(a, c, b)) return true;
82
+ if (o2 === 0 && onSegment(a, d, b)) return true;
83
+ if (o3 === 0 && onSegment(c, a, d)) return true;
84
+ if (o4 === 0 && onSegment(c, b, d)) return true;
85
+ return false;
86
+ }
87
+
88
+ export function segmentHitsRect(a, b, rect) {
89
+ if (pointInsideRect(a, rect) || pointInsideRect(b, rect)) return true;
90
+ const tl = { x: rect.x, y: rect.y };
91
+ const tr = { x: rect.x + rect.w, y: rect.y };
92
+ const br = { x: rect.x + rect.w, y: rect.y + rect.h };
93
+ const bl = { x: rect.x, y: rect.y + rect.h };
94
+ return segmentsIntersect(a, b, tl, tr) || segmentsIntersect(a, b, tr, br) || segmentsIntersect(a, b, br, bl) || segmentsIntersect(a, b, bl, tl);
95
+ }
96
+
97
+ export function segmentHitsEllipse(a, b, rect) {
98
+ const rx = rect.w / 2;
99
+ const ry = rect.h / 2;
100
+ const cx = rect.x + rx;
101
+ const cy = rect.y + ry;
102
+ const ax = (a.x - cx) / rx;
103
+ const ay = (a.y - cy) / ry;
104
+ const dx = (b.x - cx) / rx - ax;
105
+ const dy = (b.y - cy) / ry - ay;
106
+ const len2 = dx * dx + dy * dy;
107
+ const t = len2 ? Math.max(0, Math.min(1, -(ax * dx + ay * dy) / len2)) : 0;
108
+ const px = ax + t * dx;
109
+ const py = ay + t * dy;
110
+ return px * px + py * py < 1;
111
+ }
112
+
113
+ export function segmentHitsPolygon(a, b, poly) {
114
+ if (insidePolygon(a, poly) || insidePolygon(b, poly)) return true;
115
+ return poly.some((p, i) => segmentsIntersect(a, b, p, poly[(i + 1) % poly.length]));
116
+ }
117
+
118
+ // ---- A route against one painted shape --------------------------------------
119
+
120
+ /** Points along the route, every few px, each weighted by the length it stands for. */
121
+ export function routeSamples(points) {
122
+ const out = [];
123
+ for (let i = 1; i < points.length; i++) {
124
+ const a = points[i - 1];
125
+ const b = points[i];
126
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
127
+ const n = Math.max(1, Math.ceil(len / 4));
128
+ for (let k = 0; k < n; k++) {
129
+ const t = (k + 0.5) / n;
130
+ out.push({ p: { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }, w: len / n });
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+
136
+ export function insideOutline(p, shape) {
137
+ if (!shape.outline) return false;
138
+ return shape.outline.poly ? insidePolygon(p, shape.outline.poly) : pointInsideShape(p, shape.rect, shape.outline.ellipse, 0);
139
+ }
140
+
141
+ /** The share of a route's length that lies inside a shape's painted outline. */
142
+ export function routeShare(points, shape, samples = routeSamples(points)) {
143
+ const total = samples.reduce((sum, s) => sum + s.w, 0);
144
+ if (!total) return 0;
145
+ return samples.reduce((sum, s) => sum + (insideOutline(s.p, shape) ? s.w : 0), 0) / total;
146
+ }
147
+
148
+ /**
149
+ * The line's ground: the painted shape under most of a free line, or — a line
150
+ * glued at both ends (`related`) — one holding all of it.
151
+ */
152
+ export function isGround(shape, points, related = false, samples = undefined) {
153
+ if (!shape.outline) return false;
154
+ if (related) return points.every((p) => pointInsideRect(p, { x: shape.rect.x - 0.5, y: shape.rect.y - 0.5, w: shape.rect.w + 1, h: shape.rect.h + 1 }));
155
+ return routeShare(points, shape, samples) > 0.5;
156
+ }
157
+
158
+ /** The route's nearest approach to `point`: the distance, and the route's direction there. */
159
+ export function nearestOnRoute(point, points) {
160
+ let best = { d: Number.POSITIVE_INFINITY, ux: 1, uy: 0 };
161
+ for (let i = 0; i < points.length - 1; i++) {
162
+ const a = points[i];
163
+ const b = points[i + 1];
164
+ const dx = b.x - a.x;
165
+ const dy = b.y - a.y;
166
+ const len2 = dx * dx + dy * dy;
167
+ const t = len2 ? Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / len2)) : 0;
168
+ const d = Math.hypot(a.x + t * dx - point.x, a.y + t * dy - point.y);
169
+ if (d < best.d) best = len2 ? { d, ux: dx / Math.sqrt(len2), uy: dy / Math.sqrt(len2) } : { ...best, d };
170
+ }
171
+ return best;
172
+ }
173
+
174
+ export const isBead = (shape) => Math.max(shape.rect.w, shape.rect.h) <= STATION_MAX_PX;
175
+
176
+ /** The route enters the shape's painted outline. */
177
+ function meets(points, shape) {
178
+ const { rect, outline } = shape;
179
+ for (let i = 0; i < points.length - 1; i++) {
180
+ if (outline.poly) {
181
+ if (segmentHitsPolygon(points[i], points[i + 1], outline.poly)) return true;
182
+ } else if (rect.w > 0 && rect.h > 0) {
183
+ if (outline.ellipse ? segmentHitsEllipse(points[i], points[i + 1], rect) : segmentHitsRect(points[i], points[i + 1], rect)) return true;
184
+ }
185
+ }
186
+ return false;
187
+ }
188
+
189
+ /**
190
+ * A station: a bead, or an opaque node — only an opaque node covers the line
191
+ * and whatever it holds; through a wash the line still strikes its text. It is
192
+ * threaded near its centre: within NODE_CENTRE_FRAC of its half-extent across
193
+ * the route (a bead: STATION_CENTRE_FRAC).
194
+ */
195
+ export function isStation(shape, points) {
196
+ const { rect } = shape;
197
+ if (!shape.outline || shape.content || (isBead(shape) && shape.lines.length)) return false;
198
+ if (!isBead(shape) && shape.alpha < OPAQUE) return false;
199
+ const { d, ux, uy } = nearestOnRoute({ x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }, points);
200
+ // The node's half-extent across the route: a disc's radius, half a bar's
201
+ // length where a line crosses it.
202
+ const across = shape.outline.ellipse ? Math.hypot((uy * rect.w) / 2, (ux * rect.h) / 2) : (Math.abs(uy) * rect.w) / 2 + (Math.abs(ux) * rect.h) / 2;
203
+ if (d > across * (isBead(shape) ? STATION_CENTRE_FRAC : NODE_CENTRE_FRAC)) return false;
204
+ return meets(points, shape);
205
+ }
206
+
207
+ /**
208
+ * The smallest painted shape whose outline a free end touches (a ring arc
209
+ * meeting its disc, a rule ending at a card's edge): the line is attached to
210
+ * it in all but name. An end deep inside a panel touches nothing.
211
+ */
212
+ export function touchedShape(point, shapes, band) {
213
+ let node = null;
214
+ for (const o of shapes) {
215
+ if (!o.outline || !pointOnOutline(point, o.rect, o.outline.ellipse, band)) continue;
216
+ if (!node || o.rect.w * o.rect.h < node.rect.w * node.rect.h) node = o;
217
+ }
218
+ return node;
219
+ }
220
+
221
+ /**
222
+ * The stations a line passes through: stations that hold none of its ends
223
+ * (`ends`: rects — a zero rect at a free point, a node's rect) and are not its
224
+ * ground.
225
+ */
226
+ export function stationsOf(points, shapes, ends, related = false) {
227
+ if (related) return [];
228
+ const samples = routeSamples(points);
229
+ return shapes.filter((o) => isStation(o, points) && !ends.some((e) => rectContainsRect(o.rect, e)) && !isGround(o, points, false, samples));
230
+ }
@@ -16,9 +16,22 @@
16
16
  // Everything else the lint engine flags (raw colors, token choices, spacing
17
17
  // scales) is a judgment call and deliberately NOT auto-fixed.
18
18
 
19
+ import crypto from "node:crypto";
19
20
  import fs from "node:fs";
20
21
  import path from "node:path";
21
22
 
23
+ import { CONVERSIONS_DIR, conversionProvenance } from "../lib/conversions.mjs";
24
+ import { statePath } from "../lib/deck-paths.mjs";
25
+ import { selectSlideFiles } from "../lib/slide-selection.mjs";
26
+
27
+ // "This source says it is converted", as two markers only the converter writes:
28
+ // the dialect pragma `slidemind adopt` stamps at the top of the file, and the
29
+ // dialect's own attributes/host in the body. Identical to the pair
30
+ // `validateSettleTarget` refuses on (src/builders/settle.mjs DIALECT_RE /
31
+ // NATIVE_RE) on purpose — one definition of the dialect across the CLI.
32
+ const DIALECT_RE = /^\s*\/\/\s*SLIDEMIND_DIALECT:\s*converted\b/m;
33
+ const NATIVE_RE = /\bdata-sm-(?:native-object|source-id)\b|<ConvertedHost/;
34
+
22
35
  // Declaration blocks inside a <style> string or a style attribute: convert
23
36
  // `letter-spacing: Nem` when the SAME block carries `font-size: Mpx`.
24
37
  const DECL_BLOCK = /\{[^{}]*\}/g;
@@ -61,28 +74,123 @@ export function fixSlideSource(src) {
61
74
  }
62
75
 
63
76
  /**
64
- * Fix every slides/*.jsx in a deck, writing only changed files.
65
- * Returns { files: <changed file count>, fixes: <total fix count> }.
77
+ * Slides this whole-file rewrite may NOT touch, by file name → why. Preview is
78
+ * a look-at-it step: a slide whose source someone has taken over keeps its
79
+ * exact bytes, or an edit safeguard anywhere else is undone by the next
80
+ * `view-draft`. Three owners are knowable from what the deck already records:
81
+ *
82
+ * - CONVERTED (`slidemind adopt`, conversions/<stem>.json): its raw values
83
+ * ARE the original .pptx's. Rewriting one is a silent edit of an imported
84
+ * slide the user never asked for.
85
+ * - UNREADABLE record: the file is there and will not parse, so whether the
86
+ * slide is converted is UNANSWERED. That is the strongest reason to keep
87
+ * hands off it, not the weakest — which is why this asks
88
+ * `conversionProvenance` per slide instead of `loadConversions`: a map
89
+ * cannot say "something was here and I could not read it", so a corrupt
90
+ * record reaches that map's callers as plain absence, i.e. as permission.
91
+ * - MARKED converted by its own source, with no record found. The record and
92
+ * the marker are two INDEPENDENT signals and either one refuses, matching
93
+ * the add-in's `slideNormalizationPolicy`. The asymmetry is deliberate:
94
+ * there the threat is a worker DELETING the marker to launder an import,
95
+ * so the persisted record is the backstop; here it is the reverse, a record
96
+ * deleted, lost or never written on a slide whose source still says what it
97
+ * is. Two signals, either sufficient, covers both directions — and without
98
+ * this one the same slide is protected in one client and rewritten in the
99
+ * other.
100
+ * - EDITED since `transcribe` stamped it (state/transcribed.json): the bytes
101
+ * no longer match the hash the machine wrote, so a person or an agent owns
102
+ * the file — the same rule `transcribe --force` already refuses on.
103
+ *
104
+ * A slide with NO stamp is not "edited": nothing ever recorded a machine
105
+ * baseline for it, so there is nothing it can differ from. Ordinary authored
106
+ * decks keep the autofix exactly as before.
107
+ */
108
+ function unrewritableSlides(deckDir, slideFiles) {
109
+ const why = new Map();
110
+ for (const name of slideFiles) {
111
+ const record = conversionProvenance(deckDir, name);
112
+ if (record === undefined) {
113
+ why.set(
114
+ name,
115
+ `its ${CONVERSIONS_DIR}/${name.replace(/\.jsx$/, "")}.json could not be read, so whether ` +
116
+ "this slide is converted is unknown — an unreadable record is not permission to rewrite it",
117
+ );
118
+ continue;
119
+ }
120
+ if (record) {
121
+ why.set(name, "converted from .pptx — its raw values are the original slide's");
122
+ continue;
123
+ }
124
+ // No record. The source itself is the second, independent signal.
125
+ let src;
126
+ try {
127
+ src = fs.readFileSync(path.join(deckDir, "slides", name), "utf8");
128
+ } catch {
129
+ continue;
130
+ }
131
+ if (DIALECT_RE.test(src) || NATIVE_RE.test(src)) {
132
+ why.set(
133
+ name,
134
+ "its source is marked converted, though no conversion record was found — " +
135
+ `the record ${CONVERSIONS_DIR}/${name.replace(/\.jsx$/, "")}.json is missing, which is worth ` +
136
+ "repairing on its own (re-run 'slidemind adopt' for this deck)",
137
+ );
138
+ }
139
+ }
140
+ let stamps = {};
141
+ try {
142
+ stamps = JSON.parse(fs.readFileSync(statePath(deckDir, "transcribed.json"), "utf8"));
143
+ } catch {
144
+ stamps = {};
145
+ }
146
+ for (const [name, hash] of Object.entries(stamps)) {
147
+ if (why.has(name)) continue;
148
+ let src;
149
+ try {
150
+ src = fs.readFileSync(path.join(deckDir, "slides", name), "utf8");
151
+ } catch {
152
+ continue;
153
+ }
154
+ if (crypto.createHash("sha256").update(src).digest("hex") !== hash) {
155
+ why.set(name, "edited since it was transcribed — the edit stands");
156
+ }
157
+ }
158
+ return why;
159
+ }
160
+
161
+ /**
162
+ * Fix every slides/*.jsx in a deck, writing only changed files — except the
163
+ * ones `unrewritableSlides` withholds, which keep their bytes.
164
+ * Returns { files, fixes, withheld: [{ file, reason, fixes }] }.
66
165
  */
67
- export function fixDeckSlides(deckDir) {
166
+ export function fixDeckSlides(deckDir, { only = null } = {}) {
68
167
  const slidesDir = path.join(deckDir, "slides");
69
168
  let files = 0;
70
169
  let fixes = 0;
170
+ const withheld = [];
71
171
  let entries = [];
72
172
  try {
73
173
  entries = fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx"));
74
174
  } catch {
75
- return { files, fixes };
175
+ return { files, fixes, withheld };
76
176
  }
177
+ entries = selectSlideFiles(entries, only);
178
+ const unrewritable = unrewritableSlides(deckDir, entries);
77
179
  for (const f of entries) {
78
180
  const p = path.join(slidesDir, f);
79
181
  const src = fs.readFileSync(p, "utf8");
80
182
  const res = fixSlideSource(src);
81
- if (res.fixes.length) {
82
- fs.writeFileSync(p, res.src);
83
- files += 1;
84
- fixes += res.fixes.length;
183
+ if (!res.fixes.length) continue;
184
+ const reason = unrewritable.get(f);
185
+ if (reason) {
186
+ // Reported, never applied: the lint still prints every finding on this
187
+ // file, so the fix is offered rather than performed.
188
+ withheld.push({ file: f, reason, fixes: res.fixes });
189
+ continue;
85
190
  }
191
+ fs.writeFileSync(p, res.src);
192
+ files += 1;
193
+ fixes += res.fixes.length;
86
194
  }
87
- return { files, fixes };
195
+ return { files, fixes, withheld };
88
196
  }
@@ -16,6 +16,7 @@ import path from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
 
18
18
  import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
19
+ import { deckSlug } from "../lib/deck-paths.mjs";
19
20
 
20
21
  // Resolve the cached engine for a deck and explain every miss actionably.
21
22
  // Returns { enginePath, version, primitives }.
@@ -23,8 +24,8 @@ function resolveEngine(deckDir) {
23
24
  const bundleDir = resolveBundleDirForDeck(deckDir);
24
25
  if (!bundleDir) {
25
26
  throw new Error(
26
- `No materialized design bundle for deck ${path.basename(deckDir)}.\n` +
27
- ` Run 'slidemind download-bundle ${path.basename(deckDir)}' first.`,
27
+ `No materialized design bundle for deck ${deckSlug(deckDir)}.\n` +
28
+ ` Run 'slidemind download-bundle ${deckSlug(deckDir)}' first.`,
28
29
  );
29
30
  }
30
31
  const enginePath = path.join(bundleDir, "lint-engine.mjs");
@@ -42,14 +43,14 @@ function resolveEngine(deckDir) {
42
43
  throw new Error(
43
44
  `The paired slidemind add-in does not serve a lint engine yet.\n` +
44
45
  ` Update/redeploy the add-in backend, then re-run ` +
45
- `'slidemind download-bundle ${path.basename(deckDir)}'.`,
46
+ `'slidemind download-bundle ${deckSlug(deckDir)}'.`,
46
47
  );
47
48
  }
48
49
  // Cache written before the engine existed (meta lacks the key), or the
49
50
  // file went missing — refetch repairs both.
50
51
  throw new Error(
51
52
  `This bundle cache predates the lint engine.\n` +
52
- ` Run 'slidemind download-bundle ${path.basename(deckDir)}'.`,
53
+ ` Run 'slidemind download-bundle ${deckSlug(deckDir)}'.`,
53
54
  );
54
55
  }
55
56
 
@@ -84,7 +85,7 @@ export async function lintSlidesOrThrow({ deckDir, slideFiles, layoutSources })
84
85
  if (typeof engine.lintSlidesInMemory !== "function") {
85
86
  throw new Error(
86
87
  `Cached lint engine is corrupt (no lintSlidesInMemory export).\n` +
87
- ` Run 'slidemind download-bundle ${path.basename(deckDir)}'.`,
88
+ ` Run 'slidemind download-bundle ${deckSlug(deckDir)}'.`,
88
89
  );
89
90
  }
90
91
 
@@ -124,25 +125,29 @@ const DYNAMIC_ID_RE =
124
125
  export function collapseLintLines(lines) {
125
126
  const out = [];
126
127
  const seen = new Set();
127
- const dynamic = new Map(); // file → { indent, refs: [] }
128
+ const arrows = new Map();
128
129
  for (const line of lines) {
129
130
  if (seen.has(line)) continue;
130
131
  seen.add(line);
131
- const m = DYNAMIC_ID_RE.exec(line);
132
- if (m) {
133
- const entry = dynamic.get(m[2]) ?? { file: m[2], indent: m[1], refs: [] };
134
- entry.refs.push(m[3].replace(/^data-(?:from|to)=/, ""));
135
- if (!dynamic.has(m[2])) {
136
- dynamic.set(m[2], entry);
137
- out.push(entry); // placeholder keeps the file's position in the listing
138
- }
139
- continue;
132
+ // Template-id refs resolve at render time; the engine from 2026-09-19 on
133
+ // no longer emits this line, and an older engine's copy is dropped here —
134
+ // the render-time endpoint check is what catches a real miss.
135
+ if (DYNAMIC_ID_RE.test(line)) continue;
136
+ // One "SVG path arrow" advice per file, naming its lines: repeated per
137
+ // arrow it fired 162 times in one x66 run, mostly on arcs no route draws,
138
+ // and buried the lines that mattered.
139
+ const arrow = PATH_ARROW_RE.exec(line);
140
+ if (arrow) {
141
+ const [, file, at] = arrow;
142
+ const prior = arrows.get(file);
143
+ if (prior) { prior.lines.push(at); continue; }
144
+ arrows.set(file, { index: out.length, lines: [at], text: line.slice(arrow[0].length) });
140
145
  }
141
146
  out.push(line);
142
147
  }
143
- return out.map((item) =>
144
- typeof item === "string"
145
- ? item
146
- : `${item.indent}[lint:note] ${item.file} — ${item.refs.length} connector ref${item.refs.length === 1 ? "" : "s"} (${[...new Set(item.refs)].slice(0, 4).join(", ")}${item.refs.length > 4 ? ", …" : ""}) use ids the slide builds with a template — they resolve at render time; check that each connector lands on its node in the draft render.`,
147
- );
148
+ for (const [file, { index, lines: at, text }] of arrows) {
149
+ if (at.length > 1) out[index] = ` [lint:warn] ${file}:${at.join(",")} — (${at.length}×) ${text.trim()}`;
150
+ }
151
+ return out;
148
152
  }
153
+ const PATH_ARROW_RE = /^\s*\[lint:warn\] ([^:\s]+\.jsx):(\d+) — (?=SVG path arrow)/;
@@ -0,0 +1,88 @@
1
+ // The native list the transcriber writes. An item (`marker` on a paragraph,
2
+ // concept-lists.mjs) is never a glyph typed into a run or a mark parked beside
3
+ // a line: it is an `<li>` of an `<L>`, so the export writes a PowerPoint bullet
4
+ // (`<a:buChar>` / `<a:buAutoNum>`) with a hanging indent. The glyph is the
5
+ // design's: `.slide ul/ol` and `.slide li::marker` in the bundle's CSS carry
6
+ // the brand bullet (layouts-css-codegen.ts); L sets none of its own. A check
7
+ // list keeps its tick (`bullet="✓"`), a don't list its cross, a numbered list
8
+ // counts (`ordered`). The marker's paint is the reader's when it named one
9
+ // (`markerVar`: the concept's accent dots stay accent), else the brand's.
10
+ // The items' words sit where the reader measured them and the marker hangs
11
+ // outside, left of them (`padding-left: 0`, `list-style-position: outside`),
12
+ // as the painting had it. Placed by `x`/`y` it stands alone; without them it
13
+ // flows inside a block's `<T>` after the paragraphs above it, at its own
14
+ // `indent` off the block's left edge (its words' offset, the marker hanging in
15
+ // it), aligned by its own `align`, and the export merges title and items into
16
+ // ONE text frame (oox/adapter/text-merge.ts).
17
+ // Pure; mirrored into the add-in by scripts/sync-transcription-runtime.mjs.
18
+
19
+ export const LIST_HELPER = ` const L = ({ id, x, y, w, px, indent, align, role = "body", tone = "dark", colorVar, bullet, ordered, markerVar, stack, block, children }) => {
20
+ const style = { ...(x != null ? { position: "absolute", left: x, top: y, width: w } : {}), ...(indent ? { marginLeft: indent } : {}), ...(align ? { textAlign: align } : {}), margin: 0, paddingLeft: 0, boxSizing: "border-box",
21
+ listStylePosition: "outside", ...(ordered ? { listStyleType: "decimal" } : bullet ? { listStyleType: \`"\${bullet} "\` } : {}),
22
+ ...(markerVar ? { "--sm-marker": markerVar } : {}), ...ROLE[role], color: colorVar ?? TONE[tone], ...(px ? { fontSize: HALF_PT(px) } : {}), lineHeight: "var(--lh-tight)" };
23
+ return (<>{markerVar ? <style>{".sm-list[data-marker] > li::marker { color: var(--sm-marker); }"}</style> : null}
24
+ {ordered ? <ol id={id} className="sm-list" data-marker={markerVar} data-sm-stack={stack} data-sm-block={block} style={style}>{children}</ol> : <ul id={id} className="sm-list" data-marker={markerVar} data-sm-stack={stack} data-sm-block={block} style={style}>{children}</ul>}</>);
25
+ };
26
+ `;
27
+
28
+ /** The `L` attributes a marker kind adds: a check or don't list's glyph, a numbered list's count. */
29
+ export const markerAttrs = (marker) => (marker === "check" ? ['bullet="✓"'] : marker === "cross" ? ['bullet="✗"'] : marker === "number" ? ["ordered"] : []);
30
+
31
+ /**
32
+ * The JSX children of a text block: one `<P>` per plain paragraph, a run of
33
+ * items with one marker as one `<L>` of `<li>`s. `para(p)` writes a plain
34
+ * paragraph, `attrs(p)` the role, paint, indent and marker attributes of the
35
+ * paragraph that opens a list, `item(p, head)` an item's text (painted like
36
+ * `head` unless its own paint differs). `single` is set when the block is
37
+ * exactly one list.
38
+ */
39
+ export function blockChildren(paragraphs, { para, item, attrs }) {
40
+ const out = [];
41
+ let lists = 0;
42
+ for (let i = 0; i < paragraphs.length; ) {
43
+ const head = paragraphs[i];
44
+ if (!head.marker) {
45
+ out.push(para(head));
46
+ i += 1;
47
+ continue;
48
+ }
49
+ let j = i;
50
+ while (j < paragraphs.length && paragraphs[j].marker === head.marker) j += 1;
51
+ const items = paragraphs.slice(i, j).map((p) => `<li>${item(p, head)}</li>`).join("");
52
+ out.push(`<L ${[...attrs(head), ...markerAttrs(head.marker)].join(" ")}>${items}</L>`);
53
+ lists += 1;
54
+ i = j;
55
+ }
56
+ return { children: out.join(""), single: lists === 1 && out.length === 1 };
57
+ }
58
+
59
+ // A styled run inside a paragraph: the accent figure in "under 21 days", the
60
+ // bold term in a sentence (text-blocks.mjs `runs`). It takes its role's rung
61
+ // and paint from the kit, never a size of its own, so one sentence stays ONE
62
+ // text frame whose runs the export writes with their own size, weight and
63
+ // colour (dom-serialiser text runs → `<a:r>`).
64
+ export const RUN_HELPER = ` const R = ({ role, bold, tone, colorVar, children }) => (
65
+ <span style={{ ...(role ? ROLE[role] : {}), ...(colorVar || tone ? { color: colorVar ?? TONE[tone] } : {}), ...(bold ? { fontWeight: "var(--fw-bold)" } : {}) }}>{children}</span>
66
+ );
67
+ `;
68
+
69
+ /**
70
+ * `body` (escaped JSX text) with each run wrapped in `<R …>`, in order: a run
71
+ * is found after the one before it, never inside a tag. `attrs(run)` names its
72
+ * role and paint, `escape` escapes its words like the body's.
73
+ */
74
+ export function wrapRuns(body, runs, attrs, escape) {
75
+ let out = body;
76
+ let at = 0;
77
+ for (const run of runs ?? []) {
78
+ const needle = escape(String(run.text ?? "").trim());
79
+ if (!needle) continue;
80
+ let i = out.indexOf(needle, at);
81
+ while (i >= 0 && out.lastIndexOf("<", i) > out.lastIndexOf(">", i)) i = out.indexOf(needle, i + 1);
82
+ if (i < 0) continue;
83
+ const tag = `<R${attrs(run).map((a) => ` ${a}`).join("")}>${needle}</R>`;
84
+ out = out.slice(0, i) + tag + out.slice(i + needle.length);
85
+ at = i + tag.length;
86
+ }
87
+ return out;
88
+ }