Skip to content

Cache-bust the rest of _emanote-static in live-server mode#676

Merged
srid merged 3 commits intomasterfrom
living-gang
Apr 27, 2026
Merged

Cache-bust the rest of _emanote-static in live-server mode#676
srid merged 3 commits intomasterfrom
living-gang

Conversation

@srid
Copy link
Copy Markdown
Owner

@srid srid commented Apr 26, 2026

The cache-busting fix from #663 only covered the JS module graph; every other asset under _emanote-static/ (skylighting CSS, self-hosted fonts, the inverted-tree CSS, the emanote logo, Stork's CSS+JS) was still served bare — editing any of them in emanote run left the browser staring at a stale copy until you restarted the server. They went around siteRouteUrl via the legacy ${ema:emanoteStaticLayerUrl} text splice, so the ?t=<mtime> cache buster never reached them.

This PR introduces an attributed Heist splice — <emanoteStaticUrl path="…">${url}…</emanoteStaticUrl> — that routes through siteRouteUrl, migrates the five default-template call-sites onto it, and refactors JsBundle.jsUrl to share the same primitive (Emanote.View.StaticUrl.emanoteStaticUrl). Now the splice is the one home for static-asset URL construction; the importmap and the <link>/<script>/<img> tags all flow through it.

The legacy ${ema:emanoteStaticLayerUrl} splice stays around with a deprecation comment. Removing it would break third-party templates that still reference the bare folder URL — soft-deprecate now, retire at the next major version.

Closes #666.

Try it locally

nix run github:srid/emanote/living-gang -- -L docs run

Then edit any of emanote/default/_emanote-static/{skylighting.css,fonts/fonts.css,inverted-tree.css,emanote-logo.svg,stork/edible.css,stork/stork.js} and watch the browser pick up the change without a restart.

Generated by /do on Claude Code (model claude-opus-4-7).

srid added 3 commits April 26, 2026 19:33
Every asset loaded via the layer-URL splice (skylighting.css,
fonts/fonts.css, inverted-tree.css, emanote-logo.svg, Stork CSS+JS)
went around siteRouteUrl and shipped without the ?t=<mtime> suffix
that lets the live server invalidate cached files. Editing any of
them required restarting `emanote run`.

Add an attributed Heist splice <emanoteStaticUrl path="…">${url}…
that routes through siteRouteUrl, migrate every template that
referenced ${ema:emanoteStaticLayerUrl}, and refactor JsBundle.jsUrl
to share the same primitive. The legacy splice stays around for
third-party templates but is now documented as cache-buster-skipping.

Closes #666.
The 'skips siteRouteUrl' phrasing didn't match the code: the splice
calls siteRouteUrl on inverted-tree.css, then strips the query string
via T.breakOn while extracting the folder URL. Reword to match what
actually happens.
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 26, 2026

Hickey/Lowy Analysis

# Lens Finding Disposition
1 Hickey Three static-URL concepts collapsed into one primitive Fixed in this PR
2 Hickey Path construction de-fragmented from templates into the splice Fixed in this PR
3 Hickey Hand-rolled inverted-tree.css lookup hack deprecated Fixed in this PR
4 Hickey Asset-presence dependency made explicit in call signature Fixed in this PR
5 Hickey Duplicated three-step transformation in JsBundle removed Fixed in this PR
6 Lowy Volatility boundary correctly placed (cache-busting protocol) No-op
7 Lowy Cross-layer reuse (Haskell + template) validates the boundary No-op
8 Lowy JsBundle delegates without consolidation — different volatilities No-op
9 Lowy Legacy ema:emanoteStaticLayerUrl retention deferred to next major No-op
10 Lowy Template call-site symmetry confirms interface design No-op

Hickey rationale

Before the diff, "URL for an _emanote-static/ asset" was a fragmented concept — JsBundle.jsUrl had its own three-step composition (modelLookupStaticFilestaticFileSiteRoutesiteRouteUrl), the legacy ema:emanoteStaticLayerUrl splice ran the same composition only to break the result back to a folder URL via T.breakOn "/inverted-tree.css" (a hand-rolled parser to recover structure from a computed value), and the templates carried hardcoded path fragments downstream of both. The cache-buster invariant ("every bundled asset cache-busts in live mode") was unenforced — the splice silently dropped it.

The new emanoteStaticUrl primitive collapses all three derivations into one, consumed by both Haskell (JsBundle.jsUrl) and the template surface (<emanoteStaticUrl> splice). The two surfaces are essential doubling because Haskell and Heist are different invocation contexts; both call the same underlying transformation. The legacy splice is documented as deprecated rather than rewritten — it's a soft-deprecate so third-party templates keep working.

Lowy rationale

The cache-busting protocol (?t=<mtime>) is the one documented axis of volatility here, and it's now encapsulated behind a single interface (siteRouteUrl, called from one place). A future move to content-hashed URLs or service-worker invalidation lives there; every call site (JsBundle.jsUrl, <emanoteStaticUrl> splice, all five templates) automatically inherits the change. Blast radius is zero.

The boundary survives the "is this functional grouping or volatility grouping?" diagnostic: it exists because what is behind it changes independently, and the cross-layer reuse (Haskell + Heist) is the signature of a genuine volatility boundary, not scaffolding. Consolidating JsBundle into StaticUrl would conflate two volatilities — the URL protocol vs. the importmap structure — which is why the refactor delegates rather than merges.

@srid srid marked this pull request as ready for review April 26, 2026 23:57
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 27, 2026

Evidence

This PR adds ?t=<mtime> cache-busting to seven _emanote-static/ template asset URLs in live-server mode. Below are curl-derived <head>/<body> snippets from emanote -L docs run — branch on :9091, master (built via nix --refresh build github:srid/emanote/master#default) on :9092 for the same docs/ corpus. The URLs are the only thing that differs; rendered pixels are identical.

This branch — every affected asset carries ?t=<mtime>:

$ curl -s http://localhost:9091/
  <link rel='stylesheet' href='_emanote-static/skylighting.css?t=1777247898' />
  <link rel='stylesheet' href='_emanote-static/fonts/fonts.css?t=1777247898' />
  <link rel='stylesheet' href='_emanote-static/stork/edible.css?t=1777247898' />
  <link rel='stylesheet' href='_emanote-static/stork/edible-dark.css?t=1777247898' />
  <script src='_emanote-static/stork/stork.js?t=1777247898'></script>
  <img ... src='_emanote-static/emanote-logo.svg?t=1777247898' alt='Emanote logo' />

$ curl -s http://localhost:9091/uptree   # page with <ema:has:uptree>
  <link rel='stylesheet' href='_emanote-static/inverted-tree.css?t=1777247898' />

Master baseline — same routes, no ?t= on the template asset URLs:

$ curl -s http://localhost:9092/
  <link rel='stylesheet' href='/_emanote-static/skylighting.css' />
  <link rel='stylesheet' href='/_emanote-static/fonts/fonts.css' />
  <link rel='stylesheet' href='/_emanote-static/stork/edible.css' />
  <link rel='stylesheet' href='/_emanote-static/stork/edible-dark.css' />
  <script src='/_emanote-static/stork/stork.js' ></script>
  <img ... src='/_emanote-static/emanote-logo.svg' alt='Emanote logo' />

$ curl -s http://localhost:9092/uptree
  <link rel='stylesheet' href='/_emanote-static/inverted-tree.css' />

@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 27, 2026

/do results

Step Status Duration Verification
sync 1s git fetch ok; forge=github; noGit=false
research 20m 15s Issue #666 understood; designed bind-style attributed splice + shared static-URL primitive
branch 16s Worktree branch living-gang tracking origin/master
implement 5m 38s Live server emits ?t=<mtime> on all 6 affected assets; importmap unchanged
check 5s cabal build all succeeded
docs 16s CHANGELOG + docs/guide/html-template/fonts.md updated
fmt 20s pre-commit run --all-files clean
commit 18s Primary commit f2964e1d pushed
hickey+lowy 3m 7s 5 hickey findings all addressed by design; 5 lowy findings all "no action needed"
police 9m 50s rules + fact-check clean; elegance found 1 misleading comment fixed in 6a4c6924
test 7m 19s cabal test all: 43/43
create-pr 1m 13s Draft PR + hickey/lowy comment posted
ci 1m 54s vira ci signed off both architectures; e2e live/static/morph all pass
evidence 2m 53s Branch-vs-master curl diff showing ?t=<mtime> propagation
Total 54m 1s

Slowest step: research (20m 15s)

Optimization suggestions

  • research dominated (37% of total) — issue Stale-cache other static-layer assets in live-server mode (CSS, fonts, Stork, SVG) #666 required understanding Heist attributed splices, the existing legacy splice's hand-rolled folder-URL hack, and the JsBundle cache-busting design. Pre-reading Emanote.View.JsBundle and Emanote.View.Common's ema:emanoteStaticLayerUrl splice before invoking /do would have shortened this materially.
  • just test is interactive — the recipe shells out to ghcid which never returns; this run used cabal test all instead. Consider splitting just test (interactive watch) from just test-once (one-shot CI-friendly) so both /do and humans get the right tool by default.
  • police's elegance pass took the longest sub-step — fact-checking the simplify reviewers' suggestions against the codebase (especially the _emanote-static literal duplication question) ate cycles. Most "consolidate constants across modules" suggestions are out-of-scope for a single-issue PR; encoding that boundary in .agency/code-police.md could pre-empt future churn.
  • CI was already fast (1m 54s for vira ci + three e2e suites) — no optimization opportunity there.

Workflow completed at 2026-04-26 19:58 UTC.

@srid srid merged commit a9c6560 into master Apr 27, 2026
6 checks passed
@srid srid deleted the living-gang branch April 27, 2026 00:05
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.

Stale-cache other static-layer assets in live-server mode (CSS, fonts, Stork, SVG)

1 participant