Ambient Contracts — probe reality, derive verified predicate contracts

Status: design note / idea (2026-07-03). Not built. Captures the direction and a concrete first spike (CSS + DOM). Related: the predicate engine (src/lang/predicate.ts), the tjs-lang/css library (src/css/), the autocomplete introspection bridge (demo/src/introspection-bridge.ts, editors/introspect-value.ts), and the VM capability model (src/vm/).

The itch

Static types are pessimistic about ambient runtime environments (the DOM, Node globals, window, host objects) in ways that create ceremony without safety. The canonical example:

element.addEventListener('input', (e) => {
  const value = e.target.value // ❌ TS: Property 'value' does not exist on 'EventTarget'
})

e.target is typed EventTarget | null, and EventTarget has no .value, so TypeScript forces (e.target as HTMLInputElement).value or a instanceof dance. But the honest situation is: at runtime the value is either there or it isn't, and if it isn't you find out immediately. The cast doesn't make it safer — it just silences a compiler that can't see the real object. A runtime predicate is the truthful tool here:

// pure, total, verifiable — reads props, no cast, no lie
function hasValueTarget(e) {
  return e != null && e.target != null && typeof e.target.value === 'string'
}
// usage: hasValueTarget(e) ? e.target.value : undefined  — no ceremony

That predicate is exactly what the verifier accepts (member access is pure; typeof → TypeOf), compiles to native, and — crucially — is serializable. So the thought: instead of hand-writing these, could tooling probe a real ambient environment and derive the predicate contracts automatically?

The payoff: author against an environment you're not running in

The headline use (user, 2026-07-03): type-checking and autocompletion against a non-ambient runtime — editing in a Node-based toolchain while targeting the browser (or the reverse, or Electron, or a specific Bun). Normally you have no target environment to introspect, so you fall back to a hand-maintained .d.ts (lib.dom) that drifts from what the engine actually does.

Because a probed contract is a serializable predicate, it decouples capture from consumption in time and space: probe the real target once (locally or in CI), bottle the contract, and a Node-side editor/typechecker consumes it with no target present. It beats a .d.ts on three axes:

Why it's plausible here and not a research project: the contract format already exists ($predicate / tjs-lang/schema), the autocomplete provider already has an introspection hook (getMembers — point it at a captured contract instead of a live object), and this is exactly the metadata the pinned "argument-type-driven completion" item wants (e.g. createElement's tag→type return shapes), sourced from reality instead of hand-authored. See introspection-autocomplete.

Contract vs. shim — the distinction that decides feasibility

"A predicate that stands in for the DOM" can mean two very different things:

  1. A contract / validator — "does this value behave enough like an HTMLElement / an input event / a CSSStyleDeclaration?" Predicates are pure validators, so this is squarely in scope: shape, property presence, method arity, pure invariants.
  2. A behavioral substitute — something that, when code calls document.createElement('div'), actually does it. Predicates cannot be this (they're pure, stateless, effect-free). That's a shim — happy-dom/jsdom territory.

The valuable, on-thesis move is #1 as the bridge to #2: probe the real environment, emit a serializable predicate contract, and use it to certify a stand-in in whatever setting. The predicate isn't the DOM — it's the portable, verifiable spec a substitute must satisfy, auto-derived from reality and from the surface a given program actually touches.

Why it fits this project

The honest constraints

Shape of the tooling (sketch)

  1. Probe — in a real environment (browser via Claude-in-Chrome, or the introspection iframe), walk an ambient object / a program's used surface and record: property names + typeof, method names + .length (arity), and a few sampled input→output pairs for pure-ish accessors.
  2. Derive — turn the probe record into a predicate cluster (shape checks + typeof guards + membership sets), run it through verifyPredicate (so the emitted contract is itself certified safe), and suggest()-mine it for the enumerable leaves.
  3. Emit — a $predicate schema (naive validators see structure, aware ones run the contract) + optionally a .d.ts-ish view for editors.
  4. Conform — a harness that validates a stand-in (happy-dom, a hand stub, a VM capability) against the contract, failing loud where it diverges. This is what would have unblocked the Phase 5 real-tosijs-theme measurement (blocked because theme.ts needs HTMLElement at import).

First spike — CSS + DOM (the convergence point)

element.style (CSSStyleDeclaration) is the ideal first target: it's an ambient, stateful host object, and we already have a full CSS value grammar as predicates (tjs-lang/css). So the loop is concrete and small:

Ask the environment directly — CSS.supports (tosijs-ui port finding)

The tosijs 2.0 port arrived at the same idea from the other side (see ../tosijs/TJS-PORT-DX.md): the strongest form of "probe reality" for CSS is to ask the browser's own validator — CSS.supports(prop, value) is exact, always current with the engine's real support (vendor prefixes, new features, the full calc()/var() grammar), and zero-maintenance. It closes the exact gap the hand-authored grammar has: isCssProperty('align-kontent') returns true (identifier-shaped), but CSS.supports('align-kontent', …) in a real browser returns false. Property names for autocomplete come from the same place (a throwaway element's .style keys / CSSStyleDeclaration.prototype).

This sharpens the contract-vs-fallback split by when validation runs:

So the ideal CSS-value check prefers CSS.supports when typeof CSS !== 'undefined' and falls back to the grammar predicate otherwise — which is exactly the probe-reality / portable-fallback pairing this whole note is about, one leaf down. Caveat (verified, and it's the same "the stand-in lies" finding): happy-dom stubs CSS.supports to return true for everything (even width: banana) — so tests can't rely on it; ground truth needs a real browser or a real CSS engine.

Findings from the first spike (2026-07-03)

Built in experiments/ambient/ (probe.ts + two demo tests, 7 green):

Net: the probe→derive→verify→conform loop works end-to-end on a real host object; the open work is sourcing the used-surface scope (trace) and the behavioral half.

Real-browser findings (via haltija /eval, live Chrome — 2026-07-03)

Confirmed against a real browser (not happy-dom), which sharpened three points:

Generalizes beyond the DOM — Node/Bun internals

The probe is environment-agnostic: process, Buffer, node:fs, Bun.*, globalThis are ambient objects with the same shape. Derive a used-surface contract from the real runtime and certify a stand-in — e.g. "does my process usage survive in the browser polyfill?" or "does Bun's process satisfy the contract my code derived from Node's?". It's the realm/stand-in problem one level up: cross-runtime portability instead of cross-frame. Same tool.

Open questions