@marver-design/marver 0.20.0 → 0.22.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.
- package/CHANGELOG.md +123 -0
- package/README.md +17 -4
- package/dist/{bake-tAb0D6Rc.mjs → bake-BSX4XR_U.mjs} +1 -1
- package/dist/board-status-CWGHIdo_.mjs +1307 -0
- package/dist/boards-BTGNPVMx.mjs +187 -0
- package/dist/{build-DwpNPl6Z.mjs → build-Dd2Qm3OG.mjs} +87 -16
- package/dist/cli.mjs +67 -8
- package/dist/config-DJxMRVD8.mjs +373 -0
- package/dist/context-DUAENlJ6.mjs +600 -0
- package/dist/{daemon-DbHvLQUL.mjs → daemon-BmkwErpC.mjs} +1 -1
- package/dist/{dev-BjdDP69b.mjs → dev-BW7a5kDF.mjs} +8 -6
- package/dist/{init-DKxuRxBr.mjs → init-NJ3xjLVu.mjs} +39 -44
- package/dist/managed-write-Bo-oPc-i.mjs +71 -0
- package/dist/{manifest-B01PSyDc.mjs → manifest-BqBcMJcd.mjs} +26 -380
- package/dist/{plugin-y4Ch7o_A.mjs → plugin-8XntSCx_.mjs} +234 -97
- package/dist/{poster-FCv_nXyP.mjs → poster-DRZDszTI.mjs} +1 -1
- package/dist/{publish-bakes-XH6Bac58.mjs → publish-bakes-CCWRV9iX.mjs} +2 -2
- package/dist/{shot-iw3SEcpn.mjs → shot-C22Ues04.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 +143 -54
- 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 +93 -14
- package/src/client/shell/styles.css +41 -9
- package/src/shared/board-tree.ts +291 -143
- 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 +4 -3
- package/templates/AGENTS-studio.md +4 -3
- 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 +78 -22
- package/templates/instructions/context.md +114 -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-BmxcT3Lc.mjs +0 -290
- package/dist/boards-PuVzw5Wp.mjs +0 -62
|
@@ -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 { i as setSceneTitle, n as isNoteFile, o as writeManifest, r as scanFrames, t as affectedFrameIds } from "./manifest-BqBcMJcd.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,96 +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
|
-
error: "folders changed on disk",
|
|
266
|
-
stale: [FOLDERS_FILE]
|
|
340
|
+
const reply = (status, body) => ({
|
|
341
|
+
status,
|
|
342
|
+
body
|
|
267
343
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
});
|
|
284
385
|
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
386
|
+
};
|
|
387
|
+
const onDisk = /* @__PURE__ */ new Map();
|
|
287
388
|
try {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
424
|
+
if (e) return e;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
};
|
|
428
|
+
{
|
|
429
|
+
const e = walk(tree, null);
|
|
430
|
+
if (e) return reply(422, { error: e });
|
|
291
431
|
}
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
path: p,
|
|
296
|
-
obj,
|
|
297
|
-
order,
|
|
298
|
-
folder
|
|
432
|
+
if (stale.length) return reply(409, {
|
|
433
|
+
error: "boards changed on disk",
|
|
434
|
+
stale
|
|
299
435
|
});
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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]
|
|
442
|
+
});
|
|
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);
|
|
308
449
|
}
|
|
309
|
-
|
|
310
|
-
folders
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
+
}
|
|
315
471
|
});
|
|
316
|
-
for (const [j, kid] of it.boards.entries()) {
|
|
317
|
-
const e = consider(kid, j, it.folder);
|
|
318
|
-
if (e) return json(res, 422, { error: e });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (stale.length) return json(res, 409, {
|
|
322
|
-
error: "boards changed on disk",
|
|
323
|
-
stale
|
|
324
472
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
w.obj.order = w.order;
|
|
329
|
-
if (w.folder) w.obj.folder = w.folder;
|
|
330
|
-
else delete w.obj.folder;
|
|
331
|
-
const next = JSON.stringify(w.obj, null, 2) + "\n";
|
|
332
|
-
atomicWrite(w.path, next);
|
|
333
|
-
sha256[w.name] = hash(next);
|
|
334
|
-
}
|
|
335
|
-
mkdirSync(boardsDir, { recursive: true });
|
|
336
|
-
let foldersSha = null;
|
|
337
|
-
if (folders.length) {
|
|
338
|
-
const next = JSON.stringify({
|
|
339
|
-
version: 1,
|
|
340
|
-
folders
|
|
341
|
-
}, null, 2) + "\n";
|
|
342
|
-
atomicWrite(foldersPath, next);
|
|
343
|
-
foldersSha = hash(next);
|
|
344
|
-
} else rmSync(foldersPath, { force: true });
|
|
345
|
-
return json(res, 200, {
|
|
346
|
-
ok: true,
|
|
347
|
-
sha256: {
|
|
348
|
-
boards: sha256,
|
|
349
|
-
folders: foldersSha
|
|
350
|
-
}
|
|
473
|
+
if (!locked) return json(res, 409, {
|
|
474
|
+
error: "folders changed on disk",
|
|
475
|
+
stale: [FOLDERS_FILE]
|
|
351
476
|
});
|
|
477
|
+
return json(res, locked.status, locked.body);
|
|
352
478
|
}
|
|
353
479
|
const boardMatch = /^boards\/([^/]+)$/.exec(path);
|
|
354
480
|
if (boardMatch) {
|
|
@@ -395,10 +521,8 @@ function apiMiddleware(root, opts = {}) {
|
|
|
395
521
|
const incoming = body.board;
|
|
396
522
|
if (incoming && typeof incoming === "object" && current) try {
|
|
397
523
|
const disk = JSON.parse(current);
|
|
398
|
-
|
|
399
|
-
if (incoming
|
|
400
|
-
if (incoming.title === void 0 && readTitle(disk.title)) incoming.title = disk.title;
|
|
401
|
-
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];
|
|
402
526
|
} catch {}
|
|
403
527
|
const next2 = JSON.stringify(body.board, null, 2) + "\n";
|
|
404
528
|
atomicWrite(p, next2);
|
|
@@ -422,7 +546,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
422
546
|
};
|
|
423
547
|
const asPng = url.searchParams.get("format") === "png";
|
|
424
548
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
425
|
-
const { shootFrame } = await import("./shot-
|
|
549
|
+
const { shootFrame } = await import("./shot-C22Ues04.mjs");
|
|
426
550
|
const r = await shootFrame({
|
|
427
551
|
root,
|
|
428
552
|
viewports: opts.viewports ?? {},
|
|
@@ -472,7 +596,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
472
596
|
if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) return json(res, 400, { error: "invalid theme" });
|
|
473
597
|
const scale = body.scale === void 0 ? void 0 : body.scale;
|
|
474
598
|
if (scale !== void 0 && !(Number.isInteger(scale) && scale >= 1 && scale <= 4)) return json(res, 400, { error: "invalid scale" });
|
|
475
|
-
const { resolveFrames, shootBatch } = await import("./shot-
|
|
599
|
+
const { resolveFrames, shootBatch } = await import("./shot-C22Ues04.mjs");
|
|
476
600
|
const sel = resolveFrames(root, body);
|
|
477
601
|
if (!sel.ok) return json(res, sel.status, { error: sel.error });
|
|
478
602
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
@@ -500,7 +624,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
500
624
|
return json(res, 400, { error: "malformed JSON" });
|
|
501
625
|
}
|
|
502
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" });
|
|
503
|
-
const { bakeBatch, ASK_MAX } = await import("./bake-
|
|
627
|
+
const { bakeBatch, ASK_MAX } = await import("./bake-BSX4XR_U.mjs");
|
|
504
628
|
let manifest = {};
|
|
505
629
|
try {
|
|
506
630
|
manifest = JSON.parse(readFileSync(join(root, "design", "manifest.json"), "utf8"));
|
|
@@ -544,8 +668,8 @@ function apiMiddleware(root, opts = {}) {
|
|
|
544
668
|
if (path === "poster" && req.method === "GET") {
|
|
545
669
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
546
670
|
const src = url.searchParams.get("src") ?? "";
|
|
547
|
-
const { ensurePoster, isLocalClip } = await import("./poster-
|
|
548
|
-
const { isLocalAssetRef } = await import("./build-
|
|
671
|
+
const { ensurePoster, isLocalClip } = await import("./poster-DRZDszTI.mjs");
|
|
672
|
+
const { isLocalAssetRef } = await import("./build-Dd2Qm3OG.mjs");
|
|
549
673
|
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
550
674
|
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
551
675
|
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
@@ -1027,7 +1151,7 @@ function marverPlugin(ctx) {
|
|
|
1027
1151
|
};
|
|
1028
1152
|
const prune = () => setTimeout(async () => {
|
|
1029
1153
|
try {
|
|
1030
|
-
(await import("./bake-
|
|
1154
|
+
(await import("./bake-BSX4XR_U.mjs")).pruneBakes(root, bakeGen);
|
|
1031
1155
|
} catch {}
|
|
1032
1156
|
}, 500);
|
|
1033
1157
|
prune();
|
|
@@ -1046,6 +1170,19 @@ function marverPlugin(ctx) {
|
|
|
1046
1170
|
rescanTheme();
|
|
1047
1171
|
}
|
|
1048
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);
|
|
1049
1186
|
server.watcher.on("change", (f) => inScope(f) && (/\.(tsx|jsx)$/.test(f) || f.endsWith("_brief.md") || isNoteFile(f)) && regen());
|
|
1050
1187
|
const configFile = join(root, "design", "config.ts");
|
|
1051
1188
|
server.watcher.on("change", (f) => {
|
|
@@ -1166,7 +1303,7 @@ function marverPlugin(ctx) {
|
|
|
1166
1303
|
});
|
|
1167
1304
|
return;
|
|
1168
1305
|
}
|
|
1169
|
-
const { shootFrame, shootBatch, resolveFrames } = await import("./shot-
|
|
1306
|
+
const { shootFrame, shootBatch, resolveFrames } = await import("./shot-C22Ues04.mjs");
|
|
1170
1307
|
if (ways[0] === "frame") {
|
|
1171
1308
|
if (typeof spec.frame !== "string") {
|
|
1172
1309
|
write({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capture, t as findChrome } from "./shot-
|
|
1
|
+
import { capture, t as findChrome } from "./shot-C22Ues04.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-C22Ues04.mjs";
|
|
3
|
+
import { ASK_MAX, bakeBatch } from "./bake-BSX4XR_U.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-Dd2Qm3OG.mjs");
|
|
341
|
+
const { ensurePoster } = await import("./poster-DRZDszTI.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.
|