Skip to content

Commit ea41296

Browse files
authored
Merge pull request #167 from ReScienceLab/prd-tab
PRD tab and the define-product skill
2 parents 502136a + 947d170 commit ea41296

60 files changed

Lines changed: 2000 additions & 384 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,30 @@ holds only *data*.
77

88
Code, shipped to every install:
99

10-
`skills/` holds `clone-prototype`, `new-ui-mock`, `prototype-canvas` and
11-
`brand-kit`.
10+
`skills/` holds `clone-prototype`, `new-ui-mock`, `prototype-canvas`,
11+
`define-product` and `brand-kit`.
1212
`.claude/skills/` and `.agents/skills/` are symlinks to it, so this checkout
1313
loads the same tree an install does.
1414

1515
`canvas/` is the tldraw viewer, built with Bun and Vite. Its server serves
1616
every project under `~/Documents/Super Prototyping` (`PROTOTYPING_PROJECTS_DIR`
17-
moves it) at `/p/<name>/`, plus the one `sp start` or the app opened, with this
18-
repo's `canvases` as the examples shown beside each project's own. A
17+
moves it) at `/p/<name>/`, and no folder anywhere else, with this repo's
18+
`canvases` as the examples shown beside each project's own.
19+
`docs/2026-09-23-projects-folder-only.md` says why. A
1920
project's boards are its `canvases`, discovered as `*/*.html` one level
2021
deep. Discovery is `boardIndex()` in `canvas/server/boards.ts`, served as JSON at
2122
`/__sp/index.json` by `canvas/server/sp.ts` and written into `dist` by the
2223
build — not an `import.meta.glob`, because a glob pattern is a build-time
23-
literal and could only ever read one hard-coded directory. `sp start`
24+
literal and could only ever read one hard-coded directory. The same index
25+
carries a project's documents, for now only the `PRD.md` at its root. Each
26+
is a tab before the canvases, shown rendered or as editable text (`DOCS` in
27+
`boards.ts`, `DocTab.tsx`). `sp start`
2428
runs the built app, `dist/server.mjs`: the release's `canvas-dist.tgz`
2529
fetched into `~/.cache/super-prototyping/<version>/` for an install, or this
2630
checkout's own `canvas/dist` when `canvas/node_modules` exists. The canvas's
27-
`dev` script mounts the same server under Vite, with this checkout open as the
28-
project, for working on the app.
31+
`dev` script mounts the same server under Vite, for working on the app. It opens
32+
on the home page with no project, as the app does: this checkout's canvases are
33+
the examples there, not a project of their own.
2934

3035
The hosted canvas is that build on Cloudflare Pages, and it lives at
3136
`prototyping.rescience.com/demo/` now: the root is the download page, whose
@@ -88,6 +93,8 @@ Rules inside a canvas folder:
8893
output. Edit the generator and re-run, never the HTML.
8994
- Commit `layout.json`, `icon.png` and `assets/`. `gen.py` inlines the
9095
images in `assets/` as `data:` URIs.
96+
- Commit `PRD.md`, the product the folder prototypes, to the `define-product`
97+
skill's template. Its Screens table lists the folder's boards.
9198
- Commit `probes.json` and `crops.json`. They are the measurement evidence
9299
behind the tokens.
93100
- Commit `assets.json` where a folder has one (three do). It is a

‎README.md‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,19 @@ is in there to copy from too.
143143
## Run the canvas
144144

145145
```bash
146-
sp open # this project, in the app
147-
sp open ~/my-app # any project, from anywhere
148-
sp start ~/my-app # the same canvas in a browser, without the app
146+
sp open # the app, on its home page
147+
sp start # the same canvas in a browser, without the app
149148
```
150149

151-
`sp open` hands the project to the app, starting it if it is not running, and
152-
prints the address. `sp start` is for where the app cannot run. From a checkout
153-
without a build, it downloads the canvas built for that version into `~/.cache/super-prototyping/`, then serves it on 127.0.0.1:5173
154-
against the project's `canvases` with node or bun, opens the browser,
155-
and prints the address. Every project under `~/Documents/Super Prototyping` is
156-
served beside it at `/p/<name>/`, the same way the app serves them;
157-
`PROTOTYPING_PROJECTS_DIR` moves that folder. `--port N` (or
150+
Every project is a folder under `~/Documents/Super Prototyping`, made from the
151+
home page's New project, and served at `/p/<name>/`;
152+
`PROTOTYPING_PROJECTS_DIR` moves that folder. No folder elsewhere is ever
153+
opened as a project, so a project's Delete cannot trash code you did not make
154+
there. `sp open` starts the app if it is not running and prints the address.
155+
`sp start` is for where the app cannot run. From a checkout without a build,
156+
it downloads the canvas built for that version into
157+
`~/.cache/super-prototyping/`, then serves it on 127.0.0.1:5173 with node or
158+
bun, opens the browser, and prints the address. `--port N` (or
158159
`SP_CANVAS_PORT`) moves the port, `sp status` and `sp stop` do what
159160
they say. `sp paths` lists the two directories it writes, and
160161
`sp clean` removes them.
@@ -167,14 +168,15 @@ folder added after boot appears on its own.
167168

168169
## The workflow
169170

170-
Four skills, in `skills/` (which `.claude/skills/` and `.agents/skills/`
171+
Five skills, in `skills/` (which `.claude/skills/` and `.agents/skills/`
171172
symlink to, so this checkout loads what an install does):
172173

173174
| Skill | Use it for |
174175
|---|---|
175176
| **clone-prototype** | Copying a real app's screens. Grid the reference, sample colours *visually*, name the type face, derive one measured token block, generate the artboards, verify by re-rendering, park the reference underneath. |
176177
| **new-ui-mock** | Designing new screens with no reference, built on existing tokens, including the empty/loading/error states and side-by-side proposals. |
177178
| **prototype-canvas** | Running and operating the canvas: boards, `layout.json`, the `window.snapCanvas` bridge, annotated-screenshot review, the force-refresh. |
179+
| **define-product** | Working out what the product is before anything is drawn: an interview, one or two questions at a time, problem before solution, gaps left TBD rather than invented. It writes the project's `PRD.md`, which the canvas shows as the first tab, with the screen inventory new-ui-mock designs from. |
178180
| **brand-kit** | Collecting a product's own brand and promotional material -- the company's own brand or press kit, store listings, verified social accounts, the newsroom -- and turning it into the image rows of a canvas folder, each asset carrying its source and whether the company published it. |
179181

180182
The rule the whole thing is built around: **every colour and every metric in

‎canvas/server/agent.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,12 @@ export function createAgentServer(options: {
435435
"the prototype-canvas skill of the super-prototyping plugin: one folder is one canvas " +
436436
"page, one .html file in it is one board, layout.json places them, and the open canvas " +
437437
"reloads by itself when a board is rewritten.",
438+
dir !== undefined &&
439+
`${path.join(dir, "PRD.md")}, when there is one, is a tab of the canvas ` +
440+
"and says what the product is for. When they start from an idea, or ask for screens " +
441+
"with no PRD.md there, offer to define the product with them first, following " +
442+
`${repoRoot}/skills/define-product/SKILL.md; never insist. When it exists, read it ` +
443+
"before designing.",
438444
// On the turn that starts the session only. A resumed one has its title.
439445
record.resume === null &&
440446
"Open your first reply with a title for this conversation on a line of its own, " +

‎canvas/server/boards.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ import { svgSignature } from "../src/svgSignature.ts";
1313
*/
1414
export const CANVASES = "canvases";
1515

16+
/**
17+
* A project's documents: the Markdown files at its root, beside its canvases, that the canvas
18+
* shows as tabs before them, in this order. For now only the PRD the define-product skill writes
19+
* with the user. An example is a project of one canvas, so its folder is its root and its
20+
* documents are read from there. */
21+
export const DOCS = ["PRD.md"];
22+
23+
export function readDocs(dir: string) {
24+
return DOCS.filter((name) =>
25+
fs.statSync(path.join(dir, name), { throwIfNoEntry: false })?.isFile(),
26+
).map((name) => ({ name, text: fs.readFileSync(path.join(dir, name), "utf8") }));
27+
}
28+
1629
/** The image types a board folder can hold, and what each is served as. */
1730
export const IMAGE_MIME: Record<string, string> = {
1831
".png": "image/png",
@@ -287,6 +300,7 @@ export function boardIndex(
287300
thumbs: [] as string[],
288301
assets: assetIndex(folder),
289302
comments: readJson(path.join(folder, "comments.json")),
303+
docs: readDocs(folder),
290304
};
291305
})
292306
.filter(

‎canvas/server/main.ts‎

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
* The canvas as a localhost app: the built `dist` served as static files, with every project's
33
* `/__sp` and `/board` server in front of it (projects.ts), the same one the Vite dev server
44
* mounts. Bundled to `dist/server.mjs` by `bun run build`, so `sp start` runs one file with node
5-
* or bun and no dev toolchain. Usage: `node dist/server.mjs --port 5173 --open <project>`.
5+
* or bun and no dev toolchain. Usage: `node dist/server.mjs --port 5173`.
66
*
77
* Every folder in the projects directory is served at `/p/<name>/`, with the examples beside
8-
* it. `--open` serves one more folder, from anywhere, which is what `sp start` opens, and `/`
9-
* redirects to it. The desktop app runs this same file and opens its folders over the parent
10-
* port instead.
8+
* it, and no other folder: `/` is the home page, where a project is made. The desktop app runs
9+
* this same file.
1110
*/
1211
import fs from "node:fs";
1312
import http from "node:http";
@@ -85,30 +84,6 @@ function serveStatic(req: http.IncomingMessage, res: http.ServerResponse) {
8584
}
8685

8786
const projects = createProjectsServer({ projectsDir, repoRoot });
88-
// Before the port is taken, so a folder that is not there fails the start rather than a server.
89-
const openArg = arg("--open");
90-
if (openArg !== undefined) projects.open(openArg);
91-
92-
/**
93-
* A folder's path, from the desktop app before it opens a project, answered with the address it
94-
* is served at. The path comes over the parent port of Electron's utility process, which only the
95-
* app that started this server holds. `sp start` runs under node, which has no parent port.
96-
*/
97-
const parentPort = (
98-
process as NodeJS.Process & {
99-
parentPort?: {
100-
on(event: "message", listener: (message: { data: string }) => void): void;
101-
postMessage(message: { address: string } | { error: string }): void;
102-
};
103-
}
104-
).parentPort;
105-
parentPort?.on("message", ({ data }) => {
106-
try {
107-
parentPort.postMessage({ address: projects.open(data) });
108-
} catch (error) {
109-
parentPort.postMessage({ error: String(error) });
110-
}
111-
});
11287

11388
const server = http.createServer((req, res) =>
11489
projects.handle(req, res, () => serveStatic(req, res)),

‎canvas/server/projects.test.ts‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ it("serves every project at its own address and makes new ones", async () => {
2727
// Both projects keep their boards where they used to be, which the server moves to `canvases`.
2828
write("projects/alpha/mockups/canvases/one/01-a.html", "alpha one");
2929
write("projects/alpha/mockups/.DS_Store", ""); // Finder's, which is no reason to keep the folder
30-
write("elsewhere/mockups/canvases/mine/01-a.html", "mine");
30+
write("elsewhere/canvases/mine/01-a.html", "mine");
3131

3232
const projects = createProjectsServer({
3333
projectsDir: path.join(tmp, "projects"),
@@ -115,14 +115,8 @@ it("serves every project at its own address and makes new ones", async () => {
115115
fs.rmSync(path.join(tmp, "projects/.workspaces"), { recursive: true });
116116
expect((await ask("/__sp/agent/run/gone-run/events")).status).toBe(404);
117117

118-
expect(projects.open(path.join(tmp, "elsewhere"))).toBe("/p/elsewhere/");
119-
expect(fs.existsSync(path.join(tmp, "elsewhere/canvases/mine"))).toBe(true);
120-
expect(await ask("/?canvas=x")).toMatchObject({
121-
status: 302,
122-
location: "/p/elsewhere/?canvas=x",
123-
});
124-
expect((await ask("/p/elsewhere/")).text).toBe("static /");
125-
expect((await ask("/p/elsewhere/board/mine/01-a.html")).text).toBe("mine");
118+
// A folder outside the projects directory is never a project.
119+
expect((await ask("/p/elsewhere/")).status).toBe(404);
126120
expect((await ask("/p/alpha/board/one/01-a.html")).text).toBe("alpha one");
127121
expect((await ask("/p/nowhere/")).status).toBe(404);
128122
// The examples come from the plugin root, beside every project's own.
@@ -131,7 +125,6 @@ it("serves every project at its own address and makes new ones", async () => {
131125
);
132126
const listed = JSON.parse((await ask("/__sp/projects.json")).text);
133127
expect(listed.map((p: any) => [p.name, p.url, p.path])).toEqual([
134-
["elsewhere", "/p/elsewhere/", path.join(tmp, "elsewhere")],
135128
["alpha", "/p/alpha/", path.join(tmp, "projects/alpha")],
136129
]);
137130
// A card's menu names a project the server has. Moving one to the Trash and showing one in

0 commit comments

Comments
 (0)