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.247.

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 (under v0.163, no version change): the enum-spelling convention is now stated — a payloadless sum is canonically written enum { A, B } (the pipe form | A | B is for sums that carry a payload); enum { … } is exactly sugar for the payloadless pipe form, so the two are the same type. This closes the keyword-hygiene batch (#548): no language change (both spellings remain valid), a docs convention only.

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.247.62”emitter::block_uses_emit now reads the checker’s own resolved Callee classification instead of a bare-Ident(“Events”) receiver name match, closing a real disagreement with project.rs’s own unit_table_uses_emit (#1202) on a locally-shadowed Events type that could previously produce TypeScript failing tsc —strict”
v0.247.61”project.rs’s unit_table_uses_emit and called_cross_context_services now read the checker’s own already-resolved Callee classification (threaded forward from per-unit checking as a new RunChecks::Checked::unit_callees field) instead of re-deriving Events.emit/cross-context-call detection from raw AST method-call syntax (internal only, byte-identical output)“
v0.247.60”project.rs’s instantiate_provider_expr now reads a provider’s given clause from bynk-emit::ir (CapRefIr) instead of walking bynk_syntax::ast::CapRef directly; ProviderBody::External also gains the given field it was silently dropping (internal only, byte-identical output)“
v0.247.59”design/tracks/the-ir.md §6 reconciled with how slices 4/5 of #1187 actually landed (both narrowed hard, not the structural cutovers the table’s own row implied) and how Provider and slice 6 (project.rs cleanup) still stand — no Rust code changes”
v0.247.58”emit_service now reads a handler’s resolved signature (params/return type/effectful-ness) and a service’s protocol data from bynk-emit::ir instead of walking bynk_syntax::ast TypeRefs/ServiceProtocol directly (internal only, byte-identical output — no language surface change)“
v0.247.57”An agent handler no longer gets an unnecessary implicit-commit wrapper when a locally-shadowed name (e.g. a handler parameter reusing a store Map/Set/Cache/Log field’s name) merely looks like a store write by name — write detection now reads the checker’s own resolved dispatch instead of matching bare identifiers (R6.5). Cell fields were never affected by this shadowing gap”
v0.247.56”bynk-emit::ir gains IrExprKind::BinOp/Neg/InterpStr — comparison, arithmetic, unary negation, and string interpolation now lower to real IR nodes instead of panicking (internal only, no language surface change)“
v0.247.55P6.x cutover slice 3 (design/tracks/the-ir.md §5/§6, #1193, #1187, narrowed — Provider deferred) — emit_capability now reads each capability op’s resolved params/return type off OpSig (bynk-emit::ir, built by lower_capability_item_ir/lower_op_sig_ir) through ts_ty, instead of walking CapabilityOp::params/return_type TypeRefs through ts_type_ref directly. emit_capability gains new ops (&[OpSig]) and commons (&TypedCommons) parameters (c (&CapabilityDecl) stays too — IrItem::Capability::def is a bare String, carrying neither documentation nor an Arc back to the declaration); its one call site (emitter.rs’s emit_project) lowers the IrItem::Capability inline, no new helper (a single caller doesn’t earn one, unlike type_shape_for’s three). Provider is deliberately not converted in this slice — despite #1187’s own pairing — because a ProviderOp always carries a real body, and lower_provider_op_ir routes it through lower_expr_ir, which still hits #1189’s open comparison/arithmetic BinOp gap; Capability ops are signature-only, no body, so no such surface exists. ast_importers does not move (emit.rs stays invisible to the probe, same as slice 1). Two accepted, narrow behaviour differences, both only reachable through a capability op since the resolver skips CommonsItem::Capability outright (context_checks::build_capability_op_info’s own pre-existing leniency) — no valid program can reach either: a param/return type name the checker never validates rendered as its raw undefined name before this slice and renders as void after, and a History type ref rendered as ts_type_ref’s “never” before this slice and renders as void after, since lower_op_sig_ir’s own resolve-or-Unit fallback treats both misses the same way. No other author-facing behaviour change; differential tests (bynk-emit/src/emitter/emit.rs, capability_op_sig_emission_tests) cover a multi-op capability, a named param/return type, and — new relative to the prior IrItem::Type/Fn cutover slices’ own corpus — a generic op’s own rigid type variable, through the project emission pipeline (capability is a context-only construct, so it never reaches the single-file emit() path)
v0.247.54P6.x cutover slice 2 (design/tracks/the-ir.md §5, #1191, #1187) — emit_wrangler_toml no longer matches HandlerKind::Cron/ServiceProtocol::Queue off bynk_syntax::ast directly; its one call site (project.rs) collects the sorted+deduped cron-expression and queue-name lists and passes them in, since project.rs already imports bynk_syntax::ast and no project-wide IrItem::Service is built there to route through instead (IrHandler::kind reuses HandlerKind unchanged even where one is, so converting to bynk-emit::ir would not have removed the cron match anyway). ast_importers moves 9 to 8. emitter/runtime_use.rs, originally paired with wrangler.rs in #1187’s own table as a joint trivial sweep, is deliberately untouched — its TypeRef field is downstream of emitter/serialisation.rs’s still-AST-driven JSON-codec renderer, a real, separate, unscoped conversion, not a relocation like this one. No author-facing behaviour change; every cron/queue wrangler.toml fixture is byte-identical to main’s output
v0.247.53The first bynk-emit::ir cutover slice (design/tracks/the-ir.md §5, #1188, #1187) — emit_type/emit_record_type/emit_sum_type/emit_refined_type now read TypeShape (bynk-emit::ir, built by lower_type_item_ir) instead of walking bynk_syntax::ast::TypeDecl/TypeBody directly. emit_project takes &CheckedProgram instead of &TypedCommons, deriving commons internally (mirrors emit()‘s own existing precedent) so lower_type_item_ir has a CheckedProgram to lower against; the emission loop resolves each type’s canonical Arc from TypedCommons::types (a table that, found during implementation, already holds an identical entry for every event’s own synthetic TypeDecl too, so one type_shape_for helper serves both the Type and Event emission loops with no special-casing). Record/sum field and variant-payload types render via ts_ty(TyId, &Arc) — the existing checker-Ty renderer already used by inline kernel-method lowerings — instead of ts_type_ref(&TypeRef); refined/opaque types are unaffected (TypeShape::Refined keeps its refinement as the unlowered AST Refinement node, never routed through lower_expr_ir, which is why Type — unlike Fn — was safe to convert ahead of the still-open comparison/arithmetic BinOp gap, #1189). A type’s own attached methods (emit_attached_methods) stay on the untouched, unconverted body-lowering path — out of this slice’s scope, Fn territory. No author-facing behaviour change; differential tests (bynk-emit/src/emitter/emit.rs, type_shape_emission_tests) cover record/generic-record/sum-with-embeds/refined/opaque emission through both the single-file and project emission pipelines
v0.247.52P6.14 (design/tracks/the-ir.md §6, #1174) — bynk-emit::ir gains IrItem::Provider, the sibling assembly to P6.12’s IrItem::Capability, plus ProviderBody, the type the reference’s own body: ProviderBody // Bynk ops
v0.247.51Redefine the ast_importers greenfield-status probe (design/tracks/the-ir.md §5/§6, #1176) so it can meaningfully reach 0 for phase 6. P6.9’s own grounding pass (#1167) found the probe’s crate-wide bynk_syntax::ast match necessarily counts bynk-emit::ir/ir/lower.rs’s own legitimate Ast → Ir lowering import, a floor this track’s own IR module structurally cannot clear. Settled by a small, named exclusion list (ir.rs, ir/lower.rs) rather than a path-prefix rule scoped to emitter/**: project.rs and project/tests_emit.rs also import bynk_syntax::ast today (EmitProjectCtx holding ActorDecl/AgentDecl fields directly; test/suite emission reading TypeRef/HandlerKind), and that import is exactly the still-open R6.13 defect this probe exists to track (P6.6’s own “closes the emitter reading AST declarations directly”), not a lowering-pass import — a prefix rule would have silently excluded both files along with ir/’s legitimate two. Mirrors the same permanent-carve-out discipline fs_below_driver’s NAMED_FS_EXCEPTIONS and emit_diagnostics’s registry cross-reference already use. ast_importers now reads 9 (was 11); design/greenfield-status.md regenerated via cargo xtask greenfield-status —apply, which also picks up unrelated trend-probe drift (wildcard_arms, keep_in_sync, test_density) accumulated since the table was last committed. No emission-behaviour change
v0.247.50P6.13 (design/tracks/the-ir.md §6, #1179) — bynk-emit::ir now lowers a from websocket service’s on open/on message/on close handler body, closing the one gap P6.11 (#1171) left behind (ProtocolIr::WebSocket was already real; only handler-body lowering was deferred). lower_service_handler_ir no longer todo!()s on this case: it re-derives the synthetic connection: Connection[out] binding the checker injects into its own params_for_check only (open_connection_param, context_checks.rs), seeds it into the body-lowering scope via lower_service_handler_body_ir (now accepting an optional connection binder alongside the existing binder), and never adds it to IrHandler::params — mirroring the checker’s own asymmetry exactly. A new ConnectionBinder type (ty, borrowed) carries the checker’s own owned-vs-borrowed linearity distinction (borrowed_held) into the IR for the first time: false for on open (fresh owned socket, disposed via transfer), true for on message/on close (the borrowed firing socket). IrItem::Service assembly (lower_service_item_ir) is now buildable for a real from websocket service end-to-end. No bynk-emit emission-behaviour change — still no consumer of bynk-emit::ir.
v0.247.49design/tracks/the-ir.md gains §3.7 (Q7, #1175), settling the-ir.md’s own real completion criterion — what §5’s “emitter/lower.rs’s AST-walking functions call only into bynk-emit::ir’s lowering pass” requires structurally, given the reference’s own TsProgram/printer split (Part 7.1, R7.2/R7.3) is phase 7’s job, not this phase’s, and no bynk-ts crate exists yet. Decision: emitter/lower.rs keeps writing TypeScript source text via its existing Lowered { pre: Vec, expr: String } shape (T2.1/R6.2) after the cutover — that cannot change until phase 7’s printer exists, which this track’s own §2 already excludes. What changes is only what each lower_* function reads: today they re-derive dispatch/commit-shape/store-target decisions from a fresh bynk_syntax::ast::Expr walk (lower_method_call/lower_call’s own “the order is load-bearing” guard chains, block_writes_state’s name-matched receiver); post-cutover they read those same decisions off an already-lowered IrExpr/IrItem/CommitShape/StoreFieldIr value bynk_emit::ir::lower computed once. Confirmed live (main@7f5115ee): zero crate::ir references anywhere in emitter.rs/emitter/*.rs — the cutover has not started. §5 gets a short cross-reference to this finding. No Rust code changes; this issue remains a scoping placeholder per its own framing — the cutover’s own slicing is not proposed here, gated on Provider (#1174) and the ast_importers probe redefinition (#1176)
v0.247.48P6.12 (design/tracks/the-ir.md §6, #1173) — bynk-emit::ir gains IrItem::Capability, the sibling assembly to P6.10’s IrItem::Agent and P6.11’s IrItem::Service, plus OpSig, the type the reference’s own ops: Vec sketch names but never defines. OpSig adapts CapabilityOp — a signature only, no body — under this module’s already-established “no arena” substitutions: params: Vec<(String, TyId)> mirrors IrItem::Fn::params exactly, and type_params: Vec mirrors the checker’s own already-resolved CapabilityOpInfo::type_params. lower_op_sig_ir resolves each op’s own params/return_ty in that op’s own rigid-variable scope (a capability op’s type parameters are scoped to the op itself, not the capability, unlike a method’s generic receiver), the same per-op treatment bynk-check::context_checks::build_capability_op_info already gives a generic op — a generic op’s own T survives as Ty::Var(“T”) rather than collapsing to Ty::Unit. Review of #1182 found a capability op’s own params/return_type are never actually resolution-checked upstream (the resolver skips CommonsItem::Capability outright, and check_capability_decls only records refs, never errors on a miss), so lower_op_sig_ir falls back to Ty::Unit on an unresolvable name rather than an ADR 0334 panic — mirroring build_capability_op_info’s own fallback and lower_agent_item_ir’s own key_ty precedent (#1169), not asserting a checker guarantee that does not actually hold. No bynk-emit emission-behaviour change — every existing e2e/emission fixture passes unmodified, and still no consumer of bynk-emit::ir
v0.247.47bynk-emit::ir’s own IrItem doc comment records the real, evidenced resolution of #1172 (whether to add an IrItem::Actor variant): a settled non-build decision, not an unbuilt gap. The reference’s own sketch (Actor { def, scheme: AuthScheme, identity: Option, claims: Option }) is the wrong shape for this codebase on three independent findings: claims: Option is unbuildable, not merely unbuilt (an actor refinement’s own hasClaim/claimEquals predicate is validated only structurally, with no Callee, no expr_types entry, no typing at all, since claims are deliberately untyped JSON lowered straight to a JS string); scheme: AuthScheme names a type the reference never defines, though a real 5-variant candidate already ships (bynk-check::actors::Scheme); and, decisively, every real consumer of actor data (the shipped emitter’s five seam resolvers, and the reference’s own R8.11/R8.13) is handler-keyed, not declaration-keyed, which is exactly why the two actor-shaped IR additions that did have a real consumer (ActorBinder, and IrHandler’s own actors: Vec) both landed on IrHandler rather than a new IrItem variant. No Rust code changes; IrItem still has no Actor variant, and this doc-only PR records why that is now a settled decision rather than an open question (#1172)
v0.247.46P6.11 (design/tracks/the-ir.md §6) — bynk-emit::ir gains IrItem::Service, the sibling assembly to P6.10’s IrItem::Agent, plus the two types the reference names but never specifies: ProtocolIr (six variants, one per real trigger per E2 — Call/Http/Cron carry no payload because the binding lives per-handler on HandlerKind) and PolicyIr/CorsIr/SecurityIr, which interpret rather than pass through ServiceDecl’s cors/security/limits blocks through the same typed accessors the shipped emitter already uses, so security: None materialises the safe defaults (nosniff on) instead of re-exporting the ambiguity. policy is Option, present only for a from http service, mirroring StoreFieldIr::init’s own kind-gated Option rather than the reference’s unconditional field: an unconditional PolicyIr on a from queue service would assert header behaviour the emitter never produces. lower_service_handler_ir is a sibling of lower_handler_ir, not a widening of it — the two seed disjoint scopes, and widening would delete the agent-only assertion that catches exactly this miswiring — and it reads a handler’s actor binding back from TypedCommons::actor_bindings (#1170), making IrHandler::binder non-None for the first time. A from websocket lifecycle handler’s body hits a named todo!() rather than a wrong tree (the synthetic connection param is checker-injected and never present in h.params; follow-up tracked as #1179); the ProtocolIr::WebSocket descriptor itself lowers for real. No bynk-emit emission-behaviour change — every existing e2e/emission fixture passes unmodified, and still no consumer of bynk-emit::ir (#1171)
v0.247.45bynk-check gains TypedCommons::actor_bindings — a service handler’s own resolved by : actor binding (handler_actor_binding’s own return value, context_checks.rs), keyed by the handler’s own span and persisted into CheckedProgram rather than discarded once check_service_decls’s per-handler loop moves on. Closes the sole blocker IrHandler’s own doc comment (bynk-emit/src/ir.rs) named for lower_handler_ir (P6.9, #1167) to stop being agent-only: a post-certify consumer can now read a service handler’s actor binding back, though lower_handler_ir itself is not widened to do so here (tracked separately, #1171). No bynk-check emission/diagnostic behaviour change — every existing e2e/checker fixture passes unmodified (#1170)
v0.247.44P6.10 (design/tracks/the-ir.md §6) — bynk-emit::ir gains IrItem::Agent, assembling every P6.7-P6.9 ingredient (StoreFieldIr since P6.7, CommitShape/IrPredicate since P6.8, IrHandler since P6.9) that had no IrItem variant to land in until now — the gap P6.9’s own Risks section named without numbering it. lower_agent_item_ir computes an agent’s store_cells/state_ty once (derived from its own already-lowered StoreFieldIr::Cell entries, not re-resolved independently) and reuses them for every downstream lower_store_field_ir/lower_invariant_ir/lower_transition_ir/lower_handler_ir call, wiring every prior slice’s own standalone constructor rather than re-deriving any of their logic. invariants/transitions are lowered once and threaded into every handler’s own lower_handler_ir call so a store-writing handler’s own Transactional commit carries the agent’s real predicates, not the empty pair a caller could otherwise silently forget to populate. IrItem::Service/Actor/Capability/Provider remain deferred, each with its own distinct blocker (see IrItem’s own doc comment). No bynk-emit emission-behaviour change — every existing e2e/emission fixture passes unmodified, and still no consumer of bynk-emit::ir (#1169)
v0.247.43P6.9 (design/tracks/the-ir.md §6) — bynk-emit::ir gains IrStmt::Assign/ActorBinder/IrHandler (Part 6.7’s own trailing construct, R6.16): real, standalone, tested constructors for an agent on call handler, the same posture P6.4 set for IrPat/IrArm/Exhaustive and P6.7/P6.8 set for StoreFieldIr/CommitShape — no IrItem variant references IrHandler yet (IrItem::Agent/Service remain unconstructed; see IrItem’s own doc comment for exactly what still blocks them). Also closes a prerequisite gap twice deferred by P6.7’s and P6.8’s own Risks sections: Statement::Assign’s own IrStmt target, todo!() since P6.1, becomes a real IrStmt::Assign — the checker’s own target resolution never keys a Callee for a cell := write, so no ExprId-keyed sink was ever actually needed. lower_handler_ir is agent-only: binder is always None, checker-enforced by bynk.actor.by_on_agent (an agent on call handler cannot carry a by clause) — a real service handler’s own non-None binder needs a bynk-check change (persisting handler_actor_binding’s own resolved pair) this slice does not make. A new handler-body lowering entry point (lower_handler_body_ir, parallel to but distinct from lower_fn_body_ir) seeds scope from the agent’s own self/store cells/handler params, finally letting the existing generic Callee-wrapping call machinery (P6.2) and the new Statement::Assign arm reach a real handler body for the first time, with no new call-lowering logic needed. No bynk-emit emission-behaviour change — every existing e2e/emission fixture passes unmodified, and still no consumer of bynk-emit::ir. Also corrects design/tracks/the-ir.md’s own ast_importers completion-probe language: the probe’s crate-wide definition necessarily counts bynk-emit::ir’s own legitimate AST import, so the prose criterion (emitter/‘s AST-walking functions gone), not the probe reading 0, is this track’s own true finish line (#1167)
v0.247.42P6.8 (design/tracks/the-ir.md §6) — bynk-emit::ir gains CommitShape/IrPredicate (Part 6.7’s trailing two types, R6.15): real, standalone, tested constructors for a handler body’s own resolved one-of-three commit shape and an agent’s own lowered invariant/transition predicates, the same posture P6.4 set for IrPat/IrArm/Exhaustive and P6.7 set for StoreFieldIr/StoreKindIr — no IrItem variant references them yet (IrHandler itself still doesn’t exist; see IrItem’s own doc comment for exactly what still blocks it). lower_invariant_ir/lower_transition_ir seed a predicate’s own scope exactly as checker::check_invariants/check_transitions already do (a store Cell field by bare name; old/new bound to the agent’s synthetic state type) and lower it through the existing lower_expr_ir machinery unchanged. lower_commit_shape_ir decides Transactional/FlushEvents/ReadOnly from a new write-detection walk over the certified AST, keyed on a resolved Callee::Store entry’s own mutating op rather than a receiver’s bare name — closing the one real, safe-direction false positive the shipped emitter’s block_writes_state still carries (a local shadowing a store field’s own name). The mutating-op-name set block_writes_state already matched inline is factored into shared bynk-emit::emitter constants both walks now read, rather than duplicated a second time. No bynk-emit emission-behaviour change — block_writes_state/mutating_op keep their current output byte-for-byte, every existing e2e/emission fixture is untouched, and still no consumer of bynk-emit::ir (#1165)
v0.247.41P6.7 (design/tracks/the-ir.md §6) — bynk-emit::ir gains StoreFieldIr/StoreKindIr/IndexIr (Part 6.6’s trailing two structs, R6.14): real, standalone, tested constructors for an agent store field’s state shape and index-table keys, the same posture P6.4 set for IrPat/IrArm/Exhaustive — no IrItem variant references them yet (IrItem::Agent/Service still don’t exist; see IrItem’s own doc comment for exactly what still blocks them, IrHandler named but not yet commissioned by any slice). StoreKindIr covers all five functional storage kinds (Cell/Map/Set/Cache/Log; Queue cannot reach a certified program); Duration substitutes to i64 milliseconds throughout (extending ADR 0333’s precedent), and IndexIr is a plain String (the indexed value-field’s own name — the map’s own key type, not the indexed field’s, fixes the sibling table’s shape). lower_store_field_ir (taking a certified CheckedProgram) reads @ttl/@retain millis and @indexed(by: …) field names directly off the field’s own annotations, in declaration order (no sort — StoreFieldIr.indexed is scoped per-field, unlike the shipped emitter’s own doubly-sorted HashMap intermediate), and constructs init only for a Cell field (a non-Cell field’s init is parsed but never type-checked, a real pre-existing checker gap left for its own follow-up). No bynk-emit emission-behaviour change — every existing e2e/emission fixture is untouched, and still no consumer of bynk-emit::ir (#1163)
v0.247.40P6.6 (design/tracks/the-ir.md §6) — bynk-emit::ir gains IrItem/TypeShape (Part 6.6): IrItem::Type/IrItem::Fn are real, constructible variants; Agent/Service/Actor/Capability/Provider are not added as variants this slice (each has its own real, named blocker, not a shared one — see IrItem’s own doc comment). lower_type_item_ir (taking a certified CheckedProgram) builds a real IrItem::Type for a record/sum/refined-or-opaque type declaration (TypeShape unifies Opaque into Refined via an opaque: bool field, mirroring emit_type’s already-shipped RefinedShape); lower_fn_item_ir wraps P6.1’s lower_fn_body_ir, adding def (reusing the program’s own Arc)/receiver (a method’s own generic self type, since self is not in params)/params/ret/effectful around its existing return value, covering free functions and methods alike. No bynk-emit emission-behaviour change — still no consumer of bynk-emit::ir (#1161)
v0.247.39P6.5 (design/tracks/the-ir.md §6) — bynk-emit::ir::lower’s ExprKind::Match arm now constructs a real IrExprKind::Match for ordinary source match expressions: scrutinee via the existing lower_expr_ir, arms/exhaustive via P6.4’s lower_arm_ir/lower_exhaustive_ir verbatim, form via the shipped string emitter’s own match_needs_if_chain/pattern_has_nested_test (now pub(crate), reused rather than re-derived). MatchForm is real and constructible — Flat/IfChain — scoped to shape only; the reference’s own tail-vs-value position axis is left to a future printer, the same way IrExprKind::If already leaves tail-vs-value entirely to its own caller. ExprKind::Question/ExprKind::Is remain todo!() in ir/lower.rs, each now citing its own real blocker (Question’s three shapes by operand type; Is’s R5.9/R5.10 narrowing/receiver-temp gap) rather than the now-stale “IrArm/Exhaustive/MatchForm are still uninhabited” text both used to carry. No bynk-emit emission-behaviour change — still no consumer of bynk-emit::ir (#1159)
v0.247.38P6.4 (design/tracks/the-ir.md §6) — bynk-emit::ir gains a real Pattern IR: IrPat (Wild/Bind/Const/Variant/Refined/Or), IrArm (pat/guard/body/binds/binding_mode) and Exhaustive (Total/Partial) are real, constructible types, inhabiting the placeholders P6.1 left. IrPat::Variant’s identity is keyed by the scrutinee’s own TyId, resolved through bynk-check’s variants_of/VariantInfo (now pub) rather than Callee::Ctor’s Arc scheme, which never fires for Ok/Err/Some/None. bynk-emit::ir::lower gains standalone constructors — lower_pattern_ir, lower_arm_ir, lower_exhaustive_ir — tested directly against real certified programs (a user sum, Result/Option via variants_of, a refined pattern, an or-pattern with shared cross-alternative bindings recorded as IrArm::binding_mode rather than an emission-time discovery); Exhaustive::Partial stays real but unreached on this pass’s own certified-only path (a certified match always has an unguarded arm), extending ADR 0334’s .expect()-not-fallback discipline to a second rule. Not wired into IrExprKind::Match/Question/Is construction — all three stay todo!(), still gated on P6.5’s own MatchForm (R5.2/R5.3). No consumer yet and no bynk-emit emission-behaviour change (#1157)
v0.247.37P6.3 (design/tracks/the-ir.md §6) — bynk-emit::ir::lower gains real desugaring for two of the row’s ten node kinds: BinOp::Implies -> Or{lhs: Not(a), rhs: b}, split out of the bundled comparison/arithmetic todo!(); and ExprKind::RecordSpread -> Block{stmts: [Let(tmp, base)], tail: Record{<complete, resolved field list>}}, a real desugar enumerating the target record’s own declared fields rather than a port of the current string emitter’s raw …spread splice. Question/Is stay deferred to P6.4’s Pattern IR (Match/IrArm are still uninhabited placeholders); Ok/Err/Some/None stay deferred pending a built-in-type identity decision no ADR has made; Expect/Val/Observation/Trace/Wire stay deferred as test-body-only, unreachable through the existing single-file lowering test harness — each todo!() now names its own specific blocker instead of a bare “not yet”. No consumer yet and no bynk-emit emission-behaviour change (#1145)
v0.247.36P6.2 (design/tracks/the-ir.md §6) — bynk-check’s Callee gains Store/Query variants, recorded at checker.rs’s store-field dispatch ladder (closing R6.5’s/R6.12’s classification-level defect); bynk-emit::ir::lower now constructs Call/Lambda/Variant driven entirely by Callee, closing R6.10 for these node kinds. No consumer yet and no bynk-emit emission-behaviour change — the production cutover (replacing lower_method_call/lower_call’s own TS-emission) is deferred to a future slice, since no IR-to-TypeScript printer exists yet for any node kind (#1143)
v0.247.35P6.1 (design/tracks/the-ir.md §6) — bynk-emit gains an internal ir/ir::lower module pair: the full Part 6.2 IrExpr/IrExprKind/IrStmt shape, and a &CheckedProgram → Ir lowering pass implemented for Const/Local/Global/Record/Field/List/Block/If/And/Or/Not/Return/Await/Send/Pure. No consumer yet and no bynk-emit emission-behaviour change — P6.2 onward wires it in (#1141)
v0.247.34P6.0 (design/tracks/the-ir.md §6) — bynk-check gains a resolved Callee classification for every call-shaped expression check_call/check_static_call/check_method_call/check_cross_context_call/check_cross_context_capability_call/check_test_service_address dispatch, recorded once during checking (TypedCommons::callees) rather than re-derived; bynk-emit’s lowering is unchanged and un-consumed by this slice (#1139)
v0.247.33Settle phase 6 of the compiler trajectory (design/tracks/the-ir.md) — the IR and its CheckedProgram → Ir lowering pass land inside bynk-emit rather than as new bynk-ir/bynk-lower crates, Callee dispatch classification is commissioned as new bynk-check work (not scope the retired phase-5 track missed), and lowering driven from a certified CheckedProgram enforces IrExpr’s total-by-construction type guarantee with a panic-not-fallback discipline rather than requiring expr_types to become an IndexVec first — scoped to that path only, since test-suite emission’s own non-certified TypedCommons producer keeps its existing fallback
v0.247.32P5.5 (design/tracks/semantics-in-the-checker.md) — bynk.project.schema_registry_corrupt and bynk.secrets.computed_name relocate from bynk-emit to bynk-check, validate.rs (empty since P5.3) is deleted, and bynk-emit’s crate doc/Cargo.toml description are corrected to what remains (TypeScript emission plus per-unit build sequencing) — no diagnostic is constructed in bynk-emit any more (emit_diagnostics true = 4, all test-module assertion strings), the track’s completion criterion (#1126)
v0.247.31P5.4 (design/tracks/semantics-in-the-checker.md) — process_tests/process_integration_tests’ checking half relocates from bynk-emit to bynk-check as bynk_check::test_suites, closing the seventh and last whole-project diagnostic category P4.2 silenced in the editor, plus the test-file go-to-definition/find-references regression that rode with it (#1126)
v0.247.30P5.3 (design/tracks/semantics-in-the-checker.md) — schema-registry reconciliation and platform-lock enforcement relocate from bynk-emit to bynk-check, closing the last two of the seven whole-project diagnostic categories that don’t already originate there (#1126)
v0.247.29P5.2 (design/tracks/semantics-in-the-checker.md) — check_function_type_boundaries relocates from bynk-emit to bynk-check, restoring the fourth of the five whole-project diagnostic categories P4.2 silenced in the editor (#1126)
v0.247.28P5.1 (design/tracks/semantics-in-the-checker.md) — check_event_subscriptions relocates from bynk-emit to bynk-check, restoring a third of the five whole-project diagnostic categories P4.2 silenced in the editor (#1130)
v0.247.27P5.0 (design/tracks/semantics-in-the-checker.md) — check_messages_bundles and check_locale_bundle_ambiguity relocate from bynk-emit to bynk-check, restoring two of the five whole-project diagnostic categories P4.2 silenced in the editor (#1128)
v0.247.26Settle phase 5 of the compiler trajectory (design/tracks/semantics-in-the-checker.md) — the remaining bynk-emit diagnostic sites relocate to bynk-check by priority (five close a named, fixture-pinned editor regression; two close for R3.5 compliance alone), check_function_type_boundaries’s reach-back hook closes, and R10.1 closes with a crate-doc correction rather than a bynk-driver split
v0.247.25bynk-ide repoints off bynk-emit onto bynk-check/bynk-project directly, closing R10.2; the editor’s project analysis silently stops reporting five whole-project check categories until phase 5 ports them (#1122)
v0.247.24bynk-check gains a project-level analysis entry point (analyse_project); symbols, context-checks, and run_checks’s orchestration relocate from bynk-emit to back it (#1115)
v0.247.23Settle phase 4 of the compiler trajectory (design/tracks/project-model.md) — bynk-project extracts today’s project-model logic below both bynk-check and bynk-emit; contract hashing and the typed ProjectGraph defer to phase 8; a new bynk-check analysis entry point closes R10.2 without moving run_checks early
v0.247.22fs_below_driver probe-precision follow-on (#1104) — the R2.3 probe now classifies each flagged file as a named floor (every production-scope std::fs touch attributable to one of content-ownership’s three settled carve-outs — discover_bynk_files, read_adapter_binding, try_read_project_paths — or a bare import declaration enabling one of them) versus a residual violation; design/greenfield-status.md’s committed bynk-emit reading now states “3 (3 named floor, 0 residual)” instead of a bare 3 a reader had to cross-reference against retired track docs to interpret
v0.247.21content-ownership track slice 5 (#1086, #1102) — deletes bynk-emit’s read_source disk-read fallback for .bynk project sources, the track’s actual target; found and fixed under implementation, three real production-path dependencies on that fallback (bynk-lsp’s run_project_diagnostics/type_receiver open-buffers-only overlay, AnalysisRoots::lower’s bynk.toml read for every manifest-backed caller, and try_read_project_paths’s plain disk-read contract), plus carved out a deliberate, permanent exception for adapter .binding.ts reads (a distinct concern whose path is only known post-parse, so no discovery walk can pre-populate it); adds a real Backend-driven behaviour test proving an unsaved edit in one file is visible to completion in another
v0.247.20content-ownership track slice 4 sub-slice 3 (#1086, #1098) — the remaining 78 diagnose_project/CompileOptions::single/::split call sites in bynkc/tests, bynk/tests, and bynk-emit’s own #[cfg(test)] module migrate to complete sources maps, finishing slice 4’s ~120-site migration; no separate CI guard was needed in the end (slice 5’s own fallback deletion is the loud-failure mechanism)
v0.247.19content-ownership track slice 4 sub-slice 2 (#1086, #1098) — all 25 bynk-lsp/tests diagnose_project(&root, &HashMap::new())/diagnose_project_with call sites migrate to bynk-testkit’s complete sources map, including project_model.rs’s shared rel_files helper (fixing 8 further tests indirectly)
v0.247.18content-ownership track slice 4 sub-slice 1 (#1086, #1098) — bynk-ide’s own 18 inline-test diagnose_project(&root, &HashMap::new()) call sites migrate to an in-crate testkit module built on production discovery; fs_below_driver stays at bynk-ide=0
v0.247.17content-ownership track slice 3 (#1086, #1096) — a new dev-only bynk-testkit crate, built directly on production discovery, replaces diagnose_project(&root, &HashMap::new()) and bare CompileOptions::single/::split in three representative test fixtures, ahead of the full ~120-site migration (slice 4)
v0.247.16content-ownership track slice 2 (#1086, #1094) — AnalysisRoots::lower()‘s bynk.toml read joins the overlay, so an unsaved edit to bynk.toml itself changes the resolved [paths] include/exclude, mirroring 343b2482’s CLI-side fix
v0.247.15content-ownership track slice 1 (#1086, #1092) — bynk-ide’s cross-file symbol lookups (go-to-declaration, hover’s cross-file fallback) take pre-read content instead of reading disk themselves; Backend::project_files retires, fs_below_driver reaches 0 for bynk-ide
v0.247.14Slice 0+1 of the content-ownership track (#1086) ships — bynk-lsp’s completion, signature help, and hover no longer read project files from disk themselves; ADR 0322’s ProjectDirs/resolve_dirs design is superseded, unneeded once implementation found bynk_ide::discover_files already closes the gap
v0.247.13Settle the content-ownership track’s remaining design questions (#1086) and front-load three ADRs — bynk-ide’s ProjectDirs seam type, R2.3’s content-only enumeration scope, and the bynk-testkit crate convention
v0.247.12Resolve #1078 — bynk.schema.lock read/write moves to bynk-driver; bynk-emit only ever sees pre-read content and hands back what to write
v0.247.11bynk-driver reads bynk.toml through its own overlay instead of bynk-emit’s disk fallback, closing a gap #1081 (#1077) left uncaught
v0.247.10Breaking (Rust API, not language surface): bynkc no longer re-exports bynk-syntax’s ast/diagnostics/error/keywords/lexer/parser/span modules, bynk-driver’s coverage/test_json modules, or the whole bynk-fmt crate as bynkc::fmt (only bynkc’s item re-exports, CompileError, CompileOptions, compile_project, and others, remain public)
v0.247.9Resolve #1049 — decline the Positive/NonNegative → InRange fold; neither base has a writable bound that could stand for infinity
v0.247.8bynk-driver’s CLI-path project discovery (#1081) no longer silently skips the no_sources/file_and_directory project checks, panics on a missing include root, or produces nondeterministic build/diagnostic order”
v0.247.7The checker interns every Ty behind a TyId handle, making type identity a u32 comparison rather than a recursive structural walk
v0.247.6The checker’s debug-only expr-identity uniqueness check (finding #28) now also covers handler and test-case bodies, not only top-level functions; ADR 0313 is superseded
v0.247.5The phase-3 track (identity and totality) settles on ExprKey(Span) scaffolding before ExprId, and amends ADR 0309 with an LSP-surface-fixture requirement
v0.247.4A ?’s propagating early return nested inside a short-circuited &&/`
v0.247.3cargo xtask ci runs the CI gates locally (--fast for the two that need no compile-and-link, also run by an opt-in .githooks/pre-push), and CI reports a CI fast gates check ahead of CI green
v0.247.2A value-position match/if IIFE’s async-wrap decision is a flag computed during lowering instead of a scan of the generated text for the substring “await “
v0.247.1The lowering pass returns hoisted statements instead of writing them into a caller-supplied sink, deleting the predictive classifier that gated the ternary-form if
v0.247.0NonEmpty now canonicalises as MinLength(1) (R12.2’s Length domain), so a boundary type spelled either way hashes and structurally matches identically — coordinated redeploy: rebuild both sides of any already-deployed pair sharing a NonEmpty-refined boundary type”
v0.246.11block_uses_send and bynk-lsp’s three statement-aware extract walkers enumerate every ExprKind variant explicitly instead of a wildcard arm (#1025)
v0.246.10block_writes_state’s expr walker enumerates every ExprKind variant explicitly instead of a wildcard arm (T1.7’, #1020)
v0.246.9fs_below_driver resolves bare fs:: call sites through glob-imported use std::fs bindings, so bynk-emit/src/project/discovery.rs counts (#1013)“
v0.246.8The Query method_not_found diagnostic lists its methods from QUERY_METHODS instead of a hand-written copy, and the registry’s drift test now catches a dispatch arm the registry doesn’t list, not only the reverse
v0.246.7tree-sitter-bynk/tests/conformance.rs widens from a fixed case list to totality: examples/, the vendored first-party .bynk sources, and every bynkc positive fixture must parse clean under tree-sitter, and every parse/lex-time negative fixture must be rejected by both parsers”
v0.246.6Track slice T0.3: [workspace.lints.clippy] wildcard_enum_match_arm = "warn" recorded in the root manifest, closing the workspace_lints probe’s absent reading; the 296-violation inventory is recorded on the spine (#996), not yet enforced per-crate
v0.246.5”T0.2′ (first slice): the three Roots::Split negative fixtures whose reported diagnostic path re-bases (ADR 0198) — 104_state_sum_field, 105_state_opaque_field, 106_state_refined_no_zero — gain a mutation-checked expected_diagnostics.txt, closing the coverage hole ADR 0198 recorded as unobserved for this population”
v0.246.4Breaking (Rust API, not language surface): bynkc no longer re-exports fourteen whole modules from bynk-check/bynk-emit (bynkc::checker, bynkc::resolver, bynkc::emitter, bynkc::project, and ten others) — only its ~30 item re-exports (CompileOptions, compile_project, BuildTarget, …) remain public”
v0.246.3closes_rule: pending-file field + stamp-materialised rule ledger, closing out T0.0’s deferred Closes-Rule provenance (#1001)“
v0.246.2cargo xtask greenfield-status — the T0.0 probe harness (13 probes, 9 gated against a committed table)“
v0.246.1Settle the compiler-architecture track — the refactor acceptance gate, the emit-ABI posture, and the lowering substrate
v0.246.0Hover and a VS Code “Show Wire Contract” panel surface a handler’s request envelope, its cross-context contract hash, and each boundary type’s re-validation strategy — derived from a new shared IR the emitter’s own codec generation now renders too
v0.245.0A VS Code webview maps a whole project’s contexts, their consumes edges, and the capabilities/providers/services/agents each one binds
v0.244.0”Events track: a via schema(N) clause on a from Events(…) subscription header dispatches delivery by the envelope’s schemaVersion, independent of the existing payload pattern.”
v0.243.0”Events track: the cross-build schema registry (bynk.schema.lock) computes each event’s schema version from its build history, auto-bumping on additive shape changes and verifying a declared @schema(N) against the computed value.”
v0.242.0An event may declare an optional @schema(N) annotation, embedded into env.schemaVersion at emission (default 1); an unknown event annotation or a malformed @schema is now a compile error
v0.241.0An event’s own fields may carry a default expression (field: T = expr), so an older wire event missing a newer field’s key deserialises using the default instead of failing structural-mismatch; a default on a non-event record field is now a compile error
v0.240.2bynkc fmt / bynk fmt read the project’s bynk.toml [fmt] section as the layer under their flags”
v0.240.1Event payloads and envelopes are validated at the Workers subscriber boundary
v0.240.0An on event(e: E, env: EventEnvelope) handler’s optional second parameter carries runtime metadata about the emission — eventId, publisherId, emittedAt, and a reserved schemaVersion — enabling the Idempotency.dedup/remember idiom keyed on env.eventId for effectful subscribers
v0.239.0A from Events(E) subscription may filter delivery with a structural pattern, from Events(E { field: value, .. }) — deliver-and-filter, no static narrowing of the handler’s parameter
v0.238.1Per-publisher event ordering is scoped to non-concurrent emission from one agent — verified empirically on real workerd, and narrowed from the broader claim design/tracks/events.md §7 originally asserted
v0.238.0The Events capability, slice 0 — event declarations, given Events emission with owner-only enforcement, and from Events(E) subscription, across contexts and across all three platforms
v0.237.1The Events track’s foundational ADRs — fan-out substrate, closed-protocol-set extension, pattern-dispatch semantics, and the replay split — land before slice 0
v0.237.0Idempotency.dedup/remember scope the caller’s key to the calling handler’s own qualified name, so two unrelated handlers can’t collide on the same literal key
v0.236.0The Idempotency capability — mechanical dedup for at-least-once delivery, slice 0 (in-memory provider)
v0.235.0A capability operation may declare its own type parameter (capability X { fn op[T](…) -> … }, ADR 0281), resolved only from an explicit call-site type argument (X.op[SomeType](…)) and emitted as a genuine generic TypeScript interface method; a generic operation requires an external (bodiless) provider and cannot be stubbed
v0.234.0LocaleTag widens past language[-Script][-REGION] to admit BCP-47 variants, extensions, and private-use subtags (messages "ca-valencia", messages "en-US-u-ca-buddhist", messages "x-custom"), while still rejecting grandfathered/irregular tags
v0.233.7The emitter now gives a same-block re-let of a name (let x = 1; let x = x + 1) its own emitted identifier instead of reusing the source name, so the second const no longer collides with the first and fails tsc (TS2451) — shadowing itself was already accepted by the checker and is a deliberate ML-family idiom (ADR 0064)
v0.233.6The checker now rejects direct construction of a uses-sourced commons sum type’s variant (bare or qualified constructor call) inside a context, where the per-context rebrand leaves that constructor out of value scope — closing the enforcement gap ADR 0256 documented, so bynkc check catches what would otherwise be a clean check followed by a tsc failure
v0.233.5A test-scaffold module’s Json.decode[T]/Json.encode on a named record now generates its own serialise_*/deserialise_* closure and namespace-qualifies the type, instead of emitting a call to a codec the unit never exports and a bare type name the module never declares
v0.233.4An inlined boundary codec, or a Json.decode[T], reaching a Worker’s compose.ts or a test-scaffold module now imports the runtime helpers and types it names, instead of emitting TypeScript that references an unimported name
v0.233.3An adapter-declared package name or version range carrying a control character no longer emits an invalid package.json; conditional runtime imports are decided from what emission referenced rather than by scanning the generated text
v0.233.2The editor now hovers and completes the first-party bynk.locale / bynk.locale.types surface (LocaleTag, Message, render, message, the with* builders); the hand-maintained per-consumer source lists are unified into one firstparty::FIRSTPARTY_SOURCES with a drift guard so a new first-party commons cannot be silently omitted again
v0.233.1A messages select placeholder now dispatches its arm by own-property check, so a MessageArg.Text value naming an Object.prototype member ("constructor", "toString", "__proto__") falls back to the mandatory other arm instead of resolving off the prototype chain
v0.233.0A messages block’s locale tag is a checked LocaleTag string literal (messages "pt-BR"), so region/script tags are declarable and an invalid tag is bynk.messages.invalid_locale_tag rather than reaching the runtime
v0.232.0bynk.locale’s types split into a leaf commons, bynk.locale.types, closing Locale slice 2’s uses-collision gap”
v0.231.0The Cloudflare Locale provider negotiates Accept-Language against a context’s message bundle (RFC 4647 basic filtering)
v0.230.0messages templates gain ICU plural/select/number/date placeholders, formatted via the host Intl
v0.229.2The sequence-diagram view links each participant’s click-to-code to its own box — Mermaid 11 emits several .actor nodes per participant, so the previous .actor index zip mislinked (or failed to link) participant boxes
v0.229.1Sequence diagrams show the handler’s by principal as an actor that originates the request and receives the replies; return-gating branches no longer collapse to an empty alt
v0.229.0A second, non-reference messages locale now actually renders — completeness and cross-locale placeholder-agreement checking, plus the bundle’s declared-locale set exported for Locale’s own negotiation
v0.228.0The messages construct compiles a locale’s message bundle to a lookup and a bundle-aware render, wired to bynk.locale’s bundle-free render (ADR 0256) as its fallback
v0.227.0bynkc test --coverage (and bynk test --coverage) reports statement/line coverage attributed to .bynk source — a rich summary table, or a coverage block under --format json — collected via V8’s NODE_V8_COVERAGE and remapped through the emitted source maps, with the generated TypeScript invisible; closes #854”
v0.226.0Capability-aware quick-fixes — add a missing consumes, fill missing record fields, and auto-uses/consumes an unresolved name (#852)
v0.225.0VS Code documentation view — a file’s doc comments as a rendered reference page (bynk/documentationModel + webview)
v0.224.0Diagnostic codes are teachable — curated codes carry a codeDescription link to their Book explanation in the editor, and bynk explain <code> prints the offline-complete blurb, an example, and the link
v0.223.1The VS Code extension renders doc comments (--- … ---) in place — heading colour, bold, italic — via editor decorations, toggled by bynk.inlineDocRendering.enable
v0.223.0”Bynk: Show Sequence Diagram” (VS Code command + per-handler CodeLens) renders a Mermaid sequence diagram for the handler under the cursor, via a new bynk/sequenceModel LSP query
v0.222.0Doc comments resolve [Name]/[Owner.member] links against the project’s binding index, navigable via document links and hover
v0.221.0Add the Locale capability (fixed "en" on every platform) and a bynk.locale commons providing LocaleTag, Message/MessageArg, and a bundle-free render
v0.220.2bynk deploy reports orphaned resources and prunes them with --prune; a deleted KV namespace self-heals like a deleted queue already does”
v0.220.1bynk deploy --env NAME for independent multi-environment provisioning and deploy; bynk dev -- --remote reads the matching environment”
v0.220.0Refined patterns (_ where <predicate>) in match arms
v0.219.0Or-patterns (`p₁
v0.218.0A system-tier case mixing Wire(…) with a by Nobody call now drives a raw no-auth driver, instead of silently reaching the typed no-auth driver unconverted
v0.217.1bynk-lsp’s root-cache write-back is guarded against a bynk.toml/workspace-folder invalidation racing an in-flight filesystem walk
v0.217.0textDocument/codeAction’s extract-function refactor now also accepts a contiguous run of full statements (optionally including the block’s tail), not just one expression, closing #813”
v0.216.5A system-tier case can mix a typed argument with Wire(...) in the same http address call
v0.216.4bynk-lsp memoises URI to project-root routing, so hover/completion/etc. stop re-walking the filesystem on every request
v0.216.3The playground editor offers context-aware completion (capability methods, types, keywords, in-scope locals, value-receiver members) via a bynk_complete wasm entry (#808, split from #397/#393)
v0.216.2Completion, symbols, locals-navigation, and signature-help move from bynk-lsp into bynk-ide, unblocking a future wasm-side completion entry (#808)
v0.216.1Completion, symbols, locals-navigation, and signature-help move from bynk-lsp into bynk-ide, unblocking a future wasm-side completion entry (#808)
v0.216.0textDocument/codeAction offers an extract-function refactor (RefactorExtract), capability-free-only”
v0.215.2LSP .-completion on a store Map receiver now offers the query builders/terminals (filter/map/sortBy/collect/…), the .entries/.keys/.values accessors, and the entry ops (put/get/update/…), closing the gap ADR 0184 left deferred
v0.215.1LSP codeAction now filters its response against CodeActionParams.context.only
v0.215.0Call hierarchy now records capability-op and agent-handler-dispatch call edges, closing the under-reporting gap in #304
v0.214.4The playground editor shows the inferred type of the expression under the cursor on hover (#397)
v0.214.3The playground’s share service expires stored snippets 30 days after creation, via Kv.putTtl, instead of retaining them indefinitely.
v0.214.2”vscode-bynk: add the missing match snippet (variant/binding arms + wildcard fallback), closing out #307”
v0.214.1”vscode-bynk: pressing Enter inside a -- line comment now continues it, and inside/after a --- doc-comment fence now keeps the same indentation instead of falling back to VS Code’s generic behaviour; the two are disambiguated so a --- fence is never treated as a -- line comment (closes #306).“
v0.214.0textDocument/codeAction offers an extract-variable refactor (RefactorExtract) for a selected expression
v0.213.0bynk-lsp implements workspace/willRenameFiles — renaming or moving a .bynk file rewrites its own declaration and every other file’s uses/consumes reference to it (closes #302). Single-file rename only (the capability filter matches files, not folders); a suite file, which addresses no name of its own, produces no edits.”
v0.212.2”vscode-bynk: a resolved bynkc-lsp older than the extension’s pinned server version now gets an actionable warning (“Download Matching Server”) instead of a passive note, since a stale server (most often one found on PATH) can silently mis-diagnose syntax the checker already accepts (closes #484).“
v0.212.1”vscode-bynk: the bynkc: check build task, the Test Explorer, and test debugging now shell the bynk driver instead of bynkc directly (closes #486), inheriting its richer compiler resolution (BYNK_BYNKC → PATH → sibling-of-bynk) in place of a bare-PATH lookup that missed a driver-first install. bynk.compilerPath is forwarded as BYNK_BYNKC so it keeps pinning an exact bynkc; bynk.bynkPath (previously only the bynk dev debug session’s setting) now also governs these three surfaces.”
v0.212.0”Generic sum types — `type ApiResult[T] =
v0.211.0A workers context generates its own cross-context boundary codecs and imports no sibling context’s module as a value
v0.210.0LSP decoration requests (semantic tokens, inlay hints, code lenses, document links, code actions) serve the last committed round instead of forcing a whole-project re-analysis on every keystroke, revalidating via workspace/*/refresh
v0.209.0A generic type may carry instance methods — fn Box.map[U](self, f: A -> U) -> Box[U] erases to a generic namespace-object method (#594)
v0.208.2The deploy ledger is written atomically and a truncated ledger is rejected rather than re-minting every namespace
v0.208.1fmt now verifies its output round-trips to the same code before writing (refusing with bynk.fmt.roundtrip rather than overwrite a file with mis-rendered or non-parsing output), writes in place atomically via a temp file + rename so an interrupted write can no longer truncate or empty the source, and honours --check on stdin (bynk fmt --check - reports a diff and exits non-zero instead of echoing the reformat and passing green)“
v0.208.0HS256 bearer verification requires an exp claim (a token with no expiry no longer verifies)
v0.207.0A match arm or if branch may now produce a refined type where a sibling produces its base (or another refined type over the same base) — the branches join to their least upper bound instead of being rejected for not being byte-identical, so match r { Ok(e: Email) => e, Err(m: String) => m } type-checks at String
v0.206.1The playground wasm installs a panic hook and converts an internal compiler panic to a diagnostic, so adversarial input no longer traps as an opaque RuntimeError (#717)
v0.206.0Unresolvable explicit call type arguments, Json.decode[T] targets, and lambda parameter annotations in handler bodies are now reported instead of silently swallowed
v0.205.1Project-level check/compile diagnostics (consumes cycles, path/name mismatches, the reserved-namespace and adapter-binding checks, uses/consumes/exports validation, provider signature matching, …) now render with ariadne source context in directory mode instead of the plain [category] message fallback
v0.205.0A bundle-mode on call … by c: Caller handler reads a live CallerId — its emitted makeSurface deploy surface threads the calling context’s name into deps.identity, where it previously emitted deps without the field and broke tsc
v0.204.0Reject redeclaring a built-in type name (List, Query, QueueResult, …) with bynk.resolve.reserved_builtin_type; document keywords as three tiers
v0.203.0”Record construction in a service/agent handler body now validates the whole field set — a missing required field, an undeclared extra field, a duplicate initialiser, or a shorthand { name } with no binding in scope is rejected (bynk.resolve.missing_field / unknown_field / duplicate_field_init / unknown_name), closing a soundness hole where such a record could cross the HTTP boundary; the checks already governed fn/method bodies (#711).“
v0.202.0A Matches refinement that nests unbounded quantifiers is rejected (bynk.types.catastrophic_regex) to close a ReDoS hole on the request boundary
v0.201.1”A boundary handler parameter whose name is a JavaScript reserved word (class, void, public, static, delete, …) no longer emits an invalid const class = … binder that breaks the Worker build — the entry-point and compose wrappers now route every such binder through ts_ident, exactly as the surface already did (#723).“
v0.201.0A route path containing a backslash, newline, or tab is now escaped through the canonical string escaper at every router emit site — previously only " was escaped, so an internal backslash silently drifted the emitted path (\b read as backspace) and a trailing backslash escaped the closing quote and failed to compile the Worker (#721)
v0.200.0A queue name (from queue("…")) or cron expression (schedule("…")/on cron) is now TOML-escaped in the generated wrangler.toml, so a source literal carrying ", \, or a newline can no longer break out of the queue = "…" / crons = ["…"] string and inject deploy-config keys (#722)
v0.199.0”Held-resource linearity (§2.9) is now enforced in fn and method bodies, not only handler bodies: a function that receives a held value (Connection[F]) owns it and must dispose it, so a leaking fn swallow(c) reports bynk.held.leak and a double c.close() reports bynk.held.use_after_consume — previously both compiled silently, leaving held-resource safety unenforced outside handlers (#718).“
v0.198.0A doc block containing */ can no longer terminate the emitted JSDoc comment early and inject top-level TypeScript
v0.197.0The held-resource linearity pass now governs match-arm pattern bindings — a Connection bound out of an Option/Result in a match arm must be disposed, closing a leak the pass missed (#719)
v0.196.1”A lex error inside a string-interpolation hole (\"…\\(…)…\") is now reported at the offending bytes within the hole instead of at the file’s opening bytes — the hole is re-lexed on its own and the error’s spans were never rebased on the failure path, so an unexpected character, an integer overflow, or any lex error pointed at the wrong location and could split a multi-byte codepoint, tripping the parser’s char-boundary invariant and panicking a source-slicing consumer (#716).“
v0.196.0A long operator or member chain (1 + 1 + … + 1, a.b.c…, !!!…) is rejected with bynk.parse.nesting_too_deep instead of overflowing the stack on a valid program (#714)
v0.195.0A system-tier test case drives an existing http path with a method it declares no handler for and observes the router’s 405 fall-through as Rejected(MethodNotAllowed) (#707)
v0.194.0”The parser and interpolation lexer bound recursion depth, so pathologically nested source reports a diagnostic (bynk.parse.nesting_too_deep, bynk.lex.interpolation_too_deep) instead of overflowing the stack and aborting the process — the front-end the CLI, LSP, and in-browser playground all share (#713).“
v0.193.0An if/match condition ending in a bare identifier no longer swallows a single-identifier brace body as a record construction
v0.192.0The tree-sitter grammar and the compiler parser are held in agreement by a cross-parser conformance test; Bytes joins the grammar’s base types, built-in generic arity is expressed in the grammar, and a lowercase sum/enum variant name is rejected (bynk.parse.variant_name_case)
v0.191.0A system-tier test case drives a secured http route with by Nobody — the no-credential principal — so the real auth seam rejects the unauthenticated request; the call yields Rejected(Unauthorized), decoded by responseToUnauthOutcome (#706)
v0.190.2”The LSP no longer panics when a multi-byte non-identifier char (\", , , an emoji) precedes the receiver/callee it extracts from the line prefix — receiver and callee extraction now advance past the matched char by its UTF-8 length instead of assuming one byte, so completion, signature help, and hover survive a keystroke inside a string literal (#715).“
v0.190.1bynk new preserves a hand-written .gitignore instead of overwriting it — a target holding only .git/ and a user .gitignore scaffolds, and the template .gitignore is written only when none is present (#737).“
v0.190.0The is operator tests nested variant patterns structurally — r is Rejected(RefinementViolation(_)) checks the inner tag as well as the outer — so a system-tier Wire test can discriminate which boundary rejection occurred (#705)
v0.189.0A system-tier test case drives an http route with a raw Wire(<String>) argument — pre-validation input the type system forbids — and observes the boundary reject it before the handler (Rejected) or handle it (Handled), via a raw driver and the responseToHttpOutcome decoder
v0.188.1A from http route’s boundary-rejection responses (400/401) now carry the service’s security headers (nosniff/HSTS) and CORS, exactly as its handled 200 does — restoring ADR 0164 D6 on the rejection path (#659)
v0.188.0The stamp workflow pushes as a GitHub App with ruleset bypass — main is protected, so the direct GITHUB_TOKEN push ADR 0206 assumed cannot work (amends ADR 0206)
v0.187.0A test case drives an http route at the system tier over a real fetch with a framework-signed credential; system_needs_wire relaxes to a serialisation edge
v0.186.0Increments are stamped on merge, not authored (tooling; ADR 0206, increment-allocation track, #691). Completes the increment-allocation track. A feature PR now lands a design/pending/<slug>.md carrying only its intent — a bump level, a one-line changelog blurb, and any ADR prose — with no version and no ADR number; a per-merge GitHub Actions workflow runs cargo xtask stamp on main to assign both in merge order and materialise them (the version bump, the changelog row, the numbered ADR + index row), deleting the consumed pending file. Two increments developed in parallel no longer touch any shared version or index line, so they stop colliding on the two serial counters — and the loser no longer silently ships a number another increment already took. Built across three slices: the pending-increment format + validator (#688), the cargo xtask stamp command (#690), and the workflow here. This is the last increment stamped by hand. No language, grammar, or runtime change; no author-facing surface beyond the new design/pending/ step.
v0.185.0A test drives an http/cron/queue handler at the unit tier (grammar + checker + emitter; ADR 0205, testing-the-boundary track, #664). A case addresses a service handler by its surface — api.POST("/todos", body), sched.schedule("*/5 * * * *"), q.message(job) — and names the identity it acts as with a call-site by <Actor>(<identity>) clause: let item <- api.POST("/todos", body) by User("bob"). The address is checked against the declared handler (arity, argument types, service_unknown_route); the principal is checked against that handler — an identity-carrying handler driven with no by, or with a unit-identity actor like by Visitor, is bynk.test.principal_required / principal_identity_mismatch (the isolation the slice exists to provide, which tsc cannot catch); and the identity is typed against the handler’s identity type, so by User("") fails UserId’s refinement. At the unit tier the identity is given and the handler runs in-process against fresh per-case agent state — scheduled and queue handlers execute under a test for the first time. The proposal’s feared deps.envmakeTestState bridge was unnecessary: bundle-mode emission already resolves an http handler’s agent via the in-memory registry. No new runtime, no fetch; the system tier is a later slice. Grammar gains call_site_actor (Rust parser + tree-sitter + formatter). #655 is not closed — its root is the cross-context makeSurface, a separate emitter concern (ADR 0205).
v0.184.0One diagnostics scheduler (tooling; closes #678, LSP foundations track, #640). The language server debounced diagnostics with two stacked delays — the configured diagnostics_debounce_ms in did_change, then a second, hardcoded 200 ms in the round scheduler — so live squiggles lagged the configured value by 200 ms; and a single-file buffer (no bynk.toml) had no generation counter at all, so a burst of keystrokes ran one diagnose per change instead of coalescing. Both are now one generation-based scheduler over both modes, debouncing exactly once at the configured delay. A round already analysing when a newer edit lands still runs to completion (a whole-project analysis on a blocking thread can’t be cancelled) but its result is discarded rather than published — unchanged, and documented. No language, grammar, or runtime change; no author-facing surface beyond the (now honest) debounce timing.
v0.183.0The language server analyses a workspace on open and registers its own file watchers (tooling; closes #676, LSP foundations track, #640). Two lifecycle gaps the server documented or leaned on a client for. First: it advertised a startup analysis but ran none, so a freshly-opened Bynk workspace showed no diagnostics until you opened a .bynk file. Now on activation the server discovers every project under the workspace folders — a bounded walk for bynk.toml, so a monorepo’s packages are all found — and analyses them, so diagnostics (and workspace-symbol search) are ready before you open anything; a folder added later is warmed the same way. Second: watched-file notifications only worked because the VS Code extension supplied the watchers client-side, so every other editor got a dead handler. Now the server registers **/*.bynk and **/bynk.toml itself (dynamic didChangeWatchedFiles), so any client is notified — and the VS Code extension drops its client-side watchers so the server isn’t told twice. No language, grammar, or runtime change; no author-facing surface.
v0.182.0The language server implements real multi-root workspaces (tooling; ADR 0204, closes #673, LSP foundations track, #640). The server advertised the workspace-folders capability but implemented one project — it took the first workspace folder and ignored the rest, and had no handler for folders being added or removed. So a file in a second folder, or a second bynk.toml project under one folder (a monorepo), was analysed against the wrong project or none. Now the server holds a project per discovered root: a request routes to its file’s nearest bynk.toml — the same project bynkc compiles it in — so several projects in one window each analyse, version, and publish independently, a file outside every project still gets single-file diagnostics, and workspace/symbol searches across all of them. Adding or removing a workspace folder adds or prunes its projects (a project with an open file is kept until you close it). No language, grammar, or runtime change; no author-facing surface — the capability the server always advertised is now true.
v0.181.0A test’s svc.call(…) is resolved, not string-matched (checker; ADR 0203, closes #654, testing-the-boundary track, #656). A test case invokes its target’s RPC service as svc.call(args). The checker used to accept that for any service in scope — matching only the method name call — without confirming an on call handler existed or that the arguments fit it. So api.call(…) on a from http service passed bynkc check and then crashed at runtime with TypeError: api.call is not a function, because the emitted http service has route methods, not a call. Now the call resolves the service’s on call handler: a from http/cron/queue service (no such handler) is bynk.test.service_no_call_handler; wrong arity is bynk.test.service_call_arity; a mismatched argument reuses bynk.types.argument_mismatch (with its argument index). A correct svc.call checks, lowers, and runs exactly as before. This is the first slice of the testing-the-boundary track — the foundation the http/cron/queue address surface builds on. No grammar or runtime change; the only surface change is that three previously-silent mistakes are now check-time errors.
v0.180.0The language server gains a library target (tooling; closes #665→#669, LSP foundations track, #640). Internal refactor, no user-visible change: bynk-lsp had a binary target and no library, so its integration tests could not name the crate and instead #[path]-included individual source files — a workaround that also ran each module’s unit tests redundantly in every test binary that included it. The crate now has a [lib] target; the server implementation moved from src/main.rs (now a thin entry point) into src/lib.rs, and the tests use bynk_lsp::… like any dependency. The behaviour-over-time tests the freshness and project-model work introduced leave the binary and live in the library. No language, grammar, protocol, or runtime change.
v0.179.0The editor answers against the buffer you’re holding (tooling; ADR 0202, closes #665, LSP foundations track, #640). Hover, go-to-definition, references, rename and the other index-backed features used to answer from the last completed analysis round. If you had typed since — inserted a line above the cursor, say — a request could resolve your cursor against text the analysis never saw, and land on the wrong symbol. It corrected itself a moment later, which is exactly what made it easy to miss. Now every such request checks whether the file has been edited since the last round and, if so, re-analyses the current buffers before answering — so it resolves against the text you actually have, or (when it genuinely can’t, like a file outside the project) answers nothing rather than something wrong. A round is fast (single-digit milliseconds for a typical project), the refresh replaces the pending debounced analysis rather than doubling it, and several requests after one edit share a single re-analysis. Published diagnostics now also carry the document version they were computed against, so your editor can discard a squiggle whose line has already moved. No language, grammar, or runtime change; no author-facing surface.
v0.178.0The LSP analyses the project bynkc compiles (tooling; ADR 0201, closes #647, LSP foundations track, #640). The editor and the compiler read the same bynk.toml and disagreed about what it meant. The language server reduced [paths] include to a single directory — the first entry, with exclude ignored — and then looked for that directory inside itself, so: a second include tree was invisible, exclude did nothing, and a flat project (.bynk at the root, no src/) found nothing at all. Anything the editor derives from the project — diagnostics, references, rename, workspace symbols, completion — could therefore disagree with bynkc about which files exist. In this repository, examples/todo/tests/todos.bynk is a real test suite the compiler compiles and the editor could not see. It can now: the editor analyses exactly the files bynkc compiles, from the same manifest, through the same discovery. Hover, completion, semantic tokens and signature help all gain that reach — they behave identically, they simply see files that were previously not analysed at all. No language, grammar, or runtime change; no author-facing surface.
v0.177.0A compiled contract hash at the cross-context boundary — the deploy-skew guard (checker + emitter + runtime + driver + docs; ADR 0200, closes #643 and #550). v0.176 made the workers boundary typed; it did not make it verified. Context A compiles against B’s contract — the compiler reads B’s source — and nothing checked that the deployed B was still that one. bynk deploy --context NAME makes the gap routine: it pushes one context against dependencies assumed live, and its existing gate checks a dependency exists, never that it matches. The failure mode was the worst kind: not an error, but a wrong answer, as a skewed callee’s response decoded against a stale codec. Now the compiler — the only party that ever sees both contracts — stamps what it knew. Each call site carries a contract hash in a reserved X-Bynk-Contract header beside X-Bynk-Caller, and the callee compares it against its own constant before reading the body, answering 409 ContractMismatch (naming the service, the expected hash, and what arrived) rather than misreading the payload. Both constants are fixed at build time; nothing is hashed at runtime, and the args body is unchanged. bynk deploy refuses first, which is what actually answers the complaint — a 409 in production is legible but late. Each Worker emits bynk-contracts.json (what it provides, what it expects); the deploy record remembers what was pushed, and a --context push that would strand a caller is refused with bynk.deploy.contract_skew before it ships. The runtime check remains the backstop for a wrangler push behind the driver’s back. The hash is only as good as the form it hashes, so the form is canonical: predicates canonicalise as a set, and record fields and sum variants sort by name — a JSON object is unordered and a sum carries a kind tag, so their order is not wire-observable and must not fire the check. That carries a fix you may notice on its own: two structurally identical types whose refinements were written in a different order (String where NonEmpty && MaxLength(10) vs String where MaxLength(10) && NonEmpty — the same type) used to be compared positionally and spuriously failed to match across a boundary. They now match. The order-insensitivity is not adjacent to the hash; it is a precondition for it — hashing source order would have shipped a spurious-failure generator. An opaque type contributes its representation but not its predicate: a consumer cannot observe it, so tightening it must not 409 callers over a change they cannot see. One-time cost: the first deploy after this version must rebuild every context. An older Worker stamps no hash, and an absent hash fails closed — deliberately, since a caller predating the check is exactly the kind most likely to be skewed. What this mints is detection, not evolution: Bynk tells you the two sides disagree; it does not yet run two contract versions concurrently, so a contract change remains a coordinated deploy. No grammar change; no args wire-format change.
v0.176.0One codec path at the workers boundary — the cross-context edge is generated, not asserted (emitter + docs; ADR 0199, closes #642 — the first half of design-review #550, which stays open on its second). Bynk’s pitch is independently deployable contexts, so the cross-context call is the seam it exists to make safe — and it was the one place the compiler gave up and asserted. The cause was three parallel codec dispatches (the real one in serialisation.rs, plus shadow dispatches on the caller and callee sides) that had drifted from each other, invisibly: the same program was fully typed under --target bundle and quietly any under --target workers. They are now one. Every value crossing a workers cross-context boundary is encoded and decoded by a generated codec — the same monomorphised helpers the bundle target and the Json codec already use. No wire position asserts a value through as JsonValue, and no return type decodes through the unvalidated ((j: any) => Ok(j)) identity the caller path used to fall back to. The asymmetries went with them: a List[T] argument was asserted outbound but validated inbound, and — the sharp one — a Bytes was cast outbound while being base64-decoded inbound. That mis-round-trip is why a bare Bytes in a workers signature had to be diagnosed rather than emitted (ADR 0142 D8); with one symmetric dispatch the cause is gone, so Bytes now crosses a workers cross-context boundary and bynk.types.bytes_at_workers_boundary is withdrawn (a rejection lifting, so no program breaks). Unifying also paid a dividend: the shared arm now reports what was required (expected: "integer") rather than the bare typeof — an Int given 3.5 used to read expected: "number", actual: "number" — and because the fix lands once, the pre-existing Json and agent-rehydration paths improve for free. An on call composition root’s parameters are now typed instead of any, which is what makes the guarantee checkable rather than asserted: with any gone, tsc --strict immediately surfaced two latent bugs the erosion had hidden (a commons brand gap, and a missed Bytes runtime import in the Worker entry), both now fixed. Deliberately not done, and named: the runtime-owned error types (ValidationError, JsonError, HttpResult, QueueResult) still pass through uncoded — they have no declaration to generate a codec from — and a context still reaches its callee’s codecs through that context’s module, so a workers build has one borrowed view of each contract. The boundary is typed; it is not yet verified — nothing checks that a deployed callee matches what its caller was compiled against. That is #643. No grammar, checker, or wire-format change.
v0.175.0File identity is not the unit-validation path (compiler; ADR 0198, closes #650, first slice of the LSP foundations track, #640). Since v0.113 made [paths] include a list, a project with two include roots had no unique name for its files: discovery strips each tree’s own root, so src/todos.bynk and tests/todos.bynk both became todos.bynk. Anything that keyed a file by that path — notably the IDE surface’s project diagnostics — folded the two together and silently dropped one file’s diagnostics. A file now carries two paths, because it was always two things: an include-root-relative one, which is what lets src/todos.bynk declare context todos and is what unit-name validation reads; and a project-relative identity, which is what names it. What you may notice: a diagnostic’s reported path is now relative to the project root rather than to the include root it was found under — math.bynk becomes src/math.bynk, and a second root’s todos.bynk becomes tests/todos.bynk. That applies to any project with a bynk.toml or a src/ directory, which is most of them; it matches the path an emitted test already reports, and it is the same path your editor and bynkc now agree on. A flat project (.bynk at the root, no src/) and a directory compiled with no manifest are unchanged. No language, grammar, or runtime change; no author-facing surface.
v0.174.0Generic records at the boundary — monomorphised per-instantiation JSON codecs (checker + emitter + docs; ADR 0197, closes #592, supersedes ADR 0183 Decision C). v0.157 shipped type Paginated[T] = { … } but made an instantiation non-boundary — rejected in every serialised position (a record field, sum payload, handler signature, agent store, or Json.encode/decode) — so the flagship API envelope could be named but not sent. It can now be sent. A generic-record instantiation crossing a boundary emits a monomorphised codec: Paginated[User] generates serialise_Paginated_User / deserialise_Paginated_User, the declared fields with the type parameters substituted by the concrete arguments, delegating to their own codecs (serialise_List_User, serialise_Option_String) — exactly the mechanism List/Map/Result already use. The emitted TypeScript interface stays the erased Paginated<T>; only the codec is per-instantiation. A generic record is serialisable iff its type arguments are: the boundary rule and the Json codec predicate now look through the application into the arguments, so Paginated[User] is admitted while Paginated[Int -> Int] is rejected at the function argument (bynk.types.function_at_boundary) and Json.decode[Box[Query]] at the query argument (bynk.types.json_uncodable) — each drawing that argument’s own boundary error. A non-serialisable field is still caught at the declaration. A recursive generic record — one that transitively contains itself, including through an Option/List wrapper (type Node[T] = { next: Option[Node[T]] }) — has no finite set of monomorphised codecs, so it is rejected at the boundary with the new bynk.generics.recursive_generic_at_boundary (a non-generic recursive record still serialises, its single codec being self-referential). MapEntry (ADR 0184) and Val[…] value fabrication for generic types stay out of scope. No grammar or runtime change.
v0.173.0bynk deploy says what a Worker reads — and admits when it can’t know (language + driver tooling; ADR 0196, closes #632). v0.172 shipped deploy-time secrets speaking totally for an actor’s auth secret and not at all for a bynk.Secrets name, and named the cost: a Secrets.get("API_KEY") you forget to supply is a silent production None. Now the compiler collects the literal names your handlers read, bynk deploy lists them marked read, and warns when one has no value. The increment’s real content is the shape of the guarantee, and it is smaller than v0.172’s deferral note promised. That note proposed forbidding computed names outright — a static rule forcing Secrets.get’s argument to a literal — on two justifications that both turned out to be false. The precedent it cited (cors/@cache/@limit all require literals) is declaration-site config, validated where a policy is written; none of it constrains an expression in an ordinary argument position, so the rule would have been the language’s first about the shape of an ordinary argument, sitting oddly next to Fetch.send(req). And it could not have bought what it claimed: Secrets.get returns Effect[Option[String]], so absence is a legitimate, handled outcome — erroring on an unsupplied read would refuse to deploy correct programs. So a read is advisory and only a declared secret is required, and the two keep the semantics their types already gave them: an unset auth secret 401s every request, an unset Secrets.get name is a None your match already covers. A computed name is warned, not forbidden (bynk.secrets.computed_name, non-failing, and raised on the check path so your editor shows it): choosing a secret at runtime is a reasonable thing to want, and a language should not take it away to tidy a deploy tool’s list. What makes the list safe is that it carries its own completeness — the manifest gains read_complete, the plan says secrets incomplete <worker> (JSON: secrets_complete: false), and it says it before the lines it qualifies. Without that flag a short list would be the most dangerous thing deploy could print: usually right, therefore trusted, and wrong exactly where nobody looks. Collection resolves the capability to its declaring unit, never the identifier — you can declare your own capability Secrets, and setting its names on Cloudflare would write a real secret to a real account for a store that was never Cloudflare’s. The emitted bynk-secrets.json goes to version: 2 and is now written whenever anything is known, not only when a secret is declared — a context with no actor at all can still read API_KEY. No grammar change, no new syntax, and no restriction: Secrets.get(pickName()) still compiles, still runs, and now says it cannot be planned.
v0.172.0bynk deploy sets your secrets, and tells you which ones it can’t know about (driver tooling; ADR 0195, closes #602, deploy track slice 3). The last v1 resource kind, and the only one that is a value rather than an id: a KV id is minted by Cloudflare and safe to commit, a queue name comes from your source, but a secret value comes from you and has to reach Cloudflare without being written down on the way. bynk deploy now moves it — from --secrets-file, the environment, or a prompt — straight to wrangler secret put on stdin, and forgets it. No secret value reaches bynk.deploy.lock, generated config, or the plan, in any format; the ledger records nothing about secrets at all, not even which are set, because presence is a live question and a remembered answer could only ever be stale. The increment’s real content is what deploy refuses to claim. The slice was proposed on the premise that the secret names a context needs are derivable from the closure walk — they are not. bynk.Secrets is fn get(name: String): an ordinary expression in an ordinary argument, so Secrets.get(someVar) type-checks and consumes bynk { Secrets } tells you only that a context reads secrets, never which. What is derivable went unmentioned in the proposal and matters most: an actor’s auth = Bearer(secret = "…") is a literal fixed at parse time, required at compile time, and fail-closed silent — unset, it answers 401 to every request in production rather than failing your deploy. So deploy speaks totally for that class and not at all for the other, and the plan marks every line declared or supplied so you can tell which. It could have guessed the rest — scanning for Secrets.get("…") with a literal would look right almost always, since every such call in Bynk’s own tree uses one — and that is precisely why it doesn’t: a list that is usually right gets trusted, and the computed name it misses becomes a production None nobody checks for. The declared list is a floor, not a census, and the guide says so where you’d otherwise misread it. Names and values are separate inputs: names come from your actors, your --secrets-file’s keys, and each --secret NAME; values from the file, else the environment, else a prompt — the environment supplies values only and is never scanned for names, since uploading everything in your shell that looks secret-ish is not a thing a deploy tool should do to you. Missing a value with no terminal to ask at is a hard error naming the secret, never a blank. Reconciliation is set-if-absent, --force to overwrite: Cloudflare won’t return values so only presence is observable, and re-setting every secret every deploy would cut a fresh secret version each time for nothing. --dry-run still never authenticates — which is why a plan says set and the skip surfaces at run time. Secrets are set before the push for a Worker already live, and after it on a first deploy: wrangler secret put against a Worker that doesn’t exist yet doesn’t fail, it quietly creates a stub Worker (and, interactively, exits 0 having set nothing if you decline) — so the first push comes first, and the window it opens is fail-closed by construction. The compiler now emits bynk-secrets.json beside each wrangler.toml carrying the declared names, because the driver reads what the compiler wrote rather than holding a model — the only shape that works when bynkc runs as a child process. No language, grammar, or checker change.
v0.171.0bynk deploy provisions queues and Durable Object migrations, not just KV (driver tooling; ADR 0194, closes #600, deploy track slice 1). Slice 0 provisioned exactly one resource kind, so a context with an agent or a service … from queue("n") still owed the manual ritual — and worse than owed it: deploy pushed the [[queues.consumers]] binding without creating the queue behind it, and a Worker consuming a queue that does not exist fails to upload. Now bynk deploy provisions every v1 resource a context’s closure commits it to. Queues are created by name — the name in your from queue("n") — before the push, which is doing real work rather than being polite: wrangler deploy will not create a queue its config binds, it checks and fails with “To create it, run: wrangler queues create”. Each queue’s existence is checked against Cloudflare before every deploy and created only if absent, so a queue deleted outside Bynk comes back on the next one. Durable Object migrations are applied by wrangler deploy itself, from the config it is already reading. The increment’s real decision is what the ledger refuses to remember. bynk.deploy.lock records nothing about migrations — not even the advisory last-applied tag the slice-0 ADR left room for — because Cloudflare already tracks applied tags, and a second record could disagree with the account (the lock says v2; a reset left Cloudflare at v1), turning a deployment problem into a debugging session about Bynk’s memory. So the plan states the tag the push will ask for, marked advisory (migration v1 (advisory — wrangler deploy applies it); in JSON, applied_by names the owner rather than a constant-true flag), and the stated cost is accepted outright: bynk deploy cannot report migration drift, which beats reporting drift that isn’t there. The queue set the lock file does gain (environments.<env>.queues, additive over version = 1 — an existing lock file loads unchanged) is advisory in the same spirit: it exists so the plan can say create or reuse without calling Cloudflare, and the provision step never consults it, reconciling each queue against the account every deploy (wrangler queues info, create only if absent) — which is what makes an out-of-band deletion self-heal, at the cost of one call per queue. That reconciliation is also where a first draft of this slice went wrong and was corrected under review: it dismissed a live pre-check as “the same race one call later” and so made the create’s already exists text load-bearing for every re-deploy — an unverifiable claim about another tool’s prose on the hot path. The race is a concurrent deploy; the common case is a queue that is simply there, which a lookup answers by exit code without reading prose at all (Cloudflare’s own deploy path reconciles queues this way). The message match survives only for the race-loser, where being wrong costs a spurious failure a re-run fixes. The ledger’s boundary is now settled rather than discovered per resource: it owns the ids it mints (KV), records names as a hint it never trusts (queues), and records nothing where another tool owns the state (migrations) — which is also why CI’s refusal to create an unrecorded resource covers KV alone, a minted id being the only thing a CI job can strand. The plan gains queue create|reuse <name> and migration <tag> lines. No language, grammar, checker, or emitter change: the stanzas were emitted correctly all along, and only the provisioning behind two of them was missing.
v0.170.0bynk deploy ships every context, in Service-Binding order (driver tooling; ADR 0193, closes #601, deploy track slice 2). Slice 0 shipped deploy for exactly one context — more than one was refused as an ambiguity — so the flagship several-context architecture could be run locally (v0.167) but never shipped. Now bynk deploy provisions and pushes every context in one command, ordered so each Worker is uploaded after the Workers it binds to, with the resolved order shown in the plan before anything is touched. That order is a correctness requirement, not tidiness, and the finding is this increment’s headline: the slice’s gating question was whether Cloudflare resolves a Service Binding by name at request time (order soft) or at upload (order hard), and the working assumption was wrong — a Worker whose bound target does not yet exist fails to deploy. The two-pass upload that would imply for a dependency cycle turned out to be unnecessary, because bynkc already rejects a consumes cycle before emit: the language’s own acyclicity invariant supplies exactly the precondition Cloudflare’s upload rule demands, so a project that compiles always has a deploy order. --context NAME re-pushes a single context for iteration and names and refuses a dependency that has never been deployed rather than sending an upload Cloudflare would reject — which the ledger now supports by recording deployed contexts, not just KV ids (additive; an existing bynk.deploy.lock still reads). A multi-context deploy is resumable, not transactional: a failure stops the run, keeps and records what already landed, and names what did not; a re-run re-pushes in the same order rather than skipping live contexts, so a changed context always ships — the plan calls those redeploy. With dev (v0.167) and deploy both acting on every context, the word “ambiguous” leaves the driver entirely: SelectError::Ambiguous and the singular select_context are deleted rather than reworded. Breaking (pre-1.0): the --format json plan now describes every context, so the top-level worker, kv, and deploy fields give way to a contexts array carried alongside the resolved order; read .contexts[0].worker where you read .worker, and .contexts[0].action (deploy or redeploy) where you read .deploy — which was hardcoded true and never carried information.
v0.169.0Three test guards kept out of the published crate (packaging; bynk-lsp only). The bynk-lsp tarball shipped three integration tests that read directories outside the crate’s own package: hover_references (v0.166) and declaration_spans (v0.168) read examples/todo and the compiler’s positive fixtures, and scaffolds_compile (v0.121) reads vscode-bynk/snippets/bynk.json. A standalone cargo test on the crates.io release would fail on the missing siblings — on tests that cannot pass there by construction, since their inputs are repo layout rather than crate contents. v0.29 (ADR 0058) already excluded legend_drift for precisely this reason, and each guard added since re-introduced the defect rather than following the precedent, so all three now join the exclude list and the comment there names it as the place the next one goes. Skipping when the root is absent is the wrong fix, not merely a rejected one: ADR 0190 D6 requires these fixtures to measure real diagnose_project output, so a skip could pass vacuously — the failure mode ADR 0191’s coverage sweep exists to catch — which is why the guards are withheld from the tarball rather than taught to tolerate its layout. In-repo coverage is unchanged: cargo test --workspace still runs all four. No language, grammar, client, or LSP behaviour change, and no new ADR — 0058 already states the rule.
v0.168.0Go-to-definition on an actor (editor tooling; closes #619). Jumping from the User in by u: User to its actor User declaration found nothing whenever go-to-definition’s index rung had not already answered — an unanalysed or mid-edit buffer, or a file outside the analysed project. The same-file rung listed an arm per declaration kind and omitted actor, and a catch-all swallowed the miss. This is v0.166’s gap in the other symbol function: ADR 0191 gave hover an arm for every kind the index resolves, and the neighbouring go-to-definition lookup had the same hole, minus the wrong-answer sharpness — the index resolves actors and outranks this rung, so the failure was a fallback-only nothing rather than a confident guess. The rung now takes each declaration’s name from the AST’s own exhaustive accessor rather than re-listing the kinds, which is behaviour-preserving plus the actor and puts the guard in the type system: a declaration kind added tomorrow stops the crate compiling instead of quietly resolving to nothing, so this needs no coverage sweep of the kind v0.166 fitted for hover. A method still answers to its bare name here — go-to-definition from a bare identifier depends on it, so ADR 0191 D2’s deliberate divergence from hover’s renderer (which guards on a free function) is unchanged, and now pinned by a test so a later tidy-up cannot close it by accident. No language, grammar, or client change — bynk-lsp only, and no new ADR: 0191 already states the rule.
v0.167.0Multi-context local dev — every context served, Service Bindings wired (driver tooling; ADR 0192, closes #552, resolves the design-review Platform #5 finding). Bounded contexts talking over Service Bindings are the flagship architecture, and the emitter has generated the wiring correctly since v0.57 — but nothing ever ran two Workers at once. bynk dev served exactly one context and failed a multi-context project as ambiguous, so the feature was unrunnable locally and a cross-context call had never been exercised at runtime anywhere (golden-file coverage of the generated [[services]] text only). Now bynk dev with no flags serves every context — one wrangler dev each, discovering one another through wrangler’s dev registry and wiring the bindings between themselves — so Payment.authorise(total) from your orders context resolves against the payment worker running next to it, and a save rebuilds the callee and hot-reloads it with the wiring intact. The emitter is untouched: the wiring was right all along, only the orchestration withheld it. --context becomes repeatable and narrows to a subset; each context gets its own port from --base-port (8787 default) and, under --inspect, its own inspector port from --inspect-port — so ports become the driver’s to allocate, and passing -- --port against an allocation is now an error naming the flag that owns it (a single-context project on the default port is unaffected). Any worker exiting stops the rest, since a survivor’s bindings would point at a context that is gone. Supersedes ADR 0096 D3. bynk deploy still ships one Worker at a time and is unchanged.
v0.166.0Hover describes every kind the index resolves; two of them were describing the wrong thing (editor tooling; ADR 0191, amends ADR 0190 D1, closes #616). v0.165 gave the renderer an arm for a record Field and left three index kinds resolved-but-unrendered — Actor, Method, CapabilityOp — on the measurement that they hovered as nothing, so arms would be tidy rather than load-bearing. That measurement was taken from the wrong end of hover’s ladder: the fall-through does not run from the index rung to qualified_callee_at, it passes rung 4, the lexical name match over the live buffer, which answers first. Re-measured at reference offsets against real analysis output, only Actor hovered as nothing; the other two answered wrongly. A method matched on its bare name with the type prefix dropped, so in a file declaring fn Counter.bump and fn Gauge.bump, every bump — the g.bump() call the index binds to Gauge.bump, and fn Gauge.bump’s own declaration — rendered Counter.bump, whichever was declared first. A capability operation resolved through a path that is not project-scoped: a context declaring its own capability Logger { fn info(message: String) } hovered the embedded platform.log.Logger.info(msg: String) — different parameter, different owner, no sign either was in play. Both are v0.165’s gap B again: a correct structural resolution discarded, a plausible guess answering instead. All three kinds now have renderer arms — an actor renders its auth scheme and config plus identity (or, for the refinement form, its base and where claim predicate), a method renders the method the call binds to, a capability operation renders its signature attributed to the capability that declares it — so ADR 0190’s rule that a structural resolution outranks a name match is now true of every kind the index carries. A bare key names a free function: the renderer’s Fn arm no longer matches a method by its bare method name, bringing it into line with signature help’s resolver, which already guarded this way; the cost is that a method in a buffer no analysis round has reached yet hovers as nothing rather than as a coin-flip between same-named methods. Where the index does not resolve — builtin statics (Stream.of), a refined type’s of/unsafe — the name-match fallback answers exactly as before. The rule also gets a tooth, rather than the “obvious next step” note it would otherwise have shipped with: this is the second time a missing arm shipped silently (v0.165’s Field label, now these three) and no test failed either time, so a coverage guard now sweeps every key the real index produces for a fixture declaring all ten kinds through the real renderer and fails on any that answers nothing — with an exhaustive match over SymbolKind beside it, so a new kind stops the crate compiling until someone declares one and the sweep can see it. Fixtures sit at reference offsets in real projects and each pins the wrong answer it replaces, not just the right one. bynk-fmt now exports its string escaper, so an actor’s auth = Scheme(secret = "…") config renders valid Bynk (the stored value is unescaped — the parser resolves escapes at lex time — so a " in it rendered a broken fence); hover is pinned against the formatter’s own output rather than a copy. No language, grammar, or client change — bynk-lsp plus one additive bynk-fmt export.
v0.165.0Hover works on references, not just declarations (editor tooling; ADR 0190, closes #611). Inside an agent handler body — the code a reader actually spends their time in — hover described the four lines that declare the agent’s state but missed the uses. Three gaps, all now closed. (1) A store/key field reference (a bare read lastSeq + 1, a := write target, an invariant subject, a store op’s receiver) renders exactly what its declaration renders. State fields are not index symbols and not let/param locals, so this resolves by name — scoped to where a bare name actually binds to state (handler bodies, invariant/transition predicates), so @indexed(by: id), which names a field of the stored value, never masquerades as a same-named key id; a local of the field’s name shadows it, matching the checker’s by-provenance dispatch. (2) A record-construction field label (Stored { seq: next, title: title }) renders the Stored type’s field. The checker already recorded labels as Field refs keyed "Type.field" (ADR 0069), so the index resolved the offset correctly — but the renderer had no arm for the compound key and fell through to the locals path, which matches by name in scope: title: rendered the enclosing handler param add(title: Title), a confidently wrong hover rather than a missing one. A resolved index hit is now rendered or nothing is; it never falls through to a name match. (3) A store operation (items.put(id, item)) renders its signature over the field’s declared kind. Its signatures come from a new enumerable store_ops registry in bynk-check — the storage analogue of kernel_methods (ADR 0063), which likewise exists because the checker’s match-arm dispatch is authoritative for typing but invisible to tooling. A drift test drives every listed operation through the real checker on a store field of the matching kind, so the table cannot list a phantom. Entry operations only: a Log’s time-window roots are covered, the lazy-Query vocabulary they feed into is not, and hover over an ordinary value-receiver method (xs.fold) still needs signature help’s receiver-typing path. Both name-resolved paths defer to the checker’s own dispatch rule — a store op binds a bare ident receiver not in the value scope, so neither a shadowing local nor a qualified receiver (p.items.put(…), an ordinary value method on a record field sharing a store field’s name) is mistaken for one. Every hover test before this used a declaration offset — which is why the gaps slipped through — so the regression fixtures assert at reference offsets in examples/todo/src/todos.bynk, the file the issue reproduces in, against real analysis output. Gap B being a fall-through bug also makes hover’s rung order the behaviour, so it moves out of the request handler into a pure hover::hover_content the handler and its tests share — reordering the rungs now fails a test instead of silently restoring the bug. No language, grammar, or client change — bynk-lsp plus the bynk-check registry.
v0.164.0The suite <target> header links to the unit under test (editor tooling; extends ADR 0095, #609). A test file binds itself to the commons/context it exercises by qualified name in its suite <target> header, but that binding was the one unit reference the editor left un-navigable — uses/consumes targets already render as underlined, Cmd/Ctrl-clickable document links (slice 6b, ADR 0095), while the suite target was dropped. textDocument/documentLink now also emits the suite’s target (and any uses the fragment brings in), resolving it through the same unit→source map: suite todos in a test file underlines todos and opens context todos’s source. Go-to-definition on the same span rides along for free. A suite whose target has no on-disk source emits no link, the same graceful degradation as uses. No language, grammar, or client change — entirely in the bynk-lsp crate.
v0.163.0Three where predicate tiers, taught explicitly; the actor … where grammar matches the compiler (grammar + docs; ADR 0189, addresses the keyword-hygiene batch #548). “One where” actually hosts three predicate sub-grammars, and ADR 0144’s “one predicate surface” describes only one of them. The reference now names all three: (1) type refinement type T = Base where <catalogue> — a closed grammar, the built-in predicate names joined by &&; (2) actor claim actor A = Base where <predicate> — a full expression a static-semantics rule restricts to the closed hasClaim/claimEquals catalogue composed with &&/`
v0.162.0Lexical tightening — a -- comment must be whitespace-preceded; a --- marker always opens a doc-block (lexer + spec; ADR 0188, addresses the keyword-hygiene batch #548). Two underspecified rules, pinned. Comments: -- opens a line comment only at the start of input or when the preceding character is whitespace. Adjacent to a token it is no longer a comment — a--b lexes as a - -b (a subtraction of a negation) and x-- as x followed by two - operators, not a comment that silently swallows the rest of the line. This resolves the a--b “comment or subtraction?” ambiguity toward subtraction; write -- (leading space) for a trailing comment. Breaking (pre-1.0) but empty in practice — no .bynk in the repo wrote a token--comment adjacency. The tree-sitter line_comment stays a context-free approximation (one byte of look-behind can’t live in a token), so an editor may over-highlight the -- in the rare a--b; the compiler lexer is normative and the spec (§3.3.1) says so. Doc-blocks: a --- … --- marker is three or more hyphens alone on a line; there is no standalone --- divider — a lone marker with no close is bynk.lex.unclosed_doc_block, not a horizontal rule (unchanged behaviour, now spec’d in §3.3.2). No grammar, checker, or emitter change.
v0.161.0Protocol source casing — from websocket (was from WebSocket) (grammar + parser + formatter + tooling + docs; ADR 0187, addresses the keyword-hygiene batch #548). The service-header protocol sources were mixed-case: from http / from cron / from queue("…") were lowercase, but the real-time one was PascalCase — from WebSocket(in: I, out: O). It is now lowercase too: from websocket(in: I, out: O). Like the others it is a contextual word (not a reserved keyword — websocket stays a usable identifier elsewhere); in/out and the on open/on message/on close lifecycle words are unchanged. Only the source-header token moved — the emitted TypeScript (the JS WebSocket/WebSocketPair globals) and the Connection[F] surface are untouched, and “WebSocket” stays the proper-noun for the technology in prose. Breaking (pre-1.0): from WebSocket(…) no longer parses — write from websocket(…). Fixtures, examples, the book, the formatter, and the from-completion candidate all migrate. Hover/completion/semantic-tokens track the lowercase spelling (ADR 0156).
v0.160.0One conjunction spelling — refinements join with &&; the and keyword is retired (grammar + parser + formatter + docs; ADR 0186, addresses the keyword-hygiene batch #548). The predicate surface had two conjunction spellings: a refinement used the keyword and (Int where MinLength(3) and MaxLength(20)), while every other predicate — a function requires/ensures, an agent invariant/transition, a test expect, an actor-claim — used the && operator. and in a refinement was never a boolean operator (no `
v0.159.0The test stub gets its own keyword — stub Cap.op(…) (was a third pun on provides) (grammar + AST + parser + emitter + tooling + docs; ADR 0185, addresses the keyword-hygiene batch #548). provides meant three things — a provider declaration (provides Cap = Impl), an external provider (the bodiless form), and a test double (provides Cap.op(_) returns v), the last distinguished only by an interior .op( shape. The test double now has its own keyword: stub Cap.op(<pattern>) returns <value> | fails. provides heads only a provider declaration / external provider; stub is a reserved keyword legal as a suite/case item. Semantics, precedence (case > suite > tier default), argument patterns, and the sequenced returns each […] form are unchanged — only the keyword moved. The four diagnostics are renamed bynk.provides.*bynk.stub.* (not_a_seam, unknown_op, rhs_type, bad_sequence). Breaking (pre-1.0): a test double written provides Cap.op(…) no longer parses — rename it stub. Fixtures, examples, the testing guide/reference/tutorial, glossary, and grammar are migrated; a stub snippet joins the VS Code extension; hover/completion/semantic-tokens gain the keyword (signature help unchanged — a stub is not a call site).
v0.158.0Map queries expose keys — map.entries/map.keys/map.values (checker + emitter + docs; ADR 0184, closes #547). A Query over a store Map[K, V] used to yield values only, so every keyed collection duplicated its key inside the value record (the todo example’s TodoItem.id was both the map key and a stored field). A store map now roots three key-aware queries: map.keys : Query[K], map.values : Query[V], and map.entries : Query[MapEntry[K, V]]. .entries lifts each entry into a MapEntry[K, V] — a compiler-known nominal record { key: K, value: V }, read with .key/.value. bynk has no tuple/pair (ADR 0120), so an entry is a named record, not (K, V); the whole single-argument query vocabulary (filter/map/sortBy/collect/…) applies to .entries unchanged. MapEntry is a generic-record instantiation and so non-boundary (ADR 0183): a read handler projects each entry into a named type (items.entries.map((e) => Row { id: e.key, … })) before its terminal, which is what lets the stored value drop the denormalised key. A persisted key is a string object-key, so an Int-typed key is decoded back with Number(…) on read. The todo example drops TodoItem.id from its stored shape and rejoins it through .entries. Unknown accessors are bynk.store.unknown_map_accessor; an unknown MapEntry field is bynk.types.unknown_field.
v0.156.0A refined type has no .unsafe — the unchecked escape hatch is opaque-only, in source and in emitted TypeScript (emitter + docs; ADR 0182, closes #545). The review called refined .unsafe “the largest credibility hole in the refinement guarantee”. It turned out Bynk source already rejected Age.unsafe(x) (.unsafe was gated to opaque bodies since v0.62), but the emitted TypeScript exported a public unsafe(value){ return value as T } on every refined and alias type — so hand-written host or adapter code could import the module and brand a value past the predicate, guarded only by the ADR 0014 convention. That emitted constructor was the real reachable hole. It is now removed: a refined or alias type emits only .of; compile-time literal admission lowers to an inline brand cast (literal as T) instead of a constructor call, and generated property-test scaffolding brands a refined draw to any (the type the old call already produced there) — no callable unchecked constructor is exported. Opaque types keep .unsafe (their representation is hidden, so the defining commons needs an in-commons constructor). A refined type’s representation is its base, always built through the validating .of, so an unchecked hatch would only subtract a check. Now no code — Bynk or host TypeScript — can mint a refined value without its predicate running, at run time (.of) or compile time (admission). Breaking at the host boundary (pre-1.0): TypeScript calling RefinedType.unsafe(x) on emitted output no longer compiles — exactly the bypass ADR 0014 forbade. Spec (§6.1.2/§6.4/§5.3/§7.3) and the reference/glossary/operators/guides are corrected; tsc --strict and the property runner pin that emitted output has no refined .unsafe yet still type-checks and runs.
v0.157.0Generic record types — type Paginated[T] = { … } (grammar + checker + emitter + docs; ADR 0183, closes #546). Capabilities abstract effects, but nothing abstracted data shape — the API envelope (Paginated[T], Page[T]) had no expression. A type may now carry [A, B] type parameters when its body is a record; a parameter is an unconstrained, bound-free name resolved as a rigid variable inside the field types. A reference applies concrete arguments (Paginated[User]); field access substitutes them (page.items : List[User]) and construction infers them argument-directed (Paginated { items: users, cursor: None }), grounded by the binding’s expected type when a field can’t (an empty list). Emission is erased TS generics — interface Paginated<T>, references Paginated<User> — exactly as generic functions erase. Only a record body may be generic (bynk.generics.generic_non_record); a wrong argument count is bynk.generics.type_arg_count. In this version a generic record is a non-boundary value: usable for internal values (construction, field access, helper params/returns, locals), but rejected in a serialised position — a record field, sum payload, handler signature, agent store, or Json.encode/decode (bynk.generics.generic_record_at_boundary); a boundary story (monomorphised codecs) and generic sums/methods are deferred. New applied_type_ref grammar rule; hover renders the type parameters.
v0.155.0Service-level by/given defaults; by relocated after the return type (grammar + normalization + emitter + tooling; closes #544). Two edits to the over-dense HTTP handler head. (1) The by clause moves from before the parameter list to after the return type, alongside given: on GET("/x") (page: Page) -> View by v: Visitor — not by v: Visitor (page: Page), which read as a call Visitor(page: Page), the one place the surface fought its own Name(args) rule. A breaking but mechanical relocation (pre-1.0); all in-repo handlers, fixtures, examples, and docs migrate. (2) A service may declare by/given defaults on its header, after the protocol — service api from http by Visitor given Clock { … } — the ambient contract every handler inherits unless it declares its own. “Public / bearer-authed” is usually a service fact, and HTTP has no safe default actor (so by was mandatory on every route); the default states it once. A handler’s own by (or given) overrides the default outright (no merge). Implemented as a post-parse normalization pass that injects each default into the handlers that omit it, so the checker, project validation, and the emitter see canonical handlers and need no default-awareness — the emitted TypeScript is byte-identical to spelling every by/given out. A malformed default is caught whether it is inherited (checked through the injected copies) or fully shadowed (validated directly at the header — the zero-inheritor case), so an unknown default actor/capability never passes silently. The protocol-implied return sugar (-> Int meaning Effect[HttpResult[Int]]) from the same review finding is deferred to a later increment. by/given remain keywords; hover/completion/semantic-tokens/signature-help unchanged (ADR 0156). New positive fixtures cover inheritance + override; negatives cover the rejected old order and a malformed default.
v0.154.0Declared error embeddings — ? auto-converts a cross-context error (grammar + checker + emitter + tooling; ADR 0178, closes #543). Every cross-context chain paid a .mapErr(toLocalError) tax to lift a called context’s error into the caller’s — the single largest ergonomic tax in the language’s flagship pattern. A sum type may now declare embeddings: `type OrderError = …
v0.153.0? lifts an Option into an HttpResult handler — None → 404 (checker + emitter; ADR 0177, addresses #543). The KV-read → decode → respond pattern was a two-deep match pyramid because ? reached only Result values, never the Option a storage read yields. Now, inside a handler whose return peels to HttpResult[T] (a bare HttpResult[T] function or Effect[HttpResult[T]]), option? lifts: Some(v) yields v, and None early-returns NotFound (404) — the canonical “absent resource” status — collapsing the outer half of the pyramid to one postfix ?. Emitted as the existing check-and-early-return shape (if (o.tag === "None") return HttpResult.NotFound;), reusing the Option/HttpResult runtime with no new construct; a commons body that names HttpResult now imports it (a latent single-file gap this exposes). Option? outside an HttpResult handler is rejected with the new bynk.types.question_option_outside_http (use .okOr(err) to make a Result elsewhere); a Result under ? is unchanged. This is the Option→HttpResult half of the review finding’s ?-extension; the Result→HttpResult direction (mapping a declared domain error to a status, via embeds) is a separate later slice, so #543 stays open. No grammar or runtime change.
v0.152.0The Effect[Result[T, E]] combinators — mapOk / mapErr / flatMapOk / flatMapErr (checker + emitter + tooling; ADR 0176, addresses #543). Effect[Result[T, E]] is the universal shape of a cross-context call, but recovery and success/error reshaping had no direct surface — every transform was a <- peel followed by a match, and the designed combinators (design doc §2.8.3) had never shipped. The four compiler-synthesised methods now transform the shape in place: e.mapOk(f: T -> U) -> Effect[Result[U, E]] and e.mapErr(f: E -> F) -> Effect[Result[T, F]] map the two sides of the split; e.flatMapOk(f: T -> Effect[Result[U, E]]) chains a further effectful-fallible step on success (single error type E, as ?); e.flatMapErr(f: E -> Effect[Result[T, F]]) attempts an effectful recovery on error (its recovery must produce the receiver’s T). They are syntactic sugar — the desugaring (mapOkmap(r => r.map(f)), etc.) is the semantics — emitted inline as an async IIFE that awaits the receiver Promise<Result<…>> and rebuilds the transformed Result, tsc --strict-clean with no runtime import. Named methods (not implicit .map lifting) resolve the “which .map?” ambiguity on Effect[Result]; unlike the eager List iterators they produce an Effect rather than run one, so they are not effectful-context-confined. Only an Effect wrapping a Result carries them; other Effect-of-X shapes are unchanged. No new diagnostic (method_not_found/argument_mismatch/method_arity reused), no grammar or runtime change; completion/signature-help via the kernel registry (ADR 0156). The ?-into-Effect[HttpResult] lifts and declared error embeddings from the same review finding remain later slices.
v0.151.0Oidc — OIDC/JWKS authentication for actors (grammar + checker + emitter + runtime; ADR 0175, closes #553). The auth-scheme set was closed to None|Internal|Bearer|Signature with no route to OIDC — real systems hit that wall immediately. The set now opens with a compiler-generated Oidc scheme: actor User { auth = Oidc(issuer = "…", audience = "…", jwks = "…"), identity = UserId } on a from http route emits, before the body runs, a fail-closed boundary that extracts Authorization: Bearer <jwt>, verifies the RS256/ES256 signature against the provider’s published JWKS (fetched and cached, refetched on a kid miss for rotation — rate-limited by a cooldown so an attacker-chosen kid cannot amplify into fetches), enforces iss/aud/exp/nbf (with a small clock-skew leeway), and mints the identity from the sub claim — 401 on any failure. Chosen as a widening of the closed scheme enum (ADR 0080), not a user-supplied Verifier[T] capability: a user verifier reintroduces the hand-written-crypto footgun ADR 0085 removed, whereas OIDC is a standard the compiler can own totally, keeping the “an actor emits no TypeScript” invariant. Unlike Bearer/Signature, an Oidc trust declaration names no secret — its root is the provider’s public keys — and it demonstrates the reconsidered stance on secrets-in-contracts: public trust parameters belong in the contract, secrets do not. alg: none and symmetric HS* are rejected (algorithm-confusion). HTTP-only and single-actor this slice (not a sum member, not a refinement base); OIDC discovery and a user Verifier[T] are deferred. The docs also state explicitly that the model covers who is at the boundary, not whose an object is — object-level authorisation is handler code, by design. New diagnostics oidc_missing_issuer/oidc_missing_audience/oidc_missing_jwks/oidc_identity_not_string_constructible/oidc_not_in_sum; new runtime verifyOidcJwt with a standing bypass-class regression guard.
v0.150.0List.traverseTry / parTraverseTry — the short-circuit collect iterators (checker + emitter + tooling; ADR 0174). The last empty cell in the effectful-iteration matrix: discard (forEach/parTraverse) and gather-all (traverseAll/parTraverseAll) shipped, but not the everyday “apply a fallible f, stop at the first Err, return the successes” form — callers had to traverseAll then hand-scan the List[Result], running every element even after a failure. traverseTry/parTraverseTry(f: T -> Effect[Result[U, E]]) -> Effect[Result[List[U], E]] are the fault-propagating counterpart to the fault-gathering traverseAll: traverseTry awaits each element in order and returns immediately on the first Err (later elements never run); parTraverseTry issues all at once, awaits, then returns the first Err in input order (in-flight calls aren’t cancelled). Shipped as new distinct names, not a return-type overload on traverse/parTraverse — the design-notes overload vision would need return-type overload dispatch (a new type-system mechanism) plus a breaking migration of traverse off the stdlib; distinct names are additive and non-breaking (Try evokes the ? short-circuit). Same check_try_fn_arg inference and argument_mismatch gate as traverseAll, effectful-context-confined, no new diagnostic; emitted inline (return Err(...) on the first, else Ok(collected)), tsc --strict-clean. Both also reach a store Map[K, Connection] via the ADR 0173 broadcast (borrow path — send OK, closeconsume_on_borrow). Completion/signature-help via the kernel registry; no grammar or runtime change (ADR 0156).
v0.149.0Map.values() and broadcast collect-all (checker + emitter + tooling; ADR 0173, closes #570). Two separable gaps the #569 review surfaced. (1) Map.values() -> List[V] — the keys() sibling, lowered [...(m).values()] over the in-memory ReadonlyMap; a value map never holds a Connection, so no held interaction. (2) Broadcast collect-all — the ADR 0172 iterators traverseAll/parTraverseAll were List-only and absent from is_query_op, so they could not reach a store Map[K, Connection] (only forEach/parTraverse could). Wiring both into the query broadcast lets conns.traverseAll((c) => …) lift the store map to Query[Connection] and route through the same proven held-borrow path — each connection is borrowed into the closure (send allowed, close/transfer rejected as bynk.held.consume_on_borrow), the map keeps ownership, and the closure returns Effect[Result[U, E]] so every outcome is gathered into Effect[List[Result[U, E]]]. This resolves #570’s coverage gap: the held-borrow path for the collect-all iterators is now reachable and fixtured (negative/332_broadcast_collect_all_consume, positive/338_ws_broadcast_collect_all) — via the Map broadcast, not a constructed List[Held]. (3) An owned List[HeldResource] is documented as an intentional non-goal: #570’s originally-recommended borrow-scoped Map.values() -> List[Connection] would need a new soundness-critical “chain-only borrow” linearity rule, and the broadcast route reaches the same capability through existing machinery, so it’s declined. No new diagnostic, no grammar or runtime change (ADR 0156).
v0.148.0List.traverseAll / List.parTraverseAll — the collect-all iterators (checker + emitter + tooling; ADR 0172). The effectful List iterators covered discard (forEach/parTraverse) and short-circuiting sequential collect (the stdlib traverse), but not the fault-gathering case: run a fallible f over each element and keep every outcome (form validation that reports all errors, bulk processing where partial success matters, compensation tracking). traverseAll(f: T -> Effect[Result[U, E]]) -> Effect[List[Result[U, E]]] runs f over each element in order; parTraverseAll runs them concurrently — neither short-circuits, gathering every Ok/Err into the result list in input order. This is sound and cheap precisely because a Bynk Result Err is a value, not a fault: f resolves to a tagged Ok/Err, never a rejection, so a sequential loop and Promise.all both collect every outcome for free (no allSettled, no fault interception). The function’s return type is inferred (via the same check_kernel_fn_arg unification map uses for U) and required to be Effect[Result[U, E]] — a non-Result effect is bynk.types.argument_mismatch (no new diagnostic); both are effectful-context-confined like forEach. Emitted inline — traverseAll as the foldEff push-collect into a typed Result<U, E>[], parTraverseAll as the parTraverse Promise.all keeping the resolved array — tsc --strict-clean, no runtime import, no churn to forEach/parTraverse/foldEff. Kernel methods per ADR 0116’s direction; the short-circuiting collect overload remains a named later slice. Completion/signature-help via the kernel registry; no grammar or runtime change (ADR 0156).
v0.147.0List.parTraverse — the concurrent sibling of List.forEach (checker + emitter + tooling; ADR 0171). ADR 0170 shipped the sequential List.forEach and deferred the parallel form; an eager List had no way to fan out N independent effects at once — a batch (notify every subscriber, fire N probes) ran serially through forEach, paying the sum of the latencies, while Query/storage already carried a concurrent parTraverse (ADR 0135). List.parTraverse(f: T -> Effect[()]) -> Effect[()] runs f over every element concurrently and awaits them together, so a slow element does not head-of-line-block the rest (the side-effect interleaving order is unspecified; the call completes only when all elements have). It has the same type as forEach — the checker arm is merged (`FOR_EACH
v0.146.0The do statement, implicit unit, and List.forEach (grammar/AST + checker + emitter + formatter + tooling; ADR 0170, closes #542). Everyday effect code carried avoidable ceremony the design review counted across the examples: an effect used only for its side effect had to be let _ <- e (~30×), a unit body closed with Effect.pure(()), a conditional effect owed else Effect.pure(()), and running an effect over a List degenerated to a unit-accumulator foldEffuptime-monitor could not loop over its targets and copy-pasted the fetch/store block per target. Four additive changes, all over shapes that already compile plus one missing terminal: (1) do e — a keyword effect statement, the binder-free let _ <- e for a unit effect; it must be Effect[()] (a valued reply keeps the explicit let _ <- e, so discarding a real value stays visible). (2) Implicit unit tail — a block may close with no tail (the parser synthesises (), auto-lifted to Effect[()]), so an effectful unit body may simply end; an empty {} is now legal (unit). (3) Else-less ifif c { e } defaults a missing else to (), legal only for a unit then-branch (bynk.types.if_without_else_requires_unit); a valued if still owes its else. (4) List.forEach(f: T -> Effect[()]) -> Effect[()] — the Query.forEach terminal over an eager list, sequential, effectful-context-confined like foldEff, emitted inline (no runtime import, no foldEff churn). New diagnostics bynk.effect.do_in_pure_context / do_on_non_effect / do_requires_unit and bynk.types.if_without_else_requires_unit; do is a reserved keyword. sessions and uptime-monitor are rewritten to the new forms. Tooling: List.forEach completion/signature-help via the kernel registry, a do keyword token in the tree-sitter grammar (parser.c + grammar.json regenerated, v0.146 corpus entries added); hover unchanged (ADR 0156). Closes #542.
v0.145.0Editor surface catches up to nested payload patterns + match-arm guards (tooling; ADR 0169/ADR 0156, closes #565). The two editor-surface halves deferred from v0.144 — the tree-sitter CLI could not be installed in that environment, and nested-variant completion was a named fast-follow. (1) tree-sitter. grammar.js now describes the post-0169 shapes: a match_arm carries an optional if guard (pattern ("if" expression)? "=>" …), and a variant payload is a full sub-pattern (recursion), so Some(Ok(x)) / Err(PollClosed) parse as nested variant_pattern nodes and _ as a wildcard_pattern — the flat positional_binding node is retired (a bare lowercase name is a payload-less variant_pattern, highlighted @variable by capitalisation, an uppercase-led one @constant). parser.c + grammar.json regenerated, a v0.145 corpus entry added for a nested pattern and a guarded arm, and the bynk-grammar production reference re-blessed. (2) LSP. A pre-existing base gap: scrutinee_variant_completions required a user-declared Ty::Named and bailed on Result/Option, so match-arm / is completion never fired for a Result/Option scrutinee — it now offers Ok/Err and Some/None (variants_for_ty). Built on that, nested-variant completion: inside OuterVariant(‸ within a match arm, the editor resolves the payload field type (Result/Option/HttpResult generic args, or a user-sum decl’s field walked from source — the same shape as bynk-emit’s payload_field_ty) and offers that type’s variants — Ok/Err inside Some(‸) on an Option[Result[…]]. Hover, semantic tokens, and signature help are unchanged (ADR 0169). No language/compiler change. Closes #565.
v0.144.0Nested payload patterns + match-arm guards (grammar/AST + checker + emitter + formatter; ADR 0169, closes #541). Patterns were shallow — a variant payload bound only a name, never a nested pattern, and a match arm had no guard — so the two everyday shapes that follow from errors-as-values could not be expressed: error causes could not discriminate (Err(PollClosed) / Err(UnknownChoice) collided with bynk.types.duplicate_variant_arm, because coverage was a flat set of outer variant names), and an Option[Result[…]] forced a double-match stack. A payload binding is now a full sub-pattern, so payloads nest — Some(Ok(x)), Err(PollClosed) — and an arm may carry a trailing if guard over its bindings (Ok(r) if r.status == 200 => …). Coverage became bounded structural: Some(Ok(_)) / Some(Err(_)) / None is provably exhaustive with no wildcard, a missing nested variant reports non_exhaustive_match with a nested witness, duplicate_variant_arm keys on pattern shape, and a guarded arm never satisfies exhaustiveness. A lowercase-led identifier in pattern position is a binding, an uppercase-led one a variant (the universal capitalisation convention) — so Err(Declined) now discriminates the nested variant where it previously bound the payload to an oddly-cased name. Nested/guarded matches lower to an if / else-if chain (a switch on .tag expresses neither a nested test nor a guard); flat, unguarded matches emit the unchanged switch, so no existing output churns, and per-arm source-map anchoring (ADR 0103) is preserved. New diagnostic bynk.types.guard_not_bool. No runtime change. Refined where patterns (#472), or-patterns (#474), and record patterns remain planned, extending this increment’s recursive Pattern node and if-chain path. The if guard already highlights as a keyword; the tree-sitter grammar parser.c regeneration and nested-variant completion are named follow-ups (ADR 0156). Closes #541.
v0.143.0Refined types inherit their base type’s read-only kernel methods (checker + emitter; ADR 0168, closes #561, resolves finding #537). A refined String dropped every string method — fn shout(n: Name) -> String { n.toUpper() } failed with bynk.types.method_not_found — so examples reached for a plain String where a refined type belonged, or laundered a value back through "\(name)" interpolation. This punished exactly the users who adopted the flagship feature. A refined receiver now resolves its base type’s read-only kernel methods (the String, numeric, Duration, Instant, and Bytes kernels) as a fallback after its own declared methods, and the result is base-typed: n.toUpper() type-checks and returns String, never Name — the same widening a refined value already undergoes in arithmetic, comparison, assignment, and ordering keys, so it needs no new mental model. Declared methods win (the kernel is a strict fallback, never a shadow). Opaque types are unaffected — they deliberately do not widen, so a kernel call on an opaque receiver stays method_not_found. Bool has no kernel, so a Bool-based refinement inherits nothing. No new syntax; the emitted TypeScript for an inherited call is byte-identical to a plain base receiver’s (a refined value erases to its branded base). Completion and signature help now offer the inherited kernel on a refined receiver (ADR 0063 methods_for; hover/semantic-tokens unchanged, ADR 0156).
v0.142.0Request body-size limits for from http services, plus numeric digit separators (emitter + runtime + a grammar/checker/lexer addition; ADRs 0165 & 0166). (1) Body limits (#494). A body-taking route (POST/PUT/PATCH) had no way to bound its request body, so a client could stream an arbitrarily large payload the service read into memory before it could reject it. A service now declares a byte ceiling with a limits { maxBody: <Int> } section in header position (beside cors { }/security { }), overridable per route with a @limit(maxBody: <Int>) handler annotation (the @cache placement, valid only on POST/PUT/PATCH). A capped route rejects a request whose Content-Length exceeds the cap with a synthesised 413 PayloadTooLarge ({ kind: "PayloadTooLarge", details: … }), produced before the body is read and before the by/Bearer auth seam — the “synthesised boundary response” posture of the method-semantics 405 (v0.139). It reuses the existing 413 status, so the closed HttpResult registry (ADR 0126/0143) is unchanged; the 413 is applyCors/applySecurityHeaders-stamped so a cross-origin caller can read it. Precedence is route @limit → service limits → none; with neither, the route has no cap and emits byte-for-byte unchanged output — opt-in (the CORS posture, not the security default-on posture). Enforcement is a Content-Length fast-reject (not a hard guarantee — Content-Length can be absent for a chunked transfer or spoofed — so it pairs with the Workers platform cap; a streamed-read cap is a named follow-on). maxBody is a positive Int byte count (26_214_400 for 25 MiB); a byte Size literal (1.mb) is a named follow-on (the Duration playbook). Diagnostics: bynk.http.limits_not_http, limits_unknown_field, limits_invalid_field, limit_on_bodyless, limit_duplicate, limit_unknown_arg, limit_bad_max_body, bynk.parse.duplicate_limits. (2) Numeric digit separators (ADR 0166). Int and Float literals now admit an _ digit separator between digit groups (1_048_576, 1_000.5) — never leading, trailing, or doubled. The separators are stripped before the value is parsed (purely visual), and the as-written lexeme is preserved so bynkc fmt keeps the author’s grouping (mirroring how Float literals already preserve their lexeme). Motivated by maxBody’s large byte counts, but applies language-wide. bynk-syntax (a limits contextual keyword + @limit/lexer _ separators) + bynk-emit (checker validation, inline Content-Length dispatch in workers_entry.rs) + bynk-fmt + tree-sitter; hover/completion track the new section and annotation (ADR 0156). Closes #494.
v0.141.0Security response headers for from http services (emitter + runtime + a grammar/checker addition; ADR 0164). A from http response carried no security headers (#493), so a JSON body could be MIME-sniffed by a browser into an executable type (a content-sniffing XSS vector), and there was no declarative way to assert HTTPS-only. A service now declares a security { } policy in header position, beside cors { }, from which the compiler stamps two curated headers — split by risk, mirroring how CORS splits derived-vs-declared. (1) nosniff on by default: X-Content-Type-Options: nosniff — the one header that always helps a data API and never hurts — is stamped on every from http response, with no opt-in; the compiler synthesises a default policy for every such service. security { nosniff: false } is the explicit opt-out. (2) HSTS opt-in: security { hsts: 180.days } stamps Strict-Transport-Security: max-age=15552000 — a deliberate opt-in because HSTS pins a browser to HTTPS (a real footgun in dev/staging and on edge-terminated TLS); max-age only, no includeSubDomains/preload. Stamped uniformly through an applySecurityHeaders runtime helper (the applyCors shape) across every variant family and the synthesised preflight/405/OPTIONS/304, composing with applyCors (disjoint headers). Content-Security-Policy/X-Frame-Options are excluded — they govern markup, which the surface does not serve (ADR 0143). Because nosniff is default-on, this is not byte-inert: every HTTP expected/workers/** fixture regenerates (reviewed as a single-header delta). The closed HttpResult registry (ADR 0126/0143) is byte-for-byte unchanged — a header layer around the result lowering, not a change to it. bynk-syntax (a security contextual keyword + SecurityPolicy AST) + bynk-emit (checker validation, workers_entry.rs stamping, a runtime.ts helper) + bynk-fmt + tree-sitter; hover/completion track the new section (ADR 0156). Closes #493.
v0.140.0Conditional caching for from http GET responses (emitter + runtime + a grammar/checker addition; ADR 0163). A from http GET carried no validator and no freshness signal (#492): every re-fetch of an unchanged resource transferred the whole body, and there was no way to say “this is fresh for five minutes”. Two behaviours, split along the line v0.131 drew for CORS — the compiler synthesises what it can derive, the author declares what only they know. (1) Automatic revalidation, on by default: every eligible GET (one returning the JSON Ok variant) now carries a synthesised weak ETag over its serialised body (FNV-1a, synchronous), and a matching If-None-Match is answered a synthesised 304 Not Modified (empty body, ETag + Cache-Control copied across). (2) Opt-in freshness: a **`@cache(maxAge: 5.minutes, scope: public
v0.139.0HTTP method correctness for from http services (emitter + runtime; ADR 0162). The entry router answered every non-matching request with 404, so a wrong-method request to a live route was indistinguishable from a missing one, no Allow header was ever emitted, and HEAD/OPTIONS — which every HTTP client and cache assumes — were unimplemented (#489, surfaced during the v0.131 CORS work). The router now answers the method contract derived from the declared routes: a wrong method to a live path is a 405 with Allow (the union of the path’s methods, + HEAD where GET exists, + OPTIONS); a plain (non-preflight) OPTIONS is a 204 with Allow; and a HEAD runs the GET handler and returns its status and headers with an empty body (a Streaming GET answered as HEAD is not drained — sseResponse is now lazy). An unknown path is still 404. The allow-method derivation is one shared table the CORS preflight now reads too (so a CORS service’s Access-Control-Allow-Methods gains HEAD); a real preflight (bearing Access-Control-Request-Method) is disambiguated from a bare discovery OPTIONS, and the synthesised 405/OPTIONS are CORS-stamped for CORS services. HEAD/OPTIONS are not author-declarable and nothing is configured — this is a router correctness fix that is always on, so every HTTP expected/workers/** fixture regenerates (the intended 404405/OPTIONS/HEAD deltas). The closed HttpResult sum is untouched (HttpResult/HTTP_VARIANTS/HTTP_STATUS/httpResultToResponse byte-for-byte unchanged); the author-returnable MethodNotAllowed variant stays bodyless (ADR 0126 D4), its Allow a deferred payload shape. bynk-emit (workers_entry.rs) + one runtime helper (headResponse); no grammar, checker, or editor-surface change (ADR 0156). Closes #489.
v0.138.0bynk check / bynk fmt / bynk test — the everyday commands reach the driver (tooling; #487). The three most common day-to-day actions, previously only on bynkc, are now exposed through the bynk developer front-end, so an editor or developer goes through the one entry point that already knows how to find the compiler (the fix direction for #486/#484). bynk check and bynk fmt run the pipeline in-process — no separately-installed bynkc required, keeping cargo install bynk self-contained — routing single-file and project inputs exactly as bynkc does; bynk test delegates to the bynkc the driver resolves (BYNK_BYNKC → PATH → sibling-of-bynk), forwarding every flag, since it orchestrates external tsc/node regardless. All three mirror bynkc’s flag surfaces exactly (--format, --check, --no-run, --case, --inspect, --seed, -o) and honour the BYNK_BYNKC override the way bynk dev does (an override shells the pinned compiler). Additivebynkc check/fmt/test are unchanged, so scripts, CI, and the VS Code extension keep working; bynk doctor is unchanged. Single-file compile/compile_with_warnings moved down into bynk-emit (the slice-7 precedent) so the driver checks a single file in-process without depending on bynkc; bynkc re-exports them, so its public API is unchanged. Closes #487.
v0.137.0Hover for the key/store agent-state surface (tooling; ADR 0161). Hovering key or store in an agent body produced no hover (#476): both are contextual keywords — lexed as identifiers, so absent from the reserved KEYWORDS registry and unseen by the keyword-hover path — and the state fields they declare are neither top-level declarations nor let/parameter locals, so every hover path fell through. Hovering key/store (or the field name each introduces) now renders the field’s signature — key id: String, store items: Map[String, Int] @indexed(by: id) @bounded(10000) (annotations through the formatter’s own renderer) — followed by a one-line doc. The key/store keyword and its field name render identically, matched by source span (never by name), so an identifier that merely reads id/items elsewhere is never mistaken for the declaration. A new parallel CONTEXTUAL_KEYWORDS registry names these words without adding them to the reserved list (which the keywords_reference drift guard pins to the lexer’s tokens), and a second mechanical coverage test extends ADR 0156’s hover floor from reserved to contextual keywords — the tooth that would have caught this. bynk-lsp + a public bynk-fmt::annotation_to_string; no grammar, checker, or emitter change. Closes #476.
v0.132.1A uses-imported refined type’s user-defined static methods reach the consumer (emitter fix). A context that uses a commons and calls a user-declared static method on one of the commons’ refined types — let r = Cents.fromInt(n) where the commons declares fn Cents.fromInt(n: Int) -> Result[Cents, ValidationError] — passed bynkc check but failed tsc (#481): the consumer rebrands the imported type to a nominal alias and re-exports a value-side const, but that const forwarded only the built-in of/unsafe, so Property 'fromInt' does not exist on the rebranded Cents. The rebrand now forwards every attached method (static and instance) by delegating to the value-imported __Commons<T> — the same delegation the of/unsafe forwarders already use, with an as unknown as cast bridging the commons brand to the context brand. Independent of file layout (single- or multi-file commons) and target (--target bundle and --target workers). Emit-only — no grammar, checker, or AST change; a consumer with no such call emits byte-for-byte identical output (the defect was latent, so no existing snapshot changed). Closes #481.
v0.132.0bynkc test resolves a commons split across a directory (emitter fix; ADR 0160). A commons split across a folder (src/thing/a.bynk, src/thing/b.bynk, each commons thing) is a supported source layout that passes bynkc check and emits correctly in production (per file, out/thing/*.ts), but bynkc test failed at tsc with TS2307 Cannot find module — the generated test module imports the commons as one namespace (import * as thing from "./thing.js"), a file a multi-file commons never produces. The test build now emits an aggregating barrel out/thing.ts that export *s each of the commons’ files, so the namespace import resolves for the directory layout exactly as it does for a single file — at all four import sites (the suite target, each consumes/uses target, and the integration harness). The barrel is test-path-only (a non-test build ships no barrel, so production output is byte-for-byte unchanged), emitted once per commons across the unit + integration passes, and composes for dotted names (commons a.b barrels at out/a/b.ts). Emit-only — no grammar, checker, or AST change; hover/completion/semantic-tokens/signature-help unchanged (ADR 0156). Closes #451. Also fixes a related multi-file-commons emit defect (#479): under --target workers, the boundary codec (serialise_T/deserialise_T) for a commons’ refined type was emitted in every file of the commons rather than once in the file that declares the type — so a sibling file (e.g. the one holding fn T.make, not type T) shipped an orphan codec with the type and its imports out of scope, breaking tsc (and, over the barrel above, colliding with TS2308). The codec is now scoped to the declaring file.
v0.131.0CORS for from http services (language + emitter; ADR 0159). A from http service emitted a fixed header set and had no OPTIONS handler, so a Bynk Worker could not be called cross-origin from a browser (#396, deferred from the retired in-browser track). A service now declares a cors { } policy in header position — origins (a static allowlist, or ["*"]), optional headers/credentials/maxAge — from which the compiler synthesises an OPTIONS preflight (answered before the by/Bearer auth seam, since a preflight is credential-less by spec) and stamps Access-Control-* on every response of that service, uniformly across the Ok/Raw/redirect/error/stream variants. Access-Control-Allow-Methods is derived from the routes (never restated); Allow-Headers defaults to content-type (+ Authorization when the service has a Bearer route). A concrete allowlist reflects the matched origin with Vary: Origin (a no-match omits the grant — fail closed); a wildcard emits *. credentials: true with ["*"] is a compile-time error (bynk.http.cors_wildcard_credentials — the Fetch spec forbids it). Opt-in and fully additive: a service without cors { } emits byte-for-byte identical output, and the closed HttpResult registry (ADR 0126/0143) is untouched. bynk-syntax (a cors contextual keyword + CorsPolicy AST) + bynk-emit (the checker validation, the workers_entry.rs preflight/stamping, two runtime.ts helpers) + bynk-fmt; hover/completion track the new section (ADR 0156). Closes #396.
v0.130.0Literal patterns in match (language). match now dispatches a primitive Int/String/Bool scrutinee — or a refinement over one — against literal patterns (31 => …, "english" => …, true => …, and negated ints -1 => …), closing the gap between the type-system spec’s §2.3.4 grammar (which always listed c) and the parser (which rejected any bare literal). This is the idiomatic way to map a raw external value to a domain type, replacing an if/else if == chain. A match is now classified as either variant-kind (sum/Result/Option) or literal-kind; the two don’t mix. Exhaustiveness: Int/String need a wildcard _; Bool is complete once both true and false appear. A repeated literal arm is rejected (duplicate_literal_arm), and a literal on the right of is is rejected (is_literal_pattern) — is tests type/refinement, not value equality. The literal set is ADR 0001’s closed set (no Float, no ()). Lowers to a value-switch (JS === semantics). where-refined patterns and or-patterns remain deferred. Closes #441. Grammar + parser + checker + emitter + formatter + tree-sitter; match-arm completion stays silent on a primitive scrutinee.
v0.129.0Refinement-family CodeLens (tooling; editor-currency track — the parked refines half of #259). Slice 6 (v0.127) shipped the providers half of #259 (N providers on a capability) and parked the refinements half, which — unlike providers — had no index relation. This adds it. A refined/opaque type (or plain alias) over a builtin base now shows an N refinements of <Base> CodeLens listing its refinement family — every type over the same base across the project (type Email = String where …, type UserId = String, type Slug = String where … all read 3 refinements of String); click to peek the family. Per #259 this is the flat “what refines T” query that fits Bynk’s model (refinement is over the seven builtin base types, which aren’t navigable symbols), not a typeHierarchy tree. Backed by a new index::RefineEdge { base, ty } captured at the type-def walk in bynk-emit (where symbol_modifiers already reads the TypeBody base and discarded it) plus refined_base / refinements_over accessors on ProjectIndex — mirroring the ImplEdge / impls_of pattern the provider lens uses. A lens is emitted only for a family of ≥ 2 (a lone refinement has nothing to navigate to), and its showReferences payload flows through the existing client hydration, so no VS Code change. Closes #259. bynk-lsp + a small bynk-check index edge (one population line in bynk-emit); no grammar, checker-semantics, or emitter-output change.
v0.128.0match-arm pattern completion (tooling; editor-currency track — the deferred half of slice 3). Slice 3 (v0.124) shipped scrutinee-variant completion at an <expr> is <cursor> position and deferred the match-arm half as a named fast-follow; this closes it. At an arm-pattern-start inside a match <scrutinee> { … }, the editor offers the scrutinee sum type’s variantsmatch order.status { <cursor> and Del<cursor> on a fresh arm both offer Pending/Shipped/Delivered/…. It reuses slice 3’s pieces unchanged: the scrutinee is typed through type_receiver (the expr_types value-member path, so it obeys the same clean-file ceiling and goes silent, never wrong, on a broken buffer), and the candidate set is the shared sum_type_variants; only the arm-position detector (match_scrutinee_offset) is new. It is deliberately conservative — it fires only at the start of an arm’s pattern (after the { or a top-level ,, before any =>), never inside an arm body or a nested constructor pattern (Ok(<cursor>). The one wrinkle over is: a fresh-line or after-comma arm already looks like a keyword/expression position, so the variants are merged in ahead of the keyword/local candidates rather than gated on an empty result — but the expensive scrutinee typing stays gated behind the cheap lexical check, so ordinary keyword-position completion is untouched. bynk-lsp only; no language/compiler change.
v0.127.0Editor-currency slice 6 — codelens depth (tooling; editor-currency track — the optional tail). Two additions widen the “healthy but narrow” codelens surface. A per-case test-run filter: bynkc test gains --case <name>, which runs only cases whose name matches — filtered at execution, not merely in the report (the emitted runner reads BYNK_TEST_CASE and threads it into every suite’s run(only), guarding each case with `only === undefined
v0.126.0Editor-currency slice 5 — the UI surface (tooling; editor-currency track). The VS Code extension’s .bynk-native affordances, absent till now, land as manifest contributions. Menus: run/debug buttons in the editor title bar and right-click menu (editor/title/run, editor/context, scoped editorLangId == bynk); New Context / Open Project Config on a folder in the explorer (explorer/context); and the Command Palette entries for the Bynk commands are now when-gated on a Bynk context, so they no longer clutter unrelated workspaces (the bootstrap New Project stays ungated). Keybindings: a Ctrl/Cmd+; chord — ; t runs the tests, ; d debugs them — scoped to bynk editors so it shadows nothing globally. Since when clauses cannot use the workspaceContains: activation predicate, the extension sets a bynk.hasProject context key (kept live by a bynk.toml create/delete watcher) that the gates read. language-configuration.json gains a wordPattern and onEnterRules (brace-aware auto-indent). No language/compiler change; extension manifest + a small activation hook only.
v0.125.0Editor-currency slice 4 — scaffold refresh (tooling; editor-currency track). The two editor snippet catalogues — the LSP’s completion::SNIPPETS and VS Code’s static bynk.json — are brought current with the language. Between them they now scaffold the constructs that had grown up unrepresented: the testing surface (suite with a case/expect, and property with for all), actor, agent invariant/transition step invariants, function requires/ensures contracts, refined and opaque types, and the uses/consumes/given clauses — alongside the ordinary declaration forms each set had been missing (SNIPPETS gained commons/fn/type record/type enum/provides; bynk.json gained adapter/a bare on call). Per the track’s DECISION A the two sets stay independent — the fill is asymmetric (each construct added to whichever set lacked it, no forced parity), and slice 0’s compile guard proves every entry still parses against the current grammar. A new union-coverage test pins that each refreshed construct is scaffolded by at least one catalogue. No language/compiler change; editor assets only.
v0.124.0Editor-currency slice 3 — completion depth (tooling; editor-currency track). Completion now fires in the non-keyword contexts ADR 0093’s matrix left as amendments. Record field-name completion on construction: Order { <cursor> offers Order’s fields (the field-type position after : stays a type position). Header/clause positions: from <cursor> offers the service protocols (http/cron/queue/WebSocket); on <cursor> the handler kinds (call, the HTTP methods, schedule, message, open, close); by <cursor> the project’s actor names; exports <cursor> the export kinds (capability/transparent/opaque); provides <cursor> the in-scope capabilities. Inside a requires/ensures predicate, the enclosing function’s parameters (and result in an ensures) are offered. And <expr> is <cursor> offers the scrutinee sum type’s variants, resolved from expr_types (subject to the clean-file ceiling; match-arm pattern completion is a named fast-follow). Field/variant/actor enumeration reuses the same project-parse the member/value-receiver cells already run; the lexical clause detectors are word-boundary-tight (a field named from, or the on inside session, does not trigger). No language/compiler change; bynk-lsp only.
v0.123.0Editor-currency slice 2 — hover depth for declarations (tooling; editor-currency track). The LSP hover renderers, which had collapsed to a v0.25-era summary, now show what each declaration actually means: a type renders its record fields, sum variants (with payloads), the refined/opaque where predicate and opaque base (was type X = record/sum/Int); a function renders its requires/ensures contract clauses (v0.115) beneath the signature; a service renders its from <protocol> header and a line per route (on GET("/path"), on schedule("…"), …) (was a bare handler count); an agent renders its store fields and invariant/transition step invariants (v0.116) (was a store-field count). Predicates and where clauses render through bynk-fmt’s own surface renderers (expr_to_string/refinement_to_string, now pub), so hover shows exactly what bynkc fmt would write — one source, no LSP-local copy that can drift (ADR 0156). Settles the track’s DECISION B: hovering a Recv.member name-receiver access (Clock.now) shows the op signature via the same resolve_label path signature help uses, over the project and the embedded bynk surface — no new index. No language/compiler change; bynk-lsp (+ two bynk-fmt functions made public) only.
v0.122.0Editor-currency slice 1 — parameter & local hover (tooling; editor-currency track). Hovering a let binding, a parameter, or self now renders its inferred type, closing the clearest of the hover regressions the track named. The hover handler reads the checker’s already-captured tables — bynk-ide::diagnose_project’s locals (each LocalBinding carrying its surface-rendered type and scope, resolved under the cursor by the same locals_nav machinery go-to-definition uses) and expr_types for self — so hover, inlay hints, and signature help render one surface form (ADR 0156’s single-source principle; the track’s DECISION C). Renders let x: <ty> / param n: <ty> (a new LocalKind on the binding sink, populated at the checker’s existing record sites) and self: <Type> — the receiver type for a method, the agent name for an agent handler (the synthetic __<Agent>Self record is un-synthesised for display). self reads expr_types and so is subject to the clean-file ceiling, degrading to slice 0’s keyword doc on a broken buffer, never to a wrong type. General expression-type hover is deliberately out of scope (a later, separately-motivated addition). No language/compiler change; bynk-lsp (+ a small bynk-check locals-sink field) only.
v0.121.0Editor-currency slice 0 — the guardrail, and its first casualty (tooling; ADRs 0156, 0157). The editor surface (bynk-lsp + vscode-bynk) is now held to a mechanical floor: a coverage test asserts every lowercase-initial keyword has both a completion doc and a hover path, and a scaffold-compiles test lexes and parses the LSP’s declaration snippets and VS Code’s static snippets/bynk.json against the current grammar (each catalogue independently — no cross-set parity requirement, ADR 0157). The first casualty: bynk-lsp’s SNIPPETS scaffold still offered test "…" { }, a container keyword suite/case replaced back in v0.112 — deleted, and the new compile test proves the rest still parse. Hovering a bare reserved keyword (requires, suite, transition, …) now renders its one-line registry doc — the gap the coverage test’s hover clause was red on before this fix, closing hover currency for the whole testing-track vocabulary landed in v0.115–v0.119. No language/compiler change; bynk-lsp/vscode-bynk only.
v0.120.0The testing track is complete and retired. The milestone marking the whole theme done — one predicate surface over the subject ladder value → domain → call → snapshot → step → history, sourced by supply-or-generation, checked at one of three checkpoints (commit boundary / dev call site / test runner), run at one of three tiers (unit/integration/system) — realising the thesis the agent-invariant model already stated: “invariants are the contract half of validation; tests are the behaviour half.” All eight landings shipped (v0.112–v0.119): expect + suite/case with structural failure reporting (v0.112), structural test-ness + flat [paths] (v0.113), property/for all with Val[T] replacing Mock[T] (v0.114), function contracts requires/ensures (v0.115), step invariants transition (v0.116), the observation surface expect Cap.op called … + trace (v0.117), the tier dial as unit | integration | system + per-seam provides (v0.118), and history properties for all run: History[Agent], the visionary tail (v0.119). The track doc (design/tracks/testing.md) is retired — its decisions live on in ADRs 0144–0155 and the spec-in-place (spec/syntactic-grammar.md + static-semantics.md, reference/testing.md + agent-invariants.md, with guides/testing/philosophy.md the keystone). Deferred follow-ons (none blocking the theme): multi-agent protocol properties (the history rung is single-agent only), the universal-emission guarantee (still without a home), a declaration-positional enum Ord for ordered-status transitions, and whether example earns its own keyword. No language/compiler change in this release; it consolidates and marks the theme.
v0.119.0History properties — for all run: History[Agent] (language/test-runtime; testing track slice 7, ADR 0155). The top rung of the subject ladder: a property whose generated subject is a run of an agent. The runner generates a bounded, seeded sequence of the agent’s handler calls, drives them through the real handlers from the agent’s initial state, and binds run — an ordinary List[Step] — for the predicate to judge. Each Step carries .call (a sum over the agent’s handlers, matched with is), .accepted (whether the handler committed vs. an invariant/transition refusal), and .old/.new (the committed state pair). Assertions reuse the List surface — “always” is run.all(...), “eventually” run.any(...), “before” a prefix check via run.upTo(step) — so there is no temporal vocabulary and no matcher library (ADR 0144); unbounded liveness is deliberately inexpressible (a history is a runner sample over bounded runs, not a proof). Behaviour is generated by driving sequences, not fabricating states — a valid state need not be reachable (DECISION P) — so every reached state is one a handler actually produced. History[Agent] is agent-only (bynk.history.not_an_agent) and generative-only, legal only in for all position (bynk.history.outside_property); the agent must be drivable (bynk.history.not_generable); a history that restates a declared invariant/transition is flagged (bynk.history.restates_invariant). Carries no as — in-process on the flake-free tier (ADR 0153); observation and provides doubles compose inside a driven run (ADR 0152). Seeded and length-bounded, shrinking the sequence (re-driving after each reduction so the counterexample stays reachable) then its arguments, with a --seed reproduce line. The driver is a test-only export on the agent module, stripped from deploy builds; single-agent histories now, multi-agent protocols a named follow-on. Additive — no rename or removal. ADR 0155.
v0.118.0The tier dial (as unit | integration | system) and per-seam stub (provides until v0.159); mocks/suite integration/wires retired (language/test-runtime; testing track slice 6, ADRs 0153 + 0154). A test declares how much of the real world runs with an as <tier> clause on the case (and suite) header — unit (the default, elided; collaborators you control seam by seam), integration (real collaborators within one context, no wire), system (contexts wired as the Workers they deploy as, across the real serialise → JSON → deserialise edge). A tier is one body promoted, not a distinct kind of test: promotion changes only the header (case stub > suite stub > tier default), the body is byte-for-byte identical, and the agent-state lifecycle is fixed across tiers. as on the suite sets a default the case overrides (case wins). Tiers are case-only (a property has no tier — bynk.tier.property_has_tier); system participants are inferred from the unit’s transitive consumes graph (no wires) and must span ≥ 2 contexts (bynk.tier.system_needs_wire). A per-seam stub Cap.method(<args>) returns <value> | fails clause overrides one collaborator method’s provision — an explicit call pattern (the one predicate surface: _, literals, is; first match wins) on the left, a value or fails, never a computed body, on the right; a sequenced returns each [<outcome>, …] supplies one outcome per call with last-outcome-repeat exhaustion. Capability-only; overriding a non-consumed capability is bynk.stub.not_a_seam, an unknown op bynk.stub.unknown_op, an ill-typed RHS bynk.stub.rhs_type, a malformed sequence bynk.stub.bad_sequence. This retires mocks (the re-implementation block), the suite integration "…" { wires … } form, and wires — folding integration into suite … as system — and the bynk.mock.* / bynk.integration.* diagnostic families. Together with Mock[T] → Val[T] (v0.114), the word “mock” is gone from the language. Test-build-only via the ADR-0147 strip. ADRs 0153, 0154.
v0.117.0Observation surface — expect Cap.op called … (language/test-runtime; testing track slice 5, ADR 0152). Inside a case, observe a consumed capability’s calls: a thin sugar on expect over a Cap.op subject — called / never called / called once / called <n> times / called … with <pred> / A.op before B.op — plus a trace(Cap.op) escape hatch that yields the recorded calls as an ordinary List of per-op records (asserted with length()/all/any/indexing). Calls are recorded automatically at the injected seam in the test build, so a pure-observation case needs no mocks or setup; the sugar and trace read one log and cannot disagree. A with <pred> is the same predicate surface over the operation’s parameters, in scope by name (with msg == "…"); it must be pure Bool (bynk.observe.with_not_bool / impure_with). Observing a non-capability is bynk.observe.not_a_seam, an unknown op bynk.observe.unknown_op, an observation or trace outside a case bynk.observe.outside_case / trace_outside_test. The sugar words are contextual (ordinary identifiers elsewhere); trace is a test-only builtin. Recording is emitted only under bynkc test and stripped from the deploy build, so a module without observation emits byte-for-byte unchanged and production carries no cost (ADR 0150). Additive — no rename or removal. ADR 0152.
v0.116.0Step invariants — transition (language/compiler; testing track slice 4, ADR 0151). The invariant subject widens from a single committed state to the move between two. Beside its snapshot invariants, an agent may declare transition <name>: <pred over old/new>, where old is the last committed state and new the state the current commit would persist — each the agent’s state record (old.status, new.balance). The predicate is the same predicate surface as an invariant/case/property/ensures — pure Bool with implies/is/operators/pure methods, no new grammar — and old/new are contextual (special only inside a transition; ordinary identifiers elsewhere, so existing code naming a value old/new still parses). A non-Bool predicate is bynk.transition.not_bool, an impure one bynk.transition.impure_predicate, a duplicate name bynk.transition.duplicate_name, a cross-agent reference bynk.transition.cross_agent_reference, and a predicate mentioning neither old nor new bynk.transition.no_step_reference (it is a snapshot claim — use invariant). Checked at one point — the commit boundary, in the generated commitState beside the snapshot invariants, from the second commit onward: the genesis commit (an agent’s first) has no old and is skipped; a violation throws the same InvariantViolation-family fault before the write, so the offending commit never persists and the check fires at every test tier for free. No runner attack (unlike a contract’s ensures, DECISION D/J): a fabricated agent state is valid but not necessarily reachable, so behavioural generation over transitions is a runner-driven handler-sequence concern, not value fabrication (a later slice). Ships with is/implies/==/!= (and ordering on numeric/temporal fields); an ordered enum transition (new.status >= old.status) needs a declaration-positional enum Ord, a named prerequisite (DECISION O). Additive — no rename or removal; emission is tsc-clean and strips clean. ADR 0151.
v0.115.0Function contracts — requires / ensures (language/compiler; testing track slice 3). A pure fn may carry named contract clauses between its return type and body: requires <name>: <pred> (a precondition over the parameters) and ensures <name>: <pred> (a postcondition over the parameters and result, the return value — the awaited element for an Effect). Each predicate is the same predicate surface as a case/property/invariant — pure Bool, no new grammar. result is contextual (special only inside an ensures; an ordinary identifier elsewhere), and referencing it in a requires is bynk.contract.result_in_requires; an impure predicate is bynk.contract.impure_predicate, a non-Bool one bynk.contract.not_bool, and a duplicate clause name bynk.contract.duplicate_name. A contract is checked at two points, for free: (1) a dev/test call-site guard checks each requires on entry and each ensures on exit at every call, throwing a failure that names the clause and the offending arguments/resultstripped from the deploy build (bynkc compile), so contracts add no production cost or behaviour (DECISION J: per-build-profile); (2) the runner attack — for every contracted function reachable from a test target, the runner generates arguments over the parameter domains (the v0.114 engine — boundary-inclusive, seeded, shrinking), filters them by the requires (like a for all … where), calls the function, and checks the ensures, reporting a shrunk counterexample with the same reproduce line a property gives. A claim about one result belongs in ensures (checked everywhere, generated for free); a property earns its keep only when the claim is relational or spans calls. A case/property that merely restates a contract is flagged bynk.contract.restated_by_test (a conservative, syntactic check). ADR 0150.
v0.114.0property / for all — generative tests, and Val[T] replaces Mock[T] (language/compiler; testing track slice 2). A property is the generative sibling of case, legal in the same suite: for all x: T binds x to a generated inhabitant of T (comma-separated for several), an optional where <pred> (a pure Bool, bynk.property.where_not_bool) filters the generated tuples, and the body is one or more expects — the same predicate surface as a case/invariant/ensures, no new assertion grammar. Generation draws from a type’s refinement domain and includes boundary values (Int Positive1/small positives/boundary; InRange(a,b)a, b, interior; String MinLength(k)/Length(k) → at/above length; sum → each variant; record → each field; opaque → over the base). A type must be refinement-generable: a String where Matches(re) has no generator and must be pinned (bynk.val.needs_pin), and an agent cannot be generated (bynk.val.agent_not_generable) — behavioural agent testing is a later slice. A property that merely re-checks a refinement its type already guarantees is flagged bynk.property.restates_refinement (a conservative, syntactic check). On failure a property reports the case count, the run’s root seed, and a shrunk counterexample with a copy-paste reproduce line; the new bynkc test --seed <hex> flag threads the root seed so a run reproduces byte-for-byte (without it, each run draws a fresh seed, printed only on failure). Mock[T] is retired and replaced by Val[T] — a straight rename of the value fabricator (Val[T] fabricates a valid inhabitant, Val[T](v) pins one, refinement-checked), with its diagnostics renamed bynk.mock.*bynk.val.* (outside_test, unknown_type, needs_pin, pin_not_literal, literal_violates, arity, pin_unsupported, unsupported_kind). The mocks collaborator block is unchanged and keeps its bynk.mock.* codes (unknown_target, duplicate_target, signature_mismatch, in_commons_test).
v0.113.0Structural test-ness — a suite is legal in any file, stripped from the build; flat [paths] (compiler/tooling; ADR 0147, testing track slice 1b). Test-ness becomes a property of the suite declaration, not of a file’s name or directory — the honest extension of ADR 0144 (the language already gates expect at the case block, not the path). A .bynk file may hold more than one top-level unit: an atomic file with commons/context and a suite together — the shareable, single-file, in-browser-playground case. The parser returns all top-level units per file and the project model partitions declarations (not files) by kind, so the source units flow to the build and the suites to bynkc test only. The build strips test-only declarations: bynkc compile skips every suite — never type-checked for the build, never emitted into the deployable — while bynkc test compiles and runs them; discovery scans the whole source tree for suites, not a designated folder. No .test.bynk marker and no path-identity for a suite — it names its target and is legal anywhere; bynk.project.inconsistent_test_path is retired (source units keep inconsistent_commons_name). [paths] src/tests → a flat include/exclude: include (trees to compile) defaults to the conventional roots that exist (src, and tests when present) or the project root itself — so a conventional and a flat project both need no config — and exclude (plus the tool’s out/node_modules/dot-directories) is pruned from discovery. A consequence to accept: placement is inert in both directions (a context under tests/ emits; a suite under src/ is stripped) — enforce the old separation with a lint, not the build. The formatter formats every unit in a multi-unit file. Migration is at leisure: old .test.bynk files are ordinary .bynk, and legacy [paths] src/tests keys are ignored, so conventional projects build unchanged. Named follow-ons: multi-unit editor (LSP) awareness of atomic files; arbitrary N-root monorepo include.
v0.112.0expect + suite/case — one predicate surface for tests (language/compiler; ADRs 0145, 0146, cites 0144; testing track slice 1a). The first slice of the testing track. assert becomes expect (DECISION B, ADR 0145) and the braced test { … } container becomes suite/case (DECISION Q, ADR 0146) — a straight rename of the keywords, not a change of meaning: a test file is a suite naming its target unit, holding named cases, and a case body checks predicates with expect. The point is one predicate surface (ADR 0144): an expect is the same pure Bool predicate as an agent invariant or a function ensuresis, implies, the operators, pure methods — so moving from writing code to verifying it introduces no second assertion grammar and no matcher library; expect r is Ok(_) reuses is. expect is valid only inside a case (bynk.expect.outside_case) and must be Bool (bynk.expect.not_bool), the same fail-closed check invariant/ensures get. Structural failure reporting: when the predicate is a top-level comparison (==, !=, <, <=, >, >=), a failed expect renders the predicate and its expected-vs-actual operands (expected: total == 900 / actual: 950 == 900) — because there is one predicate shape to decompose, the runner double-evaluates the operands (sound, predicates are pure per ADR 0144) and shows both sides via __bynkShow; the multi-line detail rides the existing --format json message field unchanged. The diagnostic family is normalised onto bynk.expect.* / bynk.suite.* (assert/test codes retired). Clean-slate rename across the whole toolchain — lexer/parser/AST, checker, emitter, formatter, tree-sitter grammar, and the VS Code TextMate keywords — with the assertexpect and testsuite/case codemod applied to every fixture; expect was already reserved (v0.7). No new runtime capability; suite integration/wires and mocks/Mock[T] are unchanged this slice (the tier dial and provides land later). Emission is tsc-clean and strips clean.
v0.111.0Raw — an author-owned HTTP body: Raw(body: Bytes, contentType: String) (language/compiler; ADR 0143). One new HttpResult payload shape and one 200-only variant, in the Streaming idiom (ADR 0129) — a single HTTP_VARIANTS row that the resolver, LSP completion, and emitter dispatch pick up for free, plus the construction-check arm, the variants_of binding, and the runtime case. The service-tier escape hatch for non-JSON bodies (robots.txt, sitemap.xml, .well-known, RSS/Atom, a CSV download, a QR-code PNG): the runtime writes the Bytes straight into the Response under the declared content-type, bypassing serialiseValue — no codec runs; the name warns that the typed-wire guarantee is deliberately off (the author owns the encoding). Carries the Bytes primitive (v0.110): a PNG flows in directly, text goes through Bytes.fromUtf8 so the UTF-8 charset is an explicit author decision. Raw is the first two-argument payload shape and the first two-field variant payload; the content-type is an opaque, unvalidated String (a refined MediaType is a named follow-on). 200-only, like Streaming — service-tier raw bodies are overwhelmingly 200; a custom-status raw body (a 404 HTML error page) is a presentation concern held out of scope (that is the frontend tier — Bynk serves bytes + a content-type, it does not template HTML), and the boundary is re-openable by one registry row. Not the workers wire boundary (ADR 0142 D8): the runtime writes the Uint8Array with no cross-context erasure, so no base64 hop and no bynk.types.bytes_at_workers_boundary interaction. Additive — no rename or removal; construction reuses the existing arity/mismatch diagnostic codes. No parser/grammar/tree-sitter change; emission is tsc-clean.
v0.110.0Bytes — a binary primitive, erased to Uint8Array, base64 on the wire (language/compiler; ADR 0142). The seventh base type, and the representation for arbitrary binary data that String (UTF-8 text) cannot hold without corruption — the shared prerequisite under every binary surface (R2 objects, binary HTTP bodies, Stream[Bytes] downloads), built alone; those consumers are named follow-ons. It is the first base type not erased to number: a Bytes lowers to a host Uint8Array (strip-only-clean, ADR 0136). No literal — constructed by Bytes.fromUtf8(s) (total), Bytes.fromBase64(s) -> Option[Bytes] (partial), Bytes.empty(); the surface is length(), toBase64() (total), and decodeUtf8() -> Option[String] (partial), with Option on the Int.parse/Float.parse precedent (ADR 0048). The one departure from the Float/Duration/Instant playbook: ==/!= compare by content (byte for byte), not host === — a Uint8Array compares by reference, so equality is real emitter codegen (__bynkBytesEqual); a record-over-Bytes gets correct equality via the ordinary hand-written field-comparator idiom (no auto-derived structural equality). Bytes is equatable but not orderable and not Map-keyable (bynk.types.unkeyable_map_key; key on toBase64() — the first equatable base type ADR 0038 excludes); no arithmetic/concat/slice/hex in v1 (deferred). On the wire it serialises as a base64 JSON string (base64-validated on read), so it round-trips through any record or store field and crosses a bundle context boundary — a fully ordinary serialisable value, the opposite of Stream/Connection. One current limit (ADR 0142 D8): the erased workers cross-context wire path does not base64-encode a bare Bytes, so that one position is diagnosed bynk.types.bytes_at_workers_boundary rather than silently mis-encoded — a Bytes inside a record crosses fine via the record’s typed codec. No parser/grammar/tree-sitter change; emission is tsc-clean.
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.