Skip to content

Project package, sandboxed boards, sp pack, and the community page - #183

Merged
Jing-yilin merged 15 commits into
mainfrom
worktree-project-package-plan
Sep 25, 2026
Merged

Jing-yilin merged 15 commits into
mainfrom
worktree-project-package-plan

Conversation

@Jing-yilin

@Jing-yilin Jing-yilin commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Implements phases 0–3 of docs/2026-09-25-project-package.md and adds the community page.

  • Board isolation:
    • Every /board response is served with CSP: sandbox allow-scripts …, and _headers does the same on Pages.
    • The server answers only when the Host is localhost or an IP literal, which stops DNS rebinding.
    • Link shapes open http(s) links only.
  • project.json format 1: a new project gets {format: 1, id}, plus author and contributors as GitHub logins. A project with a newer format is refused with 409. A project with no project.json is read as format 1.
  • sp pack <project> --check | -o <dir>:
    • It ships the project by place and leaves out scratch/, ref-*, comments.json, probes.json and crops.json.
    • It checks JSON, symlinks, references, link schemes, names, logins and sizes (50 MB per file, 200 MB total).
    • With -o, it fills in a missing id, and a missing author from gh api user, then draws thumbnail.png.
  • layout.md and the sp-canvas SKILL now say what belongs at a project's root, and "Sharing a project" gives an agent the steps to open a PR to the community repo.
  • Community (canvas/src/Community.tsx), built from the reference design:
    • The app's home page has a Community tile to the left of Join Discord. It opens a Community tab in the tab bar.
    • The same page is also its own Vite entry, community.html. ReScienceLab/super-prototyping-landing#2 serves it at superproto.dev/community.
    • It lists the projects in ReScienceLab/super-prototyping-community first, from that repo's index.json. Each card shows the project's thumbnail.png, author and contributors, and links to the project on GitHub.
    • The examples come after, each with its thumbnail drawn the way sp pack draws one. A card's Open opens the example in its own tab.
    • A thumbnail is 2400×1260, an Open Graph image twice over, set like a book cover: the canvas's ground, a spine, the app icon, the name in a serif, the board count, and the cover board with its row. sp thumbnail <canvas>... draws an example's; all 21 are committed. Cards show them at their own ratio.
  • The community repo (new): CC BY 4.0 for the work, MIT for the code.
    • Its CI runs sp pack --check on each changed project, checks the thumbnail, and ties author and contributors to the PR's GitHub account. It runs on pull_request_target with read-only permissions and never runs code from the PR.
    • On merge, it writes index.json with each person's numeric GitHub id.
    • Takedown works through an issue template or email.
  • Fixes a tsc error in sp.test.ts, left by an earlier commit, that broke bun run build.

Tests: vitest (228 tests) and tools/test_sp_canvas.py (16) pass, and tsc and oxlint are clean. The community repo's check passed on its seed PR, ReScienceLab/super-prototyping-community#1. The community page was checked in Playwright:

  • in the app: home, the Community tab, a card, Open;
  • on its own at desktop and mobile widths;
  • through the landing Worker;
  • with a shared project served from the repo's index.json.

After merge: delete the community repo's SP_REF variable, so its CI installs sp from main.

🤖 Generated with Claude Code

Jing-yilin and others added 8 commits September 25, 2026 10:07
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A board opened at its own address now runs in an origin of its own, on
the app's server and on Pages, so a project from someone else cannot
drive the canvas's write endpoints. The server answers only to localhost
or an IP address, which closes DNS rebinding. A link shape opens only a
web address. A new project gets project.json with format 1 and a UUID,
and one a newer app made is refused rather than misread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package is the project less what is left out by place: scratch,
captures, dotfiles, comments and measurement evidence. Every reference
the app makes has to resolve inside it, no symlinks, web links only,
50 MB a file and 200 MB in all. -o also draws the cover as
thumbnail.png, and mints the project's id if it has none, the only
write to the project. layout.md documents the folder and format 1, and
the sp-canvas skill tells agents to keep the root clear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Deploying super-prototyping with  Cloudflare Pages  Cloudflare Pages

Latest commit: bf52aa1
Status: ✅  Deploy successful!
Preview URL: https://8b14c980.super-prototyping.pages.dev
Branch Preview URL: https://worktree-project-package-pla.super-prototyping.pages.dev

View logs

Jing-yilin and others added 2 commits September 25, 2026 12:02
…e PR

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home gets a Community tile left of Join Discord, which opens a closable
Community chip on the bar; a card's Open brings that project forward as
its tab. The same page is community.html for the site. Each card shows the
project's thumbnail as sp pack draws thumbnail.png: the cover whole, on
the canvas's ground at 16:10. Until the community repo exists it lists the
examples, by ReScienceLab, and sharing points to Discord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jing-yilin Jing-yilin changed the title Project package: format, sandboxed boards, sp pack Project package, sandboxed boards, sp pack, and the community page Sep 25, 2026
@Jing-yilin
Jing-yilin marked this pull request as ready for review September 25, 2026 16:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83bbf94776

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread canvas/src/Community.tsx Outdated
Comment on lines +524 to +526
<a
className="cm-btn cm-btn--solid cm-btn--md"
href={canvasPageUrl(open.slug)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route standalone community cards through /demo/

When this component is rendered by communitySite.tsx at superproto.dev/community, canvasPageUrl() uses the relative base ./, so this link resolves to https://superproto.dev/?canvas=…—the download page—instead of the hosted canvas under /demo/. The same relative base passed to CoverPicture makes thumbnails request /board/… outside the proxied demo. Consequently, the public Community page cannot reliably display or open its examples; use DEMO for these URLs when openExample is absent.

AGENTS.md reference: AGENTS.md:L35-L41

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug on the site: the landing Worker (ReScienceLab/super-prototyping-landing#2) prepends <base href="/demo/"> to the page's head, so the build's relative ./?canvas=… and ./board/… resolve under /demo/. Checked through the Worker in Playwright: Open goes to /demo/?canvas=apple-app-store and every thumbnail loads.

Comment thread canvas/server/projects.ts
Comment on lines +265 to +268
fs.writeFileSync(
path.join(dir, "project.json"),
`${JSON.stringify({ format: PROJECT_FORMAT, id: crypto.randomUUID() }, null, 2)}\n`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the generated ID when naming an untitled project

When the user leaves the project name blank, AppShell.tsx immediately instructs the agent to write {"name": "…"} as project.json. This newly created file already contains format and id, so following that instruction replaces the generated identity; sp pack --check then rejects the project for lacking an ID, while sp pack -o silently assigns a different one. Update the naming instruction or flow to merge name into the existing object.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 038d8ca: the prompt now tells the agent to add name to project.json, keeping the keys already there.

Comment thread tools/sp_canvas.py Outdated
Comment on lines +718 to +720
found = _cover(project, pj)
if found is None:
raise SystemExit("error: the project has no board to make a thumbnail of")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow packages without an HTML cover

For a project whose canvas contains only a video or another non-HTML content type, _pack() accepts all files and --check can succeed, but _cover() finds no folder containing *.html and this branch aborts sp pack -o. This contradicts the new package format's explicit support for video-only projects and prevents them from being shared; generate a fallback thumbnail or derive one from the available content instead of requiring a board.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a case that exists: a canvas is a folder with *.html in it (boardIndex() in canvas/server/boards.ts), so a folder holding only a video is no canvas and the app never shows it. A video inside a canvas ships, as the test's talk.mp4 does.

Comment thread tools/sp_canvas.py
Comment on lines +590 to +592
if canvases.is_symlink():
problems.append("canvases: is a symlink")
elif canvases.is_dir():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject Windows junctions before walking canvases

On Windows, a directory junction is not reported by Path.is_symlink(), and os.walk() follows it as a directory. A project containing a canvases junction—or a nested junction handled by the identical check below—therefore packages files outside the project, potentially publishing unrelated or sensitive data despite the no-path-escape guarantee. Reject junction/reparse points explicitly or verify every resolved path remains under the resolved project root.

AGENTS.md reference: AGENTS.md:L69-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 038d8ca: every shipped file is also refused when it resolves outside the project, which catches a junction wherever it is. Test added.

Jing-yilin and others added 2 commits September 25, 2026 12:41
… one

-o sets a missing author to the login gh is signed in as; --check reports one missing,
and both refuse an author or contributors that are not GitHub logins. The community
repo holds them to whoever opens the pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reads index.json from ReScienceLab/super-prototyping-community and lists
those projects first, each by its thumbnail.png, with its contributors.
Sharing and takedown now point at that repo. The plan records Phase 3 as
built and moves the licence and rename questions to Decided.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 753cf453e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/sp_canvas.py
file, box, size, ground = found
scale = 2
if size is None:
im = Image.open(file).convert("RGBA")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rasterize SVG covers before passing them to Pillow

When a project uses a listed SVG brand image as its cover—a supported path that the UI can create—_pack() accepts it, but sp pack -o reaches this branch and passes the SVG to Pillow, which does not decode SVG files. The command therefore fails instead of producing the package; render SVG covers through the browser or another SVG rasterizer before compositing the thumbnail.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 038d8ca: an SVG cover is drawn by Chrome at the size its layout row gives it, as a board is. Checked on a real SVG.

Comment thread tools/sp_canvas.py
Comment on lines +612 to +615
total = sum(size for _, size in ship)
if total > PACK_TOTAL_CAP:
problems.append(f"{total >> 20} MB in all, over the {PACK_TOTAL_CAP >> 20} MB a "
"package can be")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count the generated thumbnail against the package cap

When the selected project files total just under 200 MiB, this check passes even though -o subsequently adds thumbnail.png and may add or enlarge project.json. The resulting package can exceed the declared total-size limit and be rejected by the community repository's validation despite sp pack reporting success; recheck the total after all generated files are known or reserve their size here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving as is: the cap is on the project's own files, and the community repo's CI runs the same sp pack --check, which leaves the generated thumbnail.png out of the count too, so the two agree. The thumbnail is a 1600 × 1000 PNG, a few MB at most.

…new project's id when naming it

From the Codex review of #183. A Windows junction is no symlink to
is_symlink(), so a shipped file is now also refused when it resolves
outside the project. Pillow reads no SVG, so Chrome draws an SVG cover
as it draws a board. The naming prompt told the agent to write
{"name": …} as project.json, which dropped the id the app had just
written; it now adds the key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 038d8cae40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

</a>
<nav>
<a href="./">Demo</a>
<a href="https://discord.gg/2DEZFFKx7k" aria-label="Discord">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point the Demo navigation to /demo/

On the standalone page served at https://superproto.dev/community, href="./" resolves to https://superproto.dev/, which is the download page, so the navigation item labeled “Demo” does not open the hosted canvas at /demo/. Point this link explicitly at /demo/.

AGENTS.md reference: AGENTS.md:L35-L41

Useful? React with 👍 / 👎.

Comment thread tools/sp_canvas.py

def json_of(rel):
try:
return json.loads((project / rel).read_text(encoding="utf-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-JSON numeric constants during packing

Python's json.loads accepts NaN, Infinity, and -Infinity by default, so an app-defined file containing one of these values passes _pack() and sp pack --check. JavaScript's JSON.parse rejects those files, meaning community CI can admit a package whose project.json, layout.json, or canvas.json the app cannot read; configure the decoder to reject non-standard constants.

Useful? React with 👍 / 👎.

Comment thread tools/sp_canvas.py Outdated
Comment on lines +701 to +703
if chosen.get("path"):
file = canvases / chosen["path"]
layout = layout_of(canvases / Path(chosen["path"]).parts[0])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore image covers no longer listed in the layout

When a previously selected brand image remains on disk after its rows[].images entry is removed, the app's projectCover() ignores that stale choice and falls back to a board, but this branch honors any existing cover.path. Packing therefore generates a community thumbnail from a different image than the local project card; require an image cover to remain listed in the layout before using it.

Useful? React with 👍 / 👎.

Jing-yilin and others added 2 commits September 25, 2026 13:08
The window is the viewport's height and clips, so the tab showed only
its first two rows and the wheel did nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thumbnail is 2400x1260, an Open Graph image's 1200x630 twice over, set like
a book's cover on the canvas's ground: a spine, the app's icon, the name in a
serif, the board count, and the cover board with the row after it. sp pack
draws a project's; sp thumbnail draws a canvas folder's, and each example now
commits one. The community page shows only thumbnails, at their own ratio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf52aa12e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/sp_canvas.py
tall, which the thumbnail sets beside it while they fit: layout.json's cover, else its
first board that is not a 00- one."""
layout = _layout_of(folder)
names = sorted((f.stem for f in folder.glob("*.html")), key=_numeric)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude reference boards from generated thumbnails

When layout.json selects a ref-* board as its cover—or the first canvas folder contains only reference boards—_pack() correctly excludes that HTML, but _canvas_cover() scans the original folder and renders it into the package's public thumbnail.png. This can publish a third-party capture that the package rules deliberately leave out; filter these candidates using the same exclusion rules as _pack().

AGENTS.md reference: AGENTS.md:L108-L113

Useful? React with 👍 / 👎.

@Jing-yilin
Jing-yilin merged commit 047bf47 into main Sep 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant