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,369 @@
1
+ // Transport-free draft diagnostics. Both clients use the same counts and asset rules.
2
+ import { contains, pathDistance, pointRef, sanitizeTrace } from "./line-fit.mjs";
3
+ import { fitNative, nodePreset, toleranceFor } from "./line-native.mjs";
4
+ import { readLines } from "./concept-lines.mjs";
5
+ import { connectorPaths, measuredFromRender } from "./settle.mjs";
6
+ import { arrange, correspond, linesWithin, repeatGroups } from "./repeat-groups.mjs";
7
+ import { fixedAspect } from "./concept-layout.mjs";
8
+
9
+ // What each draft line asks for — one table for both clients (the add-in's
10
+ // see_draft and build_and_push, the CLI's view-draft, review and delivery).
11
+ // A DEFECT is a measured fault in what would ship: text on text, a word too
12
+ // wide for its frame, one text split into boxes, a missing approved image or
13
+ // asset, an element a preserved slide lost; any other `[<rule>:error]` is a
14
+ // gate refusal. A defect holds the user's gate and delivery until a fresh
15
+ // draft shows it fixed. An ADVISORY names a difference to check against the
16
+ // concept and never holds anything (repairing every one was a second loop per
17
+ // build). Lines outside the table are notes.
18
+ export const DRAFT_DEFECT_TAGS = Object.freeze([
19
+ "overlap:warn",
20
+ "fit:warn",
21
+ "split:error",
22
+ "image:error",
23
+ "asset:error",
24
+ "preserve:warn",
25
+ "preserve:error",
26
+ ]);
27
+ export const DRAFT_ADVISORY_TAGS = Object.freeze([
28
+ "fill:warn",
29
+ "type:warn",
30
+ "image:warn",
31
+ "image:note",
32
+ "repeat:warn",
33
+ "list:warn",
34
+ "split:warn",
35
+ ]);
36
+
37
+ /** "defect", "advisory", or null for a line that is not a draft line. */
38
+ export function draftLineTier(line) {
39
+ const tag = /^\s*\[([a-z-]+:[a-z]+)\]/.exec(String(line ?? ""))?.[1];
40
+ if (!tag) return null;
41
+ if (DRAFT_ADVISORY_TAGS.includes(tag)) return "advisory";
42
+ if (DRAFT_DEFECT_TAGS.includes(tag) || tag.endsWith(":error")) return "defect";
43
+ return null;
44
+ }
45
+
46
+ export function borrowedAssetOwner(stem, file) {
47
+ const owner = /^(\d\d)[-_.]/.exec(file)?.[1];
48
+ if (!owner) return null;
49
+ const mine = /^(\d\d)[-_.]/.exec(stem)?.[1];
50
+ if (!mine || mine === owner) return null;
51
+ return owner;
52
+ }
53
+
54
+ // An arrow is a line with a head (concept-lines.mjs counts the concept's the
55
+ // same way). A render that carries no caps (the add-in's render evidence keeps
56
+ // only `{ native }`) counts every line, as before.
57
+ const HEAD = /^(?:triangle|stealth|arrow|diamond):/;
58
+ const headed = (c) => !("capStart" in c || "capEnd" in c) || HEAD.test(c.capStart ?? "") || HEAD.test(c.capEnd ?? "");
59
+
60
+ export const countRendered = (render) => {
61
+ const have = { arrows: 0, charts: 0, icons: 0 };
62
+ const walk = (box) => {
63
+ if (!box || typeof box !== "object") return;
64
+ if (box.connector && !box.connector.native && headed(box.connector)) have.arrows += 1;
65
+ if (box.chart) have.charts += 1;
66
+ if (box.imgSrc) have.icons += 1;
67
+ for (const c of box.children ?? []) walk(c);
68
+ };
69
+ for (const b of render?.boxes ?? []) walk(b);
70
+ return have;
71
+ };
72
+
73
+ // What each line tells the agent to DO, per client: the diagnosis is shared,
74
+ // the remedy names the tools that client's agent actually has. The defaults are
75
+ // the CLI's commands.
76
+ export const CLI_REMEDIES = {
77
+ sourceIcon: "search the image engine for the motif, then for what the element stands for (the step, label or claim it anchors), and attach the result that carries that meaning with `download-result --name <the referenced file>`",
78
+ specHint: "state/concept-specs/concept-slide-NN.json",
79
+ borrowed: (stem) => `download-result --name ${stem.slice(0, 2)}-<element id>.svg`,
80
+ missing: (file) => `Retry transcribe when the engine failed to answer; when it found no match, source another motif for what the element stands for (download-result --name ${file})`,
81
+ };
82
+
83
+ export function imageCountLines({ fileName, src, render, want, fromSpec = true, lowered = [], remedies = CLI_REMEDIES }) {
84
+ const source = fromSpec ? "the concept spec lists" : "the image shows";
85
+ const stem = fileName.replace(/\.[^.]+$/, "");
86
+ // Every line is a native line directive, glued or free; the render counts them.
87
+ const have = render?.boxes
88
+ ? countRendered(render)
89
+ : {
90
+ // Each line tag, then its cap attributes, in whatever order they come.
91
+ arrows: (src.match(/<[^<>]*\bdata-(?:connector|sm-line)\b[^<>]*>/g) ?? [])
92
+ .filter((tag) => [...tag.matchAll(/\sdata-cap-(?:start|end)="([^"]*)"/g)].some((m) => HEAD.test(m[1]))).length,
93
+ charts: (src.match(/<Chart\b/g) ?? []).length,
94
+ icons: (src.match(/<img\b/g) ?? []).length,
95
+ };
96
+ const label = { arrows: "rendered connector", charts: "rendered chart", icons: "rendered icon" };
97
+ const lines = [];
98
+ if (lowered.length) {
99
+ lines.push(
100
+ `[image:warn] ${stem} — the image line was lowered (${lowered.join(", ")}). The count is the image's, not the JSX's: the first count stands, add the missing elements.`,
101
+ );
102
+ }
103
+ for (const k of ["arrows", "charts", "icons"]) {
104
+ if (want[k] != null && have[k] < want[k]) {
105
+ // Icons come from an engine that may lack a motif (x29: two
106
+ // double-chevron glyphs no search returned), so an icon shortfall is
107
+ // a note; arrows and charts are drawn, so theirs is a warning
108
+ // (DRAFT_ADVISORY_TAGS: both are checked against the concept, neither
109
+ // holds a gate). A motif without a match is not a meaning without one:
110
+ // the remedy is another motif for the element's meaning, never a
111
+ // stand-in for the set (x114).
112
+ const advisory = k === "icons";
113
+ lines.push(
114
+ `[image:${advisory ? "note" : "warn"}] ${stem} — ${source} ${k}=${want[k]}, the render has ${have[k]} ${label[k]}${have[k] === 1 ? "" : "s"}. ${advisory ? `Each missing icon is a visible hole the reviewer will see: ${remedies.sourceIcon}. An engine that is unavailable or timed out has not answered — retry and keep the motif. Deleting the element, or setting a number, letter or plain shape in its place or across its set, does not fix it: every member keeps an asset in the set's one box, and only a brand with no image engine anchors a set typographically (composition.md → Icons and imagery).` : `Add the missing ${k}${fromSpec ? ` (${remedies.specHint} names each one)` : ""}.`}`,
115
+ );
116
+ }
117
+ }
118
+ return lines;
119
+ }
120
+
121
+ export function missingAssetLines({ fileName, src, hasAsset, hasBundleAsset = () => false, remedies = CLI_REMEDIES }) {
122
+ const stem = fileName.replace(/\.[^.]+$/, "");
123
+ const lines = [];
124
+ const seen = new Set();
125
+ for (const m of src.matchAll(/["'`]\.\/assets\/([^"'`{}\s]+)["'`]/g)) {
126
+ const file = m[1];
127
+ if (seen.has(file)) continue;
128
+ seen.add(file);
129
+ const owner = borrowedAssetOwner(stem, file);
130
+ if (owner) {
131
+ lines.push(
132
+ `[asset:error] ${stem} — ./assets/${file} was fetched for slide ${owner}, and its colour was chosen for that slide's background. Source the required motif with this slide's colour (${remedies.borrowed(stem)}). Do not substitute an unrelated same-slide icon or delete approved content to clear this diagnostic.`,
133
+ );
134
+ continue;
135
+ }
136
+ if (hasAsset(file)) continue;
137
+ // A brand chrome/master asset lives ONLY in the bundle cache — it is never
138
+ // copied into a deck. `rewriteBrandAssetRefs` (src/builders/build.mjs)
139
+ // redirects exactly these names to the brand URL when the deck does not
140
+ // own a file of that name, and the server resolves them from the design,
141
+ // so they are not missing. Mirror that rule rather than a bare prefix
142
+ // test: a deck that DOES own `assets/master-data.png` fell out above.
143
+ if (/^(?:chrome|master)-/.test(file) && hasBundleAsset(file)) continue;
144
+ lines.push(
145
+ `[asset:error] ${stem} — ./assets/${file} is referenced but not on disk. ${remedies.missing(file)}; do not delete approved content or substitute unrelated artwork.`,
146
+ );
147
+ }
148
+ return lines;
149
+ }
150
+
151
+ const ROUND = new Set(["circle", "ellipse", "ring"]);
152
+ const MAX_LINE_SHAPE_LINES = 6;
153
+ const SHAPE_WORD = { straight: "straight line", orthogonal: "elbow", curve: "curve", arc: "arc", polyline: "polyline" };
154
+ // A fitted connector's shape attributes as the slide writes them.
155
+ const shapeAttrs = (n) => [
156
+ ...(n.from?.node ? [] : [`data-from="${pointRef(n.start)}"`]),
157
+ ...(n.to?.node ? [] : [`data-to="${pointRef(n.end)}"`]),
158
+ `data-prst="${n.prst}"`,
159
+ ...(Object.keys(n.av ?? {}).length ? [`data-adj="${Object.entries(n.av).map(([k, v]) => `${k}:${Math.round(v)}`).join(",")}"`] : []),
160
+ ...(n.rot ? [`data-rot="${n.rot}"`] : []),
161
+ ...(n.from?.node ? [`data-from-site="${n.from.site}"`] : []),
162
+ ...(n.to?.node ? [`data-to-site="${n.to.site}"`] : []),
163
+ ].join(" ");
164
+
165
+ /**
166
+ * Every rendered connector measured against the line the concept draws, the
167
+ * way the fitter measures it: the directive's own preset, adjusts, turn and
168
+ * sites (the slide source `src`) placed over the nodes as the render measured
169
+ * them, evaluated by the shared preset evaluator, compared with the spec's
170
+ * trace. A line beyond tolerance is named with its distance — the shape the
171
+ * reviewer will see is wrong even though the count is right.
172
+ */
173
+ export function lineShapeLines({ fileName, spec, src, render }) {
174
+ if (!render?.boxes?.length || !spec || !src) return [];
175
+ const stem = fileName.replace(/\.[^.]+$/, "");
176
+ const byId = new Map((spec.elements ?? []).map((e) => [e.id, e]));
177
+ const round = (id) => { const e = byId.get(id); return e?.kind === "disc" || ROUND.has(e?.shape); };
178
+ // The concept was read onto a slot of the spec's size; a render on another
179
+ // (a layout canvas change) moves every line, which no shape knob undoes.
180
+ const slot = spec.slot;
181
+ const root = render.boxes[0]?.rect;
182
+ if (root && (Math.abs(root.w - slot.w) > 1 || Math.abs(root.h - slot.h) > 1)) return [];
183
+ const read = new Map(readLines(structuredClone(spec)).lines.map((l) => [l.id, l]));
184
+ const measured = measuredFromRender(render, spec.slot);
185
+ // A line drawn on a painted element (a mark on its disc) moves with it: its
186
+ // place is judged where that element renders, not where the sketch put it.
187
+ const carried = (line) => {
188
+ const el = byId.get(line?.source?.id);
189
+ const on = el?.parent ? byId.get(el.parent) : null;
190
+ const m = on?.fill && on.fill !== "none" && contains(on, el) ? measured.get(on.id) : null;
191
+ return m ? { x: m.x - on.x, y: m.y - on.y } : { x: 0, y: 0 };
192
+ };
193
+ // A free straight rule drawn inside a card (a divider under its title) is
194
+ // the card's layout, placed by the settle (text fit, one template per set),
195
+ // not a relationship the concept routes: its place is judged by the text
196
+ // and occlusion checks, never by the sketch's pixel position.
197
+ const rules = new Set((spec.elements ?? [])
198
+ .filter((e) => e.fill && e.fill !== "none" && !fixedAspect(e) && e.w > 0 && e.h > 0)
199
+ .flatMap((e) => linesWithin(spec, e))
200
+ .filter((c) => !c.from && !c.to && (c.route ?? "straight") === "straight")
201
+ .map((c) => c.id));
202
+ const out = [];
203
+ for (const c of connectorPaths(src, measured, spec)) {
204
+ if (rules.has(c.id)) continue;
205
+ const line = read.get(c.id);
206
+ const shift = carried(line);
207
+ const raw = (line?.path?.points ?? []).filter((p) => Number.isFinite(p?.x) && Number.isFinite(p?.y)).map((p) => ({ x: p.x + shift.x, y: p.y + shift.y }));
208
+ if (!(raw.length >= 2)) continue;
209
+ const trace = sanitizeTrace(raw, c.fromBox, c.toBox, { fromRound: round(c.fromEnd.node), toRound: round(c.toEnd.node) });
210
+ // The fitter's class: an end the concept glues keeps the glued tolerance
211
+ // after the fitter freed it onto the ink (no site near where it attaches).
212
+ const glued = Boolean(c.fromEnd.node || c.toEnd.node || line.ends?.some((e) => e.node));
213
+ const tolerance = toleranceFor(trace, glued);
214
+ const distance = pathDistance(c.points, trace);
215
+ if (!(Number.isFinite(distance) && distance > tolerance)) continue;
216
+ // The ONE directive the concept's shape names at these nodes: the fitter's
217
+ // own answer on the rendered geometry, never a search left to the agent.
218
+ const nodes = new Map(line.ends.filter((e) => e.node && measured.has(e.node)).map((e) => [e.node, nodePreset(byId.get(e.node), measured.get(e.node))]).filter(([, n]) => n));
219
+ const ends = line.ends.map((e) => ({ ...e, at: e.at && { x: e.at.x + shift.x, y: e.at.y + shift.y } }));
220
+ const fit = fitNative({ ...line, ends, path: { ...line.path, points: raw } }, nodes, null, { slot });
221
+ const shape = SHAPE_WORD[line.path?.kind] ?? "line";
222
+ const fix = fit?.object === "cxn" && fit.devPx <= tolerance && fit.devPx < distance - 1
223
+ ? `Set ${shapeAttrs(fit)} — the one directive that draws the concept's ${shape} here — and keep it.`
224
+ : `No directive at these nodes draws the concept's ${shape} closer: keep this one and name the difference in your reply.`;
225
+ out.push(`[image:warn] ${stem} — the line ${c.id} (${c.from} → ${c.to}) runs ${Math.round(distance)}px from the concept's line (tolerance ${Math.round(tolerance)}px). Judge its shape (straight, elbow, curve or arc), not its pixels: where you aligned or evened the nodes on purpose, the line follows them. ${fix} Never try presets or sites one by one to quiet this.`);
226
+ }
227
+ if (out.length > MAX_LINE_SHAPE_LINES) {
228
+ const more = out.length - MAX_LINE_SHAPE_LINES;
229
+ return [...out.slice(0, MAX_LINE_SHAPE_LINES), `[image:warn] ${stem} — ${more} more line${more === 1 ? "" : "s"} off the concept's shape.`];
230
+ }
231
+ return out;
232
+ }
233
+
234
+ const REPEAT_TOL = 3; // px: two members of one set may differ by rendering noise only (text tops round per line box)
235
+
236
+ /**
237
+ * A set the reader declared (spec.repeats) as it renders now: one size, one
238
+ * inner layout, the arrangement's symmetry. The transcriber builds it that
239
+ * way; this names what an edit changed on one member and not the others. Text
240
+ * heights follow their copy and are not compared.
241
+ */
242
+ export function repeatLines({ fileName, spec, render }) {
243
+ if (!render?.boxes?.length || !spec?.repeats?.length) return [];
244
+ const stem = fileName.replace(/\.[^.]+$/, "");
245
+ const m = measuredFromRender(render, spec.slot);
246
+ const rendered = { ...spec, elements: (spec.elements ?? []).map((e) => (m.has(e.id) ? { ...e, ...pick(m.get(e.id)) } : e)) };
247
+ const out = [];
248
+ const r = (n) => Math.round(n);
249
+ for (const g of repeatGroups(rendered)) {
250
+ // A virtual member (a row of loose parts) is its parts' box: rendered when they all are.
251
+ const members = g.members.filter((e) => (e.ids ? e.ids.every((id) => m.has(id)) : m.has(e.id)));
252
+ if (members.length < 2) continue;
253
+ const name = `${g.id} (${members.map((e) => (e.ids ? `(${e.ids.join(" + ")})` : e.id)).join(", ")})`;
254
+ // Its box follows its words, so only its inner layout is compared.
255
+ const loose = members.some((e) => e.virtual);
256
+ const size = (e) => `${r(e.w)}×${r(e.h)}px`;
257
+ // An engine icon's own aspect is kept by the equal box's `fit="contain"`,
258
+ // never by sizing each member's box to its asset.
259
+ const aspect = (els) => (els.some((e) => m.get(e.id)?.content) ? ` A picture keeps its own aspect inside an equal box (fit="contain").` : "");
260
+ const off = loose ? [] : members.filter((e) => Math.abs(e.w - members[0].w) > REPEAT_TOL || Math.abs(e.h - members[0].h) > REPEAT_TOL);
261
+ if (off.length) {
262
+ out.push(`[repeat:warn] ${stem} — ${name} is one organism but renders in ${[...new Set(members.map(size))].join(" and ")}. Give every member the same size.${aspect(members)}`);
263
+ continue;
264
+ }
265
+ const kids = correspond(rendered.elements, members);
266
+ for (const [k, first] of (kids?.[0] ?? []).entries()) {
267
+ const rel = kids.map((l, i) => ({ e: l[k], x: l[k].x - members[i].x, y: l[k].y - members[i].y }));
268
+ const text = first.kind === "text";
269
+ const odd = rel.find((q) => Math.abs(q.x - rel[0].x) > REPEAT_TOL || Math.abs(q.y - rel[0].y) > REPEAT_TOL
270
+ || Math.abs(q.e.w - rel[0].e.w) > REPEAT_TOL || (!text && Math.abs(q.e.h - rel[0].e.h) > REPEAT_TOL));
271
+ if (odd) out.push(`[repeat:warn] ${stem} — ${name}: #${odd.e.id} sits at ${r(odd.x)},${r(odd.y)} (${size(odd.e)}) in its card, #${rel[0].e.id} at ${r(rel[0].x)},${r(rel[0].y)} (${size(rel[0].e)}). One inner layout: change every member alike.${aspect([odd.e])}`);
272
+ }
273
+ const at = loose ? null : arrange({ ...g, members }, members[0].w, members[0].h);
274
+ const drift = at && members.map((e, i) => ({ e, d: Math.hypot(e.x + e.w / 2 - at[i].x, e.y + e.h / 2 - at[i].y) })).filter((q) => q.d > REPEAT_TOL);
275
+ if (drift?.length) out.push(`[repeat:warn] ${stem} — ${name} is a ${g.arrangement}${g.hub ? ` around #${g.hub.id}` : ""}, but ${drift.map((q) => `#${q.e.id} sits ${r(q.d)}px off it`).join(", ")}. Move the members together so the ${g.arrangement} stays even${g.arrangement === "ring" ? " and symmetric about its hub" : ""}.`);
276
+ }
277
+ return out;
278
+ }
279
+
280
+ const pick = (v) => ({ x: v.x, y: v.y, w: v.w, h: v.h });
281
+
282
+ // The items the concept declares (concept-lists.mjs: `marker` on a text or
283
+ // on a block's paragraphs) against the `<li>` the slide writes. x101–x105: 22
284
+ // native lists left the first draft and came back as plain paragraphs or
285
+ // typed glyphs — a flattened list the glyph lint alone cannot name when the
286
+ // glyphs are gone too. A slide built from set components counts its template
287
+ // once, so it is not compared.
288
+ export function listLines({ fileName, spec, src }) {
289
+ const stem = fileName.replace(/\.[^.]+$/, "");
290
+ const source = String(src ?? "");
291
+ if (!source || /const Repeat\d+ = /.test(source)) return [];
292
+ const declared = (spec?.elements ?? []).reduce((n, e) => n + (e.paragraphs ? e.paragraphs.filter((p) => p.marker).length : e.marker ? 1 : 0), 0);
293
+ if (!declared) return [];
294
+ const written = (source.match(/<li[\s>]/g) ?? []).length;
295
+ if (written >= declared) return [];
296
+ return [`[list:warn] ${stem} — the concept declares ${declared} list item${declared === 1 ? "" : "s"}; the slide writes ${written} <li>. A flattened list: restore the <L> (bullet="✓" for ticks) with one <li> per item — a title that should not be bulleted becomes a <P>, never the whole list.`];
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Preservation — what a preserved slide still owes, in one vocabulary and one
301
+ // wording on both clients (the add-in's preservation-gate.ts / render-batch.ts,
302
+ // the CLI's draft-check.mjs): `[preserve:warn]` an obligation this draft
303
+ // breaks (a defect), `[preserve:error]` a record nobody can read (delivery
304
+ // refuses until repaired). What no draft can answer is not a draft line: the
305
+ // agent cannot act on it, and delivery names it as a limitation.
306
+ // ---------------------------------------------------------------------------
307
+
308
+ // Failures that mean the element itself is gone.
309
+ const GONE = new Set(["removed_from_source", "absent_from_render", "absent_from_package"]);
310
+
311
+ // What a required element showed, so a line names content, not a number.
312
+ function shownAs(req) {
313
+ if (req?.excerpt) return ` "${req.excerpt}"`;
314
+ if (req?.mediaRef) return ` (picture ${req.mediaRef})`;
315
+ if (req?.kind === "native-object") return " (embedded object)";
316
+ if (req?.properties?.includes("chart")) return " (chart)";
317
+ return "";
318
+ }
319
+
320
+ const stemOf = (fileName) => String(fileName).replace(/\.[^.]+$/, "");
321
+
322
+ /**
323
+ * One `[preserve:warn]` per element with a FAILED check, every failed check of
324
+ * it in one line. An element that is gone gets the one fix — its id back on the
325
+ * element that shows it (`data-sm-source-id={87}` for an imported shape); any
326
+ * other failure is restored in the slide's file. `supersede` is the client's
327
+ * way to declare an intended removal (the CLI's scoped revision), or null where
328
+ * the agent has none of its own.
329
+ */
330
+ export function preservationFailureLines({ fileName, checks, active, supersede = null }) {
331
+ const byElement = new Map();
332
+ for (const c of checks ?? []) {
333
+ if (c?.status !== "failed") continue;
334
+ byElement.set(c.elementId ?? null, [...(byElement.get(c.elementId ?? null) ?? []), c]);
335
+ }
336
+ const stem = stemOf(fileName);
337
+ const or = supersede ? `, or ${supersede}` : "";
338
+ const refuses = "; a final replacement refuses while it stands.";
339
+ return [...byElement].map(([elementId, failed]) => {
340
+ const req = (active ?? []).find((r) => r?.elementId === elementId);
341
+ const where = elementId != null ? `#${elementId}${shownAs(req)}` : "the slide";
342
+ if (elementId != null && req?.kind !== "connection" && failed.every((c) => GONE.has(c.reason ?? ""))) {
343
+ const why = [...new Set(failed.map((c) => c.reason))].join(", ");
344
+ const idAttr = /^\d+$/.test(String(elementId)) ? `data-sm-source-id={${elementId}}` : `id="${elementId}"`;
345
+ return `[preserve:warn] ${stem} — ${where}: no longer on the slide (${why}) — it must stay: put ${idAttr} on the element that shows it${or}${refuses}`;
346
+ }
347
+ const what = failed
348
+ .map((c) => {
349
+ const detail = String(c.detail ?? "");
350
+ const said = elementId != null && detail.startsWith(`${elementId} `) ? detail.slice(String(elementId).length + 1) : detail;
351
+ return `${c.reason ?? "no reason given"}${c.scope ? ` (${c.scope})` : ""}${said ? ` — ${said}` : ""}`;
352
+ })
353
+ .join("; ");
354
+ return `[preserve:warn] ${stem} — ${where}: ${what} — restore it in slides/${fileName}${or}${refuses}`;
355
+ });
356
+ }
357
+
358
+ /**
359
+ * `[preserve:error]` — the deck's preservation record cannot be read, so
360
+ * nothing about what its slides must keep can be verified and delivery
361
+ * refuses until it is repaired. The record is not the agent's to edit.
362
+ */
363
+ export function preservationUnreadableLine({ fileName, detail = null }) {
364
+ return (
365
+ `[preserve:error] ${stemOf(fileName)} — this deck's preservation record could not be read` +
366
+ `${detail ? ` (${detail})` : ""}; nothing could be verified about what its slides must keep, and delivery ` +
367
+ `refuses until it is repaired. This is not a slide defect: never edit that file — name the problem in your reply.`
368
+ );
369
+ }
@@ -0,0 +1,29 @@
1
+ // Which route a deck is on, read from disk. Its own module because BOTH gates
2
+ // ask it — plan.mjs imports review.mjs, so the predicate cannot live in either
3
+ // without a cycle.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import { splitPlanMarkdownIntoSegments } from "./storyboard-markdown.mjs";
8
+ import { readRevision } from "../lib/revision.mjs";
9
+
10
+ /**
11
+ * A deck that already EXISTS — adopted from someone's .pptx — with no storyboard
12
+ * in its plan.md is on the EDIT route, not the authoring route. Neither gate
13
+ * applies: there is no plan to sign off (the deck is the plan, and the
14
+ * storyboard gate exists to approve a story before it is built), and no preview
15
+ * to approve (the .pptx was the user's before any agent touched it, and changes
16
+ * go straight into it — the same rule as an authored deck after its one
17
+ * review). Manufacturing an approval for either would be a checkmark nobody gave.
18
+ */
19
+ export function isEditRouteDeck(deckDir) {
20
+ const revision = readRevision(deckDir);
21
+ if (revision?.origin?.kind !== "adopted") return false;
22
+ let markdown = "";
23
+ try {
24
+ markdown = fs.readFileSync(path.join(deckDir, "plan.md"), "utf8");
25
+ } catch {
26
+ return true;
27
+ }
28
+ return splitPlanMarkdownIntoSegments(markdown).every((s) => s.type !== "storyboard");
29
+ }