Skip to content

refactor: author the islands and stylesheets in src/, serve them where they were - #451

Merged
oddharsh merged 1 commit into
mainfrom
refactor/islands-in-src
Aug 18, 2026
Merged

oddharsh merged 1 commit into
mainfrom
refactor/islands-in-src

Conversation

@oddharsh

Copy link
Copy Markdown
Owner

Stacked on #450. Second slice of lifting the parked bun branch's renames onto main.

The 15 client islands and the two stylesheets move from www/ to src/client/ and src/styles/. Their served paths do not change: /nav.js is still /nav.js, and /a/nav.7ab9cce6.js still carries the same hash, because the build stages them to where they have always been served from.

That split is the whole point. www/ is what a browser can fetch; an island is source that a build minifies, hashes, and ships beside a readable .src.js twin. Keeping authored source and served bytes in one directory is what made it possible to edit a served asset by accident.

No served byte changes

Built before and after under node with pnpm: all 1542 served artifacts byte-identical, content hashes included. That last part is the one that matters here — a moved island that re-hashed would mint new /a/ URLs and orphan every committed dictionary.

Gates

305 tests, lint, typecheck, pages:check, routes:check, perf-budget.

CLAUDE.md, MAINTENANCE.md and design/README.md come along, since all three point at these files by path.

Where this sits

slice state
Worker out of the served tree #450
islands and stylesheets this
photo pipeline out of www/ next, 56 files
www/ split + scripts/ → tools/ the big one, ~1980 files
TypeScript conversion last, and the one to verify carefully rather than assume

Both slices so far cherry-picked from #443 with a single conflict each, and that conflict was the bun migration doc, which does not belong on main.

@github-actions

github-actions Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Wire-size diff vs merge base

9609a12 → 3c60085

Brotli q11, the quality the edge serves. Every number here is deterministic:
an unchanged file produces no row. Advisory — this check fails on nothing.

Client assets

No change. 21 files, 75.15 KiB Brotli total.

Pages

No change. 48 files, 502.05 KiB Brotli total.

Worker bundle

No change. 230.11 KiB gzip.

Dictionary deltas (pd/)

No change. 144 deltas, 451.39 KiB total.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
aadhar-sh a8b443b Aug 18 2026, 03:05 PM

@oddharsh
oddharsh force-pushed the refactor/islands-in-src branch from 456df0c to 66be21b Compare August 18, 2026 14:49
oddharsh added a commit that referenced this pull request Aug 18, 2026
www/scripts/ becomes tools/photos/. Nothing in it ships: it is the encoder
chain, the EXIF reader, the histogram bake and the generators, and it sat in the
served tree only because that is where the pixels are.

Third slice of lifting the parked bun branch's renames onto main (#450, #451).
Toolchain-independent: the scripts still run under bash, node, python3 and uv
exactly as they did, and only their paths moved.

IT RETIRES A BUILD EXCEPTION RATHER THAN RELOCATING IT. www/scripts was 89% of
the tree's bytes (the zenc cargo target and the uv venv, ~232 MB across 767
files, against ~27 MB that actually ships), so build.mjs skipped staging it and
carried a paragraph explaining why. With the pipeline outside www/ there is
nothing to skip: STAGE_SKIP drops to www/images/meta alone and the paragraph
becomes a note about what used to be true. An exception that existed only
because dev tooling sat in the served tree should disappear when the tooling
leaves.

ONE SERVED ASSET CHANGES, deliberately, and it is the interesting part.
www/icons.svg embedded its generator BY PATH ("GENERATED by
www/scripts/gen-desktop-partial.mjs"), which makes a repository path part of a
content-addressed artifact: moving the generator moves the bytes and mints a new
URL. It now names the generator by FILENAME, so /a/icons.9ff130b8.svg becomes
/a/icons.8d4b5912.svg once here and can never move again for that reason.

Blast radius, checked rather than assumed: svg sits out the dictionary path by
design (build.mjs skips it, and the file's own comment says so), and no a-dict
entry names icons, so no committed dictionary is orphaned. Every other served
artifact is byte-identical across the 1542 compared.

The other differing file is www/.assetsignore, whose comment now says the
pipeline USED to live here. It is an upload filter, never served.

Gates: 305 tests, lint, typecheck, pages:check, routes:check, perf-budget,
check-wrangler.
@oddharsh
oddharsh force-pushed the refactor/worker-out-of-served-tree branch from 29f6b73 to 79222d5 Compare August 18, 2026 14:52
@oddharsh
oddharsh force-pushed the refactor/islands-in-src branch from 66be21b to 2e58b2c Compare August 18, 2026 14:52
Base automatically changed from refactor/worker-out-of-served-tree to main August 18, 2026 14:53
oddharsh added a commit that referenced this pull request Aug 18, 2026
www/scripts/ becomes tools/photos/. Nothing in it ships: it is the encoder
chain, the EXIF reader, the histogram bake and the generators, and it sat in the
served tree only because that is where the pixels are.

Third slice of lifting the parked bun branch's renames onto main (#450, #451).
Toolchain-independent: the scripts still run under bash, node, python3 and uv
exactly as they did, and only their paths moved.

IT RETIRES A BUILD EXCEPTION RATHER THAN RELOCATING IT. www/scripts was 89% of
the tree's bytes (the zenc cargo target and the uv venv, ~232 MB across 767
files, against ~27 MB that actually ships), so build.mjs skipped staging it and
carried a paragraph explaining why. With the pipeline outside www/ there is
nothing to skip: STAGE_SKIP drops to www/images/meta alone and the paragraph
becomes a note about what used to be true. An exception that existed only
because dev tooling sat in the served tree should disappear when the tooling
leaves.

ONE SERVED ASSET CHANGES, deliberately, and it is the interesting part.
www/icons.svg embedded its generator BY PATH ("GENERATED by
www/scripts/gen-desktop-partial.mjs"), which makes a repository path part of a
content-addressed artifact: moving the generator moves the bytes and mints a new
URL. It now names the generator by FILENAME, so /a/icons.9ff130b8.svg becomes
/a/icons.8d4b5912.svg once here and can never move again for that reason.

Blast radius, checked rather than assumed: svg sits out the dictionary path by
design (build.mjs skips it, and the file's own comment says so), and no a-dict
entry names icons, so no committed dictionary is orphaned. Every other served
artifact is byte-identical across the 1542 compared.

The other differing file is www/.assetsignore, whose comment now says the
pipeline USED to live here. It is an upload filter, never served.

Gates: 305 tests, lint, typecheck, pages:check, routes:check, perf-budget,
check-wrangler.
@oddharsh
oddharsh enabled auto-merge (squash) August 18, 2026 15:02
…e they were

The 15 client islands and the two stylesheets move from www/ to src/client/ and
src/styles/. Their SERVED paths do not change: /nav.js is still /nav.js, and
/a/nav.7ab9cce6.js still carries the same hash, because the build stages them to
where they have always been served from.

That split is the point. www/ is what a browser can fetch; an island is SOURCE
that a build minifies, hashes and ships beside a readable .src.js twin. Keeping
authored source and served bytes in one directory is what made it possible to
edit a served asset by accident.

Second slice of lifting the parked bun branch's renames onto main (#450 was the
first). Toolchain-independent: no bun anywhere in it.

NO SERVED BYTE CHANGES. Built before and after under node with pnpm: all 1542
served artifacts byte-identical, content hashes included.

Gates: 305 tests, lint, typecheck, pages:check, routes:check, perf-budget.

CLAUDE.md, MAINTENANCE.md and design/README.md come along, since all three point
at these files by path.
@oddharsh
oddharsh force-pushed the refactor/islands-in-src branch from 2e58b2c to a8b443b Compare August 18, 2026 15:04
@oddharsh
oddharsh merged commit db228b2 into main Aug 18, 2026
6 checks passed
oddharsh added a commit that referenced this pull request Aug 18, 2026
www/scripts/ becomes tools/photos/. Nothing in it ships: it is the encoder
chain, the EXIF reader, the histogram bake and the generators, and it sat in the
served tree only because that is where the pixels are.

Third slice of lifting the parked bun branch's renames onto main (#450, #451).
Toolchain-independent: the scripts still run under bash, node, python3 and uv
exactly as they did, and only their paths moved.

IT RETIRES A BUILD EXCEPTION RATHER THAN RELOCATING IT. www/scripts was 89% of
the tree's bytes (the zenc cargo target and the uv venv, ~232 MB across 767
files, against ~27 MB that actually ships), so build.mjs skipped staging it and
carried a paragraph explaining why. With the pipeline outside www/ there is
nothing to skip: STAGE_SKIP drops to www/images/meta alone and the paragraph
becomes a note about what used to be true. An exception that existed only
because dev tooling sat in the served tree should disappear when the tooling
leaves.

ONE SERVED ASSET CHANGES, deliberately, and it is the interesting part.
www/icons.svg embedded its generator BY PATH ("GENERATED by
www/scripts/gen-desktop-partial.mjs"), which makes a repository path part of a
content-addressed artifact: moving the generator moves the bytes and mints a new
URL. It now names the generator by FILENAME, so /a/icons.9ff130b8.svg becomes
/a/icons.8d4b5912.svg once here and can never move again for that reason.

Blast radius, checked rather than assumed: svg sits out the dictionary path by
design (build.mjs skips it, and the file's own comment says so), and no a-dict
entry names icons, so no committed dictionary is orphaned. Every other served
artifact is byte-identical across the 1542 compared.

The other differing file is www/.assetsignore, whose comment now says the
pipeline USED to live here. It is an upload filter, never served.

Gates: 305 tests, lint, typecheck, pages:check, routes:check, perf-budget,
check-wrangler.
@oddharsh
oddharsh deleted the refactor/islands-in-src branch August 18, 2026 15:06
oddharsh added a commit that referenced this pull request Aug 18, 2026
www/scripts/ becomes tools/photos/. Nothing in it ships: it is the encoder
chain, the EXIF reader, the histogram bake and the generators, and it sat in the
served tree only because that is where the pixels are.

Third slice of lifting the parked bun branch's renames onto main (#450, #451).
Toolchain-independent: the scripts still run under bash, node, python3 and uv
exactly as they did, and only their paths moved.

IT RETIRES A BUILD EXCEPTION RATHER THAN RELOCATING IT. www/scripts was 89% of
the tree's bytes (the zenc cargo target and the uv venv, ~232 MB across 767
files, against ~27 MB that actually ships), so build.mjs skipped staging it and
carried a paragraph explaining why. With the pipeline outside www/ there is
nothing to skip: STAGE_SKIP drops to www/images/meta alone and the paragraph
becomes a note about what used to be true. An exception that existed only
because dev tooling sat in the served tree should disappear when the tooling
leaves.

ONE SERVED ASSET CHANGES, deliberately, and it is the interesting part.
www/icons.svg embedded its generator BY PATH ("GENERATED by
www/scripts/gen-desktop-partial.mjs"), which makes a repository path part of a
content-addressed artifact: moving the generator moves the bytes and mints a new
URL. It now names the generator by FILENAME, so /a/icons.9ff130b8.svg becomes
/a/icons.8d4b5912.svg once here and can never move again for that reason.

Blast radius, checked rather than assumed: svg sits out the dictionary path by
design (build.mjs skips it, and the file's own comment says so), and no a-dict
entry names icons, so no committed dictionary is orphaned. Every other served
artifact is byte-identical across the 1542 compared.

The other differing file is www/.assetsignore, whose comment now says the
pipeline USED to live here. It is an upload filter, never served.

Gates: 305 tests, lint, typecheck, pages:check, routes:check, perf-budget,
check-wrangler.
oddharsh added a commit that referenced this pull request Aug 18, 2026
…452)

www/scripts/ becomes tools/photos/. Nothing in it ships: it is the encoder
chain, the EXIF reader, the histogram bake and the generators, and it sat in the
served tree only because that is where the pixels are.

Third slice of lifting the parked bun branch's renames onto main (#450, #451).
Toolchain-independent: the scripts still run under bash, node, python3 and uv
exactly as they did, and only their paths moved.

IT RETIRES A BUILD EXCEPTION RATHER THAN RELOCATING IT. www/scripts was 89% of
the tree's bytes (the zenc cargo target and the uv venv, ~232 MB across 767
files, against ~27 MB that actually ships), so build.mjs skipped staging it and
carried a paragraph explaining why. With the pipeline outside www/ there is
nothing to skip: STAGE_SKIP drops to www/images/meta alone and the paragraph
becomes a note about what used to be true. An exception that existed only
because dev tooling sat in the served tree should disappear when the tooling
leaves.

ONE SERVED ASSET CHANGES, deliberately, and it is the interesting part.
www/icons.svg embedded its generator BY PATH ("GENERATED by
www/scripts/gen-desktop-partial.mjs"), which makes a repository path part of a
content-addressed artifact: moving the generator moves the bytes and mints a new
URL. It now names the generator by FILENAME, so /a/icons.9ff130b8.svg becomes
/a/icons.8d4b5912.svg once here and can never move again for that reason.

Blast radius, checked rather than assumed: svg sits out the dictionary path by
design (build.mjs skips it, and the file's own comment says so), and no a-dict
entry names icons, so no committed dictionary is orphaned. Every other served
artifact is byte-identical across the 1542 compared.

The other differing file is www/.assetsignore, whose comment now says the
pipeline USED to live here. It is an upload filter, never served.

Gates: 305 tests, lint, typecheck, pages:check, routes:check, perf-budget,
check-wrangler.

Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh added a commit that referenced this pull request Aug 18, 2026
44 developer scripts move from scripts/ to tools/, joining tools/photos and
tools/oxlint. Nothing in any of them ships; they are the build, the checks, the
generators and the release tooling, and having two sibling directories for one
job was a split by history rather than by kind.

Fourth slice of lifting the parked bun branch's renames onto main (#450, #451,
#452). This one could NOT be cherry-picked: that commit sits on top of the
TypeScript conversion and brings 714 conflicts, so the move is done by hand
against main's layout instead.

THE REFERENCE SWEEP IS BY FILENAME, not by prefix. Rewriting `scripts/` blindly
would have renamed package.json's "scripts" KEY and any /scripts/ URL in prose,
so each of the 44 moved names was replaced exactly: 47 files updated.
design/PORTING.md keeps its `scripts/gen-desktop-partial.mjs` mention, since
that file is documented history rather than instruction.

ONE VISITOR-FACING STRING CHANGES, and it is the reason this is not a pure
rename. /lens prints a command for the reader to run:

  Use `node scripts/lens-webmcp.mjs <url>` on the current machine

That script moved, so the instruction had to move with it or it would name a
path that no longer exists. The string lives in src/client/lens-browser.js,
which is content-addressed, so it cascades exactly three links:

  lens-browser.js  821aa61e -> 62f8bf05   the string itself
  lens.js          c0c21217 -> a9ee703b   imports lens-browser by hashed URL
  lens-boot.js     a8fd9733 -> c7fac8b7   imports lens.js by hashed URL

and from there to /lens.html, its .src.html twin, and the dcz deltas for those
three assets. 10 staged files leave, 13 arrive.

Everything else is byte-identical. Six Worker sources change only in comments
naming a moved script, which reaches the bundle and no served asset.

Gates: 305 tests, lint, typecheck, build, photos:check, routes:check,
pages:check, tools:check, perf-budget, check-wrangler, the cal suite and a
wrangler dry-run.

Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh added a commit that referenced this pull request Aug 18, 2026
…rc/dict/ (#455)

www/a-dict/ and www/p-dict/ become src/dict/a-dict/ and src/dict/p-dict/. They
are the previously shipped bytes of the content-hashed shell and of each page,
kept so build.mjs can compute dcz deltas against what a returning visitor still
holds. Nothing in either directory is ever served.

Fifth slice of lifting the parked bun branch's renames onto main (#450, #451,
#452, #453), and the first of three that finish www/. Done by hand rather than
cherry-picked, for the reason #453 records: the branch's split commit sits on
the TypeScript conversion and brings 714 conflicts.

BEING OUTSIDE www/ IS A STRONGER GUARANTEE THAN THE .assetsignore ENTRY IT
REPLACES. An ignore line stops an upload; living outside the served tree means
the build never stages the files at all. The staged tree loses 130 files that
were copied on every build purely so wrangler could then decline to upload
them. The contract test that asserted the ignore entry now asserts the
directory is absent from www/ instead, which is the property that actually
matters.

NO SERVED ARTIFACT CHANGES: 1496 compared, all byte-identical. The only staged
differences are those 130 snapshots no longer being copied, and .assetsignore
losing two entries that can no longer match anything.

Also removes a filter in build.mjs's view-transition tripwire that skipped
"a-dict/" while walking www/. With a-dict gone from that tree the exclusion
could not match, and a filter that cannot fire reads as protection that is not
there.

Gates: 305 tests, build, lint, typecheck, routes:check, pages:check,
tools:check, perf-budget, photos:check, check-wrangler, cal, wrangler dry-run.

Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh added a commit that referenced this pull request Aug 18, 2026
The last of the www/ split. 36 HTML documents become src/pages/; the other 804
files, which are bytes a browser fetches unchanged, become public/. The staged
root renames with them: .build/www is .build/public, and wrangler.jsonc's
assets.directory follows.

Seventh and final slice of lifting the parked bun branch's renames onto main
(#450, #451, #452, #453, #455, #456). The mapping needed no judgement: main's
www/ held exactly 840 files, the branch's two destinations hold exactly those
840, and every one lands in exactly one of them. Every HTML is a page; nothing
else is.

EVERY SERVED ARTIFACT IS BYTE-IDENTICAL, 1497 compared with the root name
normalised. The four remaining differences are path references inside cal,
serendipity and the Worker: three comments and one JSON import specifier.

THE BUILD COMPOSES THE SERVED TREE NOW rather than copying one directory.
public/ + src/pages/ + src/content/ + src/client/ + src/styles/ all stage into
.build/public, so the URL layout is untouched while the source layout says what
each file IS. servedFiles() walks the three authored roots in one place, because
a tripwire that walks only some of them reports a clean tree it never read.

SIX THINGS THE GATES CAUGHT, and the pattern is worth stating: a blanket path
sweep cannot tell a source read from a staged path, an asset from a document, or
a path from a regex.

  * staticShellPages() read www/ for the freshness check
  * the Worker imported ../../www/images/hashes.json
  * gen-md-twins looked for page HTML under www/, then read the generated tier
    from the staged www/ root
  * the page generators in pipelines/ wrote into www/
  * the .src.html twin read its AUTHORED source from the wrong root, so it fell
    into a catch and shipped the STAGED bytes: readable twins carrying rewritten
    /a/ hashes, which is the whole point of the twin lost silently. The build's
    own counter is what showed it, 47 from staged where main says 12.
  * and one false positive: `assert.match(lens, /… 401 && www/)` is a regex whose
    DELIMITER the sweep read as a path separator, rewriting the assertion out
    from under a real variable named www.

Gates: 305 tests, build, lint, typecheck, routes:check, pages:check,
tools:check, perf-budget, photos:check, check-wrangler, cal, wrangler dry-run.

Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh added a commit that referenced this pull request Aug 18, 2026
* docs: the layout table describes the tree that now exists

Seven slices moved almost every path in this repository (#450, #451, #452,
#453, #455, #456, #457), and the automated sweeps that repointed code also
walked CLAUDE.md. That left the map wrong in two different ways.

STALE: the table still said public/ holds "pages, client scripts, CSS, photos,
dictionaries". It holds none of the first four now. Rows added for src/pages,
src/content and src/dict; scripts/ folded into the tools/ row; every row's
description checked against the tree rather than against its own history.

FALSE: the sweep rewrote HISTORICAL names, so the file claimed the Worker lived
at `public/_worker.js/` and the photo pipeline at `public/scripts/`. Neither
ever existed. A stale note is a note somebody distrusts; a false one is a note
somebody follows.

The naming note now records the full sequence, holding -> www -> the split, and
says explicitly that the rewrites deliberately left old mentions alone, so
reading `www/` in an older note is correct rather than a typo. It also drops the
"duplicate path" explanation, since scripts/ no longer exists to be confused
with tools/photos/.

And it states the test for where a new file goes, which the table never did: if
a build step transforms it, it is src/; if it ships byte-for-byte, it is public/.

* docs: the intro names the split tree too

The bullet under the header still called public/ the live site including the
_worker.js/ dispatcher, which is two moves out of date: the Worker left the
served tree in #450 and the documents left it in #457. It now names what each
root is for, which is the same question the table below answers per entry.

---------

Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh added a commit that referenced this pull request Aug 19, 2026
…eated (#467)

#452 moved the photo pipeline to tools/photos/ and #451/#457 moved the served
tree to public/, and four ignore rules kept naming the old locations:

  www/scripts/zenc/target/  ->  tools/photos/zenc/target/
  www/scripts/.venv/        ->  tools/photos/.venv/
  www/ad/                   ->  public/ad/
  www/images/meta/          ->  public/images/meta/

None of the new paths existed yet, which is why nobody hit it. The first person to
run photos:env or build zenc would have had a 13M venv and 218M of Rust build
output appear as untracked, in the one place a git add -A would sweep them in.

Verified with git check-ignore: all four new paths are ignored, and none of them
is tracked today, so no committed file changes status.

Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
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