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
@@ -0,0 +1,133 @@
1
+ // Placement decisions the transcriber makes from the elements around a line:
2
+ // the head its route carries (T28) and where a free line paints among the
3
+ // shapes (T04).
4
+ //
5
+ // Shared by both clients; mirrored by scripts/sync-transcription-runtime.mjs —
6
+ // no Node or CLI imports here (line-relations.mjs is mirrored alongside).
7
+
8
+ import { isGround, routeSamples, stationsOf, touchedShape } from "./line-relations.mjs";
9
+
10
+ const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
11
+
12
+ // ---- Heads (T28): PowerPoint draws a head 2/3/5 × the stroke, never scaled
13
+ // from less than 2pt, and the render gate refuses a connector shorter than 2.5
14
+ // of its heads (connector-derive.ts renderedArrowheadPx, MIN_CONNECTOR_ROUTE_HEADS).
15
+ const HEAD_RATIO = { sm: 2, med: 3, lg: 5 };
16
+
17
+ /** The shortest route a head of `size` fits on, on a `strokePx` stroke. */
18
+ export const headRoutePx = (size, strokePx = 0) => 2.5 * HEAD_RATIO[size] * Math.max(strokePx, 8 / 3);
19
+
20
+ /** The straight piece a cap at end `i` sits on, as the gate measures it; Infinity for a shape (not measured). */
21
+ export function capRoutePx(n, i) {
22
+ const p = n?.object === "grp" ? n.children[i ? n.children.length - 1 : 0] : n;
23
+ return p?.object === "cxn" ? dist(p.start, p.end) : Infinity;
24
+ }
25
+
26
+ /** A free straight connector stretched about its midpoint to `len`, or null when it is glued or already that long. */
27
+ export function stretched(n, len) {
28
+ if (n?.object !== "cxn" || n.prst !== "straightConnector1" || n.from?.node || n.to?.node) return null;
29
+ const d = dist(n.start, n.end);
30
+ if (!(d > 0) || d >= len) return null;
31
+ const k = (len - d) / 2 / d, dx = (n.end.x - n.start.x) * k, dy = (n.end.y - n.start.y) * k;
32
+ const start = { x: n.start.x - dx, y: n.start.y - dy }, end = { x: n.end.x + dx, y: n.end.y + dy };
33
+ return { ...n, start, end, from: { point: start }, to: { point: end } };
34
+ }
35
+
36
+ // ---- Paint order of a free line (T04). A rail, ring or divider runs BEHIND
37
+ // the shapes on it (a timeline's stops, an orbit's satellites, the disc on a
38
+ // divider) and above the panel it lies on. Painted last, it ran over its own
39
+ // stations and the occlusion gate asked the agent to move them off it.
40
+
41
+ /** Segment a→b passes through the inside of rect r (1px in): Liang–Barsky. */
42
+ export function crosses(a, b, r) {
43
+ const x0 = r.x + 1, y0 = r.y + 1, x1 = r.x + r.w - 1, y1 = r.y + r.h - 1;
44
+ if (!(x1 > x0 && y1 > y0)) return false;
45
+ const dx = b.x - a.x, dy = b.y - a.y;
46
+ let t0 = 0, t1 = 1;
47
+ for (const [p, q] of [[-dx, a.x - x0], [dx, x1 - a.x], [-dy, a.y - y0], [dy, y1 - a.y]]) {
48
+ if (p === 0) {
49
+ if (q < 0) return false;
50
+ continue;
51
+ }
52
+ const t = q / p;
53
+ if (p < 0) t0 = Math.max(t0, t);
54
+ else t1 = Math.min(t1, t);
55
+ if (t0 > t1) return false;
56
+ }
57
+ return true;
58
+ }
59
+ /** Segment a→b runs through none of `elements` but those that hold all of it (its ground). */
60
+ export const runsClear = (a, b, elements) => !elements.some((o) => o.w > 0 && o.h > 0 && !(holds(o, a) && holds(o, b)) && crosses(a, b, o));
61
+ const holds = (r, p) => p.x >= r.x - 0.5 && p.y >= r.y - 0.5 && p.x <= r.x + r.w + 0.5 && p.y <= r.y + r.h + 0.5;
62
+
63
+ const ROUND = new Set(["circle", "ellipse", "ring"]);
64
+ // Marks a line measures (a target or threshold across bars): it stays above
65
+ // them, as the image draws it. A filled path is no such mark here — on a ring
66
+ // it is a band or a bead the ring passes behind.
67
+ const MARKS = new Set(["bar"]);
68
+
69
+ /** A written element as the line relation reads it (line-relations.mjs): its painted box. */
70
+ export function shapeOf(el) {
71
+ const rect = { x: el.x, y: el.y, w: el.w, h: el.h };
72
+ const text = el.text != null && String(el.text).trim() !== "";
73
+ return {
74
+ rect,
75
+ outline: { ellipse: el.kind === "disc" || ROUND.has(el.shape), poly: null },
76
+ content: ["icon", "image", "chart"].includes(el.kind),
77
+ alpha: 1,
78
+ lines: text ? [rect] : [],
79
+ };
80
+ }
81
+
82
+ // The index of the first painted element the line must run behind — a
83
+ // station of it (line-relations.mjs: an opaque node threaded near its centre,
84
+ // the disc on a divider, a timeline's stop), or for a plain line a node one of
85
+ // its ends sits in (it runs out from behind it) — that paints after
86
+ // everything it must lie above: its ground (the shape under most of it), for
87
+ // a headed line the nodes its ends sit in (a head stays on top), and the marks
88
+ // it runs across (a threshold over its bars). Null: none, and it paints last,
89
+ // above whatever else it crosses. One relation with the draft check: it reads
90
+ // these same predicates off the render (connector-occlusion.ts), which leaves
91
+ // a node holding an end free either way.
92
+ function seatOf({ points, capped, strokePx = 0 }, placed) {
93
+ const segs = points.slice(1).map((b, i) => [points[i], b]);
94
+ const ends = [points[0], points.at(-1)];
95
+ const shapes = placed.map(({ el }) => shapeOf(el));
96
+ const endRects = ends.map((p) => ({ x: p.x, y: p.y, w: 0, h: 0 }));
97
+ for (const p of ends) {
98
+ const node = touchedShape(p, shapes, strokePx / 2 + 2);
99
+ if (node) endRects.push(node.rect);
100
+ }
101
+ // A bead reads as a dot on its line whichever paints first: drawn on it.
102
+ const stations = new Set(stationsOf(points, shapes, endRects));
103
+ const samples = routeSamples(points);
104
+ let after = -1;
105
+ const behind = [];
106
+ placed.forEach(({ el, at }, k) => {
107
+ const across = () => segs.some(([a, b]) => crosses(a, b, el));
108
+ const holdsEnd = ends.some((p) => holds(el, p));
109
+ if (isGround(shapes[k], points, false, samples) || (capped && holdsEnd) || (MARKS.has(el.kind) && across())) after = Math.max(after, at);
110
+ else if (stations.has(shapes[k]) || holdsEnd) behind.push(at);
111
+ });
112
+ const first = Math.min(...behind.filter((at) => at > after));
113
+ return Number.isFinite(first) ? first : null;
114
+ }
115
+
116
+ /**
117
+ * The slide's JSX lines with the line directives placed. `lines`: the written
118
+ * elements in paint order; `placed`: `[{ el, at }]` — each painted element's
119
+ * rect and the index of its first JSX line; `entries`: the line directives in
120
+ * order, `{ jsx, points, capped, strokePx }` for a free line (glued and headed
121
+ * relations, marks and plot strokes carry no `points`). A free line goes right
122
+ * before its first station; every other directive paints last, in order.
123
+ */
124
+ export function seatLines(lines, placed, entries) {
125
+ const before = new Map();
126
+ const last = [];
127
+ for (const x of entries) {
128
+ const at = x.points?.length >= 2 ? seatOf(x, placed) : null;
129
+ if (at == null) last.push(x.jsx);
130
+ else before.set(at, [...(before.get(at) ?? []), x.jsx]);
131
+ }
132
+ return [...lines.flatMap((l, i) => [...(before.get(i) ?? []), l]), ...last];
133
+ }
@@ -0,0 +1,135 @@
1
+ // The checks both harnesses run on a plan.md, with one wording: the
2
+ // `## Deliverable` block and the red thread against the storyboards (the deck
3
+ // frame, docs/slidemind-cli-connect/17-deck-frame.md), and the full content
4
+ // draft every body slide owes after sign-off (workflow/plan.md). Both refuse to
5
+ // open a NEW plan round on a failing plan — the CLI's `slidemind plan`, the
6
+ // add-in's `present_plan_for_review` — and never re-check an answer the user
7
+ // already gave. The content draft is checked where it is owed: the CLI's deck
8
+ // stage and concepts refusal, the add-in's `use_design_worker`.
9
+ //
10
+ // Canonical here; the add-in ships a byte-identical mirror
11
+ // (scripts/sync-transcription-runtime.mjs). Pure: no Node builtins, and what a
12
+ // layout's job is stays with each harness — the caller passes it in.
13
+
14
+ import { extractRedThreadMessages, splitPlanMarkdownIntoSegments } from "./storyboard-markdown.mjs";
15
+
16
+ /** The head of a refused plan round; each harness adds its own next step. */
17
+ export const PLAN_NOT_PRESENTED = "Not presented: the plan does not agree with itself or with the design.";
18
+
19
+ /**
20
+ * What a body slide's content draft holds — one wording for every message
21
+ * that asks for it. The rule itself is workflow/plan.md's.
22
+ */
23
+ export const CONTENT_DRAFT =
24
+ "each body slide's full content draft, written from its approved key points as one entry of connected prose that says everything the slide has to say: " +
25
+ "every key point as a plain sentence, in order, with the facts, figures, examples and caveats behind it and things it compares stated in the same terms — " +
26
+ "never the key points restated, a few words per point, headings, bullets or a line per point, and no word on presentation; " +
27
+ "drawn from the user's context, online research and your own knowledge in that order, every number from the user's context or your research (workflow/plan.md)";
28
+
29
+ /** The `## Deliverable` block, read leniently: the agent writes it and the user edits it by hand. */
30
+ export function parseDeliverable(markdown) {
31
+ const m = /^##\s+Deliverable\s*\n([\s\S]*?)(?=^##\s|(?![\s\S]))/m.exec(markdown ?? "");
32
+ if (!m) return null;
33
+ const get = (key) => {
34
+ const r = new RegExp(`^\\s*-?\\s*${key}\\s*:\\s*(.+)$`, "im").exec(m[1]);
35
+ return r ? r[1].trim() : null;
36
+ };
37
+ const shape = (get("shape") ?? "").toLowerCase().replace(/[^a-z]/g, "");
38
+ const body = Number.parseInt(get("body slides") ?? get("body") ?? "", 10);
39
+ const frameRaw = (get("frame") ?? "").toLowerCase();
40
+ const frame = /^(none|-|—|no)\b/.test(frameRaw)
41
+ ? []
42
+ : frameRaw.split(/[,;/·]+/).map((x) => x.trim().replace(/s$/, "")).filter(Boolean);
43
+ return {
44
+ shape: shape === "deck" || shape === "slides" ? shape : null,
45
+ bodySlides: Number.isFinite(body) ? body : null,
46
+ frame,
47
+ target: get("target"),
48
+ };
49
+ }
50
+
51
+ const pcWholeSlideRebuild = (data) =>
52
+ (data.assetUses ?? []).some((u) => u?.fidelity === "reconstruct" && u?.reconstructionScope === "slide");
53
+
54
+ /**
55
+ * The body slides still owing their full content draft. A frame slide (chrome
56
+ * with text slots; `isFrameSlide` is the harness's rule) owes none, nor does a
57
+ * slide rebuilt whole from a supplied image: its words are the image's.
58
+ * `drafted` names slides whose draft travels beside the plan (the add-in's
59
+ * `slideContent`).
60
+ */
61
+ export function bodySlidesWithoutContent(markdown, { isFrameSlide, drafted = new Set() }) {
62
+ return splitPlanMarkdownIntoSegments(markdown ?? "")
63
+ .filter((s) => s.type === "storyboard" && Number.isFinite(s.data?.slide))
64
+ .filter((s) => !isFrameSlide(s.data) && !pcWholeSlideRebuild(s.data) && !s.data.content?.length && !drafted.has(s.data.slide))
65
+ .map((s) => s.data.slide);
66
+ }
67
+
68
+ /**
69
+ * `[plan:error]` lines for a plan whose deliverable block, red thread and
70
+ * storyboards do not agree with each other or with the design's layouts — a
71
+ * plan round does not open while any stands. `slideJob(data)` is the slide's
72
+ * planned job (`content` or a frame kind), `isFrameJob(job)` the harness's
73
+ * frame test, `offeredJobs` every layout job the design offers.
74
+ */
75
+ export function planCheckErrors(markdown, { slideJob, isFrameJob, offeredJobs = [] }) {
76
+ const out = [];
77
+ const beats = [];
78
+ for (const segment of splitPlanMarkdownIntoSegments(markdown ?? "")) {
79
+ if (segment.type !== "storyboard" || !segment.data) continue;
80
+ const data = segment.data;
81
+ if (!data.role) out.push(`[plan:error] slide ${data.slide} has no role: write \`"role": "body"\` or \`"role": "frame"\` with its frameKind.`);
82
+ if (data.role === "frame" && !data.frameKind) out.push(`[plan:error] frame slide ${data.slide} needs a frameKind: cover, agenda, divider or closing.`);
83
+ beats.push({ slide: data.slide, job: slideJob(data) });
84
+ }
85
+ if (beats.length === 0) return out;
86
+ const frameBeats = beats.filter((b) => isFrameJob(b.job));
87
+ const bodyBeats = beats.filter((b) => !isFrameJob(b.job));
88
+ // One move per body slide: the review's storyline rail pairs them one-to-one.
89
+ const moves = extractRedThreadMessages(markdown ?? "").length;
90
+ if (moves !== bodyBeats.length) {
91
+ out.push(
92
+ `[plan:error] the red thread has ${moves} move${moves === 1 ? "" : "s"} for ${bodyBeats.length} body slide${bodyBeats.length === 1 ? "" : "s"} — write one move per body slide under \`## Red thread\`; frame slides go on its \`Frame:\` line.`,
93
+ );
94
+ }
95
+ const d = parseDeliverable(markdown);
96
+ if (!d) {
97
+ out.push(
98
+ "[plan:error] the plan has no `## Deliverable` block (shape: deck | slides, body slides: N, frame: <kinds or none>, target). " +
99
+ "Without it the frame (cover, agenda, dividers, closing) is unspecified.",
100
+ );
101
+ return out;
102
+ }
103
+ const offered = [...new Set(offeredJobs.filter((job) => isFrameJob(job)))];
104
+ // A design with no frame layout ships a deck without a frame; the plan names what is left out.
105
+ if (d.shape === "deck" && frameBeats.length === 0 && offered.length > 0) {
106
+ out.push(
107
+ `[plan:error] shape is deck but no storyboard is a frame slide — this design offers ${offered.join(", ")}; a deck ships its frame (workflow/flow.md → Loose slides or a whole presentation).`,
108
+ );
109
+ }
110
+ if (d.shape === "slides" && frameBeats.length > 0 && d.frame.length === 0) {
111
+ out.push(
112
+ `[plan:error] shape is slides but slide${frameBeats.length > 1 ? "s" : ""} ${frameBeats.map((b) => b.slide).join(", ")} ${frameBeats.length > 1 ? "are frame slides" : "is a frame slide"} (${[...new Set(frameBeats.map((b) => b.job))].join(", ")}) — loose slides extend a deck that has its own frame; drop ${frameBeats.length > 1 ? "them" : "it"} or set shape: deck.`,
113
+ );
114
+ }
115
+ if (d.bodySlides != null && d.bodySlides !== bodyBeats.length) {
116
+ out.push(
117
+ `[plan:error] Deliverable says body slides: ${d.bodySlides}, the storyboards carry ${bodyBeats.length} body slide${bodyBeats.length === 1 ? "" : "s"} (frame slides excluded).`,
118
+ );
119
+ }
120
+ for (const want of d.frame) {
121
+ if (!isFrameJob(want)) continue;
122
+ if (!frameBeats.some((b) => b.job === want)) {
123
+ out.push(`[plan:error] frame lists ${want} but no storyboard declares that frameKind.`);
124
+ }
125
+ }
126
+ const cover = frameBeats.find((b) => b.job === "cover");
127
+ if (cover && beats.some((b) => b.slide < cover.slide)) {
128
+ out.push(`[plan:error] the cover is slide ${cover.slide} — a cover is slide 1.`);
129
+ }
130
+ const closing = frameBeats.find((b) => b.job === "closing");
131
+ if (closing && beats.some((b) => b.slide > closing.slide)) {
132
+ out.push(`[plan:error] the closing is slide ${closing.slide} but slides follow it — a closing is the last slide.`);
133
+ }
134
+ return out;
135
+ }
@@ -0,0 +1,19 @@
1
+ // One bullet per plan-gate comment, exactly as the agent reads it:
2
+ // `- [Slide 3: "Align the next outcome"] text` or `- [Whole story] text`.
3
+ // Continuation lines are indented under their bullet so a multi-line note
4
+ // cannot read as a second comment. The anchor words stay English in every UI
5
+ // language: they are agent-facing.
6
+ //
7
+ // Canonical here; the add-in's plan review formats its comments with a
8
+ // byte-identical mirror (scripts/sync-transcription-runtime.mjs), so the
9
+ // in-app manager and the CLI agent read one format. Pure, browser-safe.
10
+
11
+ /** `comment`: `{ scope: "slide" | "story", slideIndex, slideHeading, text }`. */
12
+ export function planCommentBullet(comment) {
13
+ const anchor =
14
+ comment.scope === "slide"
15
+ ? `Slide ${comment.slideIndex ?? "?"}${comment.slideHeading ? `: "${comment.slideHeading}"` : ""}`
16
+ : "Whole story";
17
+ const [first, ...rest] = String(comment.text).split("\n");
18
+ return [`- [${anchor}] ${first}`, ...rest.map((line) => ` ${line}`)].join("\n");
19
+ }
@@ -0,0 +1,95 @@
1
+ // The generic plan view: plan.md as ONE Markdown value, read and saved by the
2
+ // persistent workspace on a core-owned deck. The value is the complete file;
3
+ // the hash is `sha256:` over its UTF-8 bytes, which is also the plan-approval
4
+ // task subject (plan.mjs hashes the same bytes for plan-review.json). Markdown
5
+ // has no fields to merge, so a save lands only when one side is unchanged since
6
+ // the editor's base — otherwise it is a whole-file `local_conflict`.
7
+ //
8
+ // A deck whose plan page belongs to an extension (a pack's record) is refused
9
+ // here with the command that opens it: the pack's adapter serves that plan.
10
+ // Handlers return `{ status, body }` the way the pack's reviewHandlers do, so a
11
+ // daemon route and a standalone shell reply identically.
12
+
13
+ import crypto from "node:crypto";
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+
17
+ import { planPageOwner } from "../lib/deck-paths.mjs";
18
+ import { writePlanPage } from "./plan.mjs";
19
+
20
+ export const GENERIC_PLAN_ADAPTER = "core-plan";
21
+
22
+ export function hashPlanText(value) {
23
+ return "sha256:" + crypto.createHash("sha256").update(value, "utf8").digest("hex");
24
+ }
25
+
26
+ function refusal(status, code, message, details) {
27
+ return { status, body: { ok: false, error: { code, message, retryable: false, ...(details ? { details } : {}) } } };
28
+ }
29
+
30
+ function ownerRefusal(deckDir) {
31
+ const owner = planPageOwner(deckDir);
32
+ if (owner.owner === "core") return null;
33
+ return refusal(
34
+ 409,
35
+ "extension_owned",
36
+ `the plan of this deck belongs to ${owner.owner}` +
37
+ (owner.reviewCommand ? ` — open it with \`${owner.reviewCommand}\`` : "; its owner is not installed here"),
38
+ { owner: owner.owner, reviewCommand: owner.reviewCommand, recordPath: owner.recordPath },
39
+ );
40
+ }
41
+
42
+ function readPlanFile(deckDir) {
43
+ try {
44
+ return fs.readFileSync(path.join(deckDir, "plan.md"), "utf8");
45
+ } catch (err) {
46
+ if (err.code === "ENOENT") return null;
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ /** `{ value, hash, format, adapterId }` — or 404 `plan_missing` / 409 `extension_owned`. */
52
+ export function readGenericPlan(deckDir) {
53
+ const blocked = ownerRefusal(deckDir);
54
+ if (blocked) return blocked;
55
+ const value = readPlanFile(deckDir);
56
+ if (value === null) return refusal(404, "plan_missing", "this deck has no plan.md yet");
57
+ return { status: 200, body: { value, hash: hashPlanText(value), format: "markdown", adapterId: GENERIC_PLAN_ADAPTER } };
58
+ }
59
+
60
+ /**
61
+ * Save the complete plan.md from an editor that loaded `base`. Whole-file
62
+ * three-way: the write lands when nobody else wrote since base, is a no-op
63
+ * when the editor changed nothing (or the file already holds its text), and
64
+ * is a 409 `local_conflict` carrying current/base/mine otherwise — nothing is
65
+ * written on a conflict. A real write re-renders plan.html like every other
66
+ * writer of the record.
67
+ */
68
+ export function saveGenericPlan(deckDir, { base, value } = {}) {
69
+ const blocked = ownerRefusal(deckDir);
70
+ if (blocked) return blocked;
71
+ if (typeof value !== "string") return refusal(422, "validation_failed", "value must be the complete plan.md text");
72
+ if (!base || typeof base.value !== "string" || base.hash !== hashPlanText(base.value)) {
73
+ return refusal(422, "validation_failed", "base.hash does not match base.value");
74
+ }
75
+ const current = readPlanFile(deckDir) ?? "";
76
+ let next;
77
+ if (current === base.value) next = value;
78
+ else if (value === base.value || current === value) next = current;
79
+ else {
80
+ return refusal(409, "local_conflict", "plan.md changed since this editor loaded it", {
81
+ current: { value: current, hash: hashPlanText(current) },
82
+ base,
83
+ mine: { value, hash: hashPlanText(value) },
84
+ });
85
+ }
86
+ const changed = next !== current;
87
+ if (changed) {
88
+ const planPath = path.join(deckDir, "plan.md");
89
+ const tmp = `${planPath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
90
+ fs.writeFileSync(tmp, next);
91
+ fs.renameSync(tmp, planPath);
92
+ writePlanPage(deckDir);
93
+ }
94
+ return { status: 200, body: { value: next, hash: hashPlanText(next), savedLocally: true, changed } };
95
+ }
@@ -11,9 +11,9 @@
11
11
  *
12
12
  * Pure string logic — no DOM, no Office (unit-testable).
13
13
  *
14
- * 1:1 port of the add-in's plan-markdown-edit.ts. Top-level helpers are
15
- * prefixed `pme…` to keep the single-IIFE browser bundle collision-free;
16
- * exported API names match the add-in verbatim.
14
+ * Canonical here; the add-in imports a byte-identical mirror
15
+ * (scripts/sync-transcription-runtime.mjs). Top-level helpers are prefixed
16
+ * `pme…` to keep the single-IIFE browser bundle collision-free.
17
17
  */
18
18
 
19
19
  export function isEditableKind(kind) {