Skip to content

Commit 3180a78

Browse files
docs(vrs): record the embedded-runtime decision
Settled by two independent prototypes, and independent of the remaining open questions, so it is recorded now rather than held for the wider rewrite. The deciding property is not size, it is that an external runtime hands a plan a complete platform, so purity can only be policed — globals stubbed before load, a speed bump rather than a wall. An embedded engine starts with nothing and receives only what the host names. Absent beats removed. Measurement made the packaging side one-sided anyway: ~3.4 MiB of binary and ~26s of build, linking only the C library, with no marginal closure at all, against an order of magnitude more for an external runtime. A full optimizing engine was ruled out on a sharper ground than size — its build needs network access on every path, so a hermetic build must pin fetched artifacts by hash and every upgrade becomes a fixed-output repair. Determinism was measured, not assumed: hundreds of runs yielding one result with a negative control, byte-identical across three engine versions. Two findings become normative. Compilation and evaluation use separate contexts, because compiling a module needs an evaluation capability the sandbox must not keep. And identity is the hash of the evaluated result, never of transpiled source — the transformer is young and changes often, and hashing its output would make every upgrade an identity migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QzRc44KbA3wfYYKFF9UDMa agent-session-id: cdf2c185-eeb3-4a4d-8ce0-52885f145cac agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
1 parent 7b59926 commit 3180a78

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# The JavaScript runtime is embedded, not invoked
2+
3+
Status: accepted
4+
5+
## Context
6+
7+
Plans are authored as pure TypeScript and must be evaluated to produce a stored
8+
version. Evaluation can either shell out to an external runtime or run in
9+
process against an embedded engine.
10+
11+
Two properties make this more than a packaging preference. Identity is a hash of
12+
the evaluated result, so **evaluation must be deterministic** or the same source
13+
yields different versions. And a plan is a program, so **evaluation must not
14+
grant the program any capability** — a plan has no business reading a file,
15+
opening a socket, or asking the time.
16+
17+
## Evidence and Argument
18+
19+
Both requirements point the same way, and the second decides it.
20+
21+
An external runtime hands the program a complete platform. Purity can only be
22+
*policed* — globals stubbed before the program loads, a boundary that is a speed
23+
bump rather than a wall, since anything the runtime provides may be reachable by
24+
a path the stubbing did not anticipate. An embedded engine starts with nothing
25+
and receives only what the host names. That is a difference in kind: the
26+
capability is absent rather than removed.
27+
28+
Measurement made the packaging side one-sided too. Two independent prototypes
29+
built the full pipeline. The embedded path costs roughly three and a half
30+
megabytes of binary and half a minute of build, links only the C library, and
31+
adds **no marginal closure at all**. An external runtime costs an order of
32+
magnitude more, delivered as a separate artifact that must be present, found,
33+
and version-matched at runtime.
34+
35+
A full-featured engine was ruled out on a sharper ground than size: its build
36+
requires network access on every path — either downloading a prebuilt archive or
37+
downloading its own toolchain. A hermetic build must therefore pin fetched
38+
artifacts by hash, which turns every engine upgrade into a fixed-output repair.
39+
That cost recurs forever and is paid in the most tedious currency available.
40+
41+
A pure-native-language engine avoids a C dependency but costs more than twice
42+
the binary for a less proven implementation. The C dependency is a small, fast,
43+
network-free build; that is not the constraint worth optimizing.
44+
45+
Determinism was measured rather than assumed: repeated evaluation of one input
46+
produced a single distinct result across hundreds of runs, with a negative
47+
control confirming the check detects change, and results were byte-identical
48+
across three engine versions spanning a wide range.
49+
50+
## Options
51+
52+
| Option | Tradeoffs |
53+
| --- | --- |
54+
| Embed a small interpreter | No external dependency, no marginal closure, capabilities absent by construction; adds a C build dependency and a 0.x transformer |
55+
| Invoke an external runtime | Familiar and no engine to embed; an order of magnitude more closure, a process boundary, and purity can only be policed |
56+
| Embed a full optimizing engine | Best-proven determinism for floating point; network-dependent builds that make every upgrade a fixed-output repair, for an order of magnitude more size |
57+
| Embed a pure-native-language engine | No C dependency at all; more than twice the binary, less proven, for a constraint that was not binding |
58+
59+
## Decision
60+
61+
The engine is embedded in the binary. Compass invokes no external runtime and
62+
requires none to be installed.
63+
64+
Evaluation happens against a global object the host **constructs explicitly**.
65+
Nothing is available unless it was deliberately added. This is an allowlist
66+
rather than a denylist, because a denylist widens silently whenever the engine
67+
gains a global.
68+
69+
Excluded, and each for a stated reason: the clock and anything derived from it;
70+
randomness; locale-aware behaviour, whose data is versioned independently of the
71+
engine and changes underneath a stable engine version; anything that makes
72+
garbage collection observable; and the transcendental floating-point functions,
73+
which the engine delegates to the platform library and which therefore disagree
74+
between platforms in the final bits.
75+
76+
Compilation and evaluation use **separate contexts**. Compiling a module
77+
requires an evaluation capability that the sandbox must not retain, so a module
78+
is compiled in a disposable context and the compiled form is loaded into a
79+
locked one. Dynamic code construction consequently exists but is inert.
80+
81+
**Identity is the hash of the evaluated result, never of transpiled source.** The
82+
transformer is young and changes often; hashing its output would make every
83+
upgrade an identity migration. Hashing the evaluated value absorbs that entirely,
84+
so long as semantics are preserved.
85+
86+
## Consequences
87+
88+
- Compass remains a single binary with no runtime prerequisite.
89+
- A plan cannot read a file, open a socket, or observe the time, because those
90+
capabilities are never introduced rather than being taken away.
91+
- One exception is honest: the randomness primitive arrives with the engine's
92+
base objects and cannot be structurally omitted. It is removed explicitly, and
93+
it is the only capability whose absence depends on the host remembering.
94+
- Excluding the transcendental functions is what makes the same source produce
95+
the same version on every platform. Without it, identity is platform-local.
96+
- The transformer is a young dependency that changes frequently. Hashing
97+
evaluated values rather than emitted source is what makes that acceptable, and
98+
it must not be quietly reversed for convenience.
99+
- Determinism is a property to be tested, not assumed. It rests on an explicit
100+
allowlist, and any addition to that list is a change to this decision.

0 commit comments

Comments
 (0)