---
title: "Day four: the build moves inside the Worker, and stops editing your source"
date: 2026-08-04
summary: >-
  Production Cloudflare does hand a Worker a SharedArrayBuffer, so the toolchain
  held under the hardest constraint anyone had put on it — and then the
  build-host axiom inverted to workerd only. The day's largest decision produced
  no ticket at all, because it is an axiom: Ursprung never edits your source.
  Almost everything else was subtraction — no bundler, no codegen step, no
  extraction transform, no scope analyser.
---

# Day four: the build moves inside the Worker, and stops editing your source

Day three ended holding one empirical question — whether production Cloudflare
hands a Worker a `SharedArrayBuffer` — and a build-host axiom asking Node, Bun
and workerd to be equals on one code path. Day four answered the question, then
**inverted the axiom**, and then took the largest decision the framework has made
so far: **Ursprung never edits your source.**

Four tickets resolved, four opened, three research surveys totalling about 2,690
lines, two throwaway spikes, and roughly 12,600 lines added. The map itself grew
by 529 lines. Outside the planning directory: day three's blog post, and two
ignore patterns.

The ticket count moved from twenty-four with ten resolved to twenty-eight with
fourteen resolved. But the day's two largest outcomes produced **no ticket at
all**, because what they settled were axioms, and axioms live on the map. They
came out of a written analysis that refused its own premise, and a grilling
session that ran the toolchain question end to end.

## The report that refused its own premise, and the one idea it kept

The day opened with a proposal: stop treating `@cloudflare/worker-bundler` as
"the reference for the shape" — the role ticket 8 gave it — and make it the thing
Ursprung actually builds on. That is a redraw of the map's premises rather than
an amendment, so it was written up in full, 367 lines against
`worker-bundler@0.2.2` read as source, and **refused on evidence**.

**The decisive fact is structural: Ursprung needs the intersection of two modes,
and the package splits them.** `bundle: false` has the output shape ticket 5
settled — one emitted module per input module — and `bundle: true` has the
capabilities. Neither has both. And the `bundle: false` path is worse than merely
unconfigured: **JSX is hardcoded to the React automatic runtime** with no option
to pass, **source maps are not emitted at all**, and **import specifiers are
rewritten by a single regular expression** that matches inside comments and
template literals and does not see dynamic `import()` — which is the one edge
Ursprung most depends on, because route pages are lazily imported. Ticket 7 found
two traps in exactly that area and ticket 8's answer was to get them right in one
place. A regex is the failure mode both tickets exist to avoid.

**And it does not solve the hard problem.** Ursprung's own transforms need
span-accurate parsing of Ursprung's own source, so a parser stays in the budget
either way and the largest hand-written component stays hand-written. What the
premise would have bought was real and was written down rather than minimised —
ticket 27 dissolves, ticket 26's risk evaporates, source-map composition
dissolves, the file-source seam arrives designed, and a large piece of the assets
fog arrives implemented. What it costs is ticket 5's no-bundler decision, ticket
8's product-graph walk, ticket 6's resolver policy, and diagnostics — flat
`warnings: string[]`, from a package whose README says "not recommended for
production use" and whose roadmap follows the agents platform rather than this
one.

The analysis surfaced one idea worth keeping, and flagged it as worth evaluating
**independently of the package that suggested it**:

> Invert the build-host axiom. workerd becomes the only build runtime, and Node
> and Bun reach it by embedding workerd rather than by running the build
> themselves.

That is what happened. The premise was refused; its best idea was adopted.

## The principle that finally got a name

Before the toolchain session, the map gained a cornerstone that had been running
the design for two days without a name: **formerly unreasonable expectations.**

A constraint that _would_ have been unreasonable to place on a human author —
cumbersome, repetitive, easy to forget, exactly the kind of thing a framework
historically absorbed with inference, convention or magic — is **available** now,
because the author is an agent. Agents do not tire of verbosity, do not skip a
rule because it is tedious, and read diagnostics literally. So the question to
ask of a candidate constraint is not _"would an author put up with this?"_ but
three others:

1. Is it **precisely stated**?
2. Is the violation **detectable by the build**?
3. Does the diagnostic **name the fix**?

If all three hold, the constraint is affordable however cumbersome it would once
have been. **The expectation is the design** — every such rule is machinery
deleted from the build, and machinery is where the failure modes live.

It comes with two guards, because it is easy to abuse. It licenses expectations
that are **cumbersome**, not ones that are **ambiguous or uncheckable** — a rule
an agent cannot verify before running the build is not an expectation, it is a
trap. And it must **buy something**: a removed analysis, a simpler build, a
deleted failure mode. Verbosity for its own sake is not a design.

The evidence for it is that most of the map was already obeying it: required
extensions with no probing, an explicit route table with string-literal paths, a
filename-suffix boundary with no per-export granularity, refused import cycles.
Naming it turned it into a tool, and within hours it was used to delete three
whole transforms.

## The axiom that inverted: workerd, and only workerd

**The build runs inside workerd. Only ever inside workerd.** Node and Bun do not
build; they _host_ a workerd isolate through embedded miniflare and hand it the
files.

The point is not capability — ticket 24 had already proved one loader running on
all three hosts with byte-identical output. The point is that it makes _"the
build works in a Worker"_ **structurally true rather than something CI has to
keep true**. That is the same argument ticket 8 used when it made realm
membership fall out of the walk shape instead of being tagged: prefer the
property you cannot break to the property you have to defend.

What follows, none of it optional:

- **wasm only.** oxc's native binding leaves the project entirely, and the
  host-specific `WebAssembly.Module` acquisition seam that ticket 24 found —
  ticket 27's whole reason for existing — **disappears**. wasm arrives as a
  module type, one way, always.
- **Sync throughout**, since workerd has no `Worker` and every async oxc entry
  point dies at `wasi.thread-spawn`. This clause was **flagged rather than
  amended** later the same day: Tailwind's `compile()` is irreducibly async, so
  the wording is now broader than the evidence behind it, and narrowing it to
  "the oxc chain is sync" is an axiom-level call left to the human.
- **`node:crypto`'s `createHash` is the emit hash**, locked in. It is the only
  synchronous digest in workerd and there is no longer a host where that is
  negotiable.
- **TypeScript 7 can never run inside the build** — 21 platform-native Go
  executables, no wasm target, and a JS API that is an IPC proxy to a spawned
  process.
- **Local builds pay wasm speed and isolate startup** — roughly 5.35× native on
  parse, plus the cold isolate. Accepted, and it lands hardest on the dev loop.
  The first end-to-end number, from the production probe: 201 modules through
  parse → transform → minify → hash chain is **1,044 ms cold, 765 ms warm**,
  deterministic to a byte-identical fingerprint.
- **Paid plan only**, which is a platform fact rather than a choice.

## No splicing

**Ursprung never edits your source.** It reads, refuses, transforms via oxc, and
emits. Every line of output traces to a line the author wrote or to a documented
oxc transform. There is no Ursprung-authored text in the middle.

This is the largest consequence of _formerly unreasonable expectations_, and it
was reached by asking what the build would look like if every splice site were
replaced by something the author is simply expected to write. All three turned
out to be replaceable:

- **Handler identity.** Handlers are declared at module scope and referenced by
  name, so identity is `module URL + export name` — written by the author, stable
  across builds, and satisfying ticket 8's "computable from its own module alone"
  trivially. No extraction, no registry. **Inline arrow handlers in JSX are
  gone**, which is the framework's most visible break from React-shaped
  convention and is accepted knowingly. It deletes the extraction transform _and_
  the scope analyser ticket 1 called the single largest piece of hand-written
  machinery in the build.
- **Boundary retargeting.** A universal module may not import server code, so the
  build **refuses rather than rewrites** and stub generation disappears — which
  is ticket 5's own remedy ("split the file instead") applied one level up.
- **Content-hash specifier rewriting.** The one site no expectation can remove,
  because an author cannot know a hash. It is removed a different way: **the
  client graph gets an import map** and its source specifiers are left alone.
  This reverses ticket 6's refusal of import maps, which rested on the circular
  ground that rewriting was happening anyway. The **server graph needs no hashing
  at all** — it is uploaded as a unit, so its module names can be stable.

What this buys beyond simplicity: the emit chain becomes `transform → minify`,
both maps oxc-generated, with no uninstrumented edit in between. And for an
audience that reads errors literally, **never having left the written source is
categorically better than mapping back to it accurately.**

The residue is stated rather than hidden: a bounded check that a handler body
references nothing but its parameters and module-scope bindings survives. Ticket
3 measured the equivalent in Qwik as a three-line set intersection. That is not
"no language analysis", and claiming otherwise would be overclaiming.

Three closed tickets are **reopened in part**, each scoped in its own body rather
than left to inference: ticket 5 loses stub generation and specifier rewriting
but keeps no-bundler and the boundary; ticket 6 loses its import-map refusal but
keeps its policy; ticket 8 keeps its stages and its walk while stage 3 stops
editing text. Ticket 10 is not reopened but **shrinks dramatically** — the
transform and the scope analyser are both deleted.

## The toolchain, settled

The same session closed the toolchain question end to end.

**Three oxc packages, all inside the build isolate**: `oxc-parser`,
`oxc-transform`, `oxc-minify`, at 2,343 KiB gzipped. **`oxc-resolver` is
dropped** — ticket 8 already gave resolution to Ursprung, and with third-party
server packages bounded to flat entries what remains is path joining plus
`exports` evaluation.

**No bundle, ever**, taken as a standing commitment rather than a v0 scoping
decision. **Owning the transform is non-negotiable** under every candidate
considered. **esbuild was refused a third time**, and `@cloudflare/worker-bundler`
and rolldown with it.

**Source-map composition is owned, not depended.** oxc chains maps nowhere, and
v0 has committed to both maps and minification, so the two oxc-generated maps
must be joined by hand — roughly 200 lines of Source Map v3 VLQ decode, remap and
re-encode. It is taken rather than depended because it carries no language
semantics, is exhaustively testable by reverse lookup, and is the mechanism the
diagnostics-as-agent-interface work rests on. No `@ampproject/remapping`. Ticket
25 narrows from build-or-buy to design — and **No splicing shrinks it further**,
because with no Ursprung-authored edit the chain is `transform → minify` with no
hole in the middle.

**Type checking moved outside the build**, which is what frees it onto TypeScript
7: TS 7 has no wasm build and cannot run in a Worker, so `ursprung check` runs
natively on Node or Bun, CI blocks on it, and the build strips types without
checking them. A build running inside a Worker — an agent on Cloudflare compiling
an app in place — **does not type-check at all**, and that is accepted
deliberately rather than worked around.

## The deploy that could have inverted everything

Ticket 26 was day three's cliffhanger: every measurement behind the oxc survey
was taken against local workerd, all four oxc bindings import a _shared_
`WebAssembly.Memory`, and `SharedArrayBuffer` is exactly the capability an edge
deployment gates for Spectre reasons.

**It is alive.** All three bindings instantiated in a real production isolate
holding **183.87 MiB** of shared memory, and `parseSync`, `transformSync` and
`minifySync` all produced correct output — a real module record, a JSX transform
with a source map, minified code — inside the 1 s startup gate. No silent
downgrade: the buffer really is a `SharedArrayBuffer`. `Worker` is still
`undefined` on production, so "sync APIs only" rests on the same fact at the
edge, and `createHash` works while `crypto.subtle.digestSync` does not, so the
emit-hash choice holds on the real host.

The memory answer moved in **both** directions, and the bad half is sharper than
the ticket anticipated:

- **The 128 MB ceiling is not charged against reserved wasm pages.** oxc's floor
  is not fatal. That fear is retired.
- **A single shared memory may not exceed exactly 2,048 pages (128 MiB).**
  Bisected: 2,048 succeeds, 2,049 is a `RangeError`.
- **oxc's published loaders hardcode `initial: 4000` (250 MiB) and are therefore
  refused outright on production — while succeeding locally.** The loader fork is
  not an optimisation; it is the difference between working and not working, and
  nothing in either ticket had said so.
- **Exactly three oxc-sized memories fit, and three is the maximum.** A fourth
  kills the isolate. So "three oxc packages" is now a **hard ceiling** rather than
  a preference: a future wasm dependency must displace one, not join it. Dropping
  `oxc-resolver` turns out to have been necessary as well as tidy.
- **The isolate has a finite lifetime measured in modules parsed** — about
  **90 KiB of unreclaimable parser memory per module**, with transform and minify
  flat, which puts roughly **600 modules** between isolate restarts. That is a
  mixed-source estimate and it graduated the fog into a ticket rather than being
  leaned on.

**Local workerd enforces no memory ceiling whatsoever** — demonstrated now rather
than suspected, which is precisely why the ticket existed. A 250 MiB allocation
that production refuses succeeds locally; six memories totalling 1,036 MiB
succeed locally and die at the fourth on production.

One correction went back to ticket 24 rather than forward: `eval`, `new Function`
and runtime `new WebAssembly.Module()` all **succeed during module evaluation**
and are refused during request handling, identically on both hosts. That is a
**phase** distinction, not a host one. It rescues nothing, since the build runs at
request time, but the earlier claim needed the qualifier.

The method is worth recording because it is reusable. There is no Cloudflare
account in this environment; `wrangler deploy --temporary` mints an ephemeral one
with no credentials at all. Reaching the deployed Worker over HTTP then failed
every way it was tried — from a datacenter IP, `workers.dev` returns a managed
challenge before the request reaches the Worker, and cron triggers are
free-plan-forbidden. What worked was **module-scope evaluation at deploy time**,
with the probe _throwing_ its result so wrangler surfaces it verbatim over the
authenticated API. One stage per deploy, because an isolate killed for exceeding
memory does not throw. Every production number is therefore a startup-phase
number, and should be read as _at least this strict_.

## Tailwind: the axiom conflict that did not exist

The premise analysis surfaced a gap nobody had asked about, and it predated the
session: the build-host axiom requires every build-time dependency to run inside
workerd, workerd now has **no fallback host**, and **Tailwind had never been
tested against it**. A build that skips type checking still emits correct code; a
build that skips Tailwind emits an app with no styles. If it failed, the styling
axiom and the build-host axiom were in direct conflict.

**Tailwind runs inside workerd today, unmodified.** Real CSS — preflight, theme
variables, utilities, `hover:` and `md:` variants — compiled inside a real
`workerd serve` isolate from an in-memory file map, with **no `nodejs_compat`**
and no wasm at all.

The engine split is not where anyone assumed. `@tailwindcss/oxide` _does_ publish
a wasm build, so Tailwind passes the test that qualified oxc — **and it does not
matter**, which is the actual headline. **The `tailwindcss` core package is pure
JavaScript with zero runtime dependencies**; oxide is _only_ the file scanner; and
`@tailwindcss/browser` is upstream's own oxide-free configuration, which means
Ursprung's build isolate is a supported path rather than a workaround. The oxide
wasm build was forked and loaded for the record, and then panicked at every entry
point on rayon's global thread pool — a **sharper** constraint than oxc's, with no
thread-free path at all.

**There are no filesystem assumptions.** The seam is
`compile(css) → build(candidates: string[]) => string`, candidates are an
_input_, `@source` directives come back out as inert descriptors for the caller
to resolve, and all I/O is caller-supplied through callbacks. Tailwind opens
nothing, so ticket 24's `node:fs` ENOENT trap never fires — the in-memory file
map the build-host axiom forces is the shape Tailwind already wants.

It costs **79.9 KiB gzipped** and **8 ms** for a realistic project, 0 ms on a warm
rebuild, on ordinary collectible JS heap with **no wasm memory floor** — so it
adds nothing to the production memory problem above.

One genuinely new obligation: **Ursprung owns candidate extraction.** Core ships
no JS extractor and none can be forced. Ursprung is unusually well placed — it
already parses every file with oxc, so it can extract from string literals and
JSX attribute values in the AST rather than regex over text, which is strictly
better than a text scanner and costs a parse that already happens.

## What the second Tailwind survey changed

The programmatic survey ran later the same day and found the delegation holds —
and found the terms it holds on:

**The API is completely undocumented and has broken in every v4 minor.**
`compile()` appears nowhere on tailwindcss.com; every documented route is Vite,
PostCSS, the CLI or the Play CDN. `globs` was renamed `sources` at 4.1, and both
loader callbacks were made to return `path` at 4.2 — two breaking changes to a
direct caller, shipped in _minor_ bumps. So: **pin `tailwindcss` exactly, and
budget real work at every minor** rather than a test re-run. This is the same
posture ticket 21 reached for capnweb's unversioned wire protocol, arrived at
independently, and it amends the styling axiom's **terms** rather than its
substance.

**`build()` is monotonic**, and it is the sharpest fact in the survey. A compiled
stylesheet accumulates every candidate it has ever been shown and never forgets
one — upstream's source says so in as many words. Three consequences: it _is_ the
incremental API, there is no other; warm rebuilds are free because of it; and **a
compiler cached in module scope is a silent cross-request CSS leak**, measured,
with request B's stylesheet containing request A's classes.

**Don't minify.** Measured against the exact lightningcss invocation Tailwind's
own Node package uses, minification buys **519 B gzipped** on a realistic
stylesheet and **246 B** on a large one — the ratio _improves_ as output grows,
because CSS this repetitive is what gzip is good at. The CSS minifier is refused
on arithmetic, the same way esbuild was. No `lightningcss`, in either form.

**`@plugin` and `@config` work from an in-memory map inside workerd** — real
`@tailwindcss/typography` and `/forms` compiled with no filesystem and no `eval`,
because `jiti` lives in `@tailwindcss/node` and not in core. So supporting them is
now a _choice_ rather than a constraint, and upstream labels both a
"compatibility directive for v3.x".

Two traps with teeth: `Features` and `Polyfills` are ambient `declare const enum`
values that are real objects at runtime, so a direct caller **fails TS2748 under
`isolatedModules` on both TS 5.9.3 and 7.0.2** — a type error in Ursprung's own
code, which `skipLibCheck` cannot suppress. And only `index.css` needs to ship,
because the _published_ file is flattened at publish time unlike the repo's,
which trimmed four assets to one.

Everything the survey found Ursprung must build itself — content detection,
`@source` resolution, URL rewriting, minification, dependency tracking — turns
out to live in Tailwind's **bundler plugins** rather than in its engine. None of
it is CSS handling, which keeps the "no CSS pipeline of Ursprung's own" line
intact.

## Nine decisions about types

Ticket 11 was the day's other large resolution, and every decision in it was
probed against a real toolchain before it was made. **The type surface is one
program, strict, hand-written, and inferred from what already exists — Ursprung
generates nothing.**

1. **`moduleResolution: nodenext`**, and `ursprung check` **validates** the app's
   tsconfig and `package.json` rather than only consuming them.
2. **Non-erasable syntax is refused by the checker only.** The build stays a pure
   transform with no opinions about the language.
3. **One program, not two.** Realm enforcement stays entirely with the build.
4. **`JSX.IntrinsicElements` is strict and hand-written**, HTML attribute names
   verbatim, tag list taken from `HTMLElementTagNameMap`.
5. **`JSX.Element` is a described node**, not an opaque brand — and ticket 9 owns
   its final shape.
6. **`skipLibCheck: true`.** No `@types/node`, ever.
7. **Bindings are inferred, not generated and not validated.**
8. **Ursprung's own rules ride natural signatures where they fit, and are build
   errors everywhere else.**
9. **`wrangler` is a peer dependency of `ursprung`**, for types only.

**`nodenext` over `bundler` is a measurement, not a taste.** Under `bundler`,
`import "./ok"` is accepted silently and the build refuses it — the two-resolvers-
disagreeing shape ticket 6 called the worst available. Under `nodenext` it is
`TS2835`, which is ticket 6's rule enforced in the editor for free. But only if
the app's `package.json` says `"type": "module"`; without it, nodenext classifies
`.ts` as CJS and goes as quiet as `bundler` did. That is why `ursprung check`
validates rather than consumes — the base config alone cannot deliver the rule it
exists to deliver.

**One program forces one lib set**, and `lib: ["DOM"]` together with
`@cloudflare/workers-types` in `types` is a hard collision — dozens of duplicate
identifiers, `console` and `self` redeclared, `caches.default` disappearing behind
DOM's `CacheStorage`. The resolution: **DOM wins the globals, and Workers types
are imported as module types**, re-exported from `ursprung`. The trap that leaves
is worth a stated rule — a server handler reaching for the _global_ `Request`
silently gets DOM's and loses `cf`, and nothing errors.

**The intrinsic-element tables are hand-written on purpose.** The tag list comes
from `HTMLElementTagNameMap` (112) and `SVGElementTagNameMap` (63) so coverage is
checkable rather than guessed — but _only_ the list. **The attribute types are
deliberately not generated from `lib.dom.d.ts`**, because those interfaces
describe DOM _properties_, not HTML attributes: `HTMLAnchorElement` carries
`charset`, `coords`, `rev` and `shape`, `referrerPolicy` is camelCase where the
attribute is not, and `href` is not even on it. Generating from it would import
precisely the legacy surface the framework exists to refuse. Attribute names stay
verbatim — `class`, not `className` — because that is what the HTML spec,
Tailwind's docs and the rendered DOM all say, which is three sources an agent will
read. The honest cost: every attribute Ursprung forgets is a false error blocking
an author, and 175 tags is the largest single piece of authored surface in v0.

**The best result of the session came from refusing the question's framing.**
Bindings looked like a generate-or-validate choice; wrangler's new TypeScript
config makes it neither. `InferEnv<UnwrapConfig<typeof config>>` over the app's
own `cloudflare.config.ts` infers `{ readonly MY_KV: KVNamespace<T>; readonly
SETTINGS: { theme: string } }` with literal types flowing through, and a typo'd
binding is `TS2551 ... Did you mean 'MY_KV'?`. **No codegen step, no build output
in the tree, no staleness window, no "did you run generate?" failure mode** — and
no drift to validate either, because there is only one declaration. The config
_is_ the type. The cost is decision 9, and peer rather than real because a hard
dependency would drag a large CLI into the build isolate's manifest.

**And the ticket's own premise turned out to be half wrong.** It had assumed
type-level enforcement is strictly better than build-time enforcement wherever
achievable. The realm split _is_ expressible at the type level — two programs
with different `lib` sets, measured working — and was **rejected anyway**. Twice
in the session a type-level rule arrived in tsc's words with the **wrong fix**:
`TS2835` suggests `./ok.js` where the rule requires `./ok.ts`, and `TS2584`
suggests changing the `lib` option where the fix is to move the code to a client
module. For an audience that reads diagnostics literally and acts on them, **an
error whose fix instruction is wrong is worse than an error that arrives one step
later in text Ursprung wrote entirely.** Branded types and phantom realm markers
would manufacture more of these. The conceded cost is real and was written down:
type errors arrive while the agent is still editing, build errors only after it
runs a command.

Three of the four diagnostics specimens now on the map are _authoritative-sounding
wrong advice from a dependency_ — two from tsc, one from Tailwind, whose semantic
errors carry no source position at all and whose `@apply` error recommends a
directive an Ursprung author cannot use. That is starting to look like the central
question of the agent-interface work rather than one of its bullets.

Also recorded, because it would have been a silent disaster: **oxc's automatic
JSX runtime emits `require()` unless `sourceType: "module"` is passed
explicitly** — including under `"unambiguous"`, the option that sounds safe. In an
ESM-only framework that output is dead on arrival, and a module containing any
other import masks the bug.

The honest dependency sentence, finally written:

> An app installs **`ursprung`** and **`wrangler`** — `ursprung` for the framework
> and the build, `wrangler` to deploy — and imports from `ursprung` only. Behind
> that, `ursprung` pulls in three oxc packages for the build isolate, TypeScript 7
> and miniflare for the CLI, and Tailwind, capnweb and the signals polyfill for
> what it delegates. `@cloudflare/workers-types` arrives as wrangler's peer and is
> never imported by name. There is no `@types/node`.

## Where this leaves things

Twenty-eight tickets, fourteen resolved. The framework's shape changed more today
than on any day since charting, and almost all of it by subtraction: no native
addon, no host adapter, no bundler, no CSS minifier, no source-map dependency, no
codegen step, no branded types, no extraction transform, no scope analyser, no
stub generation, no specifier rewriting. The build is a library that runs in a
Worker, reads your source, refuses what it cannot read, and emits.

Of the four tickets opened, one was resolved the same day it was written. The
three that remain carry what is left: candidate extraction from the oxc AST, a
non-threads oxc wasm build — no longer a last line of defence but _more_
attractive than before, because the memory floor is what turned out to be tight —
and build-isolate lifetime. One axiom-level call is
parked for a human rather than decided by an agent: whether "sync throughout"
narrows to "the oxc chain is sync" now that the one async stage is a dependency's
and not a design choice.
