Stale text from the old script rule (review, Claude Fable 5.1)
A spec ruling row records Paul's words and supersedes 21/24; comments in assetversion.go, passkey.js and the sweep no longer cite the carve-out as the live rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+11
−9
docs/superpowers/specs/2026-08-30-home-design.md+1 −0internal/home/assetversion.go+3 −2internal/home/noscript_test.go+3 −3internal/home/static/passkey.js+4 −4
CI failed — run details
CI log
amadan: sync for CI failed: exit status 128: error: RPC failed; HTTP 429 curl 22 The requested URL returned error: 429 fatal: expected 'acknowledgments' (and could not reset the workspace: unlinkat /home/paulca/.local/state/amadan/runner/repos/oficina__home/cache/go-mod/filippo.io/age@v1.3.2/pq.go: permission denied)
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md| index 268b384..429edb4 100644 |
| --- a/docs/superpowers/specs/2026-08-30-home-design.md |
| +++ b/docs/superpowers/specs/2026-08-30-home-design.md |
| @@ -1489,6 +1489,7 @@ The suite sharing doctrine (`oficina/suite`) binds this spec. Read against |
| | 12 | Membership is its own record, written only by an owner. This is ruling 23. | |
| | 13 | A compromised home is three compromises, conceded separately — see below. | |
| | 14 | Custody is bounded at acquisition and retired by the feed. | |
| +| 25 | **Supersedes 21 and 24: no script by default; script only to progressively enhance a page or provide interaction HTML and CSS cannot, and always prefer none.** Every screen works with scripts off; scripts are static same-origin files, never inline, and the no-`<script>` sweep names Home's script files (a closed list) rather than screens — §11 | A closed list of screens allowed script, which the busy rule on every form outgrew | Paul, 2026-09-29, verbatim: "the default [is] no script, but scripts can be added to progressively enhance and provide interaction, but always prefer no JavaScript" | |
| Calls 7 and 8 are added to §9. Call 7 is the activity pull, and it waits on |
| the identity layer (§13 step 10) because it carries a real assertion. Call 8 |
diff --git a/internal/home/assetversion.go b/internal/home/assetversion.go| index bcbc7cd..99feb6a 100644 |
| --- a/internal/home/assetversion.go |
| +++ b/internal/home/assetversion.go |
| @@ -16,8 +16,9 @@ import ( |
| // else, so a returning visitor fetches only the page, and a deploy still |
| // takes effect at once: new bytes, new version, new URL. |
| // |
| -// Scripts are left at their bare paths: the scriptless sweep's allowlist |
| -// names them there (passkey.go), and they are revalidated by ETag. |
| +// Scripts are linked the same way where a template can (busy.js); the |
| +// passkey and handoff scripts keep bare paths their own modules import, |
| +// revalidated by ETag. noscript_test.go's closed list names them all. |
| // contentVersion is the first 16 hex digits of the body's SHA-256. |
| func contentVersion(body []byte) string { |
diff --git a/internal/home/noscript_test.go b/internal/home/noscript_test.go| index e820142..e6b3e80 100644 |
| --- a/internal/home/noscript_test.go |
| +++ b/internal/home/noscript_test.go |
| @@ -148,7 +148,7 @@ func sampleData(name string) []any { |
| // mechanical (it once also named the only screens allowed any script; the |
| // rule is now no script by default and prefer none, so it names the |
| // files instead). Historically: every |
| -// screen works with scripts off, and ruling 21 carves out only the |
| +// screen works with scripts off, and ruling 21 once carved out only the |
| // passkey screens. |
| // |
| // Two stated limits, so nobody reads more into a green run than is |
| @@ -263,7 +263,7 @@ func TestServedCSPAllowsNoInlineScript(t *testing.T) { |
| // register endpoints). None was stopped by csrf; the Origin header |
| // below sees to that. So the sweep reached eighteen of |
| // twenty-three, and one of the five it missed renders the enrolment |
| - // screen, which is one of the two ruling 21 lets load script. |
| + // screen, which is one of the pages that loads a script to enhance it. |
| // |
| // So: a fresh session cookie as well. With it, all twenty-three |
| // targets reach and run their handler — probe-verified rather than |
| @@ -324,7 +324,7 @@ func TestServedCSPAllowsNoInlineScript(t *testing.T) { |
| d := strings.TrimSpace(directive) |
| name, _, _ := strings.Cut(d, " ") |
| // Inline STYLES are allowed on purpose: ui's partials set |
| - // style attributes. Inline SCRIPT is what §11 and ruling 21 |
| + // style attributes. Inline SCRIPT is what §11 and the script rule |
| // are about. |
| if name == "style-src" { |
| continue |
diff --git a/internal/home/static/passkey.js b/internal/home/static/passkey.js| index 6598320..74b3fc5 100644 |
| --- a/internal/home/static/passkey.js |
| +++ b/internal/home/static/passkey.js |
| @@ -4,10 +4,10 @@ |
| // the server says to go. |
| // |
| // It is a static, same-origin module, not an inline block: rastrillo's |
| -// default CSP forbids inline script, and §11's carve-out is for these |
| -// two static files and nothing else. Its path is stable and |
| -// unfingerprinted so the scriptless sweep can name it — a rule that |
| -// cannot name its subject is not a rule. |
| +// default CSP forbids inline script, and Home's scripts are a closed list |
| +// of static files (spec ruling superseding 21/24: no script by default, |
| +// enhance only, prefer none). Its path is stable so that list can name |
| +// it — a rule that cannot name its subject is not a rule. |
| import { register, authenticate, available } from "/static/webauthn.mjs"; |
| async function post(path, body) { |