slidemind 0.3.5 → 0.3.13
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +5 -5
- package/bin/slidemind.mjs +520 -182
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +263 -104
- package/skills/search/SKILL.md +9 -5
- package/src/bridge/build-and-push.mjs +3 -3
- package/src/bridge/bundle.mjs +91 -36
- package/src/bridge/capture.mjs +86 -8
- package/src/bridge/client.mjs +59 -23
- package/src/bridge/config.mjs +21 -2
- package/src/bridge/engine-download.mjs +26 -7
- package/src/bridge/recolor.mjs +46 -0
- package/src/bridge/setup-claim.mjs +7 -1
- package/src/builders/build.mjs +319 -5
- package/src/builders/gate-server.mjs +25 -36
- package/src/builders/gate-session.js +28 -48
- package/src/builders/lint-fix.mjs +88 -0
- package/src/builders/plan-overlay.js +507 -136
- package/src/builders/plan.mjs +337 -60
- package/src/builders/review-overlay.js +126 -109
- package/src/builders/review.mjs +118 -64
- package/src/builders/storyboard-markdown.mjs +15 -0
- package/src/builders/storyboard-schema.mjs +23 -6
- package/src/builders/visual-families.mjs +20 -4
- package/src/lib/adopt.mjs +10 -6
- package/src/lib/conversions.mjs +26 -6
- package/src/lib/overwrite-guard.mjs +14 -1
- package/src/lib/reconcile.mjs +15 -4
- package/src/pptx/tags.mjs +49 -0
|
@@ -22,8 +22,9 @@ const GATE_SESSION_PATH = path.join(__dirname, "gate-session.js");
|
|
|
22
22
|
// overlays as CFG.pollMs / CFG.pollTtlMs:
|
|
23
23
|
// GRACE_MS > POLL_MS so a waiting tab always lands one poll inside the
|
|
24
24
|
// open-browser grace window;
|
|
25
|
-
// POLL_TTL_MS
|
|
26
|
-
//
|
|
25
|
+
// POLL_TTL_MS caps how long an abandoned tab keeps polling for a NEXT round
|
|
26
|
+
// (the gates themselves have no decision timeout — a live gate
|
|
27
|
+
// serves its own tab directly; polling only matters between runs).
|
|
27
28
|
// One favicon for every page slidemind opens in a browser (gates, previews,
|
|
28
29
|
// sign-in): an inline data URI, so no server needs a /favicon.ico route and
|
|
29
30
|
// the static preview file stays self-contained.
|
|
@@ -87,7 +88,14 @@ export function serveStaticFile({ roots, pathname, res }) {
|
|
|
87
88
|
continue; // missing here — try the next root
|
|
88
89
|
}
|
|
89
90
|
const ext = path.extname(resolved).toLowerCase();
|
|
90
|
-
|
|
91
|
+
// Gate tabs deliberately reuse a stable localhost URL across review rounds
|
|
92
|
+
// while build.mjs overwrites CSS, fonts and assets in place. Without an
|
|
93
|
+
// explicit no-cache policy the browser can combine fresh HTML with stale
|
|
94
|
+
// design resources, making the review diverge from the generated PPTX.
|
|
95
|
+
res.writeHead(200, {
|
|
96
|
+
"Content-Type": MIME[ext] || "application/octet-stream",
|
|
97
|
+
"Cache-Control": "no-store, max-age=0",
|
|
98
|
+
});
|
|
91
99
|
res.end(data);
|
|
92
100
|
return true;
|
|
93
101
|
}
|
|
@@ -96,8 +104,8 @@ export function serveStaticFile({ roots, pathname, res }) {
|
|
|
96
104
|
|
|
97
105
|
// Read a JSON POST body off a gate request and hand the parsed object to `cb`
|
|
98
106
|
// (an unparseable/absent body becomes `{}` — a gate normalizes its own fields
|
|
99
|
-
// anyway). Shared by every gate POST route (
|
|
100
|
-
// the
|
|
107
|
+
// anyway). Shared by every gate POST route (both gates' decision POSTs), so
|
|
108
|
+
// the 5 MB flood guard lives in
|
|
101
109
|
// exactly one place. The guard matters: these servers bind loopback but any
|
|
102
110
|
// page in the user's browser can POST to a known localhost port.
|
|
103
111
|
export function readJsonBody(req, cb) {
|
|
@@ -130,13 +138,13 @@ export function readJsonBody(req, cb) {
|
|
|
130
138
|
// ---------------------------------------------------------------------------
|
|
131
139
|
// shared gate ROUTES + comment normalization
|
|
132
140
|
//
|
|
133
|
-
// Both gates ran byte-identical copies of the
|
|
134
|
-
//
|
|
141
|
+
// Both gates ran byte-identical copies of the approve-409 guard and the
|
|
142
|
+
// row-normalization preamble. One copy, two per-gate field shapes.
|
|
135
143
|
// ---------------------------------------------------------------------------
|
|
136
144
|
|
|
137
145
|
// The identity + text core every gate row shares: `id` dedupes a client retry
|
|
138
146
|
// of the same batch, `round` says which round the comment was written against
|
|
139
|
-
// (
|
|
147
|
+
// (rounds print it so the agent knows which render a pin sits on), and a row
|
|
140
148
|
// without text is dropped — an empty row prints as an instruction-free bullet.
|
|
141
149
|
// `fields(c)` contributes the gate's own anchor columns.
|
|
142
150
|
export function normalizeGateComments(raw, fields) {
|
|
@@ -161,37 +169,18 @@ export function normalizeGateComments(raw, fields) {
|
|
|
161
169
|
return out;
|
|
162
170
|
}
|
|
163
171
|
|
|
164
|
-
// The
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// `isSuperseded` (the gate's settled check): once another tab's drain or
|
|
169
|
-
// decision has resolved this run, a late backlog must NOT be acked "drained" —
|
|
170
|
-
// the client would flip its rows to "sent" while gate.resolve is a no-op and
|
|
171
|
-
// the reviewer's comments silently vanish. "superseded" keeps them queued, and
|
|
172
|
-
// the tab's heartbeat poll carries them into the next run.
|
|
173
|
-
export function serveBacklogPost(req, res, { normalize, onDrained, isSuperseded = () => false }) {
|
|
174
|
-
readJsonBody(req, (payload) => {
|
|
175
|
-
const comments = normalize(payload.comments);
|
|
176
|
-
const state = !comments.length ? "parked" : isSuperseded() ? "superseded" : "drained";
|
|
177
|
-
res.writeHead(200, { "Content-Type": MIME[".json"] });
|
|
178
|
-
res.end(JSON.stringify({ ok: state !== "superseded", state }));
|
|
179
|
-
if (state === "drained") onDrained(comments);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// The decision POST, with the shared 409 guard: once this run has settled —
|
|
184
|
-
// a backlog drained (the displayed round is superseded by comments the agent
|
|
185
|
-
// has already been told to work) or another tab's decision landed — a late
|
|
172
|
+
// The decision POST, with the shared 409 guard: once this run has settled
|
|
173
|
+
// (another tab's decision landed — every waiting tab reloads into a new round,
|
|
174
|
+
// so two windows posting into one run is the normal multi-window case) a late
|
|
186
175
|
// decision of EITHER kind is refused. Acking a late "comments" with 200 would
|
|
187
|
-
// mark them sent while gate.resolve is a no-op; the 409
|
|
188
|
-
//
|
|
176
|
+
// mark them sent while gate.resolve is a no-op; the 409 lets the tab keep them
|
|
177
|
+
// as unsent drafts for the next round.
|
|
189
178
|
export function serveDecisionPost(req, res, { normalize, isSuperseded, onDecision }) {
|
|
190
179
|
readJsonBody(req, (payload) => {
|
|
191
180
|
const decision = payload.decision === "comments" ? "comments" : "approve";
|
|
192
181
|
if (isSuperseded()) {
|
|
193
182
|
res.writeHead(409, { "Content-Type": MIME[".json"] });
|
|
194
|
-
res.end(JSON.stringify({ ok: false, error: "
|
|
183
|
+
res.end(JSON.stringify({ ok: false, error: "round_settled" }));
|
|
195
184
|
return;
|
|
196
185
|
}
|
|
197
186
|
res.writeHead(200, { "Content-Type": MIME[".json"] });
|
|
@@ -217,7 +206,7 @@ export function stripModuleSyntax(src) {
|
|
|
217
206
|
|
|
218
207
|
// gate-session.js as a browser IIFE exposing window.__SLIDEMIND_GATE__. Both
|
|
219
208
|
// gate pages inline this BEFORE their overlay script — the overlays' whole
|
|
220
|
-
// ledger/
|
|
209
|
+
// ledger/round machinery is that one shared factory.
|
|
221
210
|
export function bundleGateSession() {
|
|
222
211
|
return (
|
|
223
212
|
"(function () {\n" +
|
|
@@ -249,8 +238,8 @@ export function createDecisionGate({
|
|
|
249
238
|
isSettled = true;
|
|
250
239
|
resolveDecision(value);
|
|
251
240
|
},
|
|
252
|
-
// Whether a decision
|
|
253
|
-
//
|
|
241
|
+
// Whether a decision already resolved this run — the POST route refuses
|
|
242
|
+
// late input on it, because a resolve after the first is a no-op.
|
|
254
243
|
settled() {
|
|
255
244
|
return isSettled;
|
|
256
245
|
},
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// Shared browser-side session machinery for the CLI's two blocking gates —
|
|
2
2
|
// the plan sign-off gate (plan-overlay.js) and the spatial preview gate
|
|
3
3
|
// (review-overlay.js). Both overlays used to carry byte-identical copies of the
|
|
4
|
-
// sessionStorage ledger, the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// sessionStorage ledger, the round-transition detection, the round-ready
|
|
5
|
+
// announcement and the heartbeat poll; a fix landed in one and not the other is
|
|
6
|
+
// exactly how the two drifted apart. This module is the ONE copy.
|
|
7
|
+
//
|
|
8
|
+
// ROUNDS ARE SYNCHRONOUS (add-in parity). A round is: the reviewer comments,
|
|
9
|
+
// sends, the agent works, the next run presents a new round. Nothing is queued
|
|
10
|
+
// behind the agent's back: while the agent works the gate is not accepting
|
|
11
|
+
// input, and the tab says so. (An earlier design let the tab keep collecting
|
|
12
|
+
// comments in sessionStorage and flushed them to the next run as a "drain
|
|
13
|
+
// round" — that made the add-in and the CLI behave differently and was
|
|
14
|
+
// removed.)
|
|
7
15
|
//
|
|
8
16
|
// It is plain dependency-free ESM so a test can `import` it directly, and the
|
|
9
17
|
// gates inline it into their page (import-free, `export ` stripped) as
|
|
@@ -24,7 +32,7 @@
|
|
|
24
32
|
|
|
25
33
|
// The wire shape each gate's server persists and prints. Per-gate because the
|
|
26
34
|
// anchors differ (spatial pin vs. slide-anchored comment), shared here because
|
|
27
|
-
//
|
|
35
|
+
// the gate's normalizer has to accept exactly this.
|
|
28
36
|
const WIRE = {
|
|
29
37
|
// Preview pins carry FULL-PRECISION 0..1 fractions. Rounding to whole percent
|
|
30
38
|
// on the wire cost ±6px on a 1280px stage and collapsed sub-1% offsets; the
|
|
@@ -66,9 +74,9 @@ export function createGateSession(cfg) {
|
|
|
66
74
|
const store = cfg.storage;
|
|
67
75
|
const doFetch = cfg.fetch;
|
|
68
76
|
const wire = cfg.wire || WIRE[name];
|
|
69
|
-
// Deck-scoped, NOT round-scoped: the rows carry their own round, and
|
|
70
|
-
//
|
|
71
|
-
//
|
|
77
|
+
// Deck-scoped, NOT round-scoped: the rows carry their own round, and the
|
|
78
|
+
// ledger has to survive the reload INTO the next round (a new round's key
|
|
79
|
+
// would differ by definition).
|
|
72
80
|
const LEDGER_KEY = "slidemind-" + name + "-ledger:" + deck;
|
|
73
81
|
const CTX_KEY = "slidemind-" + name + "-ctx:" + deck;
|
|
74
82
|
const ROUND_READY_FLAG = "slidemind-" + name + "-round-ready";
|
|
@@ -113,19 +121,18 @@ export function createGateSession(cfg) {
|
|
|
113
121
|
}, DRAFT_DEBOUNCE_MS);
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
// The backlog: written in this tab, not yet delivered to any gate run.
|
|
117
|
-
function queued(ledger) {
|
|
118
|
-
return (ledger || []).filter(function (c) {
|
|
119
|
-
return c.status === "queued";
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
124
|
// Restore this tab's session and work out whether THIS load is a new round.
|
|
124
125
|
// Returns the ledger plus the round state; the caller applies its own ctx
|
|
125
126
|
// fields from the returned `ctx` and its own round-arrival cosmetics.
|
|
126
127
|
function restore(seed) {
|
|
127
128
|
let ledger = read(LEDGER_KEY, []) || [];
|
|
128
129
|
if (!Array.isArray(ledger)) ledger = [];
|
|
130
|
+
// A tab left open across the upgrade from the queueing release still holds
|
|
131
|
+
// "queued" rows: undelivered, so they are the reviewer's drafts — never
|
|
132
|
+
// silently relabelled as sent.
|
|
133
|
+
ledger.forEach(function (c) {
|
|
134
|
+
if (c.status === "queued") c.status = "draft";
|
|
135
|
+
});
|
|
129
136
|
let ctx = read(CTX_KEY, null);
|
|
130
137
|
if (!ctx || typeof ctx !== "object") ctx = null;
|
|
131
138
|
let roundNo = ctx && typeof ctx.round === "number" ? ctx.round : 1;
|
|
@@ -154,41 +161,16 @@ export function createGateSession(cfg) {
|
|
|
154
161
|
row.status = s.status === "addressed" ? "addressed" : "sent";
|
|
155
162
|
ledger.push(row);
|
|
156
163
|
});
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// POST the queued backlog to the gate. A non-empty backlog resolves that run
|
|
163
|
-
// at once (drain round), so the rows flip to "sent". A failure (the command
|
|
164
|
-
// already exited) leaves them queued for the next run — nothing is dropped.
|
|
165
|
-
function flushBacklog(ledger) {
|
|
166
|
-
const pending = queued(ledger);
|
|
167
|
-
if (!pending.length) return Promise.resolve(false);
|
|
168
|
-
return doFetch(cfg.backlogEndpoint, {
|
|
169
|
-
method: "POST",
|
|
170
|
-
headers: { "Content-Type": "application/json" },
|
|
171
|
-
body: JSON.stringify({ comments: pending.map(wire) }),
|
|
172
|
-
})
|
|
173
|
-
.then(function (r) {
|
|
174
|
-
return r.ok ? r.json() : null;
|
|
175
|
-
})
|
|
176
|
-
.then(function (data) {
|
|
177
|
-
if (!data || data.state !== "drained") return false;
|
|
178
|
-
pending.forEach(function (c) {
|
|
179
|
-
c.status = "sent";
|
|
180
|
-
});
|
|
181
|
-
saveLedger(ledger);
|
|
182
|
-
return true;
|
|
183
|
-
})
|
|
184
|
-
.catch(function () {
|
|
185
|
-
return false; // gate gone — the rows stay queued for the next run
|
|
186
|
-
});
|
|
164
|
+
// WORKING survives a plain refresh of the same run (the reviewer already
|
|
165
|
+
// sent this round; approving the same render again would only 409), and
|
|
166
|
+
// ends the moment a newer run serves the page.
|
|
167
|
+
const working = !newRound && !!(ctx && ctx.working);
|
|
168
|
+
return { ledger, roundNo, working, newRound, ctx };
|
|
187
169
|
}
|
|
188
170
|
|
|
189
171
|
// Where a row stands, for the ledger rail / sidebar chip.
|
|
190
172
|
function statusLabel(c) {
|
|
191
|
-
if (c.status === "
|
|
173
|
+
if (c.status === "draft") return "not sent yet";
|
|
192
174
|
if (c.status === "addressed") {
|
|
193
175
|
return "addressed" + (c.addressedInRound ? " · round " + c.addressedInRound : "");
|
|
194
176
|
}
|
|
@@ -225,7 +207,7 @@ export function createGateSession(cfg) {
|
|
|
225
207
|
}
|
|
226
208
|
location.reload();
|
|
227
209
|
}
|
|
228
|
-
// Always consumed at boot (a
|
|
210
|
+
// Always consumed at boot (a refresh must not leave the flag armed for a
|
|
229
211
|
// later parked load), but only ACTED on when this round parks.
|
|
230
212
|
function consumeRoundFlag() {
|
|
231
213
|
try {
|
|
@@ -341,9 +323,7 @@ export function createGateSession(cfg) {
|
|
|
341
323
|
saveLedger,
|
|
342
324
|
saveCtx,
|
|
343
325
|
saveCtxSoon,
|
|
344
|
-
queued,
|
|
345
326
|
restore,
|
|
346
|
-
flushBacklog,
|
|
347
327
|
statusLabel,
|
|
348
328
|
notifyPermission,
|
|
349
329
|
requestNotifyPermission,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Deterministic autofixes for the two lint warnings agents statistically never
|
|
2
|
+
// clear by hand (measured across seven sim campaigns: 7-66 unique warnings
|
|
3
|
+
// left standing at the final build in 8/10 sessions, resistant to four
|
|
4
|
+
// instruction shapes — see experiments/LEDGER.md). Both rewrites are safe
|
|
5
|
+
// because the fixed value is derived from the file itself, never guessed:
|
|
6
|
+
//
|
|
7
|
+
// 1. An inline `<svg>` with a `viewBox` but no explicit `width`/`height`
|
|
8
|
+
// gets both, taken verbatim from the viewBox — the aspect the author
|
|
9
|
+
// drew is preserved exactly, and the render's aspect check stops
|
|
10
|
+
// drifting with the flex cell.
|
|
11
|
+
// 2. `letter-spacing: <n>em` becomes px ONLY when the same declaration
|
|
12
|
+
// block names a px `font-size` to multiply against; without that context
|
|
13
|
+
// the correct px value is unknowable, so the declaration is left for the
|
|
14
|
+
// lint warning to report.
|
|
15
|
+
//
|
|
16
|
+
// Everything else the lint engine flags (raw colors, token choices, spacing
|
|
17
|
+
// scales) is a judgment call and deliberately NOT auto-fixed.
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
|
|
22
|
+
// Declaration blocks inside a <style> string or a style attribute: convert
|
|
23
|
+
// `letter-spacing: Nem` when the SAME block carries `font-size: Mpx`.
|
|
24
|
+
const DECL_BLOCK = /\{[^{}]*\}/g;
|
|
25
|
+
const LS_EM = /letter-spacing\s*:\s*(-?[\d.]+)em/g;
|
|
26
|
+
const FS_PX = /font-size\s*:\s*([\d.]+)px/;
|
|
27
|
+
|
|
28
|
+
function fixLetterSpacingBlock(block) {
|
|
29
|
+
const fs_ = block.match(FS_PX);
|
|
30
|
+
if (!fs_) return null;
|
|
31
|
+
const base = Number(fs_[1]);
|
|
32
|
+
if (!Number.isFinite(base) || base <= 0) return null;
|
|
33
|
+
let changed = false;
|
|
34
|
+
const fixed = block.replace(LS_EM, (m, em) => {
|
|
35
|
+
const px = Math.round(Number(em) * base * 100) / 100;
|
|
36
|
+
if (!Number.isFinite(px)) return m;
|
|
37
|
+
changed = true;
|
|
38
|
+
return `letter-spacing: ${px}px`;
|
|
39
|
+
});
|
|
40
|
+
return changed ? fixed : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Fix one slide source. Returns { src, fixes } — fixes is a list of labels. */
|
|
44
|
+
export function fixSlideSource(src) {
|
|
45
|
+
const fixes = [];
|
|
46
|
+
// NOTE: svg pinning (adding width/height from the viewBox) was measured to
|
|
47
|
+
// HARM fidelity and is disabled: the viewBox dimensions are drawing-space
|
|
48
|
+
// coordinates, not the layout size the author's flex cell implied, so the
|
|
49
|
+
// pinned svg resizes out of its cell (collisions/clipping) — the lint
|
|
50
|
+
// warning was satisfied by breaking the layout (sim batch 2026-08-28T06-34-11:
|
|
51
|
+
// fidelity 0.809 → 0.660). The correct px size is not statically knowable;
|
|
52
|
+
// that warning stays a judgment call for the author.
|
|
53
|
+
let out = src;
|
|
54
|
+
out = out.replace(DECL_BLOCK, (block) => {
|
|
55
|
+
const fixed = fixLetterSpacingBlock(block);
|
|
56
|
+
if (!fixed) return block;
|
|
57
|
+
fixes.push("letter-spacing-px");
|
|
58
|
+
return fixed;
|
|
59
|
+
});
|
|
60
|
+
return { src: out, fixes };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fix every slides/*.jsx in a deck, writing only changed files.
|
|
65
|
+
* Returns { files: <changed file count>, fixes: <total fix count> }.
|
|
66
|
+
*/
|
|
67
|
+
export function fixDeckSlides(deckDir) {
|
|
68
|
+
const slidesDir = path.join(deckDir, "slides");
|
|
69
|
+
let files = 0;
|
|
70
|
+
let fixes = 0;
|
|
71
|
+
let entries = [];
|
|
72
|
+
try {
|
|
73
|
+
entries = fs.readdirSync(slidesDir).filter((f) => f.endsWith(".jsx"));
|
|
74
|
+
} catch {
|
|
75
|
+
return { files, fixes };
|
|
76
|
+
}
|
|
77
|
+
for (const f of entries) {
|
|
78
|
+
const p = path.join(slidesDir, f);
|
|
79
|
+
const src = fs.readFileSync(p, "utf8");
|
|
80
|
+
const res = fixSlideSource(src);
|
|
81
|
+
if (res.fixes.length) {
|
|
82
|
+
fs.writeFileSync(p, res.src);
|
|
83
|
+
files += 1;
|
|
84
|
+
fixes += res.fixes.length;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { files, fixes };
|
|
88
|
+
}
|