@marver-design/marver 0.21.0 → 0.22.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.
- package/CHANGELOG.md +134 -15
- package/README.md +17 -4
- package/dist/{bake-BID6mo-N.mjs → bake-CvY8QR3L.mjs} +1 -1
- package/dist/board-status-CWGHIdo_.mjs +1307 -0
- package/dist/boards-BwKI0qWd.mjs +188 -0
- package/dist/{build-C7MqQ7hq.mjs → build-B1kavcpc.mjs} +74 -13
- package/dist/cli.mjs +69 -9
- package/dist/config-DJxMRVD8.mjs +373 -0
- package/dist/context-D4t59wDi.mjs +600 -0
- package/dist/{daemon-CRZFpl6K.mjs → daemon-PmNqoOAk.mjs} +1 -1
- package/dist/{dev-BNZF4Mup.mjs → dev-Cf4wLGe2.mjs} +15 -7
- package/dist/{init-C34BY3R4.mjs → init-C_K-cjBQ.mjs} +41 -45
- package/dist/managed-write-Bo-oPc-i.mjs +71 -0
- package/dist/{manifest-mMfUhPtL.mjs → manifest-CcdWx7ud.mjs} +42 -376
- package/dist/{plugin-omHLCn91.mjs → plugin-C5u07Yhv.mjs} +234 -105
- package/dist/{poster-BvxiAzy1.mjs → poster-BduzQBYz.mjs} +1 -1
- package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-b0jUwvM_.mjs} +2 -2
- package/dist/{shot-DswS4iRK.mjs → shot-BAR8hmU9.mjs} +2 -2
- package/docs/boards-and-folders.md +161 -0
- package/docs/context.md +117 -0
- package/docs/sharing.md +12 -2
- package/package.json +1 -1
- package/src/client/shell/BoardList.tsx +33 -3
- package/src/client/shell/ContextMenu.tsx +16 -5
- package/src/client/shell/StatusPicker.tsx +91 -0
- package/src/client/shell/board-icons.tsx +75 -0
- package/src/client/shell/store.ts +88 -9
- package/src/client/shell/styles.css +34 -3
- package/src/shared/board-tree.ts +30 -13
- package/src/shared/board-types.ts +103 -0
- package/src/shared/context.ts +265 -0
- package/src/shared/status.ts +157 -0
- package/templates/AGENTS-embedded.md +12 -0
- package/templates/AGENTS-studio.md +12 -0
- package/templates/context/INDEX.md +50 -0
- package/templates/context/map.json +6 -0
- package/templates/context/shipped-knowledge.md +19 -0
- package/templates/context/shipped.md +28 -0
- package/templates/instructions/boards.md +33 -0
- package/templates/instructions/context.md +123 -0
- package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
- package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
- package/templates/playbooks/reorganize-context/eval.md +93 -0
- package/dist/boards-BwiDAmPf.mjs +0 -337
- package/dist/boards-DnLewfj8.mjs +0 -71
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { a as ROUTE, i as PKG, r as NAME } from "./cli.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { A as isRegularFile, B as folderMap, D as boardFields, F as BOARD_NAME, G as validateWire, I as FOLDERS_FILE, K as wireKids, L as buildTree, M as nodeExists, N as readRegistry, O as checkBoardsDir, P as withRegistryLock, T as AUTHOR_FIELDS, U as readDescription, W as readTitle, Y as HAS_STATUS, at as hash, et as readReason, it as settableStatuses, j as listBoardFiles, n as readContextFacts, nt as readType, rt as resolveType, t as annotateBoards, tt as readStatusWord } from "./board-status-CWGHIdo_.mjs";
|
|
4
4
|
import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { a as setSceneTitle, i as scanFrames, n as isNoteFile, s as writeManifest, t as affectedFrameIds } from "./manifest-CcdWx7ud.mjs";
|
|
6
|
+
import { n as loadConfig } from "./config-DJxMRVD8.mjs";
|
|
6
7
|
import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
7
8
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -128,11 +129,88 @@ function apiMiddleware(root, opts = {}) {
|
|
|
128
129
|
const de = dirError();
|
|
129
130
|
if (de) return json(res, 422, { error: de });
|
|
130
131
|
mkdirSync(boardsDir, { recursive: true });
|
|
131
|
-
|
|
132
|
+
const files = listBoardFiles(boardsDir).boards;
|
|
133
|
+
const rows = files.map((b) => ({
|
|
132
134
|
name: b.name,
|
|
133
|
-
sha256: b.sha256,
|
|
134
135
|
...boardFields(b.json, validName)
|
|
135
|
-
}))
|
|
136
|
+
}));
|
|
137
|
+
const reg = readRegistry(boardsDir);
|
|
138
|
+
const regFolders = reg.state === "ok" ? reg.folders : [];
|
|
139
|
+
const tree = buildTree(rows, regFolders);
|
|
140
|
+
const fm = folderMap(tree);
|
|
141
|
+
const facts = readContextFacts(root);
|
|
142
|
+
const notes = annotateBoards(root, files, regFolders, (n) => fm.get(n) ?? null, facts);
|
|
143
|
+
const settable = settableStatuses(facts.present);
|
|
144
|
+
return json(res, 200, files.map((b, i) => {
|
|
145
|
+
const n = notes.get(b.name);
|
|
146
|
+
return {
|
|
147
|
+
...rows[i],
|
|
148
|
+
sha256: b.sha256,
|
|
149
|
+
...n ?? {},
|
|
150
|
+
...n?.status ? { settable } : {}
|
|
151
|
+
};
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
if (path === "boards/status" && req.method === "POST") {
|
|
155
|
+
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
156
|
+
const raw = await readBody(req);
|
|
157
|
+
if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(raw);
|
|
161
|
+
} catch {
|
|
162
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
163
|
+
}
|
|
164
|
+
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
165
|
+
const { name, status: statusRaw, reason: reasonRaw, baseHash } = parsed;
|
|
166
|
+
if (!validName(name) || name === "all-scenes") return json(res, 400, { error: "invalid board name" });
|
|
167
|
+
if (baseHash !== void 0 && typeof baseHash !== "string") return json(res, 400, { error: "invalid baseHash" });
|
|
168
|
+
const status = statusRaw === null ? null : readStatusWord(statusRaw);
|
|
169
|
+
if (status === void 0) return json(res, 400, { error: "invalid status" });
|
|
170
|
+
const reason = readReason(reasonRaw);
|
|
171
|
+
if (status === "blocked" && !reason) return json(res, 400, { error: "a blocked board says why - give a reason" });
|
|
172
|
+
{
|
|
173
|
+
const de = dirError();
|
|
174
|
+
if (de) return json(res, 400, { error: de });
|
|
175
|
+
}
|
|
176
|
+
const file = boardPath(name);
|
|
177
|
+
if (!file) return json(res, 400, { error: "invalid board name" });
|
|
178
|
+
if (!existsSync(file)) return json(res, 404, { error: `board "${name}" does not exist` });
|
|
179
|
+
if (!notSymlink(file)) return json(res, 400, { error: "refusing to write a symlinked board file" });
|
|
180
|
+
const current = readFileSync(file, "utf8");
|
|
181
|
+
if (baseHash !== void 0 && baseHash !== hash(current)) return json(res, 409, {
|
|
182
|
+
error: "board changed on disk",
|
|
183
|
+
sha256: hash(current)
|
|
184
|
+
});
|
|
185
|
+
let obj;
|
|
186
|
+
try {
|
|
187
|
+
obj = JSON.parse(current);
|
|
188
|
+
} catch {
|
|
189
|
+
return json(res, 422, { error: `board "${name}" is not valid JSON - fix the file` });
|
|
190
|
+
}
|
|
191
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return json(res, 422, { error: `board "${name}" is not a JSON object - fix the file` });
|
|
192
|
+
const reg = readRegistry(boardsDir);
|
|
193
|
+
const regFolders = reg.state === "ok" ? reg.folders : [];
|
|
194
|
+
const all = listBoardFiles(boardsDir).boards;
|
|
195
|
+
const fm = folderMap(buildTree(all.map((b) => ({
|
|
196
|
+
name: b.name,
|
|
197
|
+
...boardFields(b.json, validName)
|
|
198
|
+
})), regFolders));
|
|
199
|
+
const folder = regFolders.find((f) => f.name === fm.get(name));
|
|
200
|
+
const parent = folder?.parent ? regFolders.find((f) => f.name === folder.parent) : void 0;
|
|
201
|
+
const type = resolveType(obj.type, folder?.type, parent?.type);
|
|
202
|
+
if (!HAS_STATUS.includes(type)) return json(res, 422, { error: `a ${type} board carries no status - only feature and project boards do` });
|
|
203
|
+
if (status !== null && !settableStatuses(readContextFacts(root).present).includes(status)) return json(res, 422, { error: status === "done" ? "Done is never set by hand - it comes from context/shipped.md" : `with context/, ${status} is read from the evidence - a plan, a contract, the shipped record` });
|
|
204
|
+
if (status === null) delete obj.status;
|
|
205
|
+
else obj.status = status;
|
|
206
|
+
if (status === "blocked") obj.reason = reason;
|
|
207
|
+
else delete obj.reason;
|
|
208
|
+
const next = JSON.stringify(obj, null, 2) + "\n";
|
|
209
|
+
if (next !== current) atomicWrite(file, next);
|
|
210
|
+
return json(res, 200, {
|
|
211
|
+
name,
|
|
212
|
+
sha256: hash(next)
|
|
213
|
+
});
|
|
136
214
|
}
|
|
137
215
|
if (path === "folders" && req.method === "GET") {
|
|
138
216
|
const de = dirError();
|
|
@@ -259,104 +337,144 @@ function apiMiddleware(root, opts = {}) {
|
|
|
259
337
|
const de = dirError();
|
|
260
338
|
if (de) return json(res, 400, { error: de });
|
|
261
339
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (reg.sha256 !== b.folders) return json(res, 409, {
|
|
266
|
-
error: "folders changed on disk",
|
|
267
|
-
stale: [FOLDERS_FILE]
|
|
340
|
+
const reply = (status, body) => ({
|
|
341
|
+
status,
|
|
342
|
+
body
|
|
268
343
|
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
344
|
+
mkdirSync(boardsDir, { recursive: true });
|
|
345
|
+
const locked = withRegistryLock(boardsDir, 0, () => {
|
|
346
|
+
const reg = readRegistry(boardsDir);
|
|
347
|
+
if (reg.state === "malformed") return reply(422, { error: reg.error });
|
|
348
|
+
if (reg.state === "ok" && reg.folders.some((f) => f.parent) && parsed.protocol !== 2) return reply(422, { error: "this tab predates folders in folders - reload the canvas, then try again" });
|
|
349
|
+
if (reg.sha256 !== b.folders) return reply(409, {
|
|
350
|
+
error: "folders changed on disk",
|
|
351
|
+
stale: [FOLDERS_FILE]
|
|
352
|
+
});
|
|
353
|
+
const unseen = listBoardFiles(boardsDir).boards.map((x) => x.name).filter((n) => !(n in b.boards));
|
|
354
|
+
if (unseen.length) return reply(409, {
|
|
355
|
+
error: "boards changed on disk",
|
|
356
|
+
stale: unseen
|
|
357
|
+
});
|
|
358
|
+
const plan = [];
|
|
359
|
+
const stale = [];
|
|
360
|
+
const consider = (name, order, folder) => {
|
|
361
|
+
const p = boardPath(name);
|
|
362
|
+
if (!p || !isRegularFile(p)) {
|
|
363
|
+
stale.push(name);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const content = readFileSync(p, "utf8");
|
|
367
|
+
if (b.boards[name] !== hash(content)) {
|
|
368
|
+
stale.push(name);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
let obj;
|
|
372
|
+
try {
|
|
373
|
+
obj = JSON.parse(content);
|
|
374
|
+
} catch {
|
|
375
|
+
return `board "${name}" is not valid JSON - fix the file`;
|
|
376
|
+
}
|
|
377
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `board "${name}" is not a JSON object - fix the file`;
|
|
378
|
+
plan.push({
|
|
379
|
+
name,
|
|
380
|
+
path: p,
|
|
381
|
+
obj,
|
|
382
|
+
order,
|
|
383
|
+
folder
|
|
384
|
+
});
|
|
285
385
|
return null;
|
|
286
|
-
}
|
|
287
|
-
|
|
386
|
+
};
|
|
387
|
+
const onDisk = /* @__PURE__ */ new Map();
|
|
288
388
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
name,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
389
|
+
const raw = JSON.parse(readFileSync(join(boardsDir, FOLDERS_FILE), "utf8"));
|
|
390
|
+
if (Array.isArray(raw.folders)) {
|
|
391
|
+
for (const r of raw.folders) if (r && typeof r === "object" && typeof r.name === "string") onDisk.set(r.name, r);
|
|
392
|
+
}
|
|
393
|
+
} catch {}
|
|
394
|
+
const MANAGED = /* @__PURE__ */ new Set([
|
|
395
|
+
"name",
|
|
396
|
+
"order",
|
|
397
|
+
"parent",
|
|
398
|
+
"title",
|
|
399
|
+
"description",
|
|
400
|
+
"type"
|
|
401
|
+
]);
|
|
402
|
+
const folders = [];
|
|
403
|
+
const walk = (list, parent) => {
|
|
404
|
+
for (const [i, it] of list.entries()) {
|
|
405
|
+
if (typeof it === "string") {
|
|
406
|
+
const e = consider(it, i, parent);
|
|
407
|
+
if (e) return e;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const title = readTitle(it.title), description = readDescription(it.description);
|
|
411
|
+
const disk = onDisk.get(it.folder);
|
|
412
|
+
const type = readType(it.type) ?? readType(disk?.type);
|
|
413
|
+
const kept = disk ? Object.fromEntries(Object.entries(disk).filter(([k]) => !MANAGED.has(k))) : {};
|
|
414
|
+
folders.push({
|
|
415
|
+
...kept,
|
|
416
|
+
name: it.folder,
|
|
417
|
+
order: i,
|
|
418
|
+
...parent ? { parent } : {},
|
|
419
|
+
...title ? { title } : {},
|
|
420
|
+
...description ? { description } : {},
|
|
421
|
+
...type ? { type } : {}
|
|
422
|
+
});
|
|
423
|
+
const e = walk(wireKids(it), it.folder);
|
|
308
424
|
if (e) return e;
|
|
309
|
-
continue;
|
|
310
425
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
426
|
+
return null;
|
|
427
|
+
};
|
|
428
|
+
{
|
|
429
|
+
const e = walk(tree, null);
|
|
430
|
+
if (e) return reply(422, { error: e });
|
|
431
|
+
}
|
|
432
|
+
if (stale.length) return reply(409, {
|
|
433
|
+
error: "boards changed on disk",
|
|
434
|
+
stale
|
|
435
|
+
});
|
|
436
|
+
const sha256 = {};
|
|
437
|
+
for (const w of plan) {
|
|
438
|
+
if (w.obj.order === w.order && (w.folder ? w.obj.folder === w.folder : w.obj.folder === void 0)) continue;
|
|
439
|
+
if (hash(readFileSync(w.path, "utf8")) !== b.boards[w.name]) return reply(409, {
|
|
440
|
+
error: "boards changed on disk",
|
|
441
|
+
stale: [w.name]
|
|
318
442
|
});
|
|
319
|
-
|
|
320
|
-
if (
|
|
443
|
+
w.obj.order = w.order;
|
|
444
|
+
if (w.folder) w.obj.folder = w.folder;
|
|
445
|
+
else delete w.obj.folder;
|
|
446
|
+
const next = JSON.stringify(w.obj, null, 2) + "\n";
|
|
447
|
+
atomicWrite(w.path, next);
|
|
448
|
+
sha256[w.name] = hash(next);
|
|
321
449
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
450
|
+
mkdirSync(boardsDir, { recursive: true });
|
|
451
|
+
if ((nodeExists(foldersPath) ? hash(readFileSync(foldersPath, "utf8")) : null) !== b.folders) return reply(409, {
|
|
452
|
+
error: "folders changed on disk",
|
|
453
|
+
stale: [FOLDERS_FILE]
|
|
454
|
+
});
|
|
455
|
+
let foldersSha = null;
|
|
456
|
+
if (folders.length) {
|
|
457
|
+
const version = folders.some((f) => f.parent) ? 2 : 1;
|
|
458
|
+
const next = JSON.stringify({
|
|
459
|
+
version,
|
|
460
|
+
folders
|
|
461
|
+
}, null, 2) + "\n";
|
|
462
|
+
atomicWrite(foldersPath, next);
|
|
463
|
+
foldersSha = hash(next);
|
|
464
|
+
} else rmSync(foldersPath, { force: true });
|
|
465
|
+
return reply(200, {
|
|
466
|
+
ok: true,
|
|
467
|
+
sha256: {
|
|
468
|
+
boards: sha256,
|
|
469
|
+
folders: foldersSha
|
|
470
|
+
}
|
|
471
|
+
});
|
|
331
472
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
w.obj.order = w.order;
|
|
336
|
-
if (w.folder) w.obj.folder = w.folder;
|
|
337
|
-
else delete w.obj.folder;
|
|
338
|
-
const next = JSON.stringify(w.obj, null, 2) + "\n";
|
|
339
|
-
atomicWrite(w.path, next);
|
|
340
|
-
sha256[w.name] = hash(next);
|
|
341
|
-
}
|
|
342
|
-
mkdirSync(boardsDir, { recursive: true });
|
|
343
|
-
let foldersSha = null;
|
|
344
|
-
if (folders.length) {
|
|
345
|
-
const version = folders.some((f) => f.parent) ? 2 : 1;
|
|
346
|
-
const next = JSON.stringify({
|
|
347
|
-
version,
|
|
348
|
-
folders
|
|
349
|
-
}, null, 2) + "\n";
|
|
350
|
-
atomicWrite(foldersPath, next);
|
|
351
|
-
foldersSha = hash(next);
|
|
352
|
-
} else rmSync(foldersPath, { force: true });
|
|
353
|
-
return json(res, 200, {
|
|
354
|
-
ok: true,
|
|
355
|
-
sha256: {
|
|
356
|
-
boards: sha256,
|
|
357
|
-
folders: foldersSha
|
|
358
|
-
}
|
|
473
|
+
if (!locked) return json(res, 409, {
|
|
474
|
+
error: "folders changed on disk",
|
|
475
|
+
stale: [FOLDERS_FILE]
|
|
359
476
|
});
|
|
477
|
+
return json(res, locked.status, locked.body);
|
|
360
478
|
}
|
|
361
479
|
const boardMatch = /^boards\/([^/]+)$/.exec(path);
|
|
362
480
|
if (boardMatch) {
|
|
@@ -403,10 +521,8 @@ function apiMiddleware(root, opts = {}) {
|
|
|
403
521
|
const incoming = body.board;
|
|
404
522
|
if (incoming && typeof incoming === "object" && current) try {
|
|
405
523
|
const disk = JSON.parse(current);
|
|
406
|
-
|
|
407
|
-
if (incoming
|
|
408
|
-
if (incoming.title === void 0 && readTitle(disk.title)) incoming.title = disk.title;
|
|
409
|
-
if (incoming.description === void 0 && readDescription(disk.description)) incoming.description = disk.description;
|
|
524
|
+
const valid = boardFields(disk, validName);
|
|
525
|
+
for (const k of AUTHOR_FIELDS) if (incoming[k] === void 0 && valid[k] !== void 0) incoming[k] = disk[k];
|
|
410
526
|
} catch {}
|
|
411
527
|
const next2 = JSON.stringify(body.board, null, 2) + "\n";
|
|
412
528
|
atomicWrite(p, next2);
|
|
@@ -430,7 +546,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
430
546
|
};
|
|
431
547
|
const asPng = url.searchParams.get("format") === "png";
|
|
432
548
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
433
|
-
const { shootFrame } = await import("./shot-
|
|
549
|
+
const { shootFrame } = await import("./shot-BAR8hmU9.mjs");
|
|
434
550
|
const r = await shootFrame({
|
|
435
551
|
root,
|
|
436
552
|
viewports: opts.viewports ?? {},
|
|
@@ -480,7 +596,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
480
596
|
if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) return json(res, 400, { error: "invalid theme" });
|
|
481
597
|
const scale = body.scale === void 0 ? void 0 : body.scale;
|
|
482
598
|
if (scale !== void 0 && !(Number.isInteger(scale) && scale >= 1 && scale <= 4)) return json(res, 400, { error: "invalid scale" });
|
|
483
|
-
const { resolveFrames, shootBatch } = await import("./shot-
|
|
599
|
+
const { resolveFrames, shootBatch } = await import("./shot-BAR8hmU9.mjs");
|
|
484
600
|
const sel = resolveFrames(root, body);
|
|
485
601
|
if (!sel.ok) return json(res, sel.status, { error: sel.error });
|
|
486
602
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
@@ -508,7 +624,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
508
624
|
return json(res, 400, { error: "malformed JSON" });
|
|
509
625
|
}
|
|
510
626
|
if (!body || typeof body !== "object" || !Array.isArray(body.asks) || !body.asks.length || body.asks.length > 200) return json(res, 400, { error: "asks must list 1-200 frames" });
|
|
511
|
-
const { bakeBatch, ASK_MAX } = await import("./bake-
|
|
627
|
+
const { bakeBatch, ASK_MAX } = await import("./bake-CvY8QR3L.mjs");
|
|
512
628
|
let manifest = {};
|
|
513
629
|
try {
|
|
514
630
|
manifest = JSON.parse(readFileSync(join(root, "design", "manifest.json"), "utf8"));
|
|
@@ -552,8 +668,8 @@ function apiMiddleware(root, opts = {}) {
|
|
|
552
668
|
if (path === "poster" && req.method === "GET") {
|
|
553
669
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
554
670
|
const src = url.searchParams.get("src") ?? "";
|
|
555
|
-
const { ensurePoster, isLocalClip } = await import("./poster-
|
|
556
|
-
const { isLocalAssetRef } = await import("./build-
|
|
671
|
+
const { ensurePoster, isLocalClip } = await import("./poster-BduzQBYz.mjs");
|
|
672
|
+
const { isLocalAssetRef } = await import("./build-B1kavcpc.mjs");
|
|
557
673
|
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
558
674
|
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
559
675
|
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
@@ -1035,7 +1151,7 @@ function marverPlugin(ctx) {
|
|
|
1035
1151
|
};
|
|
1036
1152
|
const prune = () => setTimeout(async () => {
|
|
1037
1153
|
try {
|
|
1038
|
-
(await import("./bake-
|
|
1154
|
+
(await import("./bake-CvY8QR3L.mjs")).pruneBakes(root, bakeGen);
|
|
1039
1155
|
} catch {}
|
|
1040
1156
|
}, 500);
|
|
1041
1157
|
prune();
|
|
@@ -1054,6 +1170,19 @@ function marverPlugin(ctx) {
|
|
|
1054
1170
|
rescanTheme();
|
|
1055
1171
|
}
|
|
1056
1172
|
});
|
|
1173
|
+
const contextDir = join(root, "context") + sep;
|
|
1174
|
+
let statusTimer;
|
|
1175
|
+
const restatus = (f) => {
|
|
1176
|
+
if (!f.startsWith(contextDir) && !(inScope(f) && f.endsWith("_brief.md"))) return;
|
|
1177
|
+
clearTimeout(statusTimer);
|
|
1178
|
+
statusTimer = setTimeout(() => {
|
|
1179
|
+
server.ws.send("sh:boards", {});
|
|
1180
|
+
regen();
|
|
1181
|
+
}, 150);
|
|
1182
|
+
};
|
|
1183
|
+
server.watcher.on("add", restatus);
|
|
1184
|
+
server.watcher.on("unlink", restatus);
|
|
1185
|
+
server.watcher.on("change", restatus);
|
|
1057
1186
|
server.watcher.on("change", (f) => inScope(f) && (/\.(tsx|jsx)$/.test(f) || f.endsWith("_brief.md") || isNoteFile(f)) && regen());
|
|
1058
1187
|
const configFile = join(root, "design", "config.ts");
|
|
1059
1188
|
server.watcher.on("change", (f) => {
|
|
@@ -1174,7 +1303,7 @@ function marverPlugin(ctx) {
|
|
|
1174
1303
|
});
|
|
1175
1304
|
return;
|
|
1176
1305
|
}
|
|
1177
|
-
const { shootFrame, shootBatch, resolveFrames } = await import("./shot-
|
|
1306
|
+
const { shootFrame, shootBatch, resolveFrames } = await import("./shot-BAR8hmU9.mjs");
|
|
1178
1307
|
if (ways[0] === "frame") {
|
|
1179
1308
|
if (typeof spec.frame !== "string") {
|
|
1180
1309
|
write({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capture, t as findChrome } from "./shot-
|
|
1
|
+
import { capture, t as findChrome } from "./shot-BAR8hmU9.mjs";
|
|
2
2
|
import { constants, copyFileSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, sep } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { o as slideSize } from "./cli.mjs";
|
|
2
|
-
import { planShot, t as findChrome } from "./shot-
|
|
3
|
-
import { ASK_MAX, bakeBatch } from "./bake-
|
|
2
|
+
import { planShot, t as findChrome } from "./shot-BAR8hmU9.mjs";
|
|
3
|
+
import { ASK_MAX, bakeBatch } from "./bake-CvY8QR3L.mjs";
|
|
4
4
|
import { MIME } from "./serve-z5qtj_wJ.mjs";
|
|
5
5
|
import { copyFileSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -337,8 +337,8 @@ async function shootFrame(opts) {
|
|
|
337
337
|
};
|
|
338
338
|
const plan = planShot(frame, viewports, size);
|
|
339
339
|
if (frame.kind !== "html") try {
|
|
340
|
-
const { scanAssetRefs } = await import("./build-
|
|
341
|
-
const { ensurePoster } = await import("./poster-
|
|
340
|
+
const { scanAssetRefs } = await import("./build-B1kavcpc.mjs");
|
|
341
|
+
const { ensurePoster } = await import("./poster-BduzQBYz.mjs");
|
|
342
342
|
const refs = scanAssetRefs(readFileSync(join(root, frame.file), "utf8"), frame.file);
|
|
343
343
|
for (const r of refs) if (r.endsWith(".poster.png")) await ensurePoster(join(root, "design", "assets"), r.slice(0, -11));
|
|
344
344
|
} catch {}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Boards and folders
|
|
2
|
+
|
|
3
|
+
A board is a saved canvas - a set of frames, arranged. Boards sit at the top of the sidebar, and
|
|
4
|
+
folders keep them tidy: a folder holds boards and folders, a folder inside a folder - a
|
|
5
|
+
**sub-folder** - holds boards. Two levels, so a board sits at the root, in a folder, or in a
|
|
6
|
+
sub-folder.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Start here
|
|
10
|
+
Features
|
|
11
|
+
Shipper <- a sub-folder
|
|
12
|
+
New load
|
|
13
|
+
Orders
|
|
14
|
+
Phone
|
|
15
|
+
Pickup inspection
|
|
16
|
+
Carrier office <- a board, straight in Features
|
|
17
|
+
Context
|
|
18
|
+
Meetings
|
|
19
|
+
Archive
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Everything below works the same way at both levels, and the same from the sidebar as from the files
|
|
23
|
+
- so a person arranging boards by hand and an agent arranging them by writing files always agree.
|
|
24
|
+
|
|
25
|
+
## In the sidebar
|
|
26
|
+
|
|
27
|
+
- **New folder** - right-click the Boards header, or its `+`. Name it inline: what you type is its
|
|
28
|
+
title, any casing, punctuation or emoji.
|
|
29
|
+
- **New folder inside** - right-click a top-level folder: a sub-folder, at its end.
|
|
30
|
+
- **Drag** a board into any folder or sub-folder (drop it on the header), or between any two rows -
|
|
31
|
+
the blue seam shows exactly where a release lands, indented with the level it lands in. Drag a
|
|
32
|
+
folder among the boards; a folder with no sub-folders can also drop into a top-level folder and
|
|
33
|
+
become one.
|
|
34
|
+
- **Move to new folder** on a board makes a folder at the board's own level, with the board inside:
|
|
35
|
+
in its slot at the root, a sub-folder in its slot when it sits in a folder, and right after its
|
|
36
|
+
sub-folder when it sits in one (a sub-folder holds no folders).
|
|
37
|
+
- **Move to top level** - on a board in a folder, or on a sub-folder.
|
|
38
|
+
- **Rename** changes the title. **Delete folder** never deletes a board: what the folder held moves
|
|
39
|
+
up one level, into its place.
|
|
40
|
+
|
|
41
|
+
At the bottom of a folder, one gap belongs to several levels: after the last board of a sub-folder
|
|
42
|
+
you can drop into the sub-folder, after it in its folder, or after the folder at the root. Where you
|
|
43
|
+
hold the pointer decides - over the row above, its indent picks the level; over the row below, the
|
|
44
|
+
seam takes that row's level.
|
|
45
|
+
|
|
46
|
+
## In the files
|
|
47
|
+
|
|
48
|
+
Two files carry it, and they are the truth.
|
|
49
|
+
|
|
50
|
+
**A board names its folder** - the one it sits in directly, at either level - next to its rank:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{ "version": 1, "name": "new-load", "folder": "shipper", "order": 0, "nodes": [] }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`order` ranks the board among its siblings. At every level the boards and folders there share one
|
|
57
|
+
sequence: the root's boards and folders, a folder's boards and sub-folders, a sub-folder's boards.
|
|
58
|
+
|
|
59
|
+
**`design/boards/_folders.json` lists the folders**, ranks them, titles and describes them - and
|
|
60
|
+
nests a sub-folder with `parent`:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{ "version": 2, "folders": [
|
|
64
|
+
{ "name": "features", "order": 1, "title": "Features", "description": "One board per capability, by surface" },
|
|
65
|
+
{ "name": "shipper", "parent": "features", "order": 0, "title": "Shipper" },
|
|
66
|
+
{ "name": "context", "order": 2 } ] }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- A folder's `name` is its slug - what boards point at with `folder`, unique across both levels. Its
|
|
70
|
+
`title` is what people see; renaming in the sidebar changes the title and never the slug.
|
|
71
|
+
- `parent` names a top-level folder. A sub-folder never holds a folder.
|
|
72
|
+
- The file says `"version": 2` while any folder has a parent, and `1` when none does.
|
|
73
|
+
- A folder a board names but the registry lacks is still real - it shows at the top level. To nest
|
|
74
|
+
it, register it with its `parent`.
|
|
75
|
+
- A registry that breaks these rules is an error the canvas shows, never read as empty.
|
|
76
|
+
|
|
77
|
+
`npx marver boards` prints the tree as the files say it is - every folder and sub-folder, every board
|
|
78
|
+
with its rank, the landing board - and `--json` gives the same tree. The landing board is the first
|
|
79
|
+
board in that reading order, down through folders.
|
|
80
|
+
|
|
81
|
+
## Descriptions
|
|
82
|
+
|
|
83
|
+
Every folder, board, scene and frame takes one `description`: a sentence for agents - what it is
|
|
84
|
+
for, and its state when that is not obvious. A folder's sits on its entry in `_folders.json`. They
|
|
85
|
+
all land in `design/manifest.json`, the file an agent reads first, so a new session knows what each
|
|
86
|
+
folder is for before it files a single board.
|
|
87
|
+
|
|
88
|
+
## Types
|
|
89
|
+
|
|
90
|
+
Every board wears an icon for what it is for, the same on every canvas:
|
|
91
|
+
|
|
92
|
+
| Type | For |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `start` | the way in: the index, the shipped record, the timeline |
|
|
95
|
+
| `feature` | one capability: its spec, lo-fi and hi-fi |
|
|
96
|
+
| `surface` | the whole product to walk, from frames on feature boards |
|
|
97
|
+
| `project` | a deliverable or a question |
|
|
98
|
+
| `feedback` | one frame per theme |
|
|
99
|
+
| `context` | what came in from outside: meetings, threads, competitors |
|
|
100
|
+
| `deck` | slides |
|
|
101
|
+
| `archive` | snapshots and retired work |
|
|
102
|
+
|
|
103
|
+
A board states `"type"` in its JSON, or wears the type of the folder it sits in - `"type"` on the
|
|
104
|
+
folder's entry in `_folders.json` - or of that folder's parent; else it is plain. Moving a board
|
|
105
|
+
changes an inherited type, never one the board states. The type never decides how a board publishes,
|
|
106
|
+
but `marver build` suggests a publish type from it - `slides` for a deck, `refs` for a context board,
|
|
107
|
+
`doc` for a project - and the publish row decides.
|
|
108
|
+
|
|
109
|
+
## Status
|
|
110
|
+
|
|
111
|
+
Feature and project boards also wear a status, read from the project's `context/` - Backlog, To do,
|
|
112
|
+
In progress (filling by phase), Done, Done reported, Unknown - or decided on the board itself:
|
|
113
|
+
`"status": "archived"`, `"paused"`, or `"blocked"` with a `"reason"`. **Done is never set by hand.**
|
|
114
|
+
The tooltip says what decided it. [Context](context.md) has the rules.
|
|
115
|
+
|
|
116
|
+
**Set it from the sidebar:** right-click a feature or project board, **Change status…**. The picker
|
|
117
|
+
shows what the evidence says and offers only what a person decides - Blocked (it asks why), Paused,
|
|
118
|
+
Archived, and **Back to the evidence** to undo a decision; without `context/`, Backlog, To do and In
|
|
119
|
+
progress as well. Type to filter, or press a number. It writes `status` and `reason` into the board
|
|
120
|
+
file and nothing else.
|
|
121
|
+
|
|
122
|
+
One rule draws them: a status still open is an outline in its colour - grey, yellow filling by phase,
|
|
123
|
+
red for blocked - and a settled one is filled: Done a green disc, Archived a solid brown box. Done,
|
|
124
|
+
reported is the green outline - a written claim, not yet confirmed.
|
|
125
|
+
|
|
126
|
+
## Starting points
|
|
127
|
+
|
|
128
|
+
- `npx marver init --kind product|knowledge` - on a fresh canvas, the typed folders every canvas
|
|
129
|
+
shares: Start here, Features and Surfaces (a product) or Projects (knowledge work), Feedback,
|
|
130
|
+
Context, Archive. Without the flag a fresh canvas gets product folders when an app is detected,
|
|
131
|
+
knowledge otherwise, and says which. On an existing canvas only `--kind` adds them, and only the
|
|
132
|
+
missing ones - init never renames or moves a folder.
|
|
133
|
+
- `npx marver folders add decks` - one more typed folder later: `start`, `features`, `surfaces`,
|
|
134
|
+
`projects`, `feedback`, `context`, `decks`, `archive`.
|
|
135
|
+
- `npx marver boards new <name> --folder features` - a board in its type's starting layout: a
|
|
136
|
+
feature's three phase scenes as three bands (spec, lo-fi, hi-fi), a start board rendering
|
|
137
|
+
`context/INDEX.md` and `context/shipped.md`, a deck on its title slide. `--type` overrides the
|
|
138
|
+
folder's; `--title`, `--description`, `--capability` fill the rest.
|
|
139
|
+
|
|
140
|
+
## Agents
|
|
141
|
+
|
|
142
|
+
Agents make the same moves by editing those two files (`npx marver boards` prints the tree with each
|
|
143
|
+
board's type and status); `instructions/boards.md` in your project
|
|
144
|
+
teaches each one - creating a sub-folder, moving a folder in or out, renaming a slug (members'
|
|
145
|
+
`folder` and sub-folders' `parent` move with it), deleting (contents up one level). The sidebar
|
|
146
|
+
refuses a write that would overwrite an edit it has not seen, so an agent's file write and a
|
|
147
|
+
person's drag never silently erase each other.
|
|
148
|
+
|
|
149
|
+
## Publishing
|
|
150
|
+
|
|
151
|
+
A published canvas shows the folders of the published boards only - a sub-folder's parent included,
|
|
152
|
+
since the tree needs it. A folder with nothing published at any depth never reaches the bundle, so
|
|
153
|
+
its name and description stay private. Board types ship; statuses do not - a published board loses its
|
|
154
|
+
`status`, `reason` and `capability`, and shows a status only where its publish row says
|
|
155
|
+
`"showStatus": true` and the evidence behind it is `publishable` - never a blocked reason.
|
|
156
|
+
|
|
157
|
+
## Mixed versions
|
|
158
|
+
|
|
159
|
+
Marver 0.20 and earlier read one level of folders. They refuse a version-2 registry with an error
|
|
160
|
+
instead of rewriting it flat, so upgrade the whole team before nesting a folder. A browser tab opened
|
|
161
|
+
before the upgrade is refused the same way once folders nest - reload it.
|