Assumptions ledger

Every design decision here rests on a belief about how code, models, or attackers behave. Most such beliefs are never checked. This is the list of ours, each with a verdict and a link to the evidence.

The point is the verdict column. Detail lives in linked documents; this page exists so the current state of what we actually know is readable in thirty seconds, and so a refuted assumption is impossible to miss.

Status Meaning
✅ supported tested, held up
❌ refuted tested, was wrong — and what we changed
⚠️ nuanced true only under conditions worth knowing
🔍 untested believed, not yet measured

Security & cost model

# Assumption Verdict Evidence
S1 Fuel bounds the work a program can do ❌→✅ was refuted, now fixed — size-proportional atoms charged a flat cost; jsonStringify serialized 2,000,000 elements for 1.2 fuel under a 10-fuel budget. Fixed with chargeForSize; fuel now scales linearly. cost-invariant.test.ts
S2 Fuel also bounds memory ❌→✅ was refuted, now fixed — fuel is a time budget; at ~10KB/fuel a legitimate 100k budget bought ~1GB of live string. Added maxHeapBytes (64MB default). cost-invariant.test.ts
S3 A capability can't hand the guest a live host reference ❌→✅ was refuted in SCOPE on 2026-08-26 (#38), now fixed — enforced by the structured-clone membrane at one choke point; adversarially tested. Hardened 2026-08-03: the pre-walk used to read v[k], invoking getters — so the boundary ran host code while inspecting it. Accessors are now rejected via descriptors. The caveat: membraneValue has exactly one call site, inside if (atom.effects === 'io'), and defineAtom defaulted to 'pure' — so an embedder atom that did not opt in bypassed the choke point entirely, getters intact. Not a weaker guarantee for that population: no guarantee at all, and silently. Fixed in 0.13.6 by inverting the default to 'io' (BREAKING); core atoms are swept back to 'pure' explicitly. The row now holds for the VM, not for a subset. malicious-actor.test.ts
S4 Termination is guaranteed ✅ supported by construction — fuel makes bounded execution a precondition; the halting question never arises. Not formally proven — see the honest scope note in the README. README
S5 Recursive agent calls can't amplify across an open graph ⚠️ nuanced, and inherently so (re-framed 2026-08-03) — the X-Agent-Depth guard is cooperative and cannot be otherwise: budget does not cross a system boundary, only tokens and data do, and the other side takes care of itself. So the honest guarantee is over what we control — a time box on every run, abort on every exit path so we never leave outbound work alive, and per-atom quotas on what we summon. Chasing a carried fuel envelope was the wrong goal. README
S6 Our sandbox resists a determined attacker 🔍 untested — layered and adversarially reviewed, never externally red-teamed. An escape-attempt corpus (vm2 CVEs, SES challenges) is the open item. —

Agent legibility

# Assumption Verdict Evidence
A1 Our error messages help a model fix its code ❌ refuted — shipped diagnostics produced a 0% repair rate, identical to saying nothing. Worked examples: 80%. Fixed: remedies now show code, locked by a deterministic test. FINDINGS · diagnostic-remedy.test.ts
A2 More documentation helps small models ❌ refuted — a 0.6k cheat sheet beat our 7.6k guide 2×; adding the missing prose rule changed nothing. Guidance matters enormously (no guidance = 0%) but is not monotonic. FINDINGS
A3 Models learn our rules from prose ❌ refuted, three times independently — prose rule added to the guide: no change. Prose remedy in a diagnostic: 50% vs 80% for the same remedy shown as code. Models repair from examples, not rules. FINDINGS
A4 Syntax choice (braces/indentation/parens) drives model comprehension ❌ refuted — it's the paradigm, not the surface. Explicit state-threading scored 80% and mutation 20% regardless of syntax; s-expressions and braces tied once the paradigm was held constant. FINDINGS
A5 Newline-terminated statements (TjsStandard) don't hurt comprehension ⚠️ weakly supported — braces-with-semicolons and braces-without were indistinguishable (same rate, same error mode). N=5; suggestive only. FINDINGS
A6 AJS is easy for small models to write ⚠️ nuanced — achievable (67% with a good cheat sheet) but sensitive to guidance; the dominant failure is reaching for for loops. ajs-grokkability.test.ts
A7 Small models can write TJS ❌ refuted, and not expected to hold — a 1.5B model reverts to TypeScript syntax even when told otherwise, and emits hybrids (nums: [number]) that TJS quietly accepts. TJS targets larger models; AJS is the small-model surface. FINDINGS
A8 Executed verdicts repair better than type errors 🔍 untested — the obvious experiment is confounded by training-data asymmetry. The clean version is within-TJS: same language, vary only the feedback string. FINDINGS
A11 Leaning on TS convention costs us the autocomplete work ⚠️ nuanced — mostly not. Measured 2026-07-31: the completion engine is driven by the type descriptor, not by example values (.type 35×, .kind 13×, .params 11×, .example only 3×) — and : string yields the same descriptor as : '', so the engine is indifferent to spelling. The three example sites are a menu that becomes additive (offer string/int alongside ''/5) plus hover detail. The differentiating work is untouched: predicate-mined value completion (suggest(), guaranteed-valid because candidates are run through the compiled predicate) and introspection-driven member completion — neither references examples, and neither is expressible in TypeScript. Net: lose ~one completion list, gain tsserver/.d.ts/TS-tooling leverage. predicate.ts · ajs-language.ts
A15 A .tjs file's EXTENSION tells a reader which semantics apply ❌ refuted (2026-08-27) — it carries nothing. Shown one switch as .js a model traced C fallthrough 5/5 and explicit break 5/5; shown the identical text as .tjs it scored 0/5, applying C fallthrough every time — the worst possible result, and not from uncertainty. Its own reasoning: "there is a language 'TJS'… maybe". There is no prior for the extension. A one-line comment stating the RULE restored it to 5/5 — but only on a 27B; a 4B model is unmoved by anything in the file (0/5 every arm), extending A7 to reading. Naming the language WITHOUT the rule is worse than silence: 5 no-answers, the model speculating about what tjs is rather than concluding. So the affordance must be IN THE CODE and must be a rule, not a pointer. Probe every .tjs-only semantic change before shipping it. FINDINGS · switch-probe.ts
A9 Autocomplete meaningfully helps 🔍 untested — and testable without a model: truncate valid programs and measure hit@k of suggest() against the real next token. —
A10 Types-by-example is the right identity for TJS ⚠️ nuanced — resolved 2026-07-31 as a decision, not a measurement: it is neither/both. TJS accepts TS type names OR examples, equal standing, with examples buying finer grain than TS can express (int, unsigned, and pattern-constrained strings). So the on-ramp is "keep writing TypeScript" — which needs no teaching, satisfying A3/A7 — and examples are a feature you graduate into, not a concept you must learn first. The ladder: TS name → example → Predicate(…). Asymmetric get/set is explicitly not a pillar of this (see A12). ts-type-names.test.ts
A12 Asymmetric get/set types are a core language need ❌ refuted (by scope, 2026-07-31) — the general case is already covered: computed properties work, and proxy behaviour is effectively computed types. Asymmetry is chiefly needed for (a) autocomplete quality and (b) describing external/ambient types — a tooling and contracts concern, not a missing language primitive. Re-scoped to ambient-contracts; no longer gates positioning. ambient contracts
A13 Developers (and models) would ADOPT TJS after using it 🔍 untested — never measured, and it is the actual product question. The legibility harness measures whether a model can write TJS; nobody has asked whether, after converting real code and reading real errors, it would switch. Two bars, scored separately: (1) DX alone — inline tests, docs, flight recorder, observe mode — which requires no belief in runtime types and is likely the easier win; (2) capabilities — runtime safety, inline WASM, safe eval. Needs a plain-TS control and forced comparisons rather than direct approval questions, which measure agreeableness. —
A14 You can adopt TJS without changing program behavior ❌ refuted (2026-08-01) — there is no observe mode. All three configured behaviors (return / log / throw) change what a function returns, so validating a legacy codebase necessarily alters it. The zero-risk bottom rung of the migration ladder does not exist. runtime.ts

Language & platform

# Assumption Verdict Evidence
L1 TJS is a superset of JS (dialect: 'js' preserves JS semantics) ✅ supported — enforced as a guardrail; a subset violation is a bug. subset-invariant.test.ts
L2 A verified predicate can serve as cache key, pushdown payload and auth object ✅ supported — canonical form collapses formatting/naming, distinguishes meaning and cluster helpers; demonstrated end-to-end. ⚠️ structural: refactoring into a local mints a new identity. predicate-canonical.test.ts · predicate-pushdown.test.ts
L3 Verified predicates compile to fast native JS ("safe is fast") ⚠️ nuanced — measured on one workload (~0.5ms to validate a whole theme); the systematic overhead campaign that would generalise it is unfinished. css/perf.bench.test.ts
L4 Published bundle-size claims are accurate ❌→✅ was refuted, now guarded — every row of the README table was stale (VM 66→74KB gz). Corrected, dated, and pinned by a test that fails on drift. bundle-size.test.ts
L5 Emitted code runs standalone without a runtime ❌→✅ was refuted, now fixed (2026-08-06) — held until parameter annotations began routing through declared Types, at which point the Type(…) schema gate optional-chained to a globalThis.__tjs.validate the inline stub does not have, went falsy, and made every declared type reject every value: double(4) errored standalone while returning 8 under the full runtime. The gate now fails OPEN — unchecked-but-working is the correct degradation (TJS ⊇ JS); turning a working program into a broken one based on whether a runtime happens to be loaded is not. Nothing else in the suite exercises the inline stub, which is why it shipped. codegen.test.ts · declared-type-annotation.test.ts

| L6 | Block syntax is self-evident — nobody assumes an implicit last-expression return | ❌ refuted (2026-08-06) — five functions in shipped teaching material ended a wasm { } block with a bare expression and returned undefined: dot, addInts, factorial, lerp, sumArray. The tell is that every fallback { } twin has an explicit return — the authors knew the JS rule and assumed the block was expression-oriented. Silent: it compiles, and :! returns mean no signature test. Decision it feeds: a block declaring a non-void return with no reachable return should be an ERROR; predicate { … } requires return, predicate => … is the one-liner (the arrow-function rule, so nothing new to learn). | examples.test.ts |


Engineering practice

Assumptions about our own process rather than about models, users or the language. These had no home before 2026-08-06, which is why the scanner-consolidation bet went unrecorded through the release that made it.

# Assumption Verdict Evidence
E1 Consolidating onto one shared scanner ends literal blindness ❌ refuted (2026-08-16), and the test was pre-registered — 0.13.0 routed fifteen call sites through scanLiterals(), and issue #25 stated the deciding evidence in advance: "how many NEW literal-blindness defects appear now that one scanner exists". Answer within 24 hours: two — splitParameters (parser-params) and splitParams (js-tests), both evading the sweep by splitting on commas rather than scanning quotes, so neither looked like a literal scanner. Answer by 2026-08-16: at least FOURTEEN. Six more found by review (transformConstBang's raw .replace rewriting the contents of user strings; all five declaration scanners detecting on raw source, where the single-quoted form injected unescaped quotes and REJECTED legal JavaScript — a JS ⊆ TJS breach; /* unsafe */ read out of a parameter default, turning validation OFF for the whole function; two dts.ts scanners emitting a phantom exported type into consumers' .d.ts), plus nine revealed at once when the dogfood conversion ratchet finally ran — six of them labelled "undiagnosed" for weeks and all nine the same defect, invisible because both ratchets gate on SKIP_BENCHMARKS, which test:fast sets and CI inherits. Consolidation removes duplication; it does not remove the class, because the class is "a pass that reads source without understanding it" and that has more shapes than one. Two refinements from the later batch: a comment can be accurate about the wrong implementation, and the obvious fix is sometimes wrong in a way that looks right (maskLiterals blanks comments too, so using it to find /* unsafe */ erases the marker and passes every hostile test while deleting the feature). literal-blindness.test.ts · #25
E2 A gate that compiles an artifact proves the artifact works ❌ refuted (2026-08-06) — the playground examples were compile-checked and their inline tests run, but their bodies never executed, so an example with no inline tests was never run at all. Adding execution found five functions printing undefined against their own comments. Compiling is a claim about syntax; running is a claim about meaning, and only the second is what a reader experiences. examples.test.ts
E3 A benchmark with a fixed iteration count stays valid ❌ refuted twice (2026-08-06) — a count is tuned to the hardware of the day and rots silently as hardware improves. wasm-memory timed a single pass at 0.17ms and printed "0.00ms" under Firefox's 1ms clock clamp; the vector benchmark's ratio swung ~20×–80× on an M5 Max because its denominator went sub-millisecond. Size by duration, and calibrate against the FASTEST path, since that is the term that vanishes first. vector-search.bench.test.ts

Decisions and their outcomes

The ledger above records what we believe. This records what we did, and what happened — including when the outcome was worse than expected, which is the only entry type that changes future behaviour.

Date Decision Why Outcome
2026-08-06 : in a destructuring pattern means required, not defaulted the colon value is a type and a worked example; conflating it with a default made : mean nothing ✅ clean — no pre-existing test depended on the old behaviour. The only failures were three repro cases written an hour earlier. For a semantic change, that is about as strong a safety signal as exists.
2026-08-06 Type X<T> subsumes Generic X<T>; Generic becomes an alias two keywords made type → Type non-mechanical in both directions (disposal tax) ✅ clean — no regressions; both spellings pinned byte-identical so they cannot drift into two implementations.
2026-08-06 Route parameter annotations through declared Types the other half of 0.13.0's sound-type-names work; prerequisite for type → Type ⚠️ caused a regression — standalone emitted code then rejected every value, because the schema gate optional-chains to a validate the inline stub lacks and so failed CLOSED. Fixed same day by failing open. Refutes L5 as previously written.
2026-08-06 Revert type arguments (Box<int>) rather than ship them parsing worked, but Box<0> accepted 1.5 standalone — the inline stub's Generic does typeof where the real one infers structure ✅ right call — shipping would have been a knowing silent under-check, the exact class the release spent a week removing. Blocker is now a stated design question rather than a latent defect.
2026-08-06 Calibrate benchmarks by duration, in two passes fixed counts rot; one pass measures a cold loop and under-scales ✅ better than expected — run-to-run spread fell from 14–21% to ~6.5%, and revealed the old numbers were biased low: 500×128 reported ~8.5× for something that is ~12.7×.
2026-08-06 Add a run gate for playground examples compiling is not running ✅ found five broken functions immediately, all shipped in the npm package and the live playground.
2026-08-07 Do not convert === → == in fromTS output; comment only where it would SIMPLIFY === is already safe; TJS's == is safer than JS's but strictly more work ✅ decided against, on a measurement that was not in the original framing — Eq costs 1.23× a raw === on a comparison-dominated loop, so the rewrite is a pessimization paid for nothing whenever operands cannot be boxed or null. It is also not semantics-preserving: x === null discriminates null from undefined and TJS == does not, so the rewrite would need to skip exactly the comparisons TS authors write deliberately under strictNullChecks. And a TS reader seeing == in converted output distrusts the converter, which is the conversion's whole value. Narrowed to a comment where the source manually reimplements == (x === null || x === undefined, a.valueOf() === b) — teaching where there is something to teach, silent otherwise.

How to add to this ledger

  1. State the assumption as something that could be false. "Our errors are helpful" is checkable; "our errors are good" is not.
  2. Build the cheapest probe that could refute it. experiments/agent-legibility/ has runnable examples. Comprehension probes need no parser; autocomplete probes need no model.
  3. Record the verdict here in one line, with the number that decides it. Detail goes in the linked document — this page must stay skimmable.
  4. Act on refutations, then encode the fix as a deterministic test where possible. The experiment justifies the invariant; the invariant then holds without rerunning the experiment. (A1 → diagnostic-remedy.test.ts is the worked example.)
  5. Re-check the entries a change touches. L5 read "✅ supported" for three days after the work that broke it — because nobody looked. A ledger nobody revisits records the moment it was written, not the state of the project.

How to add a decision

The assumptions table records what we believe. The decisions table records what we did and what followed, which is the half that changes future behaviour.

  1. Log it when the outcome is known, not when the decision is made — an entry with no outcome is a TODO wearing a ledger's clothes.
  2. Say what would have made it wrong, in the "Why" column. "Because it seemed better" is not reviewable a year later.
  3. Record bad outcomes in the same table as good ones. A decisions log with no ⚠️ rows is a marketing document. Routing annotations through declared types is in there precisely because it caused a same-day regression; that row is worth more than the four clean ones.
  4. Prefer the surprising result to the confirming one. "No pre-existing test depended on the old behaviour" and "the old benchmark numbers were biased low, not merely noisy" are both facts nobody predicted, and both change how the next change gets made.

Pre-registration works and is nearly free. Issue #25 stated its own deciding evidence ("how many NEW literal-blindness defects appear now that one scanner exists") before any had appeared. Two arrived within 24 hours, and because the question was written down first, the answer was unambiguous instead of arguable. Where a decision has an observable consequence, say what would count as evidence at the time you decide (E1).

Methodological lessons paid for in this repo, worth reading before designing a probe: an apparatus that fails closed looks exactly like a strong negative result (a harness bug scored 0/8 while the model was correct); an instrument saturated at its floor discriminates nothing; and changing two things at once will hand you a confident, wrong conclusion — a "Lisp wins" result evaporated once syntax and paradigm were separated.