← Commit history

wiki-bloat: case study on talking past the --because gate. wiki-readme-html: connect-src is 'self' now (not none), per-file 4MB render cap, and why a failed sandbox init is silent

John Lauer ·202c605a84 ·2mo ago ·parent 40c471d
3 files changed +86−12
package.json+2−2
@@ -1,7 +1,7 @@ {   "slug": "adom-wiki-skillpack",   "title": "Adom Wiki Skill Pack",-  "version": "0.8.2",+  "version": "0.8.3",   "type": "skill",   "hero": {     "path": "screenshots/hero.png"@@ -119,4 +119,4 @@     }   ],   "discovery_pitch": "Anything touching wiki.adom.inc: publishing, page archetypes, readmes, heroes, discovery, and the wiki-bloat rule (find where a thing already lives before creating any page)."-}\ No newline at end of file+}
skills/wiki-bloat/SKILL.md+50−3
@@ -81,9 +81,56 @@ Three compounding failures, each independently avoidable:    a STOP sign with a reason behind it: find out WHY it was deleted before even considering    undelete, and never undelete just because your content needs somewhere to land. -(Tooling gap, filed the same day: the CLI reprimands on shared-identity publishes and placeholder-changelogs, but `page create` and `page undelete` ask no bloat question at all. Until they do, this-skill IS the gate.)+(Tooling update: `page create` now REQUIRES `--because` — "why no existing page is the right+home for this content" — and rejects placeholder answers. That closes half the gap named here.+It does not close the other half: the gate only checks that you wrote *something* persuasive,+and a determined agent will happily write a paragraph justifying the page it already decided to+make. See the 2026-08-20 case study below. `page undelete` still asks nothing.)++## Case study (real, 2026-08-20): talking your way past the gate++An agent spent an afternoon debugging why adom-tsci's 3D viewer renders blank inside a wiki+readme. It found the cause (a `localStorage` read that throws in a sandboxed document), fixed+the board, and was then asked to "write this up as a skill so future users get it."++It created `adom/adom-tsci-export-wiki-readme-html`. A new page. New slug, new hero, new+listing. The founder's response was the same as 2026-07-26: *"dude, did you fucking create+wiki-bloat?"*++Three homes already existed, and the agent had touched two of them that same session:++- **`adom/adom-tsci` already owns `guides/export-wiki.md`** — literally a guide on this exact+  subject, with a deploy path (`adom-tsci install` → `~/.claude/skills/adom-tsci/guides/`) so+  it reaches every user of the app automatically. The agent had READ this guide earlier in the+  session to learn the publish flow, and still did not put the content back into it.+- **`wiki-readme-html` in this pack** already documented the storage shim, the `blob:`→`data:`+  rule and the size budget. The "discovery" was a rediscovery.+- **`adom/wiki#60` and `#47`** had both reported it years-equivalent earlier and been closed.++What makes this case worth recording is the `--because` gate. The agent DID answer it, with+something that reads perfectly reasonable:++> "The existing adom-tsci export-wiki guide describes an auto-rendered iframe the wiki no+> longer does, never mentions README.html, and predates every CSP-sandbox failure documented+> here."++Every clause of that is TRUE. And every clause is an argument for **fixing that guide**, not+for standing up a competitor next to it. "The existing doc is stale" is the single most common+bloat justification, and it is always backwards: staleness is the reason to edit, never the+reason to duplicate.++Tells that you are about to do this:++- Your `--because` is a critique of an existing page rather than proof that no page covers the+  territory. If you can name the page you are complaining about, you have found its home.+- You are writing "this supersedes X" instead of editing X.+- You searched for prior art AFTER drafting the content, so the draft is now sunk cost.+- A human said "write a skill" and you heard "create a page" (see the checklist below).++The repair: move the content into `adom-tsci/guides/export-wiki.md`, put the generic sandbox+corrections into `wiki-readme-html` where they belonged, soft-delete the new page, and fix+every cross-link that pointed at it. All of that is strictly more work than doing it right,+and it permanently burns the slug.  ## Checklist before any POST /pages or new slug 
skills/wiki-readme-html/SKILL.md+34−7
@@ -197,22 +197,49 @@ readme.html runs in `sandbox="allow-scripts allow-downloads"`, no same-origin. C each found the hard way:  1. **`localStorage` THROWS** (SecurityError) in a sandboxed document, and the viewer touches it on-   init, which aborts the whole rig. Shim it in `<head>` BEFORE the bundle loads:+   init, which aborts the whole rig. Shim it in `<head>` BEFORE the bundle loads.++   Two things that make this hard to recognise when it bites you:+   - **`typeof window !== "undefined"` is NOT a sufficient guard**, and shipped code gets this+     wrong. `window` exists in a sandboxed iframe; the *origin* is null, so the property read+     itself throws. Only try/catch works.+   - **If the throw lands in an un-awaited async init, everything after it is silently+     abandoned** — no console error at all. A partial init looks like this:+     `lights: []`, `environmentTexture: false`, `activeRenderLoops: 0`, `getCamera(): null`,+     while the meshes are all present. Any downstream feature that guards on the camera+     (a scripted tour, a fly-to) then no-ops without complaining, which reads as a second,+     unrelated bug. Catch it with+     `addEventListener('unhandledrejection', e => console.warn(e.reason))`.    ```html    <script>try{localStorage}catch(e){var _m={};Object.defineProperty(window,'localStorage',      {value:{getItem:k=>_m[k]??null,setItem:(k,v)=>{_m[k]=String(v)},removeItem:k=>{delete _m[k]},clear:()=>{_m={}}}});}</script>    ``` 2. **`blob:` images are blocked** (img-src has no blob:). Anything doing    `URL.createObjectURL` for textures needs a shim that converts to `data:` URIs instead.-3. **`connect-src 'none'`: NO fetch/XHR at all.** You cannot download a GLB at runtime. Ship model-   bytes INSIDE the page instead: base64 `data:` URIs, or (the proven trick for multi-MB models)-   pack the GLB bytes into a lossless PNG's RGB channels (4-byte little-endian length header),-   reference the PNG as a normal repo image, and decode via canvas at runtime. Images load fine;-   fetch does not.+3. **`connect-src` — CHECK THIS, it changed.** Measured 2026-08-20, the render endpoint now+   sends `connect-src 'self' data:`, not `'none'`. Same-origin `fetch` of your own page's+   assets WORKS: a viewer bundle can pull its own `3d.glb`, `circuit.json` and split JS parts+   over plain fetch. The PNG-channel packing trick below is no longer required for+   same-origin assets — keep it only for a genuinely cross-origin payload.+   ```+   sandbox allow-scripts allow-downloads allow-popups allow-popups-to-escape-sandbox;+   default-src 'none'; script-src 'unsafe-inline' https:; style-src 'unsafe-inline' https:;+   img-src data: https:; media-src data: https:; font-src data: https:;+   frame-src https:; connect-src 'self' data:; form-action 'none'; object-src 'none'+   ```+   Verify the live header before designing around it —+   `curl -sD- -o /dev/null <render-url> | grep -i content-security-policy` — because this is+   the second time it has moved.  ### Size budget -A readme.html has roughly a **4-5 MB practical cap**. Optimize models before embedding:+A readme.html has roughly a **4-5 MB practical cap** — and so does **every individual file**+served through `/<owner>/<slug>/render/<path>`, which is the one that catches people shipping a+JS bundle. Over ~4 MB it returns HTTP 413 `{"error":"File too large to render"}`, and a+`<script src>` cannot surface that; it just silently fails to load. Split anything larger into+<3.5 MB parts, fetch them, concatenate as **bytes** (a multi-byte character can straddle a+boundary, so never decode per part), and inject with `script.textContent` — `script-src` allows+`'unsafe-inline'` but not `blob:`, so `URL.createObjectURL` + `script.src` is refused. Optimize models before embedding: service-step2glb / gltf-transform (join, weld, prune, Draco) turns a raw ~16 MB board GLB into ~465 KB. Budget models first, code second.