Skip to content

Version compatibility & changelog

Bynk is pre-1.0 and developed in small, spec-first increments (see Versioning & roadmap). This book is written against v0.109.

This page is a high-level summary of notable increments, not an exhaustive per-commit history. While Bynk is pre-1.0, increments may change behaviour.

Docs & examples (under v0.78, no version change): the examples/ gallery was refreshed — six of the seven projects now ship unit tests, with each one’s pure logic (a refined type’s boundary, a key helper, a windowing or health policy) factored into a tested commons. The gallery’s testing note was corrected to reflect what is testable today (see #291 for the platform-capability test-surface limitation the split works around).

VersionHighlights
v0.109.0The in-browser track is complete and retired. The milestone marking the whole theme done — the Browser platform, the JS emit path, the wasm toolchain, the REPL/playground, and its polish — turning the design notes’ “a REPL is ambitious and probably v2/v3” aside (§19) into a shipped, zero-install on-ramp: type Bynk, press Run, see it execute, no install and no server. The track doc (design/tracks/in-browser.md) is retired — its decisions live on in ADRs 0136–0140 and in the playground/ app — and the design notes (§18–§19) now record the browser binding and the REPL as shipped. No language/compiler change in this release; it consolidates and marks the theme.
v0.108.5Playground polish — live diagnostics + an in-memory analyse seam (in-browser track, slice-5 polish). The playground gained an examples gallery, web-tree-sitter highlighting (the same tree-sitter-bynk grammar the editor/CLI use), a snippet-share backend written in Bynk (a Workers + KV program compiled by bynkc, reached same-origin), and — the part that touches the compiler — live, on-type diagnostics in the editor. That last one adds bynk_emit::analyse_in_memory(source, target, platform): like compile_in_memory (slice 3) but Mode::Analysenon-bailing, full parse→resolve→check, all diagnostics at once, no emission — so a type error in a context surfaces as you type (inline squiggles + gutter via a bynk_analyze wasm entry), not only on Run. run_checks is unchanged; this only adds a caller. Most of the polish is playground-only; this version reflects the one published-crate API addition.
v0.108.4The in-browser REPL / playground — the track closes (playground app; ADR 0140, in-browser track slice 4; security-bearing, /security-review-gated). Type Bynk, press Run, see it execute — no install, no server: the compiler runs in the browser as wasm (slice 3), and the compiled JavaScript runs in a sandbox. A fully static, client-side app under playground/ (esbuild + vanilla TS + CodeMirror 6) deploying to two Cloudflare Pages origins — playground.bynk-lang.org (editor + bynk_compile wasm) and sandbox.bynk-lang.org (execution). The safety boundary (D2, defence-in-depth): untrusted code runs only in the separate sandbox origin, embedded as <iframe sandbox="allow-scripts"> (an opaque origin) wrapping a Web Worker under a hard wall-clock timeout (terminate() on overrun); Fetch/Secrets already throw in the Browser binding (slice 2), so the sandbox reaches neither the network nor secrets. Linking (D3): bynk_compile’s full JS graph is linked into blob-URL ES modules in topological order with import-specifier rewriting (Workers lack import maps); the Worker calls composeApp(), invokes the zero-argument service handler, and captures Logger output + the returned value. Message trust (D4): the sandbox acts only on the app origin’s messages; the app accepts results only from its sandbox iframe; only a structured-clone result crosses. Deep-link (D5, Q7): a shared snippet is the source compressed into the URL fragment — #base64url(deflate-raw(utf8(source))) via the native Compression Streams API, no library, the same format the documentation track emits — decoded on load, with a Share button; whole-unit granularity + a starter template. Programs reaching Workers/Cloudflare-only shapes show not runnable in-browser via the slice-2 platform lock. Deployment (two Pages projects + DNS) is a maintainer ops step; web-tree-sitter highlighting (Q4) is the named follow-on (its grammar-wasm build needs emcc/docker). With this slice the in-browser track — strip-only emitter → JS artefact → Browser platform → wasm toolchain → playground — is complete: the educational on-ramp the design notes always pointed at.
v0.108.3The wasm toolchain — the compiler compiles to wasm32 (compiler/tooling; ADR 0139, in-browser track slice 3). The in-browser REPL needs the compiler in the browser — so the syntax → check → emit pipeline now compiles to wasm32. (To be clear: this is the compiler’s distribution form, not the program’s — WASM-as-program-output stays rejected per §19; a Bynk program still lowers to TS/JS.) The in-memory seam: a new bynk_emit::compile_in_memory(source, target, platform) runs the whole project pipeline over one in-memory source with no filesystem — first-party injection (the bynk surface), the per-platform binding, and the strip-only emitter all behave exactly as for an on-disk build, returning the complete module graph (the user unit + runtime.ts + the bynk-<platform>.ts binding + compose.ts). The seam is deliberately tiny: run_checks gained an optional pre-discovered file list (so it skips phase_discovery), the source rides the existing editor overlay, and the module’s logical path is derived from its declared unit name (app.demoapp/demo.bynk) so the name↔path alignment check passes without real files. The entry: a new publish = false bynk-wasm crate exposes one wasm-bindgen function — bynk_compile(source) → { files: [{path, contents}], diagnostics: [{path, line, col, severity, category, message}] } — composing compile_in_memory (Bundle/Browser) with the strip pass to return a runnable JavaScript module graph with no tsc and no Node. strip_project_to_js moved from bynkc into bynk-strip so the CLI and the wasm entry share one implementation (no cycle — the language server still never pulls oxc). Q3 settled: ship bynk-check — diagnostics are the point of a REPL, and the whole pipeline (including oxc and ariadne) compiles to wasm32-unknown-unknown cleanly with no feature-gating; payload squeezing (wasm-opt/thin-LTO/lazy-load) is a measured concern for the REPL slice. Verified by native tests of the compile path + a wasm32 build-gate CI leg; executing the wasm in a browser, and the REPL shell itself, land in the next slice. Subset enforcement is free — a program reaching Cloudflare/Workers-only shapes is rejected through the slice-2 platform lock on the in-memory path too.
v0.108.2The Browser platform — --platform browser (checker/emitter; ADR 0138, in-browser track slice 2). A third deploy platform alongside cloudflare/node: Platform::Browser, with a bynk-browser.ts binding implementing the bynk capability surface over Web APIs, composed with the Bundle topology only. The prerequisite for the in-browser REPL, where the binding is also the safety boundary. Clock/Random/Logger are byte-identical to the Node binding — Date.now(), Web Crypto’s crypto.randomUUID(), Math.random(), and console are Web standards on every platform. The two substitutions are the playground boundary (Q2 / §4): Fetch is withheld and Secrets is unavailable, and both fail loudly by throwing rather than degrading silently — a browser can fetch, but arbitrary egress from the playground origin invites SSRF and exfil-by-proxy, and a browser has no secret store (resolving Secrets.get to None would be indistinguishable from “unset”, a silent way to run with blank values). No FetchError.Unavailable variant is added — that would be a breaking change to the bynk surface — so throwing is the loud, surface-stable realisation; a same-origin proxied or opt-in Fetch is the named follow-on. Browser is Bundle-only: --platform browser --target workers is rejected up front with a new bynk.target.browser_bundle_only (a browser cannot run the Workers wire-call model — Service Bindings, Durable Objects, cross-context calls). The lock against Cloudflare-only units comes for free — a browser build that pulls in bynk.cloudflare is rejected at validate time (bynk.target.vendor_required) by the existing native-platform machinery, which is exactly how the REPL will surface “this program uses Workers-only shapes” rather than failing at runtime. The Browser platform serves the playground and education (not real browser apps); TypeScript-first output is untouched.
v0.108.1A first-class JavaScript artefact — bynkc compile --emit js (emitter/tooling; ADR 0137, in-browser track slice 1). bynkc lowers Bynk to TypeScript; --emit js (the default stays ts) writes the same modules with their types stripped — a runnable JavaScript artefact with no tsc in the loop. Because the emitter is strip-only (ADR 0136), this is emit-then-strip: erase type syntax, change nothing else. DECISION A settles the production route in favour of a built-in strip pass over the external-tsc route — tsc reintroduces the Node dependency the in-browser track exists to remove and cannot run in the browser at all (the wasm toolchain slice needs JS produced in-process). DECISION B: the stripper is oxc (pure-Rust TS parse + type-erase + codegen, wasm-safe so the wasm slice reuses it in-browser) in a dedicated bynk-strip crate — kept out of bynk-emit so the language server never pulls it; a hand-rolled stripper was rejected because the emitted surface’s : / <…> / as / type-specifier ambiguities need a real parser. DECISION C: pure type-stripping (only_remove_type_imports) — every value import is preserved even when unused, only import type / type specifiers and type syntax are erased, matching Node’s stripTypeScriptTypes rather than TypeScript’s usage-based elision. DECISION D: stripping is a post-emit step (bynkc::strip_project_to_js) — the emitter stays TypeScript-only — that rewrites .ts.js, drops tsconfig.json, drops source maps, and passes other files (wrangler.toml) through; import specifiers are already .js, so the renamed tree resolves unchanged. Verified by node --check over every emitted .js (also a residue check — a surviving annotation would fail it); --emit js is target-agnostic, so it also strips a --target workers build. TypeScript-first output is untouched and stays primary; JS is additive.
v0.108The emitter’s output is strip-only — the in-browser track opens (emitter/tooling; ADR 0136, in-browser track slice 0). The first slice of the in-browser track, which front-loads the work toward a zero-install REPL/playground. Pure type-stripping (Node --experimental-strip-types, and the node:module stripTypeScriptTypes it is built on) erases type syntax but cannot erase type-directed constructs — constructor parameter properties (constructor(private x: T) {}), enum, namespace — which tsc accepts, so the tsc --strict gate never caught them. DECISION A: the emitter now emits only erasable TypeScript across its whole surface, a standing invariant rather than a per-target branch. The provider given-injection constructor de-sugars unconditionally from constructor(private deps: {…}) to a declared typed field + an assigning constructor — under the emitted ES2022 tsconfig (useDefineForClassFields) the end state is identical, the field stays typed for the tsc/strict path, and it strips to constructor(deps) { this.deps = deps; }removing the one type-directed site rather than adding a branch. DECISION B: the same de-sugaring fixes the three shipped first-party bindings (bynk-cloudflare.ts, bynk-node.ts, cloudflare.binding.ts), whose constructor(private env?: unknown) had silently broken every bynkc test --inspect debug session that exercised Secrets or a given-clause provider (the module fails to parse under strip-only Node before any breakpoint binds — an audit gap; the track had named only the emitter site). DECISION C: a load-bearing regression test (all_emitted_typescript_strips_under_node) checks every emitted .ts across the project fixtures with stripTypeScriptTypes(code, { mode: 'strip' }) — the exact strip-only oracle, one process over the staged tree; node --experimental-strip-types --check is deliberately not used (a file that leads with a type/declare statement trips its module detection and false-fails even though it strips cleanly). Complements ADR 0104 (the --inspect build already runs emitted .ts strip-only); TypeScript-first output is untouched — the emitted TS still type-checks under tsc --strict, and JS/browser work stays additive and opt-in for later slices.
v0.107from WebSocket broadcast + the §20 chat-room end-to-end — the real-time track closes (language/runtime; ADR 0135, real-time track slice 4). The held-aware iteration broadcast over a store Map[K, Connection] already compiled on both targets (slice-2’s closure borrow + 3b-ii’s connId resolution): conns.forEach((c) => c.send(frame)) iterates the connections — resolving connIds and skipping closed ones on Workers — and sends to each, the closure parameter a borrowed held binding. Slice 4 closes it. parTraverse (DECISION D1) is the parallel broadcast primitive: type-identical to the sequential forEach, but lowering to await Promise.all(xs.map(f)) so one slow or half-dead connection does not head-of-line-block the whole room — the §20 form and the production-correct one. Exclude-self by key (D2): “everyone but the sender” filters on the sender’s UserId, not c != connConnection stays non-comparable by design (bynk.types.held_not_comparable). A latent borrow-enforcement gap is fixed (D3): the store-map forEach/parTraverse receiver’s lifted Query[V] type was never recorded, so the linearity pass could not see it as held-bearing and silently did not enforce the borrow on the closure parameter (forEach((c) => c.close()) compiled); now the receiver type is recorded, so send is allowed and a consuming op (close/transfer) on the borrowed c is bynk.held.consume_on_borrow. Proven end-to-end: the §20 chat-room runs under node on the bundle target — two participants join one room, a message fans out to both, one leaves and the next message reaches only the other (a TestConnection behaviour test). The Workers emission (the connId-resolving parTraverse + Promise.all) stays covered by tsc --strict (fixture 238) + the node strip-types guard; no real Workers runtime proof (needs Miniflare/workerd). The bare-map iteration is the v1 surface; the .values accessor and lambda parameter-type inference are named ergonomic follow-ons. Ran /security-review (no new boundary) + /code-review. With this slice the real-time / WebSocket track that began with Stream[T] (v0.100) is complete: the §20 chat-room — edge auth, a held connection transferred to an agent, surviving Durable Object hibernation, inbound frames decoded and dispatched, and a message fanned out to every connection in the room — compiles, type-checks, and runs end-to-end.
v0.106from WebSocket inbound — on message/on close, the receive half of the channel (language/runtime; ADR 0134, real-time track slice 3b-iii, security-bearing). Slices 3a–3b-ii built the outbound path (an authenticated on open transfers a Connection to an agent that sends to it, surviving hibernation); the in: ClientFrame type was declared but no handler consumed it — a client could not talk back. 3b-iii adds the two remaining lifecycle handlers the §20 design commits to: on message(frame) and on close. They are service handlers (like on open), authenticated by the same by actor, and they match the decoded frame and dispatch to the agent’s ordinary on call methods — so variant routing is just a match, no new routing machinery. DECISION D1 (the crux): on message/on close run in the hosting Durable Object (the self-agent lowering, so an agent transfer is a this-self-call), and the by identity + route values come from the socket’s serializeAttachment (extended to { connId, identity, args }, written at on open) — not re-derived from the frame and not re-verified per message; the socket is authenticated once, the attachment is server-side and not client-forgeable. D2: the inbound frame is decoded against in: fail-closed in webSocketMessage — a structurally-invalid or refinement-violating frame closes the socket (1003/1008) and is never dispatched (the client-bytes trust boundary). D3: the firing connection is a borrowed held binding — send is allowed but close/transfer is rejected (bynk.held.consume_on_borrow) and it carries no disposal obligation (a new borrowed_held set threaded into the linearity pass; contrast on open’s owned connection). D4: the on message frame is the parameter typed as the service’s in (bynk.ws.message_frame_param otherwise), the rest are route values recovered from the attachment — reusing the existing single-parameter-list parser (no syntax change); at most one on message/on close each. D5: on close is an optional domain hook (a closed socket resolves to None fail-soft, not a live leak; no auto-prune). D6: on the bundle target there is no webSocketMessageon message/on close lower to callable surface methods a TestConnection test drives (the §20 inbound echo runs green under node). New HandlerKind::Close; a from WebSocket on message reuses HandlerKind::Message (disambiguated from the queue consumer by the protocol). Deferred to slice 4 (the closure): broadcast-to-all-connections (the held-aware iteration borrow surface) + the full §20 chat-room end-to-end. Proven on the generated code: the inbound echo runs green on bundle; the Workers webSocketMessage/webSocketClose dispatch + __wsMessage/__wsClose bodies type-check under tsc --strict (fixtures 236/238). Ran /security-review + /code-review.
v0.105from WebSocket hibernation re-association — a held connection survives Durable Object eviction (language/runtime; ADR 0133, real-time track slice 3b-ii, security-bearing). Slice 3b-i shipped a working edge-authenticated upgrade using the non-hibernatable server.accept() model — the socket lived in the DO’s isolate memory and was lost on eviction. 3b-ii swaps that for Cloudflare’s hibernatable WebSocket API so a stored connection survives, realising design notes §2.9.6 (“a Connection[F] stored in agent state survives the agent’s hibernation”). The crux (DECISION D1): the stored value becomes the connId, not the socket — a serialisable string. The DO accepts via state.acceptWebSocket(server, [connId]) (a fresh crypto.randomUUID() tag) instead of server.accept(), serializeAttachment({ connId }) persists the id on the socket, and every Connection access re-resolves connId → live socket via state.getWebSockets(connId) — so no live socket is held across requests and hibernation is transparent. A held store Map[K, Connection] now persists Record<string(K), connId> (D2), reversing 3b-i’s in-memory heldStore split now that the value is serialisable: it rejoins the durable state record (interface, zero, rehydration key-check, the staged commit), put records connIdOf(conn), get resolves the connId (None if the socket has since closed — resolution is fail-soft, D3: a missing socket is normal lifecycle, not corruption), and a query resolves the present connections. remove now resolves-closes-deletes (D4) — finally emitting the §2.9 “removes-and-closes” contract the 3b-i lowering only deleted. Runtime acceptHibernatableConnection/resolveConnection/connIdOf replace the 3b-i heldStore; a narrow HibernatableState cast keeps the hibernation API off the shared DurableObjectState (so the bundle TestConnection model is unchanged, D5 — the connId representation is Workers-only). Deferred to a named slice 3b-iii: inbound webSocketMessage dispatch (a new protocol surface — inbound message handlers + frame→handler routing), independent of and larger than the hibernation binding; 3b-ii keeps the send path durable. Proven on the generated code: the §20 chat-room re-emits + type-checks under tsc --strict on Workers with the hibernatable handlers (no real-hibernation runtime proof — needs Miniflare/workerd — so coverage is the shape-snapshot fixtures + tsc --strict + the node strip-types guard). Ran /security-review + /code-review.
v0.104The from WebSocket Workers wire path — authenticate at the edge, accept into the Durable Object (language/runtime; ADR 0132, real-time track slice 3b-i, security-bearing). The Workers half of the protocol (slice 3a shipped the bundle vertical): a from WebSocket service now compiles for --target workers, so the slice-3a platform-lock is removed (bynk.target.websocket_workers_unsupported). The topology is the security boundary made runtime-real (DECISION A): a live socket cannot cross a Durable Object RPC and hibernation needs the socket in the DO, so the upgrade request is what moves, not the socket. The Worker authenticates the actor at the edge — reads the Bearer token from the first Sec-WebSocket-Protocol subprotocol element (a browser sets it via new WebSocket(url, [token]); DECISION C), verifies it fail-closed with the same audited JWT verifier HTTP uses, runs the refinement-actor authorization predicate (403), and validates each route param through its .of constructor (400) — and only on success forwards the request to the addressed DO, with the verified identity in a trusted internal header (the DO is reachable only through the Worker, the same internal-channel trust the cross-context caller seam uses). No unauthenticated request reaches the DO; no socket is accepted before auth. The hosting DO is resolved statically from the single connection transfer the on open makes (Room(roomId).join(…, connection) → the ROOM namespace keyed by roomId; DECISION B) — a zero/multiple/non-routable shape is the compile error bynk.ws.open_transfer_shape. Inside the DO, the on open body runs as a this-self-call (the connection never crosses a boundary): WebSocketPair + accept, a runtime WorkersConnection<F> (send JSON-encodes a frame, close ends the socket), the welcome frame, then the agent-local join, returning the 101. A held store Map[K, Connection] can’t be JSON-persisted (a live socket), so on Workers it lives in an in-memory side-table (heldStore, keyed by the durable state object) split out of the durable record — lost on eviction, the non-hibernatable model. Deferred to 3b-ii (named, not silently dropped): hibernation re-association (acceptWebSocket/serializeAttachment/getWebSockets), inbound webSocketMessage dispatch, and broadcast-to-all-connections. Proven on the generated code: the §20 chat-room emits + type-checks under tsc --strict on Workers; the on-open transfer-shape constraint is pinned by a negative fixture. Ran /security-review (the edge-auth path) + /code-review.
v0.103The from WebSocket protocol — the bundle vertical (language/runtime; ADR 0131, real-time track slice 3a, the security-bearing one). A service <Name> from WebSocket(in: ClientFrame, out: ServerFrame) declares a WebSocket endpoint: the HTTP upgrade authenticates the actor at the edge via by before the connection is accepted (fail-closed — like HTTP, a WebSocket has no safe default actor, so on open must name its actor; there is no anonymous upgrade), then the on open handler receives a fresh, owned Connection[out] the framework supplies. The connection is governed by the slice-2 linearity discipline — it must be disposed (the canonical disposal is transfer into an agent: Room(roomId).join(user.identity, connection)); an undisposed one is bynk.held.leak. Inbound frames arrive at the agent as ordinary typed messages, not service handlers, so the service holds exactly one on open. The WS boundary admits None/Bearer auth and rejects Signature — a browser WebSocket cannot set an Authorization header, so a Bearer token is read from the Sec-WebSocket-Protocol subprotocol. On the bundle target the connection is a TestConnection — a capture-and-inspect channel recording every frame sent — so a WebSocket service is fully developable and testable with no Durable Object: the §20 chat-room runs under node (the on open handler sends a welcome frame, captured on the TestConnection, then transfers the connection into the Room agent). The Workers Durable Object hibernatable mapping (the WebSocketPair upgrade, acceptWebSocket, hibernation re-association, and inbound-frame dispatch) is the next increment; until it lands, a from WebSocket service on --target workers is reported (bynk.target.websocket_workers_unsupported). Proven on the generated code: the chat-room type-checks under tsc --strict and runs green; the security rules (no by, Signature at the WS boundary, an undisposed connection, the Workers target) are each pinned by a negative fixture.
------
v0.102Held-resource linearity — the Connection[F] type and the ownership discipline (language; ADR 0130, real-time track slice 2). Realises bynk-type-system.md §2.9, settled-in-shape since the design notes but never built: Connection[F], a handle to a long-lived WebSocket connection, is the one instance of a closed Held kind. Held values are runtime-produced (no constructor — they come from a capability operation or a handler parameter) and non-serialisable, non-boundary, and not value-comparable (identity, not value-equality). The operations are c.send(f) (write a frame; non-consuming) and c.close() (consuming). They may be stored only in Cell[Option[Connection]] / Map[K, Connection] (put consumes, remove removes-and-closes) — a Set/Log/Cache rejects them. A new linearity-check pass (the spec’s §3 step 11) tracks each held binding through owned → borrowed → consumed and enforces the §2.9 discipline at compile time: a connection must be disposed (stored, closed, or transferred) before its scope exits (bynk.held.leak), may not be used after a consuming op (bynk.held.use_after_consume), and must be left in a consistent state across if/match branches (bynk.held.branch_divergence). Fault paths are settled for the within-handler case (Q5): a connection owned at an abnormal exit is implicitly closed by the runtime, a stored one rolls back with agent state. This slice is compile-time — tested against a hand-written capability source, with no socket; it emits against a runtime Connection<F> interface whose implementations (TestConnection, the hibernatable-WebSocket binding) and the from WebSocket protocol that produces real connections arrive in the next slice. Proven on the generated code: a received connection sent-then-closed, and a Map[K, Connection] join/leave agent, both type-check under tsc --strict.
v0.101Streaming HTTP response — Stream[T]’s first end-to-end use (language/runtime; ADR 0129, real-time track slice 1). A from http handler can now return a streamed body, consuming the Stream[T] primitive (v0.100) with no socket, no Durable Object — the early payoff of the real-time track. A new fifth HttpResult payload shape, Streamed, and one variant Streaming (200) carrying a Stream[String], mirror ADR 0126’s Location precedent: one registry row extends the three exhaustive arms (construction → HttpResult[()], pattern-bind, runtime status map) once each. Streaming(stream) lowers to a text/event-stream Server-Sent Events Response — each stream element is one data: event, framed by a runtime sseResponse helper that wraps the AsyncIterable<string> in a ReadableStream<Uint8Array> (a Web standard, so Workers and Node unchanged). Streaming is 200-only — a response commits its status before the first chunk, so a pre-stream failure returns an ordinary variant instead (NotFound/Unauthorized/…, which share HttpResult[()] and so coexist with Streaming in one handler), and a mid-stream failure rides in-band as a Result element the producer maps into the string stream. A bounded take guards an unbounded response; a structured SseEvent type and a streamed 202 are named follow-ons. Proven on the generated code: the framing emits exactly data: …\n\n events under node, and a streaming handler type-checks under tsc --strict.
v0.100Stream[T] — the value-over-time primitive (language; ADR 0128, real-time/WebSocket track slice 0). The language had Effect[T] (a value that resolves once) and Query[T] (a snapshot read over storage) but no word for a value produced incrementally over time — a token feed, a progress stream, an incremental response. Stream[T] is that word: a lazy, pull-shaped sequence, modelled almost line-for-line on Query[T] and joining it (with Effect/Fn/held resources) in the non-serialisable / non-storable / non-boundary / non-comparable family — a live source is built and consumed in place, never persisted, sent across a context boundary (bynk.types.stream_at_boundary), or compared with == (bynk.types.stream_not_comparable). The v1 vocabulary is deliberately minimal: the static constructor Stream.of(xs) (List[T] -> Stream[T], mirroring Duration.millis/Instant.fromEpochMillis), the lazy builders map/take, and the terminal collect that drains to Effect[List[T]] (awaited with <-). Errors ride in-band as Result elements (Stream[Result[T, E]]); a richer algebra (filter/scan/merge), live runtime sources, and the streaming-HTTP response body are later slices. Stream[T] lowers to a host AsyncIterable<T>, emitted inline (async-generator IIFEs, no runtime import) so non-stream files are byte-identical and the strip-only invariant holds; the in-memory ofcollect path is deterministic, so streamed output is assertable in tests. The type parameter is committed now; the element semantics (whether a non-chunk element may cross a boundary / must be serialisable) are deferred to the first consumer that needs T ≠ Chunk — shrinking the irreversible surface to what slice 0 exercises. Proven on the generated code: of/map/take/collect and a Result-element stream type-check under tsc --strict.
v0.99Capability requirements gain provenance — a materializable ghost given — and by is rejected on an agent handler (compiler/LSP; ADR 0127). A storage Cache/Log op needs given Clock for its TTL eviction / timestamping, but nothing in the source names Clock — the requirement was real yet invisible at the handler signature. v0.99 makes it discoverable. The checker now keeps a requirement ledger: every capability-consuming site — a direct Cap.op(...) call, a store op — is recorded as { capability, site, source }, covered or not, and its reason is a total function of the source (DirectCall“calls Cap.op, correct for any capability including user-defined ones; StoreOp → a storage-feature fragment; Builtin → the builtin’s surface), so adding a capability needs zero new reason text. On a handler whose body has a requirement its given does not cover, the editor renders a materializable ghost clause… -> Effect[()] «given Clock» — whose one-click edit writes the real given Clock (the same given_insertion_edit the undeclared-capability quick-fix uses); already-declared handlers show no ghost. A source-level @requires is rejected — the requirement is derivable, so authoring it would restate an internal. Separately, by on an agent on call handler is now a clean error (bynk.actor.by_on_agent): by is a service-edge clause and an agent has no actor, so the parser-accepted-but-silently-dropped clause is rejected (zero blast radius). The sibling correctness half — an agent owns its capabilities so a capability-free handler can call a given Clock agent method and the bundle type-checks — is promoted to the agent-capability-encapsulation feature track after its spike showed it pulls in a new bundle composition root. No existing program changes behaviour.
v0.98Cell.update — the method-shaped read-modify-write (language; ADR 0125). A store n: Cell[T] field gains its one method-shaped operation, n.update(f) : Effect[()] with f: (T) -> T — a read-modify-write that makes the prior-value dependency visible (and the combiner retry-safe). It is the operation the self-referencing-:= diagnostic (bynk.cell.self_reference) has always steered toward: n := n + 1 is rejected, and let _ <- n.update((c) => c + 1) is the fix that now compiles. read/write stay sugar (the bare name reads, := writes) — not callable methods, so there is one way to do each thing. The checker dispatches the op by receiver provenance, a sibling of the Map/Set/Cache/Log helpers; the emitter lowers it to a staged read-modify-write over the in-memory working state (Map.update’s lowering minus the key-absent guard — a cell is always present, so no fault path), committed by the same end-of-handler flush through the invariant gate that := uses. The combiner is a pure (T) -> T; an effectful body (including a bare read of another cell) is rejected for free. Returning the new value is the explicit two-liner — update, then read the bare name back (read-your-writes). Proven on the generated code: an update persists across invocations and a same-handler read sees it, under node. This settles the Cell operations in the type system from Open to normative.
v0.97Storage track — rehydration validation (language/runtime; ADR 0124, the track’s final slice). An agent’s persisted state is now validated on load, realising a long-standing design-notes promise that was an unguarded cast (loadState was stored ?? zero()). When stored state exists, a generated __rehydrate<Agent>State gate runs each value position — a Cell’s T, a Map/Cache’s V, a Log’s T, and textual Set elements / Map keys — through the same boundary deserialiser the HTTP/queue seams use, against the current type definition. A failure is an internal fault — RehydrationViolation, the load-time twin of InvariantViolation (logged with agent + field, never the key/value), not a caller-facing 400: the supplier is trusted past-self, not the untrusted caller (Q6). A refinement that tightens across a deploy faults on load (orphaned data is indistinguishable from corruption); breaking migrations stay by convention (no coercion, no silent drop, no v1 migration hook). Additive evolution is automaticloadState now merges { ...zero(), ...stored }, so a store field added in a later deploy takes its default instead of reading undefined (D4, also fixing a latent load bug). The boundary deserialisers now emit on both targets for agent-state types (workers and bundle). Proven on the generated code: a tampered or schema-tightened record faults with RehydrationViolation, a structurally-corrupt field faults, and an absent (additive) field defaults — all under node. With this, the storage track retires: the kind catalogue, the parity cutover, and rehydration are all shipped; a versioned-schema migration capability, per-field default-on-read, and a soft recovery handler are named follow-ons.
v0.96Storage track — the parity cutover (language; ADR 0123). The legacy agent-state surface is removed: the state { } block, the commit statement/keyword, and the self.state receiver are gone, leaving store fields as the agent’s sole state surface (ADR 0108). State is read by bare name, written with :=, and committed atomically when the handler returns (ADR 0109) — no commit step. state/commit are no longer reserved words (they parse as ordinary identifiers); the parser, checker, the entire state-record emitter path, formatter, LSP, tree-sitter grammar, and TextMate highlighting drop the surface across all crates. The five removed diagnostics (bynk.commit.outside_agent/wrong_state_type/two_reachable_commits, bynk.parse.duplicate_state_block) retire with it; bynk.agents.non_zeroable_state_field and bynk.agents.bad_state_initialiser stay — they apply to store fields (a Cell still needs a zero or an initialiser). The in-repo corpus, examples, and book were migrated to store in the preceding increment (no observable behaviour change — a store-agent’s cells already were its state record), so this is a pure surface removal. Agent rehydration (Q6/Q7) is the storage track’s remaining open question.
v0.95Storage track — Log is functional (language; ADR 0121). A store history: Log[T] @retain(<duration>) field is now an append-only, time-indexed sequence. history.append(e) stamps the current time (Clock.now() -> Instant), requires given Clock, and is the one non-idempotent storage write (dedup-key / future Idempotency is the documented safe-use story). Reads are lazy Query[T] over the entry values, with Log-specific roots — since(Instant)/before(Instant)/between(Instant, Instant)/recent(Int)/reversed() — composing with the query vocabulary (filter/map/collect/count/…). The window roots take explicit Instants, so reads need no clock — narrower than Cache, whose eviction reads consult it. Optional @retain(<duration>) (the second functional storage annotation) prunes on append, keeping reads clock-free and bounding the array. Persisted as an ordered Array<{ t, v }> state field, committed atomically (ADR 0109) — proven on the generated code: append, since/recent/collect, and retention pruning all run under node with a mock clock. The remaining kind (Queue) is the last storage slice.
v0.92Lazy storage queries — Query[T] over a store Map (language; ADRs 0115/0119, query-algebra slice 2). The combinator vocabulary now reads agent-local storage: a chain rooted in a store reservations: Map[K, V] field is lazy, dispatched by receiver provenance (generalising ADR 0110 from op-set to evaluation strategy). A builder lifts the map’s values into a Query[V] (reservations.filter(r => r.status == Pending).map(r => r.payload)) and chains build further queries; a terminal executes it and is Effect-typed.collect() -> Effect[List[V]], .first(), .count(), .sum(key), .min/.max/.average, .any/.all, .fold, .forEach — awaited with <-, folding into the storage capability the store fields carry (no new given). Query[T] is a first-class, by-reference type — nameable, returnable from a pure helper, passable — but non-storable and non-boundary (like Effect/Fn): rejected in any storable/boundary position (bynk.types.query_at_boundary). Builds are pure; terminating is effectful. A query is agent-local and reads staged state (read-your-writes). It lowers to a scan over the in-memory Record of the wholesale-persisted map — a deferred thunk so a let-bound or chained query reads state at terminal time (tsc-strict verified). @indexed routing, joins/groupBy, and the given Map pure-helper form are later slices.
v0.91The bynk.list free functions are deprecated (language/tooling; ADR 0116 D6, query-algebra slice 1c). With the method-chain vocabulary shipped (v0.88) and the warning channel in place (v0.89), the first-party bynk.list free functions whose method forms exist — map/filter/find/any/all — now emit a non-failing bynk.list.deprecated_function warning at each call site, with a machine-applicable auto-fix to the method form: map(xs, f)xs.map(f), find(xs, p)xs.filter(p).first(), and so on (one-click in the editor; the build still succeeds). reverse and traverse keep their free-function form (no method equivalent yet — traverse rides slice 5). The deprecation fires in project mode (where uses bynk.list resolves), routed by import provenance so a user’s own map is untouched. The repo’s examples migrate to the method form. This closes the track’s Q12: the warning channel (ADR 0117) made a deprecation — rather than a build-breaking removal — possible.
v0.90The Instant primitive (language; ADR 0114, query-algebra slice 1b). Instant joins Int/String/Bool/Float/Duration as a base type — an absolute point in time, erased to a TS number of Unix epoch milliseconds (the Clock unit). It has no literal: an Instant is minted by Clock.now() (now typed Effect[Instant]) or built from an Int via Instant.fromEpochMillis(n). Arithmetic composes with Duration: Instant ± Duration -> Instant (advance/retreat) and Instant - Instant -> Duration (the span between); comparison is chronological and Instant is orderable (so sortBy/min/max key on it), but not numeric (sum/average reject it). The conversion t.toEpochMillis() -> Int is the escape to raw millis; codec is a JSON number, integer-on-read; the zero is the epoch. Breaking — supersedes ADR 0112 D4: the Int + Duration -> Int clock-math coercion is withdrawn — timestamp math now goes through Instant (now + 5.minutes is Instant + Duration), and every InstantInt mix is a no_numeric_coercion error; Instant is now a reserved type name. Code that bound Clock.now() as an Int migrates to Instant (or toEpochMillis()). Lowers to number operations — emitted output for instant-as-millis code is unchanged. Unblocks slice 2’s instant-field storage queries and the Log slice.
v0.89A non-failing warning channel (compiler/CLI; ADR 0117). A diagnostic’s severity now decides whether it fails the build: a Warning surfaces but bynkc compile/check succeed (exit 0) and emit output; an Error fails as before. Previously every diagnostic — even the two warning-category ones — failed compilation (Severity was display/LSP-only). The split is a severity-aware collection sink: it classifies each diagnostic on push, so the build-failure gate counts error-severity only, while every warning source (commons-fn checks, service/agent handler validation, the parser in project mode) is captured uniformly. compile_project carries warnings on success (ProjectOutput.warnings); the CLI prints them; the LSP is unchanged (it already rendered warnings). The two existing warning-category diagnostics — bynk.given.unused_capability and (in project mode) bynk.parse.orphan_doc_block — become true warnings; positive fixtures gain an expected_warnings.txt surface and assert no warnings by default. -Werror and single-file parser-warning surfacing are noted follow-ons. Unblocks the bynk.list→methods deprecation (ADR 0116 D6) and @indexed hygiene warnings. (Builds on v0.88’s List vocabulary.)
v0.88Query-algebra track — the eager List vocabulary (language; ADR 0116, slice 1). List[T] gains the query algebra’s eager in-memory combinator vocabulary as kernel methods, so a chain reads xs.filter((x) => x > 2).map((x) => x * 2) instead of nested bynk.list.* calls. Builders: map/filter/flatMap/sortBy/take/skip/distinct/distinctBy. Terminals: count/any/all/first/firstOrElse/sum/min/max/average. Ordering keys (sortBy/min/max) come from a closed orderable base set — Int/Float/String/Duration, refined types widening, opaque keys rejected (bynk.types.key_not_orderable); numeric keys (sum/average) are Int/Float/Duration (bynk.query.sum_needs_numeric), with average -> Float (no truncation) or a Duration (integer-rounded); distinct/distinctBy need a value-keyable element/key (bynk.types.unkeyable_distinct). Empty aggregates are totalfirst/min/max/average return Option, sum the zero — fixed at the type because the storage half (a later slice) learns emptiness only by executing. The same names will carry a lazy storage Query[T] (ADR 0115); these are the eager receiver. Lowers to native array operations (tsc-strict verified). The bynk.list→methods deprecation (ADR 0116 D6) is split out, pending a non-failing warning channel; the free functions still work.
v0.87Storage track — Cache is functional (language; ADR 0113). A store live: Cache[K, V] @ttl(5.minutes) field is now a TTL-bounded map: the storage-Map op set (put/get/update/upsert/remove/contains/size, awaited with <-) with per-entry expiry. @ttl(<duration>) is required — it becomes the first functional storage annotation (closing the loop the v0.85 registry opened) and sets the entry lifetime (a keyed store with no expiry is a Map; bynk.store.cache_ttl_required). Eviction is lazy, check-on-read: get/contains/size skip an entry past its expiry, reaped at the next commit. The current time comes from given Clock, not an ambient clock — a handler performing a time-consulting cache op must declare given Clock (bynk.store.cache_needs_clock); this makes eviction testable (a mocked Clock drives expiry deterministically) and the time dependency visible at the handler signature. Persisted as a Record<string, { v, exp }> state field, committed atomically (ADR 0109) — proven on the generated code: put/get within the window, expiry after the clock advances, and TTL reset all run under node. Per-put TTL override, alarm-based reaping, and @bounded caps remain follow-ons.
v0.86The Duration primitive (language; ADR 0112). Duration joins Int/String/Bool/Float as a base type, a span of time erased to a TS number of milliseconds (the Clock unit). A literal <int>.<unit> over a closed unit set — 5.minutes, 30.seconds, 1.hours, 2.days, 100.milliseconds — recognised over the existing IntLit . Ident shape (no new lexer token). The operator surface: Duration ± Duration, Duration * Int / Int * Duration (scalar scaling), and Duration comparison; subtraction is unclamped (may go negative). One sanctioned IntDuration mixInt + Duration -> Int (and -) for advancing a millisecond instant (clock.now() + 5.minutes), the deliberate exception to the no-coercion rule (ADR 0041); every other mix is a no_numeric_coercion error. Conversions are explicit: d.toMillis() -> Int and the static Duration.millis(n: Int) -> Duration. A Duration round-trips through the codec as an integer JSON number. Breaking: Duration is now reserved — a user type named Duration must be renamed (as Int/Float already are). Unblocks @ttl/@retain for the Cache slice.
v0.85Storage track — the annotation surface (language; ADR 0111). store fields now parse @name(args) annotations between the kind and the initialiser — store sessions: Cache[SessionId, Session] @ttl(5.minutes), store items: Map[K, V] @indexed(by: orderId). The vocabulary is a closed registry of four (@ttl/@retain/@indexed/@bounded); an unknown name (bynk.store.unknown_annotation), a wrong-kind use (bynk.store.annotation_kind_mismatch), or an annotation whose slice has not yet landed (bynk.store.annotation_unsupported) is a diagnostic. This slice lands the grammar + registry only — every annotation gates as unsupported for now; each becomes functional with its kind’s slice (@ttl next, with Cache). Annotation arguments are compile-time literals; @ttl/@retain will take a Duration (5.minutes), introduced as a prerequisite slice before Cache. The formatter, tree-sitter grammar, and TextMate highlighting all cover the new surface. No emitted-code change — agents without annotations are byte-identical.
v0.84Storage track — Set is functional (language; ADR 0110). A store members: Set[T] field is now a storage set: effectful membership methods add/remove/contains/size, awaited with <-. As with Map, it is one type, two op sets — a store field of Set[T] is the storage set; a value of Set[T] is the immutable collection (pure methods), unchanged — disambiguated by receiver provenance. The set persists as a Record<string, boolean> field of the agent’s state record (a JS Set does not serialise), committed atomically at handler end like a Cell/Mapadd/remove stage into the working record; a fault before the flush persists nothing (proven on the generated code: idempotent add, remove, contains, and size all run under node). add is idempotent and remove of an absent member is a no-op (no fault). The remaining kinds (Log/Queue/Cache) remain follow-ons.
v0.83Storage track — Map is functional (language; ADR 0110). A store items: Map[K, V] field is now a storage map: effectful entry methods put/get/update/upsert/remove/contains/size, awaited with <-. One type, two op sets — a store field of Map[K,V] is the storage map; a value of Map[K,V] is the immutable collection (pure methods), unchanged — disambiguated by receiver provenance, no new type name. update on an absent key faults (use upsert for default-if-absent). The map persists as a Record<string, V> field of the agent’s state record, committed atomically at handler end like a Cell — a mutating op stages into the working record; a fault before the flush persists nothing (proven on the generated code: put/get/upsert/remove and atomic revert on a faulting update all run under node). A keyed map.get(k) is admissible in invariants (ADR 0108 D5). Also fixes a latent emitter bug: a lambda returning a record ((x) => T { … }) is now parenthesised. @indexed, the query/iteration surface, and per-entry storage keys remain follow-ons.
v0.82Storage track — store/Cell is functional (language; ADRs 0108/0109). The Cell storage kind now type-checks and compiles: a store count: Cell[Int] = 0 field reads by bare name (implicit deref), writes with :=, and is committed atomically at handler end with the invariant gate (ADR 0109) — a fault before the commit persists nothing. The checker enforces kind validity, the :=-references-LHS read-modify-write rule (bynk.cell.self_reference), value types, and resolves invariants over cells; emission stages writes through a mutable working record and flushes once via commitState. A store-agent’s cells are its state record, so the whole machinery (zero factory, load/commit, invariant gate) is reused. Validated on the generated code: read-your-writes, cross-handler persistence, and atomic revert on an invariant violation all run under node. The other kinds (Map/Set/Log/Queue/Cache), Cell.update, and refined element types remain follow-ons. Agents on state { } are byte-identical.
v0.81Storage track, slice 1 — store fields and the := write (syntax) (language; ADRs 0108/0109). The first slice of the storage track: the successor agent-storage surface parses, formats, and highlights, but is gated as not-yet-functional (bynk.store.unsupported) — kind-aware checking and the staged-commit lowering land in later slices. New surface: store <name>: <Kind>[…] [= init] agent fields (coexisting with the state { } block during the track, per ADR 0108 D3), the := Cell-write statement, and the StoreKind grammar. store is a contextual keyword (like key), so existing identifiers such as a cache.store context keep working. Also lands the track’s reserved-keyword ↔ TextMate drift test, which closed six pre-existing highlighting gaps (actor, as, by, expect, protocol, self) and makes that lag structurally impossible. No emitted-code change — agents written against state { } are byte-identical.
v0.80Agent invariants (language; ADR 0107). An agent may now declare invariants — universally-quantified predicates that must hold of every committed state: invariant paid_has_payment_ref: status == Paid implies paymentRef.isSome(). They sit in a phase between the state { } block and the handlers, and read state fields by bare name. A new lowest-precedence operator implies (P implies Q ≡ `!P
v0.79Asynchronous message send (~>) (language; ADR 0106). A new fire-and-forget statement: ~> Logger.info("…") sends an effect without awaiting its reply — no let _ <-. The model separates two axes the surface used to conflate: does the reply carry a value, and must the caller wait. let r <- e awaits a valued reply; let _ <- e awaits and discards a unit reply (the durable-write case); ~> e sends and moves on. The caller chooses the form — operations keep a plain -> Effect[...] signature, no call/cast keywords and no Oneway type. An error gate restricts ~> to Effect[()] (bynk.send.requires_unit/non_effect/in_pure_context), so a reply’s value or error can never be silently dropped. The marker is a leading ~> glyph (distinct from <-; a send keyword would have clashed with Fetch.send). Emits ctx.waitUntil(…) on the Workers target — the send settles after the response returns rather than being cancelled with it — threaded only into contexts that use ~>, so all other output is byte-identical. Scope is the immediate tier and the marker; the buffered/at-commit tier and migrating first-party logging defer to the events channel.
v0.78Run/Debug a test from the editor (tooling; vscode-bynk only — no language or compiler change). Each test now shows a **`▷ Run Test
v0.77Quiet the lowered-temp noise — Phase 2’s reshapes complete (tooling; semantic-debugging track, slice 4; ADR 0105). Stepping through a handler, the Variables pane no longer carries the compiler temporaries the lowering spills (__r0, __d, the ?/match spill bindings) — what’s left is your bindings and the Bynk groups. The same editor-side rewrite (slice 2’s Local-scope variables pass) drops __-prefixed locals. Inference-only, no compiler change: Bynk’s lexer already reserves __ (a user let __x is a parse error — _ is the discard token), so a __-named local is exclusively a compiler temp — zero false positives. Same bynk.debug.semanticValues toggle restores the raw view; both runtimes. With this, Phase 2’s planned reshapes are complete — values, the frame’s variables, the call stack, and now the noise, all read in Bynk; the by actor (riding the slice-3 debug-metadata sidecar) is the remaining follow-on. No language surface.
v0.76The call stack reads in Bynk — handler frames named by their operation (tooling; semantic-debugging track, slice 3; ADR 0105). The Call Stack now names a handler frame for its Bynk operationGET "/", bump(amount) — instead of the emitted JS function (http_GET); toolchain/runtime frames were already greyed out (skip-stepped), and clicking a frame still navigates to its .bynk line. This is the first feature inference can’t carry (the route/signature isn’t in the emitted name), so it introduces the emitter debug-metadata sidecar the track anticipated: bynk-emit writes a profile-sibling <file>.bynkdbg.json mapping each emitted handler to its Bynk label (additive — no change to the emitted .ts, never bundled into a deployed Worker), and the editor-side DebugAdapterTracker loads it to rewrite the stackTrace response. Total-by-default (a missing/garbled sidecar just leaves the raw frame name). Same bynk.debug.semanticValues toggle; runtime-agnostic. No language surface.
v0.75The handler frame reads in Bynk — capabilities & state as groups (tooling; semantic-debugging track, slice 2; ADR 0105). Slice 1 made values read in Bynk; this makes the frame’s shape read in Bynk. Stopped in a handler, the Variables pane now groups the consumed capabilities under Capabilities and an agent’s state under State (both floated to the top, still expandable), with your own bindings and request parameters below — instead of a flat list of emitted locals (deps, currentState, …). The same editor-side DebugAdapterTracker rewrites the variables structure now, not just value strings (ADR 0105 D4), on both runtimes. Inference-first (ADR 0105 D5): capabilities and state are recognised from the emitter’s fixed local names, so no compiler change — the by actor (not dependably a local) and a robust emitter debug-metadata sidecar are the named follow-on. Same bynk.debug.semanticValues toggle. No language surface.
v0.74Semantic debug values on both runtimes — including workerd (tooling; semantic-debugging track, slice 1; ADR 0105). Slice 5 (v0.73) rendered Ok(42) only on Node — its customDescriptionGenerator ran in the debuggee, which workerd forbids. This brings Bynk-vocabulary values to the dev-server (workerd) path by rewriting the debugger’s responses editor-side instead (the interposition model ADR 0105 settled): a DebugAdapterTracker parses js-debug’s value preview ({tag: 'Some', value: 'hi'}) and re-renders it as Some("hi") — runtime-agnostic, so the same code serves Node and workerd. The parser is a real recursive parser (braces/commas inside strings don’t fool it) and total (any non-Bynk value passes through untouched). Bound to Bynk sessions, gated by the existing bynk.debug.semanticValues toggle. The editor-side path is bounded by the preview’s depth (a nested value shows one level, the rest one expand away), so the slice-5 in-debuggee generator is kept for the Node test path (full inline nesting) and the interposer covers workerd — they compose (the rewrite is idempotent). No language surface.
v0.73Debug values read in Bynk’s vocabulary (tooling; debugging track, slice 5 — Phase 2’s on-ramp; ADR 0104 D1). Phase 1 made control read in Bynk (breakpoints/stepping/stack land on .bynk lines); this makes values read in Bynk. When you inspect a value while debugging a test, Ok(42) shows as Ok(42) — not {tag: "Ok", value: 42} — with Some/None, sum variants (BadRequest("…"), NotFound), and nesting (Ok(Some(42))) all in Bynk constructor syntax. The mechanism is the cheap half of ADR 0104 D1: js-debug’s customDescriptionGenerator (a function it evaluates in the debuggee), injected by the slice-4 provider into the attach it already builds — no custom Debug Adapter, no runtime change (the generator reads the emitted tagged shape; structural recognition, no false positives). New bynk.debug.semanticValues toggle (default on). The spike’s verdict split the runtimes: it works under Node (bynkc test --inspect) and ships there; workerd rejects the in-debuggee evaluation (it would break variable inspection outright), so bynk dev --inspect sessions keep the raw shape — workerd-vocabulary values are the deferred custom-adapter follow-on. No language surface.
v0.72One-click debugging in VS Code — the debugging-track finale (tooling; debugging track, slice 4; ADR 0104). Set a breakpoint in a .bynk file and press Debug — no terminal, no manual attach. The extension contributes a bynk debug type whose DebugConfigurationProvider compiles, starts the V8 inspector by shelling the slice 2–3 --inspect CLIs, reads the inspector port, and hands off to VS Code’s built-in JavaScript debugger (a delegated pwa-node attach) — glue, not a Debug Adapter (ADR 0104 D1). Two runtimes, one mechanism: the Test Explorer gains a Debug action beside Run (bynkc test --inspect, Node), and a launch.json config debugs the dev server worker (bynk dev --inspect, workerd via wrangler). The load-bearing fix is in the emitter: a source map’s sources is now the .bynk file’s absolute path, so a breakpoint set on the real file resolves to the same source the debugger loads (a project-relative name resolved against the emitted .ts’s directory — the wrong place; the CLI scenarios never hit this because they set breakpoints by generated line, but an editor sets them by file path). New bynk.bynkPath setting resolves the bynk driver for the dev path. This completes the track’s pragmatic Phase 1 — step-debug .bynk under both runtimes, from the editor. No language surface.
v0.71Debug your worker under bynk dev--inspect (tooling; debugging track, slice 3; ADR 0104). bynk dev --inspect serves with wrangler dev’s V8 inspector enabled and prints an inspector URL; attach any JavaScript debugger (VS Code, Chrome DevTools) and a breakpoint set in a .bynk handler binds and pauses on a real request, resolved to the exact statement (per-statement since v0.70). The maps just work end-to-end: wrangler/esbuild composes the emitted .ts.map into the worker bundle, so no bynk-side bundling is needed. --inspect-port sets the port (default 9229). One wrinkle documented: wrangler’s inspector requires an Origin header on the WebSocket (VS Code sends it; a hand-rolled CDP client must too). This closes the track’s two open questions (the wrangler inspector port and bundle map composition). No language surface; the one-click VS Code launch is next (slice 4).
v0.70Per-statement source maps in handlers and tests (tooling; debugging track; ADR 0103). v0.68 mapped free-function bodies and declarations; the bodies that lower through a spliced buffer — service/agent/provider handlers and test-case bodies — stayed at declaration granularity, so a worker breakpoint landed on the service line, not the statement. Those bodies now map per-statement: the source-map builder gains a line-anchored merge that rebases each spliced body’s checkpoints, and test modules gain multi-source maps (a test group can span several .bynk files). A breakpoint on a handler or test-body statement now resolves to that exact .bynk line — under Node (bynkc test --inspect, v0.69) and, once composed through the wrangler/esbuild bundle, under workerd. No emitted-TypeScript change — only the .ts.map contents are richer; test modules now carry a map where they had none. This is the prerequisite that makes the upcoming bynk dev --inspect (workerd debugging) land per-statement.
v0.69Debug your tests under Node — bynkc test --inspect (tooling; debugging track, slice 2; ADR 0104). bynkc test --inspect compiles a debug build and launches the emitted test runner under Node’s inspector (node --inspect-brk), printing the inspector URL for a JavaScript debugger to attach. A breakpoint set in a .bynk source binds and pauses there, resolved through the v0.68 source maps. The trick: the debug build emits .ts import specifiers and runs the emitted .ts directly under Node’s line-preserving type-stripping (Node ≥ 22.6) — no tsc, so the .ts.map applies to the running file with no source-map chaining. bynkc test’s output now carries the .ts.map siblings on disk, and the emitted AssertionError is strip-clean (explicit field assignment, no TS parameter properties). Production-code breakpoints reached through a test work today; breakpoints inside test bodies and the one-click VS Code experience follow (the test-body/handler-body source maps and the extension DebugConfigurationProvider are noted follow-ons / slice 4). No language surface.
v0.68Source maps — the step-debugging foundation (tooling; debugging track, slice 1; ADR 0103). The emitter now carries the source spans the AST already holds through to a source-map builder, and write_output emits a sibling <file>.ts.map (source-map v3) plus a //# sourceMappingURL trailer for every .bynk-sourced .ts. Maps are line-level and statement-anchored: each generated line maps back to its enclosing source statement, so the lowered expansion of ? (temp / Err-guard / unwrap) and match (per-arm case/binding/return) collapses to one source step under a source-map-aware debugger — the granularity the slice-0 spike ratified. sourcesContent embeds the .bynk for local fidelity; generated glue (runtime, worker entry, wrangler.toml, package.json) carries no map. No emitted-TypeScript change beyond the trailer — every golden is byte-identical. This is the foundation only; the VS Code debugger attach (Node + workerd) follows in slices 2–4.
v0.67Pre-execution test discovery (tooling; ADR 0098). bynkc test --no-run --format json emits a discovery document — every suite and case with its source location — without running the suite, built from the same names and spans the runner emits so a discovery document reconciles cleanly against a later run. A VS Code Test Explorer can populate its tree before the first run.
v0.66bynk links the compiler in-process — the crate-decomposition finale (internal re-architecture; crate-decomposition track, slice 7; ADR 0101, amends 0084). The bynk driver now links the compiler pipeline (bynk-emit::compile_project) instead of shelling the bynkc binary, and renders diagnostics in-process via bynk-render. It drops its dependency on the bynkc crate entirely (the NODE_MAJOR_FLOOR constant moved to bynk-emit). The win: a fresh cargo install bynk is self-contained — bynk dev no longer needs a separately-installed, version-matched bynkc on PATH. bynkc survives as the thin compile/check/fmt/test binary for CI and cargo install bynkc. bynk doctor is amended (ADR 0084): the compile capability is now “in-process — always available”, and the external-bynkc resolution + version-skew check narrows to the BYNK_BYNKC override path (a power user pointing bynk at an external compiler) — so doctor stops reporting a skew failure mode that no longer exists for normal use. This completes the crate decomposition: bynkc is fully split into bynk-syntax/-render/-fmt/-check/-emit/-ide, with bynk, bynkc, and bynk-lsp as front-ends over the library set. No language surface; bynk dev behaves identically (same build output, same diagnostics).
v0.65bynk-render — diagnostic rendering becomes a shared crate (internal re-architecture; crate-decomposition track, slice 6; ADR 0100). The diagnostic renderers — ariadne human output and the short/json-feeding line forms over CompileError — move down out of bynkc into a new published crate, bynk-render, which depends on the bynk-syntax leaf only (plus ariadne). This is the structured-data/rendering split: the library crates emit structured diagnostics, and one shared presentation layer renders them, so every front-end renders identically. The AttributedError → CompileError flattening (project-failure attribution) stays in bynkc and delegates to bynk-render, so there is no render → emit dependency cycle. No behaviour change — every committed diagnostic transcript and golden-error fixture renders byte-identical.
v0.64bynk-ide — the language server stops linking the compiler (internal re-architecture; crate-decomposition track, slice 5; ADRs 0099/0102). The IDE/LSP analysis surface (the non-bailing single-file and whole-project diagnostics) moves down out of bynkc into a new published crate, bynk-ide, over bynk-syntax + bynk-check + bynk-emit. The language server bynk-lsp now links bynk-ide + the analysis libraries directly and drops its dependency on bynkc entirely — it no longer pulls in the CLI, the bynkc test JSON surface, or the ariadne renderer it never used. This closes the track’s original motivation (the editor server shouldn’t link the whole compiler binary’s crate). The Severity classification moved into the bynk-syntax leaf (shared by the IDE diagnose path and the short/json renderers). No behaviour change and no language/LSP surface change — the full bynk-lsp suite and the index/diagnose drift tests pass byte-identical.
v0.63bynk-emit — build orchestration + TS emission becomes its own crate (internal re-architecture; crate-decomposition track, slice 4; ADRs 0099/0102). The emitter (Bynk → TypeScript lowering) and the project driver (discovery, the dependency graph, validation, symbols, paths, and compile_project) move down out of bynkc into a new published crate, bynk-emit, over bynk-syntax + bynk-check. bynkc is now just the CLI surface plus the thin compile/diagnose glue over the library set. The line_col source utility moved into the bynk-syntax leaf (shared by the emitter and by bynkc’s diagnostic rendering). No behaviour change and no language/CLI surface change — golden emission, tsc-verification of the embedded runtime, the end-to-end fixtures, and every project-form test pass byte-identical.
v0.62bynk-check — the semantic-analysis layer becomes its own crate (internal re-architecture; crate-decomposition track, slice 3; ADRs 0099/0102). Name resolution, type checking, the kernel-method and builtin registries, the first-party embedded sources, actor analysis, and the captured analysis tables (binding index, inlay hints, expression types, locals) move down out of bynkc into a new published crate, bynk-check, over the bynk-syntax leaf. bynkc keeps the emitter, project orchestration, and the CLI, and re-exports bynk-check’s modules so its public API is unchanged. The largest decomposition step so far — bynkc is now a thin emit/driver layer over bynk-syntax → bynk-check. No behaviour change and no language/CLI surface change — the whole suite (golden emission + every analysis/index drift test) passes byte-identical; the only difference is the crate boundary.
v0.61bynk-fmt becomes a real leaf — the formatter stops linking the compiler (internal re-architecture; crate-decomposition track, slice 2; ADR 0099). The formatter implementation moves down out of bynkc into the bynk-fmt crate, which now depends on the bynk-syntax leaf only — previously bynk-fmt was a one-line façade over a bynkc dependency, so anything using it linked the entire compiler. Formatting is an AST-walk, so it needs syntax, not the checker or emitter; its dependency tree is now bynk-syntax alone. bynkc re-exports the formatter as bynkc::fmt, so its fmt command and existing consumers are unchanged. No behaviour change and no language/CLI surface change — the formatter’s golden + round-trip suites pass byte-identical.
v0.60bynk-syntax — the compiler’s syntax foundation becomes its own crate (internal re-architecture; crate-decomposition track, slice 1; ADRs 0099/0102). The lexer, parser, AST, spans, keywords, the CompileError type, and the diagnostic-code registry move down out of bynkc into a new published leaf crate, bynk-syntax, which bynkc now depends on and re-exports. This is the first step of slimming bynkc from a monolith toward a layered library set, so the layers that don’t need the whole compiler (the formatter, the LSP) can stop linking it. No behaviour change and no language/CLI surface change — the whole test suite (including golden emission fixtures) passes unchanged; the only difference is the crate boundary.
v0.59bynkc test --format json and a VS Code Test Explorer (tooling; proposal v0.59). bynkc test gains a --format selector (rich default
v0.58bynk new — scaffold a new, runnable project (driver tooling; proposal v0.58, ADR 0097; the first step of the doctor → new → dev arc, shipping after dev). The driver gains its third command: bynk new <path> writes a complete, runnable single-context HTTP service — bynk.toml, .gitignore, and src/<name>.bynk — chosen so bynk dev serves it unmodified. That end-to-end loop is the unlock: not “write a config file for me” but “hand me a running service to start editing”. Unlike dev, new needs no toolchain — it shells nothing, compiles nothing, and reads no network (pure std::fs file-writing), so it works before bynkc, Node, or wrangler are installed, which is exactly why it can be the true first command. The starter, manifest, and .gitignore are embedded via include_str! (the ADR 0086 first-party precedent) with the name substituted at write time; a standing test renders the starter with a non-default name and asserts it compiles and is bynk-fmt-clean, so the scaffold can’t rot. The project name is validated by the real lexer (a dash, dot, leading digit, or reserved keyword is rejected) and used for both [project] name and the context — a non-identifier directory like my-app fails with a fix-it naming --name rather than mangling silently. new never overwrites: a non-empty target is refused (touching nothing), though an empty dir — and one holding only VCS/OS cruft like .git/.DS_Store — is fine; it writes a .gitignore covering just /.bynk and does not run git init. No language surface — driver tooling only. With this the doctor → new → dev on-ramp is complete. Deferred as named follow-ups: init (scaffold in place), --template (a second project shape), and in-project generators.
v0.57bynk dev — build + serve a project locally in one step (driver tooling; proposal v0.57, third step of the doctor → new → dev arc). The driver gains its second command: bynk dev collapses the manual bynkc compile + cd + wrangler dev recipe into one command runnable from anywhere inside a project. It locates the project root, pre-flights the deploy capability (reusing doctor’s Node + wrangler gate and remedy text), compiles to a managed, gitignored .bynk/dev/ build dir (the workers/ tree cleared each build so a stale context can’t trip selection), selects the worker (one context served automatically; --context chooses among several; ambiguous lists them), and runs wrangler dev from inside it. The unlock: local dev needs no provisioningwrangler dev runs in local mode (Miniflare), simulating KV / Durable Objects / queues by binding name, so the generated wrangler.toml is served untouched. The driver owns one flag (--context) and forwards everything after -- to wrangler verbatim (so -- --port, -- --var KEY:VALUE for local secrets); there is no --remote and no provisioning — those are deploy’s problem. No language surface — driver tooling only. Deferred as named follow-ups: the watch / incremental-recompile loop and multi-worker local dev.
v0.56The Karn → Bynk rename (release; no language surface). The toolchain, driver, manifest (bynk.toml), and first-party surface adopt the Bynk name end-to-end; emitted output and behaviour are otherwise unchanged.
v0.55The LSP & editor experience — a completion overhaul, plus navigation, docs, and polish (LSP tooling track; ADRs 0093/0094/0095). A tooling release: the compiler/runtime language surface is unchanged (the only emitter change is additive JSDoc in the generated first-party modules). Completion is rebuilt against a canonical cursor-context × candidate-kind surface contract (ADR 0093) — coverage-tested so it can’t silently narrow as the language grows. . is now a trigger character; the name-receiver context offers the full built-in statics (List.empty/Map.empty/Effect.pure) and the built-in HttpResult/QueueResult variants; expression position offers the value constructors, in-scope type names, and free functions (the current unit’s own fns plus uses-imported stdlib/project combinators); and receiver typing is error-tolerant — Analyse mode records best-effort partial expr_types (ADR 0094), so value-member completion and signature help survive an unrelated type error elsewhere in the file (Build stays Ok-only — codegen untouched). completionItem/resolve fills hover-quality docs lazily on the focused item, and capability-op detail renders typed signatures. Docs: the embedded first-party sources (the bynk surface, the bynk.list/map/string stdlib, bynk.cloudflare) carry --- doc blocks that surface in hover/completion and emit as JSDoc. Navigation: go-to-type-definition (a value → its type’s declaration); document links and go-to-definition on uses/consumes unit names, via a new unit_sources map the analysis now exposes (ADR 0095). Polish: per-kind inlay-hint granularity and a [bynk] default-formatter (so format-on-save works out of the box). typeHierarchy was assessed and declined — an OO feature Bynk’s type model doesn’t fit.
v0.54Actors, slice 6 — cross-context Caller value (ADR 0092; actors feature track). The deferred runtime half of Q7: a cross-context on call … by c: Caller (…) handler now reads a live CallerId — the calling context’s qualified name — instead of the undefined placeholder. The call side (callService) stamps the caller’s name (a compile-time constant) into a reserved X-Bynk-Caller header beside the unchanged args body; the callee’s /_bynk/call/ dispatch reads it, threads it into deps, and c.identity lowers to it (mirroring the Bearer identity path). An absent/empty caller on a by c: Caller handler is fail-closed (the internal analogue of 401); a binder-less on call reads no header and is byte-unchanged. Trust is static / channel-based — no crypto (the Internal Service-Binding channel is the assertion, not externally routable, first-party); this mints identity, not authorisation. A standing behavioral test (bynkc/tests/cross_context_caller.rs) drives the callee with and without the header (live id vs 401). With this slice the actors track’s planned Q1–Q7 scope is complete; Q8 (replay/ordering) is the only remaining item and is cross-track (Events). Scope is the caller-name value; signed caller headers, inter-context authorisation, and a structured CallerId are later. Non-Caller output is unchanged.
v0.53Actors, slice 5 — authorisation invariants (ADR 0091; actors feature track). The reserved refinement form is admitted: actor Admin = User where hasClaim("admin") declares an authorisation invariant — an Admin is a User who additionally satisfies a claim predicate. A handler by a: Admin makes the compiler emit, at the boundary: verify the User (Bearer) scheme (failure → 401), check the predicate against the verified JWT claims (failure → 403), then mint the identity and run the body — completing the 401/403 split (who you are vs whether you may) as structurally distinct response channels. The predicate is a closed claim-predicate sethasClaim("name") (present and truthy) and claimEquals("name", "value") (string equality), composed with &&/`
v0.52Actors, slice 4 — multi-actor sum dispatch (ADR 0090; actors feature track). A by clause may now name an ordered sum of peer actorson GET("/notes/:id") by who: User | Visitor (id: String) — resolved first-wins, the body matching on the resolved actor. The track’s novel construct: it composes the three landed schemes (None/Bearer/Signature) rather than adding one. The boundary tries each peer’s scheme in declared order and binds the first that verifies (Bearer against the Authorization header, Signature against the raw body, None unconditionally); the body matches the resolved actor, each arm binding that actor’s identity directly (User(u)u is the UserId; a unit-identity peer like Visitor binds nothing). A single boundary wrapper owns the whole boundary, so a mixed User | Webhook route reads the raw body once, verifies, and parses from the same bytes — composing a header member with a body member never re-reads or re-serialises. Total verification failure is fail-closed → 401. Static rules: a sum must bind the resolved actor (bynk.actor.sum_requires_binder); members are peer base actors — no refinement member (bynk.actor.refinement_in_sum); no two members share a scheme (bynk.actor.duplicate_sum_scheme); a None catch-all (Visitor) must come last (bynk.actor.unreachable_sum_arm); a sum is HTTP-only and every member admissible (bynk.actor.scheme_not_admissible); the body match must be exhaustive (bynk.types.non_exhaustive_match). A standing behavioral test (bynkc/tests/multi_actor_sum.rs) drives the emitted resolution — first-wins, fall-through on an invalid earlier member, and fail-closed-total. Scope is scheme-level peer keying; same-scheme multi-provider (concrete-verification keying), refinement members + the 403 path, and internal-channel sums are later. Single-actor and non-sum HTTP output is unchanged.
v0.51Actors, slice 3 — Signature (ADR 0089; actors feature track). The second authenticated scheme, for inbound webhooks. actor Webhook { auth = Signature(secret = "WEBHOOK_SECRET", header = "X-Signature", timestamp = "X-Timestamp", tolerance = 300) } consumed on a binder-less by Webhook (body: Event) clause makes the compiler emit — at the boundary, before the body runs, fail-closed — the code that recomputes HMAC-SHA256 over the raw request body (WebCrypto, constant-time crypto.subtle.verify; verifySignatureHmacSha256) and compares against the configured signature header (accepting a bare hex digest or a sha256=<hex> prefix, the GitHub shape), and — when a timestamp is configured — verifies the signed timestamp is within tolerance seconds of now (binding <timestamp>.<body> as the signed string), a replay window. Any failure → 401 (HttpResult.Unauthorized); the body never runs. The seam reads the body once as text, verifies over those exact bytes, then deserialises the body param from the same text — never a re-read or a re-serialisation (the byte-fragile webhook footgun). No app-written HMAC. A Signature actor must name its secret (bynk.actor.signature_missing_secret) and header (bynk.actor.signature_missing_header), takes no identity (bynk.actor.signature_identity_unsupported), a tolerance requires a timestamp (bynk.actor.signature_tolerance_without_timestamp), and a handler MUST take a body (bynk.actor.signature_requires_body); Signature is HTTP-only (bynk.actor.scheme_not_admissible). The scheme config generalises to keyed args (Scheme(key = value, …), string- or integer-valued). A standing behavioral bypass-class test (bynkc/tests/signature_auth.rs, parallel to bearer_auth.rs) signs bodies with WebCrypto and asserts every class fails closed. Scope is canonical HMAC-SHA256 + a configurable header; provider presets (Stripe’s compound format), replay dedup (Idempotency), and asymmetric signatures are later. Non-Signature HTTP output is byte-identical.
v0.50The actor by binder is optional (ADR 0088, amends 0082). A handler that doesn’t consume the identity drops the dead binder: on GET("/ping") by Visitor () -> … instead of by v: Visitor. by <name>: <Actor> still captures the identity (name.identity). This is a ceremony reduction, not a return to ambient authority — the actor and scheme are still declared explicitly at the boundary and verified before the body; you decline to capture an identity you don’t use. Applies to all schemes: by User (Bearer, binder-less) is a legitimate verify-and-discard gate — the token is verified fail-closed, the identity simply not minted. _ is not admitted as a binder (by _: Actor is rejected pointing at the binder-less form); HTTP still requires a by clause. One grammar tweak (optional binder, one-token : lookahead); existing by <name>: <Actor> handlers emit byte-identically; the docs adopt the terser form.
v0.49Security CI hardening (ADR 0087; CI/tests only — no language surface). The emitted Bearer verifier gains a standing behavioral regression guard: bynkc/tests/bearer_auth.rs imports the runtime and feeds it crafted JWTs, asserting every bypass class fails closed (tampered signature, alg:none, algorithm confusion, expired, nbf-future, malformed exp, missing/empty sub, malformed token) and the accept path mints the sub — the durable guard the one-time /security-review can’t be (a future change that reopens a bypass fails here). CodeQL (SAST) runs as a committed, SHA-pinned workflow over javascript-typescript (the extension + the now-real runtime.ts) and rust, reporting to the Security tab (not a hard PR gate); it satisfies the OpenSSF Scorecard SAST check. npm audit joins cargo audit as a required gate (the JS packages). Secret scanning is GitHub-native push protection (a repo setting). Builds on v0.48’s relocation, which made runtime.ts a real file SAST and the auth tests can see.
v0.48First-party sources as testable files (ADR 0086; internal refactor — byte-identical emitted output). The first-party Bynk surface/adapters, the Bynk-written collection/string commons, the per-platform TypeScript bindings, and the emitted runtime move from Rust r#"…"# string literals into real .bynk/.ts files under a package-shaped bynkc/src/firstparty/ tree, each embedded at compile time via include_str! (same &'static str, no call-site change). They are now visible to the compiler’s own pipeline, bynk-fmt, bynk-lsp, tsc, and editors. The stale 39-line bynkc/runtime/runtime.ts stub is deleted — the live runtime (which now carries the v0.47 Bearer JWT verifier) has one source of truth. New standing checks: each .bynk source must parse and be bynk-fmt-clean, and the embedded runtime.ts passes tsc --strict standalone. Vendored, not published (the bindings/runtime are part of the emit ABI; publishing is a future ADR). No language surface; every emitter golden, tsc_verify, and runtime_helpers test stays green and unedited. Unblocks the v0.49 security tooling (the now-real runtime.ts is a real import target for auth-bypass tests and visible to SAST).
v0.47Actors, slice 2 — BearerToken (ADR 0085; actors feature track). The first authenticated scheme. actor User { auth = Bearer(secret = "AUTH_JWT_SECRET"), identity = UserId } consumed on a by clause makes the compiler emit — at the boundary, before the body runs, fail-closed — the code that extracts Authorization: Bearer …, HS256-verifies the JWT (WebCrypto, constant-time; alg: none/confusion rejected) against a secret sourced from the env the Secrets capability reads, enforces exp/nbf, and mints u.identity : UserId from the sub claim through the identity type’s refinement. Any failure → 401 (HttpResult.Unauthorized); the raw token never reaches the body. No app-written crypto. A Bearer actor must name its secret (bynk.actor.bearer_missing_secret) and declare a string-constructible identity (bynk.actor.bearer_identity_not_string_constructible); Bearer is HTTP-only (bynk.actor.scheme_not_admissible). This is the first real (non-unit) minted identity — it threads through the handler’s deps, so <binder>.identity reads the verified value (resolving the v0.45 lowering note). Scope is HS256 only; RS256/JWKS, opaque-token lookup, the 403 authorisation-invariant split (Q3), and multi-actor sums (Q4) are later slices. Non-Bearer HTTP output is byte-identical.
v0.46bynk doctor — a first-class environment check, and the bynk driver it stands up (ADRs 0083–0084). A new bynk binary — a thin orchestrator over bynkc and the Node toolchain, as cargo is to rustc — ships its first command, bynk doctor: an upfront check that answers given what you want to do with Bynk, is your machine ready, and if not, what do you run? Probes are grouped by capability (compile/check/fmt · bynk test = Node + tsc/tsx · dev/deploy = Node + wrangler · editor bynkc-lsp · build-from-source), each reporting presence + version + provenance (global PATH vs project-local node_modules/.bin vs npx fetch-on-demand — which is reported as provisionable, never a green “ok”). It also flags driver↔compiler version skew (a global bynk shelling a stale bynkc). The exit contract: bare bynk doctor is informational (exits 0 unless bynkc itself is unusable); --only <capability> gates on one capability; --strict turns every warning into a failure, for CI. Output is a grouped table by default, with --format short and --format json as the pinned scriptable surface. Detection is portable (the which crate, not Unix-only). No language surface — no grammar/checker/emitter change. new and dev follow in later slices.
v0.45Actors, slice 1 — foundations (ADRs 0080–0082; actors feature track). The boundary contract becomes a typed, first-class thing. An actor declaration is a nominal contract on a closed, compiler-known authentication scheme — actor Visitor { auth = None }, actor Backend { auth = Internal }, optionally , identity = T — and a handler consumes one on a by <name>: <Actor> clause sitting after the protocol config: on schedule("*/5 * * * *") by s: Scheduler () -> …. The verified identity binds to <name> and reads as <name>.identity (a context-sealed value — minted at the boundary, threaded service→agent, never re-checked; ADR 0081). Per-protocol default actors (ADR 0082): omit by and a handler inherits its protocol’s default — cron→Scheduler, queue→Producer, on callCaller, all Internal — but HTTP has no safe default, so by is required there (bynk.actor.missing_by_on_http); a public route writes by v: Visitor explicitly. This slice builds the whole machine — declaration, by clause, identity binding, contract checking, the verification seam, per-protocol defaults — against the two zero-crypto schemes only (None, Internal); Bearer/Signature and the refinement form actor Admin = User where … are reserved-and-rejected with fix-its (ADR 0080). The verification seam reuses the channel trust already implicit in service-binding/platform dispatch, so emission has no topology changeNone admits, Internal is the existing structural trust, actors emit nothing. All in-repo HTTP handlers migrate to by v: Visitor. The bynk.actor.* diagnostics, the binding/semantic-token (a new actor token) indices, and hover/document-symbols cover the new surface. Authenticated identity values (Bearer/Signature, the live calling-context payload) arrive in later slices — Foundations wires the typed machinery.
v0.44Service protocol on the header (ADRs 0077–0079) — the protocol moves from the per-handler keyword to the service header: service api from http { … }, one protocol per service. HTTP handlers become method-builders (on GET("/notes/:id") by v: Visitor (id: String)), cron on schedule("*/5 * * * *"), queue from queue("name") { on message(m: T) }. A from-less service is the contract-mediated default and admits only on call; mixing a wire protocol with on call, an unknown protocol (from kafka → use from queue), or a handler form that doesn’t match the header are diagnosed (bynk.service.{mixed_protocols,missing_from,unknown_protocol}). QueueResult (ADR 0078) — queue handlers return Effect[QueueResult] (Ack/Retry, non-generic; Retry carries a reason), the runtime routing on the verdict instead of overloading Result[(), E]; the agency rule (a protocol earns a verdict type iff the handler makes a dispatch decision) is why cron keeps Result[(), E]. Protocols are a closed set (ADR 0079); the three handler productions collapse to one protocol descriptor, and the protocol keyword is reserved for an openable-later seam. A behaviour-changing surface move with no emitted-target change — HTTP/cron Worker output is byte-identical; only the queue verdict mapping is renamed. All in-repo fixtures/examples migrate; the old on http/on cron/on queue forms are removed.
v0.43String interpolation (ADR 0075, #45) — string literals gain \(expr) holes, so "Hello, ".concat(subject).concat("!") becomes "Hello, \(subject)!" (the headline line of examples/hello-world). \( was an invalid escape, so the syntax is backward-compatible (\\( escapes a literal \(); ${…} was rejected as it would silently re-mean existing literals. A hole holds a full expression and must type to a base scalarString/Int/Float/Bool — or a refinement of one (which widens to its base, so Subject displays as its String); every other type is a static error (bynk.types.interpolation_non_scalar) — map it to a String first, foreclosing JS’s [object Object]. Emits a TS template literal (`Hello, ${String(subject)}!`); a plain string with no holes stays a StrLit, so existing code is untouched. Delivered in three slices under v0.43.0: slice 1 (core: lexer/AST/parser/checker/emitter), slice 2 (surface tooling: bynk-fmt round-trip, the tree-sitter grammar, and the TextMate grammar so editors highlight holes), and slice 3 (LSP: go-to-definition, references, hover, semantic tokens, and Type./Cap.-member completion all reach inside holes — these fall out of the binding index and expr_types, which recurse into hole expressions, so the slice is verification + regression tests).
v0.42Numeric toString (ADR 0074, #44) — i.toString() / f.toString() render an Int or Float as a String (the missing direction; Int.parse covered parsing). The most common wall after hello-world — displaying a counter, timestamp, or measurement — is gone. Emits String(n); the Float contract is the host’s number→string (ECMAScript Number::toString, shortest round-trip), pinned normatively like the v0.22a string kernel. Added at the established numeric-kernel extension point (checker dispatch + registry + emitter). Unified test-file naming (#47): split-paths mode now also accepts the self-identifying <target>.test.bynk form (single-tree mode already used it) — previously the suffix was rejected by bynk.project.inconsistent_test_path, a trap for anyone copying a fixture. Both forms align; the rule is stated once in the project-layout guide.
v0.41Maintenance & fixes. Release automation (#142, #65): a version-tag push now publishes crates.io + npm automatically (OIDC, re-run-safe), retiring the manual phase-2 dispatch. Fix: status bar “no project” for a nested bynk.toml (#77): the extension’s findBynkToml now walks upward from the active .bynk file to the nearest bynk.toml (mirroring the LSP’s find_project_root), then falls back to workspace-folder roots — so a project below the opened folder (e.g. examples/hello-world/) is recognised. Fix: bynkc check / bynkc compile rooting (#46): both now honour a bynk.toml / src/ layout the same way bynkc test already did (a shared project_options rooting helper), so bynkc check . from a conventional project root works instead of erroring on src/-prefixed paths. Prescriptive hint for .raw on a refined value (#48): field access on a refined type (subject.raw) now adds a note — a refined value is usable wherever its base type is expected; pass it directly (.raw is for opaque types) — plus a machine-applicable “remove .raw quick-fix when that’s what was written.
v0.40.1Fix: clicking the N references CodeLens (#143) — the reference-count lens rendered but clicking it threw “argument does not match one of these constraints…”. The lens carries the built-in editor.action.showReferences command, whose arguments the server sends as plain LSP JSON; VS Code validates them with instanceof, so the plain objects were rejected. Added a provideCodeLenses client middleware (vscode-bynk) that re-hydrates the [uri, position, locations] arguments into real vscode.Uri / Position / Location[] instances. Extension-only; no server change.
v0.40InRange-swap quick-fix (ADR 0073) — an inverted refinement bound (Int where InRange(120, 0), bynk.types.inverted_range) now offers a one-click code action that swaps the bounds in place (InRange(0, 120)). Works for ints and floats (float lexemes preserved). Backed by a small AST change: each InRange bound records its source span (a new value-only IntBound, and a span on FloatBound) — so the formatter stays byte-stable and the ~20 internal readers became mechanical .value accesses, behaviourally inert (e2e + bynk-fmt idempotence fixtures guard it). No language change.
v0.39.1Generic-instantiation inlay hints (ADR 0072, richer-hints slice 2) — completes the richer-hints work. At a generic call the user wrote without type arguments, the inferred ones now show after the function name (identity[Int](5)), reusing the slice-1 HintKind discriminator. Recorded at the end of check_generic_call from the ground substitution, in type-parameter declaration order; shown only when the call omitted the arguments (an explicit identity[Int](5) gets none) and every type variable resolved. No language change.
v0.39Parameter-name inlay hints (ADR 0072, richer-hints slice 1) — inlay hints gain the callee’s parameter name before each call argument (area(width: w, height: 3)), alongside the v0.27 inferred-type hints. Recorded by the checker at the free-fn, generic, method, and cross-context op/service argument loops, behind the existing bynk.inlayHints.enable toggle. Suppressed when it would be noise — the _/self placeholders, or an argument that is the identically-named identifier (f(count) for parameter count). Driven by a new HintKind discriminator on the hint sink (Type anchors after a name, Parameter anchors before an argument with trailing padding). Generic-instantiation hints (identity[Int]) follow in slice 2. No language change.
v0.38.1Project build task + problem-matcher (ADR 0071, B-2 slice 2) — completes the extension polish. bynkc check --format short emits one terse path:line:col: severity[category]: message line per diagnostic (the rich ariadne rendering stays the default); the extension contributes a $bynkc problem-matcher and a bynkc: check build task (a TaskProvider running bynkc check . --format short, compiler resolved from a new bynk.compilerPath setting else PATH), so a whole-project type-check routes errors — including in unopened files — into the Problems panel. The terse format has a bynkc test pinning the line shape. No language change.
v0.38Extension authoring affordances (ADR 0071, B-2 slice 1) — the VS Code extension gains snippets for every construct (context/commons/type/enum/fn/capability/provides/service/on http/on cron/agent, bodies mirroring the worked examples), scaffolding commands (Bynk: New Project writes bynk.toml + a starter context; Bynk: New Context adds a context file — both refuse to overwrite), and a Get Started with Bynk walkthrough. Extension-only — no LSP-protocol or compiler change; validated by the existing tsc + esbuild + bundle-guard + vsce package gate. The bynkc problem-matcher + build task (backed by a terse bynkc check --format short) is slice 2. No language change.
v0.37Folding & selection ranges (ADR 0070) — textDocument/foldingRange collapses the structural constructs (contexts/commons, type bodies, service/agent handlers, fn/handler blocks, match & arms, if, blocks, record/list literals) plus multi-line comment runs; textDocument/selectionRange gives smart expand-selection (cursor → expression → block → declaration → file). Both are structural — served from the per-file recovered AST via one shared span visitor, no binding-index or analysis dependency, so they work even when the project doesn’t check. AST-driven (no tree-sitter), consistent with the other structural providers; comment-run folding rides the lexer’s comment-token spans. Clause-list and per-statement folding deferred. No language change.
v0.36.1Record fields & capability ops in the index (ADR 0069, members slice 2) — completes member indexing. Record fields ("Type.field") and capability operations ("Cap.op") become first-class index symbols, so go-to-definition, references, rename, and semantic-token colouring extend to them. Fields are recorded from every reference form — read access, construction labels, and spread overrides — so rename is complete; ops from their calls (local and cross-context, the latter recorded already-qualified into the providing unit). Fields colour as property, ops reuse method. Capability-op call-graph edges are out of scope (call hierarchy stays fn/method). Locals (rename) and generic params remain deferred. No language change.
v0.36Methods in the binding index (ADR 0069, members slice 1) — instance methods become first-class index symbols, so go-to-definition, references, rename, semantic-token colouring, and call-hierarchy method edges all extend to them. Methods are keyed by a compound "Type.method" name: the def is registered at the walk, and a method call is recorded already-spelled from the receiver type the checker resolved, then qualified through the same uses/consumes path as a cross-file type reference (so a same-named method on two types stays distinct). Rename edits the member segment only (never the Type. prefix). Record fields and capability ops are slice 2; locals (rename) and generic params remain deferred. No language change.
v0.35Implementation navigation (ADR 0068) — textDocument/implementation on a capability jumps to the provider(s) that implement it (the Bynk analogue of “go to implementations” on an interface). Like v0.34, the link was already collected by the index — a provides Cap = Provider clause records a capability reference whose enclosing owner is the provider — so it falls out of the v0.34 owner resolution as an ImplEdge side table, no new analysis. A provides-flag distinguishes the provided capability from the provider’s own given deps (also capability refs owned by the same provider). External providers land on the Bynk provides declaration, not the off-tree .binding.ts; the reverse (provider → capability) is already goto-definition. textDocument/typeDefinition (value→type, consumed-context → source) is deferred. No language change.
v0.34Call hierarchy (ADR 0067) — prepareCallHierarchy + incoming/outgoing calls. “Who calls this fn / what does this fn call”, with peekable call sites. The caller→callee attribution it needs was already collected by the binding index (every RefEdge carries its enclosing declaration) and dropped at assembly; this preserves it — resolved to the caller’s SymbolKey — as a CallEdge side table, no new analysis. Callees are Fn only and any indexed owner may be a caller (a service handler that calls a free fn shows the service as a caller); method/op/dispatch edges are deferred with the index kinds. Served from the cached analysis round. The other half of A-3 — type-definition / implementation navigation — is a separate increment. No language change.
v0.33CodeLens reference counts (ADR 0066) — a "{n} reference(s)" lens above each top-level definition (types, free fns, capabilities, services, agents, providers), clickable to peek the references (editor.action.showReferences, no extension support needed). It falls straight out of the binding index — the count is refs.len() per symbol, served from the cached analysis round; "0 references" is shown (a dead-code signal). Locals/methods/fields aren’t indexed and get no lens. The test-run lens (”▶ Run”) — which needs test discovery + a run command — is deferred. No language change.
v0.32.1Signature help for value receivers (ADR 0065) — completes signature help. A typed value-receiver method call (xs.fold(, s.split(, o.map() now shows its kernel-method signature with the active parameter highlighted: the receiver is typed by re-analysing the buffer rewritten so it parses (the .method(args dropped), type_at_offset → the type, then the kernel_methods registry signature — the same machinery value-member completion uses, factored into a shared type_receiver helper. Carries the clean-file ceiling (no help when the file doesn’t check). With this, signature help covers both name callees (v0.32) and value-receiver kernel methods. No language change.
v0.32Signature help (ADR 0065) — completion’s partner. While typing a call’s arguments, textDocument/signatureHelp shows the callee’s Bynk-syntax signature with the active parameter highlighted. Context detection is lexical — the innermost unclosed ( before the cursor, the callee before it, and the active parameter from a bracket-aware top-level comma count (so `f(g(x
v0.31.2Locals completion (ADR 0064) — the final locals slice, and the long-deferred completion slice 4. In-scope local bindings (let/let <-, fn/handler/lambda params) are now offered at keyword position (alongside the reserved keywords + snippets) and at expression position — after =/(/,, a => lambda arrow, or a binary operator — each as a variable item with its inferred type as detail. Sourced from the cached analysis (the last good round’s bindings around the cursor), so they survive the mid-edit buffer the keystroke produced — positions convert against the cached snapshot. Detection is conservative (the type arrow -> is excluded; locals are appended to a specific context’s results only at keyword position, never to type/member completion). Completes the comprehensive-completion arc (positional → name-member → value-member → locals) and lifts the recurring deferral across references/rename, semantic tokens, and completion. Match-arm/is bindings and a parameter-token split remain later refinements. No language change.
v0.31.1Locals semantic tokens (ADR 0064) — local bindings and their uses now colour (the variable token, a standard LSP type VS Code themes by default — no extension declaration needed). The frozen semantic-token legend (ADR 0057) gains variable appended at index 6 (never reordered — the legend test pins it); the producer merges local-binding occurrences (def carries the declaration modifier) into the same sorted token stream as the index symbols, disjoint because locals are never top-level. Occurrences come from a pure lexer scan over the snapshot (locals_nav::local_token_sites), precomputed by the handler and passed in so the producer stays free of that dependency (the #[path]-include test trap). Param-vs-let distinction (parameter token) is a later refinement; match-arm/is bindings stay deferred. No language change.
v0.31Locals navigation (ADR 0064) — the first slice of the recurring deferral: local bindings (let/let <-, fn/handler/lambda params) now resolve for references, go-to-definition, and document-highlight. A LocalsSink (mirroring the v0.27 inlay-hint sink) records each binding at its checker site with its lexical scope range — taken from the enclosing block/body span the checker already has (let: [stmt end .. block end]; params: the body span), so nesting falls out of the checker’s recursive block-checking and shadowing resolves in the query (latest in-scope def wins). Homed per-file on the analysis (ProjectAnalysis.locals, parallel to hints/expr_types) — locals are file-local, so not in the cross-file index. Use sites are recovered in the LSP by a pure lexer scan over the snapshot (identifier tokens of the name within scope that resolve back to the binding — shadowing-safe, def-tokens excluded), so the checker change is the binding sites only. References/definition/highlight try the index first, then fall back to the scope-correct locals resolver. (Match-arm pattern bindings and is-narrowing bindings have subtler scopes and are deferred.) Semantic-token colouring and expression-position completion for locals follow as later slices, reading the same FileLocals. No language change.
v0.30.2Value-receiver .-member completion (ADR 0063) — completion slice 3, the daily-driver tier. After a lowercase receiver., offer the kernel methods of the receiver’s type (xs.fold/xs.get, s.split/s.trim, o.map/o.getOrElse, i.abs/f.round) plus, for a record, its fields (order.total). Built on a completed feasibility spike’s three pieces: (1) expr_types retained to the analysis via an ExprTypeSink (mirroring the v0.27 inlay-hint sink) — captured on the Ok path, so a file that fails to check records nothing (the clean-file ceiling: graceful degradation, offer nothing over wrong); (2) a rewrite-on-trigger — a bare mid-edit x. doesn’t parse and loses the receiver, so the LSP drops the trailing .partial, re-analyses, and types the receiver via a new type_at_offset query; (3) enumerable kernel registries (bynkc::kernel_methods) listing each kernel’s methods + a methods_for(Ty) map, drift-pinned by a test that drives every listed method through the real checker (the checker’s golden-tested method_not_found messages stay untouched). (Verified in passing: the bynk.list/bynk.map combinators map/filter/… are free functions map(xs, f), not methods — so they’re correctly not offered as members; only the method-callable kernel is.) Locals/params in scope need a scope-at-offset query and are slice 4. No language change.
v0.30.1Name-receiver .-member completion (ADR 0062) — completion slice 2. After an UpperIdent. whose receiver is a name (read straight from the line prefix, not a typed value), offer its statically-enumerable members: sum-type variants (Color.Red), refined/opaque of/unsafe constructors, capability operations, and built-in type statics (Int.parse/Float.parse/Json.encode/decode). Members come from the same mid-edit-safe recovery parse as slices 0–1 (no typed model, no scope query). A feasibility scout drove the re-slice (ADR 0062): .-member splits by what sits before the dot — name receivers (this slice) vs value receivers (list.map), which need the receiver’s type (expr_types is discarded on the LSP path, keyed by span, and unavailable mid-edit when the buffer doesn’t parse — the scout’s #1 risk) → slice 3, with locals/params-in-scope. Conservative detection: a single uppercase-initial segment, excluding the decimal 1. and the lowercase value receiver. (Verified in passing: a plain type Id = Int alias is branded — the emitter emits of/unsafe for every Refined body — so they’re correctly offered; a record type yields nothing, its fields being value-receiver.) No language change.
v0.30Positional completion (ADR 0061) — the first slice of comprehensive textDocument/completion, lifting it past the narrow v0.17 consumes/given surface. Two new lexical contexts: type position (after :, in -> T, inside a [ … ] type-argument list) offers built-in types + the bynk-surface transparent types + project type declarations (STRUCT); keyword position (a bare word at a declaration/statement start) offers the reserved keywords with their registry docs + declaration snippets (KEYWORD/SNIPPET, with ${n:…} tab stops). Context detection stays lexical (it must work mid-edit on an unparseable buffer); candidates are semantic — drawn from parsing the other project files with recovery, plus the static bynkc::{keywords, builtin_names, firstparty} registries (built-ins/surface aren’t indexed — the v0.28 finding — so they come from the registries, never the index). Detection is conservative: a list-literal [ is excluded, out-of-context prefixes yield nothing; the one accepted false positive is a record construction value, lexically identical to a record field-type. complete() is pure-function tested per context. .-member completion (x.method/Type.of/Cap.op) and locals/params in scope need receiver typing + a scope-at-offset query and are slice 2. No language change.
v0.29.14The second application of the named-concern-modules convention (ADR 0060) — parser.rs split into named submodules, and the first split of an impl-method file. The 5,295-line parser.rs (one struct Parser with ~120 methods across two impl blocks) becomes a parser/ directory of per-concern impl Parser blocks: declarations (unit/commons/context/test/mock/adapter/binding declarations + the v0.5 capability/provider/service/agent/handler context-body decls), types (type declarations, signed literals, type references), statements (fn declaration, block, statement, param, lambda), and expressions (the precedence ladder, record construction, match/pattern, if, Ok/Err). The parent keeps struct Parser, the scanning core (peek/bump/expect/eat + trivia/doc helpers), the free entry points (parse/parse_unit/parse_unit_with_recovery), the string-literal helpers, and the #[cfg(test)] mod tests. Each submodule does use super::* and opens its own impl<'a> Parser<'a> block; the moved methods reach the scanning core as ancestor privates via self, so visibility widening is compiler-driven — only the cross-concern entry points (parse_unit/parse_expr/parse_type_ref/parse_fn_decl/parse_block/parse_param/parse_lambda/parse_type_decl and a few more) became pub(crate), a far smaller surface than widening all ~120 methods. Parent 5,295 → 1,007 lines. Behaviour-preserving — content-preservation verified (identical named-item multiset), 172 parser tests pass, parse trees unchanged. Landed in three PRs (declarations, expressions, then types + statements + context-body declarations).
v0.29.13The first application of the named-concern-modules convention (ADR 0060) — emitter.rs split into named submodules. The 5,772-line emitter.rs (already carrying an emitter/ directory yet still the largest source file — the “a submodule dir doesn’t mean the parent is small” trap 0060 names) splits in two slices behind the flat use super::* + parent-glob re-export pattern: (1) emitter/lower.rs — the LowerCtx-driven expression/statement lowering engine (lower_block_to_async_bodyrefined_check_as_bool: the lower_*/mock_*/emit_statement/emit_match_*/emit_if_*/kernel lowerers); (2) emitter/emit.rs — the per-declaration emission engine (emit_typeemit_agent: type/refined/record/sum declarations and their checks, attached methods, free functions, capabilities, providers, services, contexts, agents, plus the worker-dispatch lowering helpers those emitters use). LowerCtx, the ts_* renderers, and the codec/reference/import/header helpers stay in the parent; the children reach them as ancestor privates via super, so visibility churn is compiler-driven and minimal (pub(crate) only on the functions the parent calls back). Parent 5,772 → 2,255 lines. Behaviour-preserving — content-preservation verified (identical top-level multiset across each split), all emitter golden fixtures + the tsc_verify strict-compile gate pass unedited. Landed in two PRs (the lowering engine, then the emission engine).
v0.29.9Refactor track, item 3 (second half) — check_v0_5_declarations decomposed + renamed (ADR 0059). The ~522-line context-declaration validator (project/validate.rs) — a flat sequence of per-declaration-kind passes over one errors vec — becomes a ~120-line parent that builds the shared ResolvedCommons snapshot + capability map and then calls check_capability_decls / check_provider_decls / check_service_decls / check_agent_decls in the same order. Renamed to check_context_declarations (the v0.5 name predated providers/services/agents — an in-place application of the marker-convention cleanup). Behaviour-preserving — pass bodies moved verbatim, diagnostic order/text/spans unchanged; all multi-error diagnostic + golden fixtures pass unedited.
v0.29.10Refactor track, item 4 — checker.rs navigation + CapabilityCtx (ADR 0059), the track’s final increment. The 7,000-line checker.rs (one cohesive type-checker, no section banners) gains structure in three slices: (0) 28 characterization pins for the pure type-system helpers (unify/substitute/the peel_to_* family/refinement-consistency) — capturing latent quirks like unify’s _ => true catch-all before any move; (1) a split into thematic submodules (refinements/calls/kernels/expressions, 76 free functions moved verbatim) behind a parent facade keeping the type defs, entry points, and type-system core; (2) Ctx’s six capability-bookkeeping fields grouped into a CapabilityCtx sub-struct (cx.given_remainingcx.caps.given_remaining). Behaviour-preserving — all golden/diagnostic fixtures + the 28 pins pass unedited; the LSP (which consumes the checker) stays green, confirming the facade. Completes the refactor track: 10 of 13 queue items delivered, item 9 (CodeWriter) shelved on inspection, items 12/13 left latent by design.
v0.29.12Refactor track, item 10 — insta dropped (ADR 0059). The insta snapshot-testing crate was a declared workspace dev-dependency but entirely unused (no assert_snapshot!, no .snap files) — the project’s bespoke golden-fixture harness does the equivalent, and the whole refactor track (every increment + the tsc_verify gate) ran on it without ever reaching for insta. Removed from the workspace + bynkc/bynk-fmt dev-dependencies and the lockfile. No code or behaviour change.
v0.29.11Refactor track, item 8 — built-in names centralised (ADR 0059). The language’s built-in type/method names (Json/List/Map/Int/Float/HttpResult; of/unsafe/raw/foldEff) were compared as bare string literals scattered across ~32 sites in checker.rs/emitter.rs/the project/ submodules — a typo was a silent never-match. They now live in one builtin_names::{types,methods} constants module; the comparison/match sites reference the constants (match-arm sites as &str const patterns). Behaviour-preserving — same string values, all fixtures pass unedited; a registry-value test guards the constants. Coincidental literals ("JsonError", the Map[…]/HttpResult.{} templates) were deliberately left untouched.
v0.29.8Refactor track, item 7 — the second TypeScript emitter eliminated (ADR 0059). The test-emission path (project/tests_emit.rs) carried its own TS formatters duplicating emitter/. The three genuine duplicates now route to the emitter: ts_type_ref_emitemitter::ts_type_ref (output-identical), ts_type_ref_emit_qualified → a new emitter::ts_type_ref_qualified (the renderer parameterised over an optional namespace-qualification via a shared ts_type_ref_with core), and escape_ts_stringemitter::escape_ts_string — which also retires a latent escaping divergence (the test copy left \r raw; the emitter escapes it — invisible today since no fixture emits \r, but now a single source of truth). Behaviour-preserving — all test/integration golden fixtures + the tsc_verify gate pass unedited. (Scope-corrected from the proposal: ts_type_ref_display renders Bynk syntax for diagnostics, not TS, and the sanitise_* helpers differ from sanitise_path_segment — neither is a duplicate, both left in place.)
v0.29.7Refactor track, item 3 — lower_expr decomposed (ADR 0059). The ~600-line expression lowerer (emitter.rs) — a single match over ExprKind — becomes a dispatcher of one-line delegations (down to ~270 lines). The substantial arms (Call/BinOp/FieldAccess/Lambda/Ident/RecordConstruction/RecordSpread/ConstructorCall) extract into per-arm lower_* helpers mirroring the existing lower_match_as_iife/lower_is/lower_block_as_expr template; the ~285-line MethodCall arm — a cascade of receiver-typed guard branches whose kernel dispatch already delegated to lower_*_kernel helpers — moves into lower_method_call, with its biggest inline branches (the Json codec, the cross-context service call) split off as Option-returning sub-helpers. Behaviour-preserving — bodies moved verbatim; all golden fixtures + the tsc_verify strict-compile gate pass unedited (byte-identical TS). Landed in two PRs. (Item 9, CodeWriter, was shelved — its “hand-threaded indentation” premise was contradicted by the emitter’s hardcoded-literal indents.)
v0.29.5Refactor track, item 5 — CompileOptions + the pipeline mode split (ADR 0059). The six compile_project* variants (over target × platform × paths × error-shape) collapse into one compile_project(&CompileOptions) with a small builder (CompileOptions::single/split, chainable .target()/.platform()) — the error shape becomes a projection (.map_err(ProjectFailure::flatten)), retiring the _full twins. Internally the pipeline splits into typed compile_project / analyse_project entry points over a shared run_checks, retiring PipelineResult and the two unreachable!() guards (the sum-type-as-two-return-types smell). Mode is retained — build and analyse genuinely diverge (build short-circuits on errors and emits; analyse runs full for complete diagnostics), so it is real internal state, not a smell. Behaviour-preserving — all golden fixtures + the diagnose_project/LSP analyse suites pass unedited. Landed in two PRs (the API collapse, then the mode split).
v0.29.4Refactor track, item 6 — the UnitInfo aggregate (ADR 0059). The nine parallel HashMap<String, _>s keyed on unit name (kinds/unit_tables/unit_uses/unit_consumes/unit_flattened/unit_consumes_aliases/exports_visibility/unit_file_index/groups) collapse into one HashMap<String, UnitInfo>, assembled once after the producer phases: the “all maps share one keyset” invariant becomes a type, the per-iteration .unwrap()s vanish (the phase-8 loop iterates for (name, info) in &unit_info), exports defaults empty (retiring the unwrap_or dance), and the consumer helpers shed their map params (merge_consumed_exports 13→8, emit_unit 21→15, check_unit_files 28→20). Behaviour-preserving — all golden fixtures + the v0.29.1 pins pass unedited, plus an assembly-invariant unit test. (The too_many_arguments #[allow]s remain — those functions still exceed the threshold on their composed-data/sink params, which UnitInfo doesn’t touch; removing them is separate, later work.)
v0.29.3Refactor track, item 2 — compile_project_pipeline decomposed (ADR 0059). The ~1,810-line pipeline becomes a readable sequence of named phase functions: the front half (phases 1–7) extracted into per-phase functions (phase_discovery/phase_parse/phase_group/…), and the deeply-nested phase-8 per-unit loop carved into compose_unit_symbols / merge_consumed_exports / collect_unit_methods / check_unit_files / emit_unit — the loop body dropping from ~454 to ~78 lines. Behaviour-preserving — statements moved verbatim, every continue conserved exactly (none converted to a signal), the existing parallel maps threaded as explicit params (the too_many_arguments lints are the deliberate signal for the UnitInfo aggregate, item 6); all golden fixtures pass unedited. Landed in two PRs (front half, then the back-half carve).
v0.29.2Refactor track, item 1 — project.rs split into submodules (ADR 0059). The 8,264-line file becomes a project/ directory — paths/discovery/consistency/graph/symbols/diagnostics/validate/tests_emit — behind a re-exporting parent that keeps the orchestrator (compile_project_pipeline, the composition root) and the public facade in place. Pure relocation: no behaviour, output, or API change — every item moved verbatim, all 116 preserved, the v0.29.1 pins distributed to the submodules that now own their helpers, and all golden fixtures pass unedited. The ~1,806-line pipeline stays whole (its decomposition is the next item).
v0.29.1The internal refactor track opens (ADR 0059) — a dedicated, behaviour-preserving paydown of the bynkc quality backlog, run under a feature freeze, trunk-based and patch-versioned. No language, behaviour, or output change. This increment adds only characterisation tests pinning the pure helpers (normalize_rel, unit_path_matches, canonicalise_cycle, the consumes-cycle DFS, the path maps, and the duplicate TS-emitter formatters) that the upcoming structural splits relocate — so those moves are verifiable, not aspirational. The escaping pins also captured a latent divergence between the two escape_ts_string copies (the project test-emitter leaves \r raw; the production emitter escapes it), which the later de-duplication must reconcile.
v0.29A-2 lands in the editor (B-1, ADR 0058) — an extension-only increment that makes v0.28’s semantic tokens actually colour. vscode-bynk declares the legend’s custom token types (capability/service/agent/provider, each with a standard superType) and modifiers (refined/opaque/platformNative) in contributes, with semanticTokenScopes TextMate fallbacks — so the Bynk-distinctive tokens render out of the box. The declared names are a cross-component contract with the server’s frozen legend, enforced by a bynk-lsp test that parses vscode-bynk/package.json against semantic_tokens_legend() (one source of truth; the test is excluded from the published crate since it reads a sibling file). Adds a bynk.inlayHints.enable toggle via a client provideInlayHints middleware (the persistent per-language preference; editor.inlayHints.enabled is the instant one), and documents editor.semanticHighlighting.enabled. No bynkc/server/language change.
v0.28A-2 continues — LSP semantic tokens (ADR 0057): resolution-aware highlighting for the index kinds (types, fns, capabilities, services, agents, providers), additive over the client’s syntactic layer, served from the cached round over a frozen legend (custom capability/service/agent/provider token types; declaration/refined/opaque/platformNative modifiers — refined only with a refinement present, a plain type X = Int alias carries neither). First-party (bynk.*) references — which symbols deliberately drops (synthetic defs aren’t on disk) — colour via a tokens-only foreign_refs side table filled by a second qualification pass, so Kv lights up platformNative without perturbing the v0.25 navigation invariants. full + range; delta, locals/params/generic type parameters deferred. Test files get tokens for free. No language change.
v0.27The A-2 headline — LSP inlay hints for inferred types (ADR 0056). A HintSink (the RefSink analogue) threads through the checker, recording (binding-name span, ": " + Ty::display()) at each annotation-absent binding as its final type is computed: let bindings, let <- bindings (the peeled Effect[T] payload — the binding’s actual type), and lambda parameters typed from the expected fn type; _ and synthetic/test units excluded. ProjectAnalysis retains the per-file set (no Ty crosses the public surface) and the LSP serves textDocument/inlayHint from the cached analysis round, positions against the analysed snapshot. Because the sink is a &mut parameter (not the Ok payload check_record drops), hints survive a transient type error at every site the checker still reaches (a fn-body error suppresses that file’s handler-body hints until it clears). Deferred: generic-instantiation hints (type args not stored queryably), parameter-name hints, inlayHint/resolve. No language change.
v0.26The A-1 headline — LSP code actions from structured suggestions (ADR 0054). CompileError gains Suggestions (message, span→replacement edits, rustc-style Applicability), authored .with_suggestion(…) at the diagnosis site. The seed catalogue is the prescriptive given pair: remove unused capability and add undeclared capability (bare and cross-context B.Cap), with list-aware edits computed in the checker — comma/whitespace handled, removing the only entry drops the given keyword, an absent clause is synthesised after the return type. The LSP serves textDocument/codeAction (QuickFix) keyed on the diagnostic’s span (the edits land away from the squiggle) from the cached analysis round (now retaining per-file diagnostics), as versioned edits. Riders (ADR 0055): workspace/symbol + documentHighlight as binding-index queries. Deferred: the InRange bound-swap (the predicate AST carries no bound spans), CLI --fix. No language change.
v0.25The second A-tier LSP increment — the project-wide binding index plus references & rename (A-0 slice 2, ADR 0053). A reference-table sink (the ErrorSink analogue) records use→def edges at the resolution sites themselves — resolver, checker (capability/service op-calls), and project driver (given/exports/consumes clauses) — assembled and unit-qualified on the v0.24 analyse pass: binding-correct, never name-matched, covering test/integration units. The LSP serves textDocument/references and rename/prepareRename from it; rename is validated by re-analysis (collisions refuse on any new diagnostic) plus index-equality modulo the rename (silent capture/escape refuses), and emits versioned edits so stale buffers reject rather than mis-apply. Rider: definition and hover re-point at the index, fixing their duplicate-name mis-navigation. Deferred: methods, record fields, op names, local bindings; unit rename is the A-3 file-operations increment. No language change.
v0.24The first A-tier LSP increment — project-wide diagnostics (A-0 slice 1, ADR 0052). bynkc::diagnose_project: non-bailing (a broken unit no longer hides other units’ errors), overlay-aware (unsaved buffers diagnosed), file-attributed at the collection point (no Span change — a span→file map would be unsound). Context files get full resolve/check diagnostics for the first time; the LSP publishes project-wide with clear-on-fix semantics (a unit-tested pure diff) and converts positions against the analysed snapshot. Rider: project-mode CLI errors now render with full ariadne source context (previously bare [category] message lines). No language change.
v0.23The Cloudflare adapter extended — Kv.list and putTtl. list(prefix) -> Effect[List[String]] is a binding-side drain (the cursor loops in host code — forced by the recorded given-on-free-functions gap: no Bynk routine can both recurse and hold a capability); putTtl writes with expirationTtl (distinct camelCase op over an options record). Structured values are v0.22-codec composition, shipped as the first executed adapter-op test (fake env.KV, drain paging proven, Json.encode/decode[Entry] round-trip). No new lock machinery; wrangler unchanged.
v0.22bThe wider stdlib, second slice — the typed JSON codec. Json.encode(v) / Json.decode[T](s) -> Result[T, JsonError], compiler-backed onto the boundary codec machinery (no untyped Json value); type application on qualified statics (decode[Order], decode[List[Order]], the v0.20b forcing case); JsonError as a compiler-known record (kind/path/message) putting boundary failures in the program’s hands; encode throws on non-finite Float (the 0040 contract). And the bare-Int integrality tightening: every boundary deserialisation of a bare Int now requires Number.isInteger — a deliberate wire-contract change, re-blessed in isolation. Completes v0.22.
v0.22aThe wider stdlib, first slice — kernel methods everywhere. The string kernel (split/trim/contains/replace-all/slice/indexOf -> Option/chars code-points/concat, UTF-16 code units normatively), Option/Result combinators as built-in methods (map/andThen/getOrElse/isSome/isOk/mapErr/okOr — value methods, not free functions, so nothing collides and chaining works day one), numeric helpers (abs/min/max/clamp; isNaN/isFinite), and Int.parse/Float.parse -> Option statics (full-string, safe-integer/finite). bynk.string ships Bynk-written join. Purely additive — no boundary change; the typed JSON codec is v0.22b.
v0.21Float — a fourth base type for decimal data, distinct from Int (both erase to TS number; the checker is the only thing keeping them apart). Float literals (digit-both-sides fractions, exponents; lexeme-stable emission), no implicit IntFloat coercion (bynk.types.no_numeric_coercion), the numeric kernel (i.toFloat(); f.round()/floor/ceil/truncate — no ambiguous toInt), operand-typed division (Int keeps truncating, Float true-divides), refinement over Float (InRange(0.0, 1.0), Positive/NonNegative; bounds must match the base), and a finite boundary: deserialise_ requires Number.isFinite (JSON admits 1e999 as Infinity), serialising a non-finite Float throws. Arithmetic non-finites stay host-defined. The v0.22 typed-JSON unblock.
v0.20bThe functional core, second slice — built-in collections + the combinator stdlib. List[T]/Map[K, V] as compiler-known generic types (immutable; readonly T[] / ReadonlyMap<K, V>), the [a, b, c] list literal, a thin kernel (fold/foldEff/prepend/get/length; Map.empty()/insert/get/keys), and bynk.list/bynk.map — first-party commons written in Bynk over the kernel (map/filter/find/any/all/traverse; values/contains/getOr), injected on uses. Collections serialise at boundaries (Map as an insertion-ordered entries array); the function-type boundary rule looks through them. Map keys are confined to value-keyable types. Fetch’s missing-headers compromise becomes retirable.
v0.20aThe functional core, first slice — first-class functions (lambdas (params) => expr, function types A -> B with right-associative arrows, named functions as values, value application) and generic functions (fn name[A, B](…), argument-directed inference + explicit name[T](…), erased TS generics). Function types are effect-structural (A -> Effect[B] is the traverse shape) and confined to non-boundary positions; effectful function-value calls obey the capability-call confinement. Open-narrow: no generic user types, no bounds. List/Map + the combinator stdlib follow in v0.20b.
v0.19The first platform adapter and live platform locking — bynk.cloudflare exporting a minimal Kv (get/put/delete, collection-free), injected like the bynk surface and named inside the reserved prefix. Consuming it types env.KV into the Worker Env, emits the [[kv_namespaces]] wrangler stanza, and (bundle) threads an optional env through composeApp. Platform-lock enforcement goes live: bynk.target.vendor_required / vendor_conflict over the in-process given-closure, per deployment unit.
v0.18Adapter dependencies & the ambient surface — adapters gain consumes U { Cap, … } (adapter-to-adapter), external providers’ given is wired (compose passes a by-name deps object to the binding constructor, transitively), bynk.Fetch + bynk.Secrets join the first-party surface, and --platform node makes the platform axis observable. Config-as-capability: the tokens/weather exemplars drop their secret/URL parameters.
v0.17Adapters — the host boundary. The adapter declaration kind: capability contracts beside a named TypeScript binding (external, bodiless providers), consumes U { Cap, … } bare-name flattening for consumers, the reserved bynk namespace and first-party bynk surface (Clock, Random, Logger), npm requires pinning, and a minimal --platform axis.
v0.16Multi-Worker integration testing (test integration "…" { wires … }) — stand several contexts up as in-process Workers and exercise a flow across the real cross-context wire (serialise/deserialise), no mocks. Covers cross-context service calls, cross-context capabilities, and cross-Worker agents (Durable Objects, backed in-memory with state fresh per case). The MVP’s final increment.
v0.15Cross-context capability resolution — a context exports capability { … }; a consumer depends on it via a qualified given B.Cap and its provider is instantiated locally (in-process). The platform/framework-context pattern.
v0.13Refinement narrowing — value is RefinedType checks the refinement at runtime and narrows the value to that type in the branch (flow-sensitive counterpart to .of).
v0.12Provider composition (provides … given) — a provider may depend on other capabilities; the composition root wires the dependency graph in topological order.
v0.11Agent state-field initialisers (state { status: OrderStatus = Pending }), enabling sum-typed state machines (and opaque/refined state) — no more Option-wrapping.
v0.10bQueue consumers (on queue) — message deserialisation, the Worker queue entry point with Ok/Err ack/retry, and wrangler.toml [[queues.consumers]].
v0.10aCron handlers (on cron) — scheduled tasks compiling to the Worker scheduled entry point and wrangler.toml [triggers].
v0.9.4Refined-literal admission (write a literal where a refined type is expected); Mock[T] value fabrication for tests.
v0.9.1assert as an expression; project-mode hardening; a tsc verification stage.
v0.9HTTP handlers (on http), HttpResult, and the Cloudflare Workers target.
v0.7.1Tail-position auto-lift of plain values into Effect.
v0.6Cross-context service calls (consumes) and composition roots.
v0.5The effect system (Effect[T], <-) and the generated runtime.

Earlier increments established the core: commons/context units, the type system (opaque, sum, record, refined types), match/is, Result/Option, agents, capabilities, and testing.

Events, sagas, and storage kinds are designed but not yet shipped — see Versioning & roadmap.

This summary will become a precise per-increment changelog as the docs-delta discipline (docs shipped with each increment) takes hold.