@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +134 -15
  2. package/README.md +17 -4
  3. package/dist/{bake-BID6mo-N.mjs → bake-CvY8QR3L.mjs} +1 -1
  4. package/dist/board-status-CWGHIdo_.mjs +1307 -0
  5. package/dist/boards-BwKI0qWd.mjs +188 -0
  6. package/dist/{build-C7MqQ7hq.mjs → build-B1kavcpc.mjs} +74 -13
  7. package/dist/cli.mjs +69 -9
  8. package/dist/config-DJxMRVD8.mjs +373 -0
  9. package/dist/context-D4t59wDi.mjs +600 -0
  10. package/dist/{daemon-CRZFpl6K.mjs → daemon-PmNqoOAk.mjs} +1 -1
  11. package/dist/{dev-BNZF4Mup.mjs → dev-Cf4wLGe2.mjs} +15 -7
  12. package/dist/{init-C34BY3R4.mjs → init-C_K-cjBQ.mjs} +41 -45
  13. package/dist/managed-write-Bo-oPc-i.mjs +71 -0
  14. package/dist/{manifest-mMfUhPtL.mjs → manifest-CcdWx7ud.mjs} +42 -376
  15. package/dist/{plugin-omHLCn91.mjs → plugin-C5u07Yhv.mjs} +234 -105
  16. package/dist/{poster-BvxiAzy1.mjs → poster-BduzQBYz.mjs} +1 -1
  17. package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-b0jUwvM_.mjs} +2 -2
  18. package/dist/{shot-DswS4iRK.mjs → shot-BAR8hmU9.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 +33 -3
  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 +88 -9
  28. package/src/client/shell/styles.css +34 -3
  29. package/src/shared/board-tree.ts +30 -13
  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 +12 -0
  34. package/templates/AGENTS-studio.md +12 -0
  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 +33 -0
  40. package/templates/instructions/context.md +123 -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-BwiDAmPf.mjs +0 -337
  45. package/dist/boards-DnLewfj8.mjs +0 -71
@@ -0,0 +1,188 @@
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
+ if ([...notes.values()].filter((n) => n.status).length && !existsSync(join(root, "context"))) console.log(`\nno context/ yet - every feature and project board reads Backlog. Agent: end your reply with the context line in design/AGENTS.md ("Before the method").`);
84
+ }
85
+ /** `marver boards new <name>` - a board in its type's starting layout (spec 20): a feature's three
86
+ * phase scenes (spec, lo-fi, hi-fi) as three bands, a start board rendering context/INDEX.md and
87
+ * context/shipped.md, a deck's title slide. The type is `--type`, else the folder's. Never
88
+ * overwrites a board, a scene brief or a frame that exists. */
89
+ function boardsNew(root, name, opts) {
90
+ if (!isBoardName(name) || name === "all-scenes") throw new Error(`"${name}" is not a board name - lowercase letters, numbers and dashes`);
91
+ const dir = join(root, "design", "boards");
92
+ const de = checkBoardsDir(root, dir);
93
+ if (de) throw new Error(de);
94
+ const file = join(dir, `${name}.json`);
95
+ if (existsSync(file)) throw new Error(`design/boards/${name}.json exists - a board is never overwritten`);
96
+ if (opts.type !== void 0 && !knownType(opts.type)) throw new Error(`--type ${opts.type}: use ${BOARD_TYPES.join(", ")}`);
97
+ if (opts.folder !== void 0 && !isBoardName(opts.folder)) throw new Error(`--folder ${opts.folder} is not a folder name`);
98
+ if (opts.capability !== void 0 && !readCapability(opts.capability)) throw new Error(`--capability ${opts.capability} is not a capability slug`);
99
+ const reg = readRegistry(dir);
100
+ if (reg.state === "malformed") throw new Error(reg.error);
101
+ const folder = opts.folder ? reg.state === "ok" ? reg.folders.find((f) => f.name === opts.folder) : void 0 : void 0;
102
+ const parent = folder?.parent && reg.state === "ok" ? reg.folders.find((f) => f.name === folder.parent) : void 0;
103
+ const type = resolveType(opts.type, folder?.type, parent?.type);
104
+ const label = opts.title ?? humanize(name);
105
+ const created = [];
106
+ const put = (rel, body) => {
107
+ const f = join(root, rel);
108
+ const scene = rel.split("/")[2];
109
+ const bad = checkRealDirs(root, [
110
+ [join(root, "design"), "design"],
111
+ [join(root, "design", "scenes"), "design/scenes"],
112
+ [join(root, "design", "scenes", scene), `design/scenes/${scene}`]
113
+ ]);
114
+ if (bad) throw new Error(bad);
115
+ mkdirSync(join(f, ".."), { recursive: true });
116
+ try {
117
+ writeFileSync(f, body, { flag: "wx" });
118
+ created.push(rel);
119
+ } catch (e) {
120
+ if (e.code !== "EEXIST") throw e;
121
+ }
122
+ };
123
+ const brief = (scene, title, body, phase) => put(`design/scenes/${scene}/_brief.md`, `---\ntitle: ${title}\n${phase ? `phase: ${phase}\n` : ""}---\n${body}\n`);
124
+ let nodes = [];
125
+ let layout;
126
+ if (type === "feature") {
127
+ const cap = opts.capability ?? name;
128
+ const [specs, lofi, hifi] = [
129
+ `${name}-specs`,
130
+ `${name}-lofi`,
131
+ name
132
+ ];
133
+ 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");
134
+ brief(lofi, `${label} - lo-fi`, `${label} in throwaway wireframes: structure and copy, no styling.`, "lofi");
135
+ brief(hifi, label, `${label} in hi-fi, from the product's real components.`, "hifi");
136
+ layout = { rows: [
137
+ [specs],
138
+ { space: 3 },
139
+ [lofi],
140
+ { space: 4 },
141
+ [hifi]
142
+ ] };
143
+ } else if (type === "start") {
144
+ const strip = "(md: string) => md.replace(/^<!--[^\\n]*-->\\n/, '').replace(/^---\\n[\\s\\S]*?\\n---\\n/, '')";
145
+ 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`);
146
+ brief(name, label, "The way in: the index and the shipped record, rendered from context/.");
147
+ frame("index", "The index", "context/INDEX.md", "context/INDEX.md, rendered - where every question is answered");
148
+ frame("shipped", "Shipped", "context/shipped.md", "context/shipped.md, rendered - what runs where, with its evidence");
149
+ nodes = [{ frame: `${name}/index` }, { frame: `${name}/shipped` }];
150
+ layout = { rows: [[name]] };
151
+ } else if (type === "deck") {
152
+ brief(name, label, `${label}: the deck. Read design/instructions/slides.md before the first slide.`);
153
+ 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`);
154
+ nodes = [{ frame: `${name}/01-title` }];
155
+ layout = { rows: [[name]] };
156
+ }
157
+ const board = {
158
+ version: 1,
159
+ name,
160
+ auto: false,
161
+ ...opts.type ? { type: opts.type } : {},
162
+ ...opts.folder ? { folder: opts.folder } : {},
163
+ ...opts.title ? { title: opts.title } : {},
164
+ ...opts.description ? { description: opts.description } : {},
165
+ ...opts.capability ? { capability: opts.capability } : {},
166
+ nodes,
167
+ ...layout ? { layout } : {}
168
+ };
169
+ mkdirSync(dir, { recursive: true });
170
+ try {
171
+ writeFileSync(file, JSON.stringify(board, null, 2) + "\n", { flag: "wx" });
172
+ } catch (e) {
173
+ if (e.code === "EEXIST") throw new Error(`design/boards/${name}.json exists - a board is never overwritten`);
174
+ throw e;
175
+ }
176
+ created.unshift(`design/boards/${name}.json (${type}${opts.type ? "" : type === "plain" ? "" : `, from ${opts.folder}`})`);
177
+ return created;
178
+ }
179
+ /** `marver folders add <module...>` - the shared sidebar's typed folders (spec 20): start, features,
180
+ * surfaces, projects, feedback, context, decks, archive. Appended; never renames or moves one. */
181
+ function foldersAdd(root, modules) {
182
+ const unknown = modules.filter((m) => !FOLDER_MODULES[m]);
183
+ if (unknown.length) throw new Error(`unknown folder module: ${unknown.join(", ")} - use ${Object.keys(FOLDER_MODULES).join(", ")}`);
184
+ if (!modules.length) throw new Error(`name a module: ${Object.keys(FOLDER_MODULES).join(", ")} (e.g. \`npx ${NAME} folders add decks\`)`);
185
+ return addFolders(root, modules.map((m) => FOLDER_MODULES[m]));
186
+ }
187
+ //#endregion
188
+ 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, n as checkBoardsDir, p as isBoardName, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BwiDAmPf.mjs";
3
- import { c as loadConfig, r as scanFrames, u as detectHost } from "./manifest-mMfUhPtL.mjs";
4
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-omHLCn91.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 { i as scanFrames } from "./manifest-CcdWx7ud.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-C5u07Yhv.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 } : {},
@@ -365,10 +368,16 @@ function publishedTree(published, allBoards, folders) {
365
368
  /** The bundle's manifest: the published frames, and descriptions of published things ONLY -
366
369
  * the project's, the published scenes' (their brief path only when source is revealed),
367
370
  * the published boards' and the folders those sit in. No unpublished name or sentence. */
368
- function publishedManifest(manifest, pubFrames, publishedNames, strip) {
371
+ function publishedManifest(manifest, pubFrames, publishedNames, strip, showStatus = /* @__PURE__ */ new Set()) {
369
372
  const pubScenes = new Set(pubFrames.map((f) => f.scene));
370
373
  const pubBoardSet = new Set(publishedNames);
371
- 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
+ });
372
381
  const pubFolderSet = new Set(pubBoards.map((b) => b.folder).filter(Boolean));
373
382
  for (const f of manifest.folders ?? []) if (f.parent && pubFolderSet.has(f.name)) pubFolderSet.add(f.parent);
374
383
  const pubFolders = (manifest.folders ?? []).filter((f) => pubFolderSet.has(f.name));
@@ -387,6 +396,39 @@ function publishedManifest(manifest, pubFrames, publishedNames, strip) {
387
396
  frames: pubFrames
388
397
  };
389
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
+ }
390
432
  /** The folder registry: absent = no folders (boards still imply theirs); malformed fails the build. */
391
433
  function readFolders(root) {
392
434
  const reg = readRegistry(join(root, "design", "boards"));
@@ -502,6 +544,17 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
502
544
  });
503
545
  const allBoards = readBoards(root);
504
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
+ }
505
558
  const rights = Object.fromEntries(Object.entries(policy.boards).map(([n, p]) => [n, p.max]));
506
559
  const strip = !policy.reveal.source;
507
560
  const buildSalt = randomBytes(16).toString("hex");
@@ -509,7 +562,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
509
562
  const { tree, names: publishedNames } = publishedTree(Object.keys(rights), allBoards, readFolders(root));
510
563
  const includeAll = publishedNames.includes("all-scenes");
511
564
  const boards = {};
512
- 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]);
513
566
  let frames = manifest.frames;
514
567
  if (!includeAll) {
515
568
  const wanted = /* @__PURE__ */ new Set();
@@ -520,7 +573,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
520
573
  ...f,
521
574
  file: opaquePath(f.id, f.kind === "html" ? ".html" : "")
522
575
  })) : frames;
523
- 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);
524
578
  for (const n of publishedNames) {
525
579
  const pb = policy.boards[n];
526
580
  if (pb?.type !== "slides" && pb?.open !== "slides") continue;
@@ -552,6 +606,10 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
552
606
  const t = boardFields(allBoards[n], isBoardName).title;
553
607
  return t ? [[n, t]] : [];
554
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
+ }]] : []));
555
613
  const bakeGen = textures ? Date.now() : void 0;
556
614
  const data = {
557
615
  bakes: bakeGen,
@@ -560,6 +618,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
560
618
  names: publishedNames,
561
619
  tree,
562
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() } : {},
563
623
  default: publishedNames.find((n) => n !== "all-scenes") ?? publishedNames[0],
564
624
  rights,
565
625
  policy: {
@@ -568,6 +628,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
568
628
  ...lockedShell ? { lockedShell: true } : {}
569
629
  }
570
630
  };
631
+ assertProjected(data, showStatus);
571
632
  const registryFile = posix(join(clientDir, "frame-host", "registry.ts"));
572
633
  const lockedFile = posix(join(clientDir, "shell", "LockedApp.tsx"));
573
634
  const tsxFrames = frames.filter((f) => f.kind === "tsx");
@@ -693,7 +754,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
693
754
  const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
694
755
  for (const r of refs) {
695
756
  if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
696
- const { ensurePoster } = await import("./poster-BvxiAzy1.mjs");
757
+ const { ensurePoster } = await import("./poster-BduzQBYz.mjs");
697
758
  const g = await ensurePoster(assetsDir, r.slice(0, -11));
698
759
  if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
699
760
  console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
@@ -760,7 +821,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
760
821
  }));
761
822
  let textureLine = " textures: skipped (--no-textures)";
762
823
  if (textures && bakeGen) {
763
- const { bakePublished } = await import("./publish-bakes-BqzAAa3w.mjs");
824
+ const { bakePublished } = await import("./publish-bakes-b0jUwvM_.mjs");
764
825
  const pubFrameFile = new Map(pubFrames.map((f) => [f.id, f]));
765
826
  const r = await bakePublished({
766
827
  root,
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
3
3
  import { cac } from "cac";
4
- import { readFileSync, realpathSync } from "node:fs";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  //#region src/client/const.ts
@@ -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-C34BY3R4.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-C_K-cjBQ.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-BNZF4Mup.mjs");
93
+ const { dev } = await import("./dev-Cf4wLGe2.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-C7MqQ7hq.mjs");
103
+ const { buildSite } = await import("./build-B1kavcpc.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,65 @@ 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-DnLewfj8.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-BwKI0qWd.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 (/\((feature|project)\b/.test(made[0] ?? "") && !existsSync(join(resolve$1(opts.root), "context"))) console.log(`\n note: no context/ yet - this board's status reads Backlog until there is one. Agent: end your reply with the context line in design/AGENTS.md ("Before the method").`);
157
+ 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)`);
158
+ } else throw new Error(`unknown action "${action}" - \`boards\` lists, \`boards new <name>\` creates`);
159
+ } catch (err) {
160
+ console.error(`[${NAME}] ${err.message}`);
161
+ process.exit(1);
162
+ }
163
+ });
164
+ 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) => {
165
+ const { foldersAdd } = await import("./boards-BwKI0qWd.mjs");
166
+ try {
167
+ if (action !== "add") throw new Error(`unknown action "${action}" - \`folders add <module ...>\``);
168
+ const { added, existing } = foldersAdd(resolve$1(opts.root), modules);
169
+ for (const a of added) console.log(` + design/boards/_folders.json: ${a}`);
170
+ for (const e of existing) console.log(` = ${e} exists - left as it is`);
171
+ } catch (err) {
172
+ console.error(`[${NAME}] ${err.message}`);
173
+ process.exit(1);
174
+ }
175
+ });
176
+ 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) => {
177
+ const ctx = await import("./context-D4t59wDi.mjs");
178
+ const root = resolve$1(opts.root);
145
179
  try {
146
- boardsCommand(resolve$1(opts.root), opts);
180
+ if (action === "init") {
181
+ if (opts.kind !== "product" && opts.kind !== "knowledge") throw new Error(`--kind ${opts.kind}: use product or knowledge`);
182
+ const created = ctx.contextInit(root, opts.kind);
183
+ for (const c of created) console.log(` + ${c}`);
184
+ if (!created.length) console.log(" context/ is already set up - nothing to add");
185
+ 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`);
186
+ } else if (action === "check") {
187
+ const { readFileSync } = await import("node:fs");
188
+ const fromEnv = ctx.pullRequestFromEnv() ?? {};
189
+ const given = {
190
+ ...opts.base !== void 0 ? { base: String(opts.base) } : {},
191
+ ...opts.head !== void 0 ? { head: String(opts.head) } : {},
192
+ ...opts.bodyFile !== void 0 ? { body: readFileSync(opts.bodyFile, "utf8") } : {}
193
+ };
194
+ const merged = {
195
+ ...fromEnv,
196
+ ...given
197
+ };
198
+ if (given.base !== void 0) delete merged.prError;
199
+ const r = ctx.contextCheck(root, merged);
200
+ if (opts.json) console.log(JSON.stringify(r, null, 2));
201
+ else ctx.printCheck(r);
202
+ process.exit(r.exit);
203
+ } else if (action === "index") {
204
+ const { words, changed } = ctx.contextIndex(root);
205
+ console.log(` context/INDEX.md: ${changed ? "capability table regenerated" : "already current"} - ${words} / ${ctx.INDEX_BUDGET} words`);
206
+ } else throw new Error(`unknown action "${action}" - init, check or index`);
147
207
  } catch (err) {
148
208
  console.error(`[${NAME}] ${err.message}`);
149
209
  process.exit(1);