@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +17 -4
  3. package/dist/{bake-tAb0D6Rc.mjs → bake-BSX4XR_U.mjs} +1 -1
  4. package/dist/board-status-CWGHIdo_.mjs +1307 -0
  5. package/dist/boards-BTGNPVMx.mjs +187 -0
  6. package/dist/{build-DwpNPl6Z.mjs → build-Dd2Qm3OG.mjs} +87 -16
  7. package/dist/cli.mjs +67 -8
  8. package/dist/config-DJxMRVD8.mjs +373 -0
  9. package/dist/context-DUAENlJ6.mjs +600 -0
  10. package/dist/{daemon-DbHvLQUL.mjs → daemon-BmkwErpC.mjs} +1 -1
  11. package/dist/{dev-BjdDP69b.mjs → dev-BW7a5kDF.mjs} +8 -6
  12. package/dist/{init-DKxuRxBr.mjs → init-NJ3xjLVu.mjs} +39 -44
  13. package/dist/managed-write-Bo-oPc-i.mjs +71 -0
  14. package/dist/{manifest-B01PSyDc.mjs → manifest-BqBcMJcd.mjs} +26 -380
  15. package/dist/{plugin-y4Ch7o_A.mjs → plugin-8XntSCx_.mjs} +234 -97
  16. package/dist/{poster-FCv_nXyP.mjs → poster-DRZDszTI.mjs} +1 -1
  17. package/dist/{publish-bakes-XH6Bac58.mjs → publish-bakes-CCWRV9iX.mjs} +2 -2
  18. package/dist/{shot-iw3SEcpn.mjs → shot-C22Ues04.mjs} +2 -2
  19. package/docs/boards-and-folders.md +161 -0
  20. package/docs/context.md +117 -0
  21. package/docs/sharing.md +12 -2
  22. package/package.json +1 -1
  23. package/src/client/shell/BoardList.tsx +143 -54
  24. package/src/client/shell/ContextMenu.tsx +16 -5
  25. package/src/client/shell/StatusPicker.tsx +91 -0
  26. package/src/client/shell/board-icons.tsx +75 -0
  27. package/src/client/shell/store.ts +93 -14
  28. package/src/client/shell/styles.css +41 -9
  29. package/src/shared/board-tree.ts +291 -143
  30. package/src/shared/board-types.ts +103 -0
  31. package/src/shared/context.ts +265 -0
  32. package/src/shared/status.ts +157 -0
  33. package/templates/AGENTS-embedded.md +4 -3
  34. package/templates/AGENTS-studio.md +4 -3
  35. package/templates/context/INDEX.md +50 -0
  36. package/templates/context/map.json +6 -0
  37. package/templates/context/shipped-knowledge.md +19 -0
  38. package/templates/context/shipped.md +28 -0
  39. package/templates/instructions/boards.md +78 -22
  40. package/templates/instructions/context.md +114 -0
  41. package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
  42. package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
  43. package/templates/playbooks/reorganize-context/eval.md +93 -0
  44. package/dist/boards-BmxcT3Lc.mjs +0 -290
  45. package/dist/boards-PuVzw5Wp.mjs +0 -62
@@ -0,0 +1,187 @@
1
+ import { r as NAME } from "./cli.mjs";
2
+ import { $ as readCapability, B as folderMap, D as boardFields, E as addFolders, H as isBoardName, J as FOLDER_MODULES, L as buildTree, N as readRegistry, O as checkBoardsDir, Q as knownType, R as flatten, V as humanize, a as STATUS_LABEL, j as listBoardFiles, k as checkRealDirs, q as BOARD_TYPES, r as PHASE_LABEL, rt as resolveType, t as annotateBoards } from "./board-status-CWGHIdo_.mjs";
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ //#region src/cli/boards.ts
6
+ /**
7
+ * `marver boards` - the sidebar as the agent sees it: every folder and board in reading
8
+ * order - sub-folders indented under their parent - from the files (no dev server needed). One
9
+ * call answers "what folders exist, what is in them, what ranks where" before the agent writes `folder` on a board or edits
10
+ * `design/boards/_folders.json`. `--json` gives the tree shape the shell uses.
11
+ */
12
+ function boardsCommand(root, opts) {
13
+ const dir = join(root, "design", "boards");
14
+ const de = checkBoardsDir(root, dir);
15
+ if (de) throw new Error(de);
16
+ const { boards, skipped } = listBoardFiles(dir);
17
+ const reg = readRegistry(dir);
18
+ if (reg.state === "malformed") throw new Error(reg.error);
19
+ const rows = boards.map((b) => ({
20
+ name: b.name,
21
+ ...boardFields(b.json, isBoardName)
22
+ }));
23
+ const tree = buildTree(rows, reg.folders);
24
+ const hasAll = boards.some((b) => b.name === "all-scenes");
25
+ const fm = folderMap(tree);
26
+ const notes = annotateBoards(root, boards.filter((b) => b.name !== "all-scenes"), reg.folders, (n) => fm.get(n) ?? null);
27
+ if (opts.json) {
28
+ const out = rows.filter((r) => r.name !== "all-scenes").map((r) => ({
29
+ ...r,
30
+ ...notes.get(r.name) ?? {}
31
+ }));
32
+ console.log(JSON.stringify({
33
+ tree,
34
+ boards: out,
35
+ landing: flatten(tree)[0] ?? (hasAll ? "all-scenes" : null),
36
+ registry: reg.state === "ok" ? "design/boards/_folders.json" : null
37
+ }, null, 2));
38
+ return;
39
+ }
40
+ if (!tree.length && !hasAll) {
41
+ console.log("no boards yet - design/boards/ is empty");
42
+ return;
43
+ }
44
+ const order = (n) => {
45
+ const o = rows.find((r) => r.name === n)?.order;
46
+ return o === void 0 ? "" : ` order ${o}`;
47
+ };
48
+ const title = (t) => t ? ` "${t}"` : "";
49
+ const desc = (d) => d ? ` - ${d}` : "";
50
+ const kind = (n) => {
51
+ const a = notes.get(n);
52
+ if (!a) return "";
53
+ const st = a.status ? ` · ${STATUS_LABEL[a.status.status]}${a.status.fill ? ` (${PHASE_LABEL[a.status.fill]})` : ""}${a.status.reason ? `: ${a.status.reason}` : ""}` : "";
54
+ return a.type === "plain" && !st ? "" : ` [${a.type}${st}]`;
55
+ };
56
+ const boardLine = (n) => {
57
+ const r = rows.find((x) => x.name === n);
58
+ return `${n}${title(r?.title)}${kind(n)}${order(n)}${desc(r?.description)}`;
59
+ };
60
+ const count = (f) => {
61
+ const boards = f.items.filter((k) => k.kind === "board").length, subs = f.items.length - boards;
62
+ return `${boards} board${boards === 1 ? "" : "s"}${subs ? `, ${subs} folder${subs === 1 ? "" : "s"}` : ""}`;
63
+ };
64
+ const print = (items, pad, parent) => {
65
+ for (const it of items) {
66
+ if (it.kind === "board") {
67
+ console.log(`${pad}${boardLine(it.name)}`);
68
+ continue;
69
+ }
70
+ const where = parent ? `sub-folder of ${parent}` : "folder";
71
+ const implied = reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json";
72
+ console.log(`${pad}${it.name}/${title(it.title)} (${where}${it.type ? `, ${it.type}` : ""}, ${count(it)}${implied})${desc(it.description)}`);
73
+ print(it.items, `${pad} `, it.name);
74
+ if (!it.items.length) console.log(`${pad} (empty)`);
75
+ }
76
+ };
77
+ print(tree, "", null);
78
+ if (hasAll) console.log("all-scenes (auto, always last)");
79
+ const landing = flatten(tree)[0];
80
+ if (landing) console.log(`\nlanding board: ${landing}`);
81
+ console.log(`registry: ${reg.state === "ok" ? "design/boards/_folders.json" : "none (no empty or ranked folders yet)"}`);
82
+ if (skipped.length) console.log(`skipped (not regular files): ${skipped.join(", ")}`);
83
+ }
84
+ /** `marver boards new <name>` - a board in its type's starting layout (spec 20): a feature's three
85
+ * phase scenes (spec, lo-fi, hi-fi) as three bands, a start board rendering context/INDEX.md and
86
+ * context/shipped.md, a deck's title slide. The type is `--type`, else the folder's. Never
87
+ * overwrites a board, a scene brief or a frame that exists. */
88
+ function boardsNew(root, name, opts) {
89
+ if (!isBoardName(name) || name === "all-scenes") throw new Error(`"${name}" is not a board name - lowercase letters, numbers and dashes`);
90
+ const dir = join(root, "design", "boards");
91
+ const de = checkBoardsDir(root, dir);
92
+ if (de) throw new Error(de);
93
+ const file = join(dir, `${name}.json`);
94
+ if (existsSync(file)) throw new Error(`design/boards/${name}.json exists - a board is never overwritten`);
95
+ if (opts.type !== void 0 && !knownType(opts.type)) throw new Error(`--type ${opts.type}: use ${BOARD_TYPES.join(", ")}`);
96
+ if (opts.folder !== void 0 && !isBoardName(opts.folder)) throw new Error(`--folder ${opts.folder} is not a folder name`);
97
+ if (opts.capability !== void 0 && !readCapability(opts.capability)) throw new Error(`--capability ${opts.capability} is not a capability slug`);
98
+ const reg = readRegistry(dir);
99
+ if (reg.state === "malformed") throw new Error(reg.error);
100
+ const folder = opts.folder ? reg.state === "ok" ? reg.folders.find((f) => f.name === opts.folder) : void 0 : void 0;
101
+ const parent = folder?.parent && reg.state === "ok" ? reg.folders.find((f) => f.name === folder.parent) : void 0;
102
+ const type = resolveType(opts.type, folder?.type, parent?.type);
103
+ const label = opts.title ?? humanize(name);
104
+ const created = [];
105
+ const put = (rel, body) => {
106
+ const f = join(root, rel);
107
+ const scene = rel.split("/")[2];
108
+ const bad = checkRealDirs(root, [
109
+ [join(root, "design"), "design"],
110
+ [join(root, "design", "scenes"), "design/scenes"],
111
+ [join(root, "design", "scenes", scene), `design/scenes/${scene}`]
112
+ ]);
113
+ if (bad) throw new Error(bad);
114
+ mkdirSync(join(f, ".."), { recursive: true });
115
+ try {
116
+ writeFileSync(f, body, { flag: "wx" });
117
+ created.push(rel);
118
+ } catch (e) {
119
+ if (e.code !== "EEXIST") throw e;
120
+ }
121
+ };
122
+ const brief = (scene, title, body, phase) => put(`design/scenes/${scene}/_brief.md`, `---\ntitle: ${title}\n${phase ? `phase: ${phase}\n` : ""}---\n${body}\n`);
123
+ let nodes = [];
124
+ let layout;
125
+ if (type === "feature") {
126
+ const cap = opts.capability ?? name;
127
+ const [specs, lofi, hifi] = [
128
+ `${name}-specs`,
129
+ `${name}-lofi`,
130
+ name
131
+ ];
132
+ brief(specs, `${label} - spec`, `${label}: what it does, for whom, and its limits - the thinking before the screens. Its contract is context/product/${cap}.md once accepted.`, "spec");
133
+ brief(lofi, `${label} - lo-fi`, `${label} in throwaway wireframes: structure and copy, no styling.`, "lofi");
134
+ brief(hifi, label, `${label} in hi-fi, from the product's real components.`, "hifi");
135
+ layout = { rows: [
136
+ [specs],
137
+ { space: 3 },
138
+ [lofi],
139
+ { space: 4 },
140
+ [hifi]
141
+ ] };
142
+ } else if (type === "start") {
143
+ const strip = "(md: string) => md.replace(/^<!--[^\\n]*-->\\n/, '').replace(/^---\\n[\\s\\S]*?\\n---\\n/, '')";
144
+ const frame = (file, title, src, desc) => put(`design/scenes/${name}/${file}.tsx`, `// Renders ${src} - the file is the truth; this frame only shows it.\nimport { Doc, Md } from '@marver-design/marver/content'\nimport text from '../../../${src}?raw'\n\nexport const meta = { title: '${title}', intent: 'spec', description: '${desc}' }\n\nconst body = ${strip}\n\nexport default () => (\n <Doc>\n <Md>{body(text)}</Md>\n </Doc>\n)\n`);
145
+ brief(name, label, "The way in: the index and the shipped record, rendered from context/.");
146
+ frame("index", "The index", "context/INDEX.md", "context/INDEX.md, rendered - where every question is answered");
147
+ frame("shipped", "Shipped", "context/shipped.md", "context/shipped.md, rendered - what runs where, with its evidence");
148
+ nodes = [{ frame: `${name}/index` }, { frame: `${name}/shipped` }];
149
+ layout = { rows: [[name]] };
150
+ } else if (type === "deck") {
151
+ brief(name, label, `${label}: the deck. Read design/instructions/slides.md before the first slide.`);
152
+ put(`design/scenes/${name}/01-title.tsx`, `export const meta = { title: '${label.replace(/'/g, "\\'")}', slide: true }\n\nexport default function Frame() {\n return (\n <main style={{ minHeight: '100vh', display: 'grid', placeItems: 'center', padding: 96 }}>\n <h1 style={{ fontSize: 72, margin: 0 }}>${label.replace(/[<>{}]/g, "")}</h1>\n </main>\n )\n}\n`);
153
+ nodes = [{ frame: `${name}/01-title` }];
154
+ layout = { rows: [[name]] };
155
+ }
156
+ const board = {
157
+ version: 1,
158
+ name,
159
+ auto: false,
160
+ ...opts.type ? { type: opts.type } : {},
161
+ ...opts.folder ? { folder: opts.folder } : {},
162
+ ...opts.title ? { title: opts.title } : {},
163
+ ...opts.description ? { description: opts.description } : {},
164
+ ...opts.capability ? { capability: opts.capability } : {},
165
+ nodes,
166
+ ...layout ? { layout } : {}
167
+ };
168
+ mkdirSync(dir, { recursive: true });
169
+ try {
170
+ writeFileSync(file, JSON.stringify(board, null, 2) + "\n", { flag: "wx" });
171
+ } catch (e) {
172
+ if (e.code === "EEXIST") throw new Error(`design/boards/${name}.json exists - a board is never overwritten`);
173
+ throw e;
174
+ }
175
+ created.unshift(`design/boards/${name}.json (${type}${opts.type ? "" : type === "plain" ? "" : `, from ${opts.folder}`})`);
176
+ return created;
177
+ }
178
+ /** `marver folders add <module...>` - the shared sidebar's typed folders (spec 20): start, features,
179
+ * surfaces, projects, feedback, context, decks, archive. Appended; never renames or moves one. */
180
+ function foldersAdd(root, modules) {
181
+ const unknown = modules.filter((m) => !FOLDER_MODULES[m]);
182
+ if (unknown.length) throw new Error(`unknown folder module: ${unknown.join(", ")} - use ${Object.keys(FOLDER_MODULES).join(", ")}`);
183
+ if (!modules.length) throw new Error(`name a module: ${Object.keys(FOLDER_MODULES).join(", ")} (e.g. \`npx ${NAME} folders add decks\`)`);
184
+ return addFolders(root, modules.map((m) => FOLDER_MODULES[m]));
185
+ }
186
+ //#endregion
187
+ export { boardsCommand, boardsNew, foldersAdd };
@@ -1,7 +1,8 @@
1
1
  import { a as ROUTE, r as NAME } from "./cli.mjs";
2
- import { a as listBoardFiles, d as flatten, f as isBoardName, n as checkBoardsDir, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
3
- import { c as loadConfig, r as scanFrames, u as detectHost } from "./manifest-B01PSyDc.mjs";
4
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-y4Ch7o_A.mjs";
2
+ import { D as boardFields, H as isBoardName, L as buildTree, N as readRegistry, O as checkBoardsDir, R as flatten, Z as PROPOSED_PUBLISH, i as PUBLISHABLE, j as listBoardFiles, o as publishableStatus } from "./board-status-CWGHIdo_.mjs";
3
+ import { r as scanFrames } from "./manifest-BqBcMJcd.mjs";
4
+ import { i as detectHost, n as loadConfig } from "./config-DJxMRVD8.mjs";
5
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-8XntSCx_.mjs";
5
6
  import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
7
  import { basename, dirname, join, sep } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
@@ -258,7 +259,7 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
258
259
  structure: true,
259
260
  source: false
260
261
  };
261
- const row = (max) => ({
262
+ const row = (max, _n) => ({
262
263
  max,
263
264
  type: "mix"
264
265
  });
@@ -268,12 +269,12 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
268
269
  const missing = names.filter((n) => !known(n));
269
270
  if (missing.length) throw new Error(`--boards names not found in design/boards/: ${missing.join(", ")}`);
270
271
  return {
271
- boards: Object.fromEntries(names.map((n) => [n, row("comment")])),
272
+ boards: Object.fromEntries(names.map((n) => [n, row("comment", n)])),
272
273
  reveal
273
274
  };
274
275
  }
275
276
  if (allBoardsFlag) return {
276
- boards: Object.fromEntries(["all-scenes", ...Object.keys(allBoards).filter((n) => n !== "all-scenes")].map((n) => [n, row("comment")])),
277
+ boards: Object.fromEntries(["all-scenes", ...Object.keys(allBoards).filter((n) => n !== "all-scenes")].map((n) => [n, row("comment", n)])),
277
278
  reveal
278
279
  };
279
280
  const policyFile = join(root, "design", "publish.json");
@@ -290,13 +291,14 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
290
291
  for (const [n, level] of entries) {
291
292
  if (!known(n)) throw new Error(`design/publish.json names an unknown board: ${n}`);
292
293
  if (level === "read" || level === "comment") {
293
- out[n] = row(level);
294
+ out[n] = row(level, n);
294
295
  continue;
295
296
  }
296
297
  if (typeof level !== "object" || level === null) throw new Error(`design/publish.json: board "${n}" has level "${level}" - use "read" or "comment", or a v2 object`);
297
298
  const p = level;
298
299
  if (p.max !== "read" && p.max !== "comment") throw new Error(`design/publish.json: board "${n}" needs "max": "read" | "comment"`);
299
300
  const type = p.type ?? "mix";
301
+ if (p.showStatus !== void 0 && typeof p.showStatus !== "boolean") throw new Error(`design/publish.json: board "${n}" has showStatus "${p.showStatus}" - use true or false`);
300
302
  if (!ARTIFACT_TYPES.includes(type)) throw new Error(`design/publish.json: board "${n}" has type "${type}" - use ${ARTIFACT_TYPES.join(" | ")}`);
301
303
  if (p.open !== void 0 && !VIEW_MODES.includes(p.open)) throw new Error(`design/publish.json: board "${n}" has open "${p.open}" - use ${VIEW_MODES.join(" | ")}`);
302
304
  if (p.transition !== void 0 && !DECK_TRANSITIONS.includes(p.transition)) throw new Error(`design/publish.json: board "${n}" has transition "${p.transition}" - use ${DECK_TRANSITIONS.join(" | ")}`);
@@ -306,6 +308,7 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
306
308
  out[n] = {
307
309
  max: p.max,
308
310
  type,
311
+ ...p.showStatus === true ? { showStatus: true } : {},
309
312
  ...p.open ? { open: p.open } : {},
310
313
  ...p.lock ? { lock: true } : {},
311
314
  ...p.transition ? { transition: p.transition } : {},
@@ -339,15 +342,24 @@ function readBoards(root) {
339
342
  return out;
340
343
  }
341
344
  /** Switcher order = the sidebar's reading order: the folder tree over the PUBLISHED boards only
342
- * (a folder left with no published board drops out - its name never reaches the bundle),
345
+ * (a folder left with no published board at any depth drops out - its name never reaches the bundle),
343
346
  * flattened depth-first; all-scenes always LAST (it is the expensive everything-board, never
344
347
  * the landing). `names[0]` is where `/` opens. Folder names of published boards are structure,
345
348
  * like board names: they ship. */
346
349
  function publishedTree(published, allBoards, folders) {
347
- const tree = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
350
+ const built = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
348
351
  name: n,
349
352
  ...boardFields(allBoards[n], isBoardName)
350
- })), folders).filter((it) => it.kind === "board" || it.boards.length > 0);
353
+ })), folders);
354
+ const prune = (items) => items.flatMap((it) => {
355
+ if (it.kind === "board") return [it];
356
+ const kids = prune(it.items);
357
+ return kids.length ? [{
358
+ ...it,
359
+ items: kids
360
+ }] : [];
361
+ });
362
+ const tree = prune(built);
351
363
  return {
352
364
  tree,
353
365
  names: [...flatten(tree), ...published.includes("all-scenes") ? ["all-scenes"] : []]
@@ -356,11 +368,18 @@ function publishedTree(published, allBoards, folders) {
356
368
  /** The bundle's manifest: the published frames, and descriptions of published things ONLY -
357
369
  * the project's, the published scenes' (their brief path only when source is revealed),
358
370
  * the published boards' and the folders those sit in. No unpublished name or sentence. */
359
- function publishedManifest(manifest, pubFrames, publishedNames, strip) {
371
+ function publishedManifest(manifest, pubFrames, publishedNames, strip, showStatus = /* @__PURE__ */ new Set()) {
360
372
  const pubScenes = new Set(pubFrames.map((f) => f.scene));
361
373
  const pubBoardSet = new Set(publishedNames);
362
- const pubBoards = (manifest.boards ?? []).filter((b) => pubBoardSet.has(b.name));
374
+ const pubBoards = (manifest.boards ?? []).filter((b) => pubBoardSet.has(b.name)).map(({ status, ...b }) => {
375
+ const shown = showStatus.has(b.name) ? publishableStatus(status) : null;
376
+ return shown ? {
377
+ ...b,
378
+ status: shown
379
+ } : b;
380
+ });
363
381
  const pubFolderSet = new Set(pubBoards.map((b) => b.folder).filter(Boolean));
382
+ for (const f of manifest.folders ?? []) if (f.parent && pubFolderSet.has(f.name)) pubFolderSet.add(f.parent);
364
383
  const pubFolders = (manifest.folders ?? []).filter((f) => pubFolderSet.has(f.name));
365
384
  return {
366
385
  ...manifest.project ? { project: manifest.project } : {},
@@ -377,6 +396,39 @@ function publishedManifest(manifest, pubFrames, publishedNames, strip) {
377
396
  frames: pubFrames
378
397
  };
379
398
  }
399
+ /** What a published status may carry, and a published board's meta - nothing else (spec 20). */
400
+ const STATUS_KEYS = /* @__PURE__ */ new Set(["status", "fill"]);
401
+ const META_KEYS = /* @__PURE__ */ new Set(["type", "status"]);
402
+ /** The fields of a board file that point into context/ - stripped from every published board. */
403
+ const EVIDENCE_FIELDS = [
404
+ "status",
405
+ "reason",
406
+ "capability"
407
+ ];
408
+ function withoutEvidence(board) {
409
+ if (!board || typeof board !== "object" || Array.isArray(board)) return board;
410
+ const out = { ...board };
411
+ for (const k of EVIDENCE_FIELDS) delete out[k];
412
+ return out;
413
+ }
414
+ /** The build's own check of the projection: if a stripped field survives into the bundle, the
415
+ * build fails - a later edit to the assembly must never quietly ship the evidence trail. */
416
+ function assertProjected(data, showStatus) {
417
+ const statusOk = (n, st) => {
418
+ if (!st || typeof st !== "object" || Array.isArray(st)) return false;
419
+ const o = st;
420
+ return showStatus.has(n) && PUBLISHABLE.has(o.status) && Object.keys(o).every((k) => STATUS_KEYS.has(k));
421
+ };
422
+ for (const [n, b] of Object.entries(data.boards)) for (const k of EVIDENCE_FIELDS) if (b && typeof b === "object" && k in b) throw new Error(`build: board "${n}" would ship its "${k}" - the publication projection failed`);
423
+ for (const b of data.manifest.boards ?? []) {
424
+ const st = b.status;
425
+ if (st !== void 0 && !statusOk(b.name, st)) throw new Error(`build: board "${b.name}" would ship a status its publish row did not allow`);
426
+ }
427
+ for (const [n, m] of Object.entries(data.meta ?? {})) {
428
+ if (Object.keys(m).some((k) => !META_KEYS.has(k))) throw new Error(`build: board "${n}" would ship meta beyond its type and status`);
429
+ if (m.status !== void 0 && !statusOk(n, m.status)) throw new Error(`build: board "${n}" would ship a status its publish row did not allow`);
430
+ }
431
+ }
380
432
  /** The folder registry: absent = no folders (boards still imply theirs); malformed fails the build. */
381
433
  function readFolders(root) {
382
434
  const reg = readRegistry(join(root, "design", "boards"));
@@ -492,6 +544,17 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
492
544
  });
493
545
  const allBoards = readBoards(root);
494
546
  const policy = resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag);
547
+ let named = {};
548
+ try {
549
+ named = JSON.parse(readFileSync(join(root, "design", "publish.json"), "utf8")).boards ?? {};
550
+ } catch {}
551
+ for (const [n, pb] of Object.entries(policy.boards)) {
552
+ const row = named[n];
553
+ if (row && typeof row === "object" && row.type !== void 0) continue;
554
+ const bt = manifest.boards?.find((b) => b.name === n)?.type;
555
+ const suggested = bt ? PROPOSED_PUBLISH[bt] : void 0;
556
+ if (suggested && suggested !== pb.type) console.log(` note: board "${n}" is a ${bt} board - its publish row could say "type": "${suggested}" (it publishes as "${pb.type}" until it does)`);
557
+ }
495
558
  const rights = Object.fromEntries(Object.entries(policy.boards).map(([n, p]) => [n, p.max]));
496
559
  const strip = !policy.reveal.source;
497
560
  const buildSalt = randomBytes(16).toString("hex");
@@ -499,7 +562,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
499
562
  const { tree, names: publishedNames } = publishedTree(Object.keys(rights), allBoards, readFolders(root));
500
563
  const includeAll = publishedNames.includes("all-scenes");
501
564
  const boards = {};
502
- for (const n of publishedNames) if (allBoards[n]) boards[n] = allBoards[n];
565
+ for (const n of publishedNames) if (allBoards[n]) boards[n] = withoutEvidence(allBoards[n]);
503
566
  let frames = manifest.frames;
504
567
  if (!includeAll) {
505
568
  const wanted = /* @__PURE__ */ new Set();
@@ -510,7 +573,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
510
573
  ...f,
511
574
  file: opaquePath(f.id, f.kind === "html" ? ".html" : "")
512
575
  })) : frames;
513
- const pubManifest = publishedManifest(manifest, pubFrames, publishedNames, strip);
576
+ const showStatus = new Set(publishedNames.filter((n) => policy.boards[n]?.showStatus));
577
+ const pubManifest = publishedManifest(manifest, pubFrames, publishedNames, strip, showStatus);
514
578
  for (const n of publishedNames) {
515
579
  const pb = policy.boards[n];
516
580
  if (pb?.type !== "slides" && pb?.open !== "slides") continue;
@@ -542,6 +606,10 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
542
606
  const t = boardFields(allBoards[n], isBoardName).title;
543
607
  return t ? [[n, t]] : [];
544
608
  }));
609
+ const meta = Object.fromEntries((pubManifest.boards ?? []).flatMap((b) => b.type || b.status ? [[b.name, {
610
+ ...b.type ? { type: b.type } : {},
611
+ ...b.status ? { status: b.status } : {}
612
+ }]] : []));
545
613
  const bakeGen = textures ? Date.now() : void 0;
546
614
  const data = {
547
615
  bakes: bakeGen,
@@ -550,6 +618,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
550
618
  names: publishedNames,
551
619
  tree,
552
620
  ...Object.keys(titles).length ? { titles } : {},
621
+ ...Object.keys(meta).length ? { meta } : {},
622
+ ...Object.values(meta).some((m) => m.status) ? { statusAsOf: (/* @__PURE__ */ new Date()).toISOString() } : {},
553
623
  default: publishedNames.find((n) => n !== "all-scenes") ?? publishedNames[0],
554
624
  rights,
555
625
  policy: {
@@ -558,6 +628,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
558
628
  ...lockedShell ? { lockedShell: true } : {}
559
629
  }
560
630
  };
631
+ assertProjected(data, showStatus);
561
632
  const registryFile = posix(join(clientDir, "frame-host", "registry.ts"));
562
633
  const lockedFile = posix(join(clientDir, "shell", "LockedApp.tsx"));
563
634
  const tsxFrames = frames.filter((f) => f.kind === "tsx");
@@ -683,7 +754,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
683
754
  const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
684
755
  for (const r of refs) {
685
756
  if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
686
- const { ensurePoster } = await import("./poster-FCv_nXyP.mjs");
757
+ const { ensurePoster } = await import("./poster-DRZDszTI.mjs");
687
758
  const g = await ensurePoster(assetsDir, r.slice(0, -11));
688
759
  if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
689
760
  console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
@@ -750,7 +821,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
750
821
  }));
751
822
  let textureLine = " textures: skipped (--no-textures)";
752
823
  if (textures && bakeGen) {
753
- const { bakePublished } = await import("./publish-bakes-XH6Bac58.mjs");
824
+ const { bakePublished } = await import("./publish-bakes-CCWRV9iX.mjs");
754
825
  const pubFrameFile = new Map(pubFrames.map((f) => [f.id, f]));
755
826
  const r = await bakePublished({
756
827
  root,
package/dist/cli.mjs CHANGED
@@ -77,15 +77,20 @@ const resolve$1 = (dir) => {
77
77
  }
78
78
  };
79
79
  const cli = cac(NAME);
80
- cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "studio | embedded", { default: "studio" }).option("--no-demo", "Skip the demo scene (the demo ships unless this flag is passed)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
81
- const { init } = await import("./init-DKxuRxBr.mjs");
80
+ cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "studio | embedded", { default: "studio" }).option("--no-demo", "Skip the demo scene (the demo ships unless this flag is passed)").option("--kind <kind>", "product | knowledge - the sidebar's typed folders (a fresh canvas: product when an app is detected)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
81
+ if (opts.kind !== void 0 && opts.kind !== "product" && opts.kind !== "knowledge") {
82
+ console.error(`[${NAME}] --kind ${opts.kind}: use product or knowledge`);
83
+ process.exit(1);
84
+ }
85
+ const { init } = await import("./init-NJ3xjLVu.mjs");
82
86
  init(resolve$1(opts.root), {
83
87
  mode: opts.mode === "embedded" ? "embedded" : "studio",
84
- demo: opts.demo !== false
88
+ demo: opts.demo !== false,
89
+ kind: opts.kind
85
90
  });
86
91
  });
87
92
  for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot reload, comments, Live Jam)"], ["canvas", "Start the local canvas - same as dev"]]) cli.command(name, desc).option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
88
- const { dev } = await import("./dev-BjdDP69b.mjs");
93
+ const { dev } = await import("./dev-BW7a5kDF.mjs");
89
94
  let port;
90
95
  if (opts.port !== void 0) {
91
96
  const n = Number(opts.port);
@@ -95,7 +100,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
95
100
  await dev(resolve$1(opts.root), port);
96
101
  });
97
102
  cli.command("build", "Static export → design/.dist (what ships comes from design/publish.json - publishing is default-closed)").option("--boards <names>", "Publish only these boards (comma-separated); overrides the publish policy").option("--all-boards", "Publish every board - the loud override for the default-closed policy").option("--embed-seeds", "Copy comment history INTO the web root (identifying - every event carries its author's email)").option("--no-textures", "Skip compiling the glass textures the published frames rest under (needs Chrome; a CI without one skips on its own)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
98
- const { buildSite } = await import("./build-DwpNPl6Z.mjs");
103
+ const { buildSite } = await import("./build-Dd2Qm3OG.mjs");
99
104
  try {
100
105
  const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
101
106
  await buildSite(resolve$1(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true, opts.textures !== false && !process.env.MARVER_NO_TEXTURES);
@@ -140,10 +145,64 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
140
145
  process.exit(1);
141
146
  }
142
147
  });
143
- cli.command("boards", "The sidebar as files: every folder and board in reading order, the landing board, the registry").option("--root <dir>", "Host repo root", { default: "." }).option("--json", "The tree as JSON ({ tree, landing, registry })").action(async (opts) => {
144
- const { boardsCommand } = await import("./boards-PuVzw5Wp.mjs");
148
+ cli.command("boards [action] [name]", "The sidebar as files: every folder and board in reading order, with types and statuses · new <name>: a board in its type's starting layout").option("--root <dir>", "Host repo root", { default: "." }).option("--json", "The tree as JSON ({ tree, boards, landing, registry })").option("--type <type>", "new: start | feature | surface | project | feedback | context | deck | archive (default: the folder's)").option("--folder <folder>", "new: the folder it sits in").option("--title <title>", "new: what humans see").option("--description <sentence>", "new: what agents read").option("--capability <slug>", "new: the capability it shows, when not its own name").action(async (action, name, opts) => {
149
+ const { boardsCommand, boardsNew } = await import("./boards-BTGNPVMx.mjs");
150
+ try {
151
+ if (action === void 0) boardsCommand(resolve$1(opts.root), opts);
152
+ else if (action === "new") {
153
+ if (!name) throw new Error("name the board: `boards new <name>`");
154
+ const made = boardsNew(resolve$1(opts.root), name, opts);
155
+ for (const c of made) console.log(` + ${c}`);
156
+ if (made.some((c) => c.endsWith("/_brief.md")) && !made.some((c) => /\.(tsx|jsx|html)$/.test(c))) console.log(`\n next: a frame you make in these scenes shows on the board once it is pinned - a node in\n design/boards/${name}.json (an agent does this as it works: design/AGENTS.md)`);
157
+ } else throw new Error(`unknown action "${action}" - \`boards\` lists, \`boards new <name>\` creates`);
158
+ } catch (err) {
159
+ console.error(`[${NAME}] ${err.message}`);
160
+ process.exit(1);
161
+ }
162
+ });
163
+ cli.command("folders <action> [...modules]", "The shared sidebar's typed folders · add <start|features|surfaces|projects|feedback|context|decks|archive ...>").option("--root <dir>", "Host repo root", { default: "." }).action(async (action, modules, opts) => {
164
+ const { foldersAdd } = await import("./boards-BTGNPVMx.mjs");
165
+ try {
166
+ if (action !== "add") throw new Error(`unknown action "${action}" - \`folders add <module ...>\``);
167
+ const { added, existing } = foldersAdd(resolve$1(opts.root), modules);
168
+ for (const a of added) console.log(` + design/boards/_folders.json: ${a}`);
169
+ for (const e of existing) console.log(` = ${e} exists - left as it is`);
170
+ } catch (err) {
171
+ console.error(`[${NAME}] ${err.message}`);
172
+ process.exit(1);
173
+ }
174
+ });
175
+ cli.command("context <action>", "The project's context/ (spec 19) · init: create it · check: keep it true (exit 0 pass, 1 fail, 2 cannot determine) · index: regenerate the index's capability table").option("--root <dir>", "Host repo root", { default: "." }).option("--base <ref>", "check: the pull request's base - a change to a contracted capability's code must change its contract or say why (in ci, read from the event)").option("--head <ref>", "check: the pull request's head (default HEAD)").option("--body-file <file>", "check: the pull request's body, for `no-contract-change: <capability> - <why>`").option("--json", "check: the findings as JSON ({ exit, failures, unsure, notes })").option("--kind <kind>", "init: product (a shipped record) or knowledge (a delivered record)", { default: "product" }).action(async (action, opts) => {
176
+ const ctx = await import("./context-DUAENlJ6.mjs");
177
+ const root = resolve$1(opts.root);
145
178
  try {
146
- boardsCommand(resolve$1(opts.root), opts);
179
+ if (action === "init") {
180
+ if (opts.kind !== "product" && opts.kind !== "knowledge") throw new Error(`--kind ${opts.kind}: use product or knowledge`);
181
+ const created = ctx.contextInit(root, opts.kind);
182
+ for (const c of created) console.log(` + ${c}`);
183
+ if (!created.length) console.log(" context/ is already set up - nothing to add");
184
+ console.log(`\n next: tell your agent "Read ${ctx.conventionsPath(root)} and set up our context" - the setup interview, then a first draft from evidence.\n in ci: npx ${NAME} context check (with full history: fetch-depth: 0)\n`);
185
+ } else if (action === "check") {
186
+ const { readFileSync } = await import("node:fs");
187
+ const fromEnv = ctx.pullRequestFromEnv() ?? {};
188
+ const given = {
189
+ ...opts.base !== void 0 ? { base: String(opts.base) } : {},
190
+ ...opts.head !== void 0 ? { head: String(opts.head) } : {},
191
+ ...opts.bodyFile !== void 0 ? { body: readFileSync(opts.bodyFile, "utf8") } : {}
192
+ };
193
+ const merged = {
194
+ ...fromEnv,
195
+ ...given
196
+ };
197
+ if (given.base !== void 0) delete merged.prError;
198
+ const r = ctx.contextCheck(root, merged);
199
+ if (opts.json) console.log(JSON.stringify(r, null, 2));
200
+ else ctx.printCheck(r);
201
+ process.exit(r.exit);
202
+ } else if (action === "index") {
203
+ const { words, changed } = ctx.contextIndex(root);
204
+ console.log(` context/INDEX.md: ${changed ? "capability table regenerated" : "already current"} - ${words} / ${ctx.INDEX_BUDGET} words`);
205
+ } else throw new Error(`unknown action "${action}" - init, check or index`);
147
206
  } catch (err) {
148
207
  console.error(`[${NAME}] ${err.message}`);
149
208
  process.exit(1);