Version compatibility & changelog
Bynk is pre-1.0 and developed in small, spec-first increments (see Versioning & roadmap). This book is written against v0.289.
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 | Bis 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 testedcommons. 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).
Recent increments
Section titled “Recent increments”| Version | Highlights |
|---|---|
| v0.289.41 | Resolves #1483 — emit_stub_class (bynk-emit/src/project/tests_emit.rs) now returns one real bynk_ts::TsStmt instead of a plain String. The class wrapper (header/fields) stays hand-written text — Decision C, the same emit_provider precedent (#1480) this function’s own in-place comment already cited — wrapped as one TsStmt::raw with no nested_map: this function has no LowerCtx/source-map involvement anywhere in its own construction (lower_stub_value_block/emit_stub_rhs build already-formed text, not through a body-local SourceMapBuilder), unlike emit_provider/emit_service/emit_agent’s own per-handler bodies, so there is nothing to merge. Each method still prints as its own real TsClassMethod fragment via bynk_ts::print_class_method, spliced directly into the wrapper exactly as before — no internal construction logic changed. The one call site (emit_test_module) now prints the returned node through bynk_ts::print_stmt instead of pushing the raw String directly, byte-identical output. Verified: cargo test -p bynk-emit (187/187) and the full cargo test --workspace (all green, byte-identical fixture output via the tsc_verify corpus — both full-corpus runs, ~180s each). ts_writes (gated probe) unchanged — no write!/writeln!/format! call site was retired, only the function’s own return type and its one call site’s plumbing. |
| v0.289.40 | Resolves #1482 — emit_agent now returns a real Vec<bynk_ts::TsStmt> (was out: &mut String plus source_map: Option<&RefCell<SourceMapBuilder>>). Per #1462’s own end-to-end audit of emit_agent’s 99 ts_writes sites: the 66 already-argued print-and-splice sites (state interface, registry, zero factory, rehydration gate, construction factory) now push their already-real top-level nodes directly into the returned Vec instead of being printed through the removed out parameter — no internal construction logic changed. Two sites stay genuinely opaque, each captured as its own TsStmt::raw, mirroring emit_provider’s Decision-C precedent (#1480): the DO class (export class {Name} { ... }, emit_agent’s own instance of Decision C, cited in place since #1371) is built into a local buffer/map pair — every internal print-and-splice site inside it (loadState/commitState/the per-handler loop via emit_class_method_and_merge_source_map/the WS-hosted DO methods via emit_ws_do_method/fetch) is unchanged, just now merging into that local map instead of the function’s former external parameters — and wrapped as one TsStmt::raw carrying a class-wide nested_map. The history-driver (__bynkDriveHistory_*, gated on ctx.history_target_agents) is #1386’s own standing permanent-exclusion decision, reconfirmed by #1462’s own correction (test-support only, stripped from deploy, thinnest fixture coverage in the track, its own per-handler discriminated-union algebra deferred to any by design) — genuinely raw hand-templated text with zero bynk_ts/LowerCtx involvement, captured as a second, separate TsStmt::raw with no nested_map (nothing to merge), not decomposed further. Both wraps use TsStmt::raw, not TsStmt::verbatim/VerbatimOrigin — per TsStmtKind::Raw’s own doc, Verbatim is reserved for temporary, not-yet-converted residue the verbatim_sites/verbatim_origins probes track toward zero, and using it for genuinely permanent content (lower.rs’s own exclusion, emit_provider’s own class wrapper, and now these two) would misrepresent it as unfinished work. emit_agent’s one call site in emitter.rs now loops the returned Vec through extend_printed_and_merged (already established by #1480 for emit_free_fn), the same composition emit_service/emit_provider already use. Verified: cargo test -p bynk-emit (187/187) and the full cargo test --workspace (all green, byte-identical fixture output via the tsc_verify corpus — both full-corpus runs, ~180s each). ts_writes (gated probe) moves 826 → 822 — the four retired writeln!(out).unwrap() blank-line calls for the state interface/zero factory/rehydration gate/construction factory, now plain TsStmt::blank pushes instead. |
| v0.289.39 | Resolves #1481 — emit_service no longer hand-rolls its own Raw-body print-time-offset recovery by string-matching the printed text’s own tail (the “suppress rather than mis-record” degrade-to-no-merge #1360 finding 3 established); emit_service now returns one bynk_ts::TsStmt (was out: &mut String plus source_map: Option<&RefCell<SourceMapBuilder>>) — the whole export const {name} = { ... }; object literal, built into a local buffer via bynk_ts::print_object_entry_and_merge (#1477’s shared TsObjectEntry::Method merge helper, replacing the per-handler hand-rolled .merge( call this issue named directly) once per handler, carrying an object-wide nested_map that combines every handler’s own body_smb, rebased from body_out-relative to raw_body-relative via shift_checkpoints (the same rebase already used for the subscriber-filter/schema-dispatch prologues) before being set on the handler’s Raw body statement. Its one call site in emit_project() merges this single combined map into the real module map via print_stmt_and_merge, the same composition emit_provider (#1480) already established. Each handler body stays one opaque, correctly-source-mapped Raw node — no further decomposition (ADR 0391). emit_class_method_and_merge_source_map/RawBody are left untouched — still used by emit_agent’s own not-yet-converted call site (#1482). Verified: cargo test -p bynk-emit (187/187), cargo test -p bynkc --test source_map --test source_map_bodies (14/14, including service_handler_body_keeps_its_own_statement_lines_inside_the_events_iife, service_handler_body_maps_per_statement, and service_handler_with_subscriber_and_schema_prologue_maps_per_statement — the tests that directly exercise this code path), and the full cargo test --workspace (all green, byte-identical fixture output via the tsc_verify corpus). ts_writes (gated probe) moves 827 → 826. |
| v0.289.38 | Resolves #1480 — emit_free_fn and emit_provider no longer hand-roll their own Raw-body print-time-offset recovery by exact arithmetic (guarded by a debug_assert!); #1477’s bynk_ts::print_stmt_and_merge/print_class_method_and_merge (via the new TsStmt::nested_map field) now recover that offset themselves. emit_free_fn returns Vec<bynk_ts::TsStmt> (an optional doc comment, the function decl with its own body’s nested_map set, a trailing blank) instead of writing into out: &mut String; both call sites (emit()/emit_project()) print through the new extend_printed/extend_printed_and_merged helpers in emitter.rs. emit_provider returns one bynk_ts::TsStmt — the whole class-wrapper text (Decision C, unchanged: still one hand-written, permanently-opaque TsStmt::raw construction per provider declaration, not decomposed further — a real TsDecl::Class would need every method’s own body captured into a local buffer for Raw-embedding anyway, and this class’s own spacing convention already disagrees with TsDecl::Class’s established one) — built into a local buffer, carrying a class-wide nested_map that combines every op method’s own body_smb (each merged in via bynk_ts::print_class_method_and_merge at its own real offset within that local buffer); its one call site in emit_project() merges this single combined map into the real module map via print_stmt_and_merge. emit_class_method_and_merge_source_map/RawBody (the hand-rolled recovery helper #1477 was meant to retire) are left untouched — still used by emit_service/emit_agent’s own not-yet-converted call sites (#1481/#1482). Verified: cargo test -p bynk-emit (187/187), cargo test -p bynkc --test source_map --test source_map_bodies (14/14, including contract_guarded_body_keeps_its_own_statement_lines and provider_op_body_keeps_its_own_statement_lines — the two tests that directly exercise these code paths), and the full cargo test -p bynkc fixture-diff + e2e suite. ts_writes (gated probe) moves 829 → 827 — both from emit_free_fn’s own retired offset-recovery code: its debug_assert! message built via format!, and its final writeln!(out) trailing-blank-line push, now a plain TsStmt::blank in the returned Vec instead. |
| v0.289.37 | Resolves #1479 — tests_emit.rs’s own no-body direct callees now build real bynk_ts trees instead of pre-printed String/out: &mut String: emit_ns_destructure (-> Vec<TsStmt>, printed by every caller via a new crate::emitter::extend_printed_at(out, stmts, depth) — extend_printed_at generalizes #1478’s extend_printed with an explicit depth parameter, since this file’s own scaffold-body callees print nested at depth 2, not emitter.rs’s module-level depth 0), emit_test_deps/emit_integration_harness (each returns the one real TsStmt they ever built, was pre-printed String), observation_call_record_types (-> Vec<TsStmt>), the codec-helper glue inside emit_test_module (now calls serialisation::decls_as_stmts[_block] + extend_printed_at instead of serialisation::print_decls[_block]), and emit_system_http_support/sysdrive_driver (SystemHttpSupport.code and sysdrive_driver’s return both go from pre-printed String to real TsStmt(s); the HS256 signer block, #1485’s own separate scope, stays exactly the hand-written text it always was, carried forward as one TsStmt::raw). Converting the codec-helper glue’s call sites removed serialisation::print_decls/print_decls_block’s own last two production callers — deleted both (dead code), fixing the stale intra-doc [\print_decls`]links elsewhere inserialisation.rsthat referenced them.emit_test_module/emit_integration_modulethemselves are **not** converted by this issue, matching its own scope — that's the capstone, #1486, once every direct callee across bothemitter.rs/tests_emit.rshas landed. Byte-identical output verified:cargo test -p bynk-emit(187/187),cargo test -p bynkc(full fixture-diff + e2e suite, includingtsc_verify/bless_positive_fixtures/positive_fixtures). ts_writes(gated probe) unchanged at 829 — every converted call site here was alreadyprint_stmt-based, not format!/write!`-based, so this slice moves construction style, not that particular count. |
| v0.289.36 | Resolves #1478 — the 12 “no-body direct callee”s emit_project/emit call at their own top-level signature now build real bynk_ts::TsStmt trees (Vec<bynk_ts::TsStmt>) instead of writing into a shared out: &mut String: write_commons_doc, emit_context_rebrands, emit_cross_context_namespace_imports, emit_project_imports, write_header, emit_capability, emit_type (plus its emit_refined_type/emit_record_type/emit_sum_type sub-dispatchers), emit_messages_bundle, emit_make_surface, emit_boundary_helpers (plus its emit_consumed_context_helpers sub-call), and emit_json_codec_helpers. Two still-String-typed unconverted siblings (emit_context_deps_interface, emit_refined_checks) are bridged the same way earlier slices already established: their output is captured into a local buffer and carried as one bynk_ts::TsStmtKind::Raw statement. A new extend_printed(out, stmts) helper in emitter.rs bridges each converted callee’s Vec<TsStmt> back to the three call sites’ own out: &mut String (emit, emit_project, and emit_project’s per-item loop) that still need String output at the top; serialisation::decls_as_stmts/decls_as_stmts_block (siblings of the existing print_decls/print_decls_block) let the boundary-helpers trio thread their real declarations through without an intermediate print. Byte-identical output verified: cargo test -p bynk-emit (187/187), cargo test -p bynkc --test tsc_verify, cargo test -p bynkc (full fixture-diff + e2e suite, including bless_positive_fixtures/positive_fixtures). ts_writes (gated probe) drops 851 → 829 — 831 from the 12 converted sites no longer writing through format!/write!, then 829 once the review’s own print_decls/print_decls_block collapse (below) dropped their own last two writeln! call sites too. --apply also picked up two unrelated, already-stale trend numbers (keep_in_sync 236 → 235, bynk-ts test_density 58.4% → 59.6%): neither is attributable to this diff (no bynk-ts file is touched here) — the committed table was last regenerated at the #1471 stamp and missed a re-run after #1477 (PR #1488) touched bynk-ts/src, so this PR’s --apply is the first run to pick that drift up alongside its own real ts_writes move. |
| v0.289.35 | Resolves #1476 — emit_project’s runtime-import line is now built once, after the per-item body-emission loop populates ctx.runtime_use, with bytes()/icu() folded directly into write_header’s own construction instead of spliced in afterward by inject_runtime_imports. write_header’s own call moves to the end of emit_project; its output is prepended, and the module’s SourceMapBuilder checkpoints (recorded against the item-only buffer) are shifted by the header’s own final length first — the same shift_checkpoints mechanism emit_service’s own prologue insertion already uses. inject_runtime_imports/missing_bindings are not deleted (a real gap in the issue’s own original grounding, found during implementation): project/tests_emit.rs still calls them directly for emit_test_module/emit_integration_module’s own identical post-print splice — filed as its own follow-up, #1489. Byte-identical output verified across the full fixture corpus (tsc_verify, bless_positive_fixtures/positive_fixtures, the full bynkc e2e suite). |
| v0.289.34 | Resolves #1477 — bynk_ts::TsStmt gains an optional nested_map: Option<SourceMapBuilder> field, and the printer merges it into the caller’s own module map at the real print-time offset, reachable from three new sibling entry points (print_stmt_and_merge, print_class_method_and_merge, print_object_entry_and_merge) that append into the caller’s own buffer rather than returning a fresh string, so offsets stay correct once spliced anywhere but the very start of it. The check is centralised inside render_stmt itself (review of #1488 caught the first draft only checking a body’s own direct children, silently dropping a nested_map on the statement handed to print/print_stmt_and_merge directly or nested inside a Block/Switch/try/catch), and a source_id threads alongside the map throughout (the same review caught it hardcoded to 0, silently wrong for a multi-source aggregate map). Existing print_stmt/print_class_method/print_object_entry/print are unchanged in behaviour; print’s own top-level loop additionally threads its module map through automatically. No bynk-emit call site converted — bynk-ts only. |
| v0.289.33 | Resolves #1475 — reads tests_emit.rs’s 83 and workers_entry.rs’s 34 ts_writes sites end to end, closing Arc F’s own long-standing sampling caveat. workers_entry.rs confirms Arc F’s “all already-argued” claim exactly (no new gap; the unelaborated “small fold-in candidate” is most plausibly a repeated format!("__r_{pname}") re-derivation across 4-5 call sites, named but not scheduled). tests_emit.rs’s 83 breaks down as 51 cheap glue, 17 permanent (10 Decision A, 7 Decision C), 12 already inside #1472’s own wrapper-function finding (confirmed, not just cross-referenced — the wrapper functions sit inside this 83, not on top of it), 2 further new small gaps in destructure_vals (folded into #1484), and 1 probe-only artifact (:592, already documented as an accepted gap by the probe’s own code). Separately: the HS256 JWT-signer block #1472 found is structurally invisible to this probe (no format!/write! in it at all), not “already counted” as this pass’s own first draft claimed — review of #1487 caught the error. Also lands two stale corrections: the track doc’s own description of pred_condition_and_message/inject_runtime_imports (both since resolved, by #1471 and #1472 respectively) had drifted. No code change. |
| v0.289.32 | Resolves #1472 (scope the verbatim_sites capstone arc) — enumerates every direct callee of emit_project/emit_test_module/emit_integration_module (15 + ~13 respectively), finds 12 of emit_project’s own 15 already cheap to convert (real-node internally, signature-only), and settles the nested source-map design question #1461 left open: the printer should report the print-time offset of an opaque body blob instead of emit_class_method_and_merge_source_map reverse-engineering it, reusing SourceMapBuilder::merge machinery that already exists. Finds inject_runtime_imports was never actually entangled with that gap — it has its own independent fix (build the runtime-import node after the body loop, once ctx.runtime_use is fully known, order it first). Names 9 new permanent Verbatim sites by callee (emit_provider/emit_agent’s class wrappers, emit_agent’s history-driver, emit_stub_class’s class wrapper, 5 tests_emit.rs include_str! blocks) and 2 genuinely new, previously-unargued conversion candidates (four-plus near-identical test-function wrappers; an HS256 JWT-signer block better suited to include_str! extraction than tree conversion). Proposes an 8-slice sequence, printer extension first. No code change. |
| v0.289.31 | Resolves #1471 — pred_condition_and_message’s condition side now builds a real bynk_ts::TsExpr (Binary/Call nodes) instead of format!ing raw JS text, unblocked by two new bynk_ts::TsBinaryOp variants, GreaterThanEq/LessThanEq, at the same precedence tier as GreaterThan/LessThan/InstanceOf/In. Both real callers (emit::emit_pred_check, serialisation::emit_inline_pred_check) now consume the real expression directly, dropping the opaque Ident(cond) wrap; the explicit Paren around it stays unconditionally, matching pre-#1471 output byte-for-byte. msg stays the same opaque, already-escape_ts_string-escaped String it always was. Zero emitted-TypeScript change; ts_writes drops from 860 to 851 as a side effect. |
| v0.289.30 | Corrects #1462’s own track-doc claim that emit_agent’s history-driver (__bynkDriveHistory_*) is “a real, tractable, previously-uncounted conversion candidate” — #1386 already decided, before #1462 landed, that this exact function is a deliberate, standing exclusion (test-support-only, thinnest fixture coverage in the track, its own real algebra question deferred to any by design regardless of conversion). The history-driver moves from the “argued, not yet attempted” bucket to the permanent one, alongside lower.rs and the Decision-C wrapper text. No code change. |
| v0.289.29 | Resolves #1463 (Arc F, slice 4) — emit.rs’s deps-object-type builders (cap_ref_ty/build_deps_object_ty_with_surface/workers_env_ty/surface_ty) now build real bynk_ts::TsType nodes instead of hand-joined strings, closing the opaque TsType::named(deps_ty) wrap #1361 deliberately deferred and emit_context_deps_interface’s own surface field residual (#1453). append_deps_field widens a real Vec<TsTypeMember> (Vec::push) instead of string-splicing — review of #1469 found the old string splice was a latent hazard, not just untidy, and tightened the helper’s own signature to make the panic path it carried impossible by construction. One field (the actor-sum who union) stays pre-rendered opaque text — its comma-separated tagged-object shape doesn’t match TsType::Object’s own semicolon convention, a real fixture byte-diff caught this, not assumed; surface_ty’s own ReturnType<T> narrowed one level further per the same review, leaving only its typeof X operand opaque. Zero emitted-TypeScript change; ts_writes drops from 875 to 860 as a side effect (most of these functions’ own internal format! calls for TS syntax are gone, not just the TsParam-level wrap). |
| v0.289.28 | Resolves #1462 (argue ts_writes’s final residual floor after Arc F) — reads emit_agent’s own 99-site cluster end to end (the one Arc F’s own pass admitted was only sampled) and finds it is NOT uniformly already-converted: 66 sites match the established print-and-splice idiom, but the history-driver (__bynkDriveHistory_*, gated on history_target_agents) is genuinely raw hand-templated text throughout, 33 sites, a real tractable conversion candidate named here for the first time. The DO-class hand-written-wrapper decision (emit_provider’s own Decision C) is confirmed to also cover emit_agent’s identical wrapper. pred_condition_and_message/inject_runtime_imports are formally collected but corrected to “argued, not yet attempted” rather than permanent. No code change. |
| v0.289.27 | Resolves #1461 (ground the verbatim_sites capstone) — its lower.rs prerequisite is already solved by ADR 0391, its source-map prerequisite only partly (top-level checkpoints only, not the nested per-statement ones emit_project relies on today); the real remaining blocker is a callee cascade neither prior pass counted (emit_provider’s own class wrapper and others already permanently opaque). Deferred as its own future multi-slice arc, not attempted here. No code change. |
| v0.289.26 | Resolves #1460 (the lower.rs collection-kernel spike) — distinctBy/joinOn/leftJoin/groupBy over a store Query[T] field (and the held-Map/plain-Map/Log scans lifted into the same query vocabulary) now build their collection buckets typed to the real element type instead of any[]/Record<string, any[]>, mirroring the List[T] sibling kernel’s own established pattern (elem_ts/join_other_elem_ts). ts_any drops from 30 to 26. Zero fixture diff except the two fixtures (228, 231) that exercise these exact sites, reblessed and re-verified against real tsc --strict. |
| v0.289.25 | Resolves #1459 (ADR 0399 follow-on) — formalises ts_any’s 26 non-collection-kernel residual sites into six argued families, and corrects ADR 0399’s own tentative recommendation to exclude workers.rs’s Durable Object stub from R7.1’s scope. No code change — a documentation/argument settling pass. |
| v0.289.24 | Resolves Arc F item 4’s investigation (#1457) — emit.rs’s emit_agent and emit_ws_do_method each re-implemented append_deps_field’s exact splice pattern inline instead of calling the shared helper (added by review of #1209 specifically to collapse this repetition); both now call it directly. The grounding read found the real conversion of cap_ref_ty/build_deps_object_ty_with_surface/workers_env_ty/surface_ty to real bynk_ts::TsType nodes tractable and scoped it as its own issue (#1463, “Arc F, slice 4”) rather than converting in this pass. Zero emitted-TypeScript change (confirmed by bynk-emit’s full fixture byte-matchers); ts_writes drops from 879 to 875 as a side effect — folding away the two duplicated call sites removes 4 format! lines, none of which built TypeScript text of their own (the shared helper’s one format! call was already counted). |
| v0.289.23 | Fixes a ts_writes probe false positive found by Arc F’s own item-4 investigation (#1457) — xtask’s is_path_construction_line now also recognises the .with_file_name(format!(...)) filesystem-path idiom, alongside the existing PathBuf::from(format! and .join(format! forms, so bynk-emit/src/project.rs’s sibling_path is no longer miscounted as TypeScript-producing. ts_writes drops from 880 to 879; no other probe changes. |
| v0.289.22 | Arc F slice 3 converts bynk-emit’s emit_context_deps_interface to build a real bynk_ts::TsDecl::Interface instead of writeln!-ing text, with two fields (surface, __eventsDispatch) deliberately kept as opaque TsType::named(...) text — the former because its own source (surface_ty) is still String-returning, the latter because bynk_ts::TsType::Fn cannot represent a named parameter — zero emitted-output change. |
| v0.289.21 | Arc F slice 2 repoints bynk-emit’s plan_agent_given_deps/native_platforms_of_context at the tree-native instantiate_provider_ts_expr, deleting the now-uncalled String-returning instantiate_provider_expr — zero emitted-output change. |
| v0.289.20 | Arc F slice 1 converts bynk-emit’s emit_helpers_for_owner[_qualified]/emit_generic_helpers[_qualified] (and emit_one) to return real Vec<TsDecl> trees instead of writeln!-ing into out: &mut String, with the boundary-print step consolidated into one shared print_decls helper at every caller — zero emitted-output change. |
| v0.289.19 | Arc F design pass — decomposes ts_writes’s remaining non-serialisation.rs surface into three concretely-scoped conversion slices, one open investigation, and a named already-argued residual |
| v0.289.18 | Arc E slice 7 converts bynk-emit’s emit_generic_helpers_qualified’s remaining ResultInst/OptionInst/ListInst/MapInst arms to build real TsDecl/TsStmt trees instead of write!-ing strings, and adds a narrow C-style TsStmt::For to bynk-ts for the List/Map deserialise-side indexed element loop. |
| v0.289.17 | Arc E slice 6 converts bynk-emit’s emit_sum/emit_sum_codec to build real TsDecl/TsStmt/TsStmt::Switch trees instead of writeln!-ing strings, adds TsSwitchCase::case_braced to bynk-ts for a real unbraced-non-default-case shape, and narrows the sum-codec’s own (value as any).<field> cast to a bare, TypeScript-narrowed value.<field> (ts_any 31 → 30), reblessing the small fixture set that cast touched. |
| v0.289.16 | Arc E slice 5 converts bynk-emit’s emit_record/emit_record_codec to build real TsDecl/TsStmt trees instead of writeln!-ing strings, with zero emitted-output change, and adds a narrow TsBinaryOp::In to bynk-ts for the field-default prevalidation guard this needed. |
| v0.289.15 | Arc E slice 4 converts bynk-emit’s emit_refined/emit_bytes_named_codec/emit_inline_refinement_checks/emit_inline_pred_check to build real TsDecl/TsStmt trees instead of writeln!-ing strings, with zero emitted-output change. |
| v0.289.14 | Arc E slice 3 converts bynk-emit’s emit_field_deserialise/emit_field_deserialise_wire to build real TsStmt trees instead of writeln!-ing strings, with zero emitted-output change. |
| v0.289.13 | Arc E, slice 2 — serialisation.rs’s duplicate qualified-type-text renderer (ts_type_ref_qualified/ts_inner_type) is deleted and its 9 real call sites (8 internal, plus lower.rs’s Json.decode[T] test-scaffold arm) fold into the general emitter.rs::ts_type_ref_qualified_multi_ts_type renderer via a new adapter, serialisation::qualified_ts_type, that bridges this file’s dotted-prefix qualifier convention onto that renderer’s bare-namespace one. No behaviour change — every fixture emits byte-identical TypeScript. |
| v0.289.12 | Arc E, slice 1 — serialisation.rs’s expression serialise/deserialise builders (serialise_field_expr/_via/_wire, serialise_expr/_via, deserialise_ref_via, serialise_ref_via, deserialise_expr/_via) now build a real bynk_ts::TsExpr instead of a hand-written String, closing cluster 2 of serialisation.rs’s own four-cluster decomposition; TsExpr::Arrow.body widens to a new TsArrowBody (Expr/Block) to represent the Float non-finite guard’s statement-bodied IIFE, the first real block-bodied arrow anywhere in the tree. No behaviour change — every fixture emits byte-identical TypeScript. |
| v0.289.11 | A for all property binding over a self-recursive Sum/Record type no longer generates a bare undefined in a real, typed constructor/field position once the runner’s random-generation depth is exhausted — gen_ts_for_ty’s own fallback now hands canon_ts_for_ty the same recursion budget the checker already used to accept the binding as generable, not an under-provisioned 1, and only for a type the checker actually validated, so an unvalidated branching variant can’t turn the fallback into a multi-megabyte emit |
| v0.289.10 | A for all property body (or where clause) that matches a sum value now binds a single named payload field to its own real name again, closing a case where it silently fell back to the generic value accessor and produced a tsc TS2339 error (or, before recent adjacent changes, a silent undefined binding) |
| v0.289.9 | A for all/contract-attack Int (bare, refined, or opaque) binding now coerces its drawn bigint value to number at the bind site, closing a runtime TypeError: Cannot mix BigInt and other types the instant a predicate or attacked function does ordinary arithmetic on it |
| v0.289.8 | Design pass — names the true floor for ts_any (a 7-file, 30-site distribution across four buckets, not the stale 2–3-site estimate) and verbatim_sites (an argued floor of at least 2, not 0 — three of the five sites are an unscoped signature capstone on emit_project/emit_test_module/emit_integration_module, not residue, with its own unnamed source-map prerequisite) |
| v0.289.7 | Arc E design pass — settles emitter/serialisation.rs’s conversion status (it converts, using the established Arc C pattern, unlike emitter/lower.rs’s permanent exclusion) and names an 8-slice decomposition order |
| v0.289.6 | A Sum-variant payload or Record field of type Int now coerces its drawn bigint value to number, closing a tsc --strict type mismatch at the real constructor/object-literal call site |
| v0.289.5 | A record-typed for all property binding’s generator arrow now wraps its object-literal body in parens, closing an unparseable/silently-wrong generated-TypeScript gap |
| v0.289.4 | A service handler’s subscriber-filter/schema-gate prologue no longer shifts its own body’s source-map lines |
| v0.289.3 | The JSON codec seed collector (R8.14) reads a resolved Callee::Intrinsic, not a bare receiver-name match — closing Arc D, the final settling slice |
| v0.289.2 | The context-brand rebranding predicate (R4.10/R8.2) is one shared function, not two independently mirrored copies |
| v0.289.1 | bynk-ir/bynk-lower carved out of bynk-emit as new workspace crates (ADR 0332’s deferred split, triggered per ADR 0385) |
| v0.289.0 | Arc C, slice 37 — the top-level module assemblers (emit_integration_module/emit_test_module in tests_emit.rs) build real bynk_ts fragments, the seventh and final of tests_emit.rs’s own 7 proposed slices (per its dedicated grounding pass) — closing Arc C entirely. Adds TsDecl::ImportDefault (a default import, bynk-emit’s first real one) to bynk-ts. No behaviour change — every test/integration module emits byte-identical TypeScript. |
| v0.288.0 | Arc C, slice 36 — the system-http driver cluster (emit_system_http_support in tests_emit.rs) builds real bynk_ts fragments, the sixth of tests_emit.rs’s own 7 proposed slices (per its dedicated grounding pass) — every __sysdrive_* request driver (typed, raw, no-auth, raw+no-auth, wrong-method) and the secrets-bootstrap loop now construct real TsDecl/TsStmt/TsExpr nodes instead of format!-built text. No behaviour change — every fixture with a service ... http route emits byte-identical TypeScript. |
| v0.287.0 | Arc C, slice 35 — the property/history/attack runner cluster (emit_test_property_function/emit_test_history_property_function/emit_contract_attack_function in tests_emit.rs) builds real bynk_ts fragments, the fifth of tests_emit.rs’s own 7 proposed slices (per its dedicated grounding pass) — closing the print-then-splice seam Arc C slice A’s own binding_gen conversion left temporarily open. No behaviour change — every fixture with a property, a history property, or a contract-attack case emits byte-identical TypeScript. |
| v0.286.0 | Arc C, slice 34 — the case/scope-setup cluster (emit_test_scope_setup/emit_test_case_function in tests_emit.rs) builds real bynk_ts fragments, the fourth of tests_emit.rs’s own 7 proposed slices (per its dedicated grounding pass). Adds bynk_ts::TsBinaryOp::InstanceOf. No behaviour change — every fixture reaching a test case emits byte-identical TypeScript, with source-map splice offsets confirmed unaffected. |
| v0.285.0 | Arc C, slice 33 — the stub cluster (emit_stub_class/emit_stub_rhs in tests_emit.rs) builds real bynk_ts fragments, the third of tests_emit.rs’s own 7 proposed slices (per its dedicated grounding pass). Adds bynk_ts::TsBinaryOp::LessThan and TsSwitchCase.default_braced, and deletes the now-dead emitter::ts_type_ref_qualified_multi. No behaviour change — every fixture with a stub clause emits byte-identical TypeScript. |
| v0.284.0 | Arc C, slice 32 — the small independent leaves (emit_integration_harness/emit_test_deps/emit_ns_destructure/observation_call_record_types in tests_emit.rs) build real bynk_ts fragments, the second of tests_emit.rs’s own 7 proposed slices (per its dedicated grounding pass). Also deletes the now-dead emitter::ts_type_ref_qualified, rerouting its own direct unit tests through its already-existing TsType-returning twin. No behaviour change — every affected fixture emits byte-identical TypeScript. |
| v0.283.0 | Arc C, slice 31 — the property-generator expression cluster (refined_gen_ts/gen_ts_for_ty/canon_ts_for_ty/binding_gen in tests_emit.rs) builds real bynk_ts::TsExpr fragments, the first of tests_emit.rs’s own 7 proposed slices (per its dedicated grounding pass) and Arc C’s own final remaining piece. No behaviour change — every property/history/contract-attack fixture emits byte-identical TypeScript. |
| v0.282.0 | Arc C, slice 30 — emit_project’s own header/import/rebrand cluster (write_header/write_header_single/emit_project_imports/emit_cross_context_namespace_imports/emit_context_rebrands/emit_boundary_helpers, plus emit_project itself) builds real bynk_ts fragments, closing step (10) of the TypeScript-tree conversion track — every step in the conversion order is now landed or decided, leaving only the final tests_emit.rs pair. Adds type_only to bynk_ts::TsDecl::ImportNamespace along the way, matching its sibling TsDecl::Import. No behaviour change — every project-form fixture (all of them reach emit_project) emits byte-identical TypeScript. |
| v0.281.0 | Arc C, slice 29 — the cross-context lowering cluster (lower_workers_cross_context_call/param_cast) builds real bynk_ts::TsExpr fragments instead of hand-written strings, closing step (8) of the TypeScript-tree conversion track. No behaviour change — every cross-context service call at the Workers target emits byte-identical TypeScript, including the zero-argument case. |
| v0.280.0 | Arc C, slice 28 — the ICU-formatting cluster (emit_message_entry_renderer/emit_icu_placeholder/emit_sub_message) builds real bynk_ts::TsExpr fragments instead of hand-written strings, closing step (11) of the TypeScript-tree conversion track. Adds two small bynk-ts primitives along the way: TsBinaryOp::Add (string concatenation) and a new print_expr fragment entry point. No behaviour change — every message-bundle fixture (literal text, plain placeholders, and all four ICU kinds: plural/select/number/date) emits byte-identical TypeScript. |
| v0.279.0 | Arc C, slice 27 — emit_agent’s workers-mode fetch method and emit_ws_open_fetch_branch convert to real bynk_ts::TsClassMethod/TsStmt fragments, closing step (9) sub-slice (5) entirely (all 3 independent slices landed: the WS-hosted DO methods, the hibernatable-WebSocket dispatch handlers, and now the DO dispatch itself). Adds a new fixture pinning the agent_uses_emit branch (an agent handler that emits directly, no service in between), which previously had zero fixture coverage anywhere. |
| v0.278.0 | Arc C, slice 26 — emit_agent’s hibernatable-WebSocket dispatch handlers (webSocketMessage/webSocketClose, via emit_ws_dispatch_handlers) convert to real bynk_ts::TsClassMethod fragments, built from real TsStmt/TsExpr nodes rather than one opaque body blob. The second of step (9) sub-slice (5)‘s own 3 independent slices; unlike every prior Arc C slice this function lowers no .bynk body at all, so no source-map merge was needed — three body statements and several leaf expressions (call/route-arg text, a generic constructor callee, structural parameter types) still stay opaque text, the same established escape hatch this track uses elsewhere. Zero diff on the first attempt. |
| v0.277.0 | Arc C, slice 25 — emit_agent’s WS-hosted on open/on message/on close DO methods (emit_ws_do_method) convert to real bynk_ts::TsClassMethod fragments, the identical shape emit_provider’s own already-landed ops established. The first of step (9) sub-slice (5)‘s own 3 independent slices, confirmed as low-risk as its dedicated grounding pass predicted — zero diff on the first attempt, no new algebra gap |
| v0.276.0 | Arc C, slice 24 — emit_agent’s factory function converts fully to real bynk_ts nodes, with no opaque carve-out at all. Splits step (9)‘s own sixth sub-slice: the history-driver, its other half, stays deferred with its own priority still undecided. Confirms the factory function alone needed its own real slice, a real floor correction to the original estimate |
| v0.275.0 | Arc C, slice 23 — emit_agent’s per-handler methods convert to real bynk_ts::TsClassMethod fragments, closing the grounding pass’s own second predicted algebra gap (TsClassMethod.doc) and extending emit_service’s own two-level offset pattern to a genuinely new third wrapper dimension (the implicit-commit closure, which can combine with the events-IIFE). The largest sub-slice of step (9)‘s own decomposition, per the grounding pass’s own prediction, confirmed accurate |
| v0.274.0 | Arc C, slice 22 — emit_agent’s commitState method converts to a real bynk_ts::TsClassMethod fragment, closing step (9)‘s own third sub-slice fully (the deferred remainder #1371 split off). Corrects that slice’s own stated expectation: commitState’s predicate lowering needs no source-map sub-builder care at all — record_span is a documented no-op with no attached builder, and neither LowerCtx it constructs is ever given one |
| v0.273.0 | Arc C, slice 21 — emit_agent’s loadState method converts to a real bynk_ts::TsClassMethod fragment. Closes a grounding-pass-predicted gap along the way — TsClassMethod.private, the first private method sites this track has hit. Splits step (9)‘s own third sub-slice: commitState, found genuinely harder once actually read (its own predicate lowering needs the same source-map care emit_free_fn/emit_contract_guarded_body already established), stays deferred as its own separate, later sub-slice |
| v0.272.0 | Arc C, slice 20 — emit_agent’s own per-agent state registry const, zero-value factory function, and (when needed) rehydration-validation function convert to real bynk_ts nodes. Closes a new algebra gap along the way — TsDecl::Function.inline, mirroring TsObjectEntry::Method.inline’s own single-line-vs-multi-line precedent — for the zero-factory’s own genuinely single-physical-line shape. The second of step (9)‘s own proposed 5-6 sub-slices (emit_agent and its websocket-dispatch cluster) per the dedicated grounding pass |
| v0.271.0 | Arc C, slice 19 — emit_agent’s own state-record interface (export interface {Name}State { ... }, covering all 5 store kinds — Cell/Map/Set/Cache/Log — plus @indexed posting-lists) converts fully to a real bynk_ts::TsDecl::Interface, with no opaque carve-out at all. The first of step (9)‘s own proposed 5-6 sub-slices (emit_agent and its websocket-dispatch cluster) per the dedicated grounding pass |
| v0.270.0 | Arc C, slice 18 — emit_make_surface converts fully to real bynk_ts nodes (a real TsDecl::Function returning a real TsExpr::multiline_object_entries of per-service TsObjectEntry::Method entries), with no opaque carve-out at all — the first slice in this track to close that cleanly. Splits step (8) of the design pass’s own decomposition order: the cross-context lowering cluster, step (8)‘s other half, is a real, separate, harder remainder, deferred rather than silently folded into “step (8) done” |
| v0.269.0 | Arc C, slice 17 — emit_service’s own handlers build real bynk_ts::TsObjectEntry::Method fragments (every param, including the synthetic connection/__bynkSchemaEnv ones, converts to a real bynk_ts::TsParam; each body source-mapped via a two-level sub-builder/merge, since the source-mapped content is nested inside a further opaque events-dispatch IIFE blob), fully closing step (7) of the design pass’s own decomposition order in one slice. The object wrapper, deps’s own dynamically-built type, and the handler’s whole prologue/body/epilogue stay hand-written/opaque — deliberate boundaries, not remaining gaps |
| v0.268.0 | Arc C, slice 16 — emit_provider’s own per-op methods build real bynk_ts::TsClassMethod fragments (printed through a new bynk_ts::print_class_method entry point, each body source-mapped via a per-method sub-builder/merge), fully closing step (6) of the design pass’s own decomposition order. The class’s own wrapper and factory const stay hand-written text — a deliberate boundary (building the whole class as one tree would need every method’s body captured for Raw-embedding, and this class’s own real spacing differs from TsDecl::Class’s established policy), not a remaining gap |
| v0.267.0 | Arc C, slice 15 — emit_capability builds real bynk_ts nodes internally (TsTypeMember::Method gains generics/doc fields), splitting step (6) of the design pass’s own decomposition order the same way slice 12 split step (4) — emit_provider’s own conversion (a full class with N real method bodies, each needing the established sub-builder/merge source-map treatment individually) is a named, deferred remainder, not converted by this slice |
| v0.266.0 | Arc C, slice 14 — emit_messages_bundle’s own outer construction converts to real bynk_ts nodes (closing step (5) of the design pass’s own decomposition order), with two named opaque carve-outs (a one-off type annotation TsType::Fn can’t name its own parameter for; each message renderer’s own not-yet-converted ICU-formatting expression). Also closes a real printer gap: nested multiline objects as a Prop’s own value now render correctly instead of silently falling back to single-line |
| v0.265.0 | Arc C, slice 13 — emit_contract_guarded_body builds real bynk_ts nodes for its precondition/postcondition guards and return result; (bynk_ts gains TsStmtKind::Throw), fully closing step (4) of the design pass’s own decomposition order. The result-capturing IIFE stays one deliberately opaque statement (Pre::lower’s own predicate lowering is a permanent emitter/lower.rs exclusion). Also fixes a second real source-map corruption bug reproducing #1352’s own class one level deeper (caught immediately by #1352’s own regression test) before it ever landed |
| v0.264.0 | Arc C, slice 12 — emit_free_fn builds a real bynk_ts::TsDecl::Function node internally (TsDecl::Function gains a generics field), splitting step (4) of the design pass’s own decomposition order the same way slice 9 split step (2) — emit_contract_guarded_body’s own internal guard construction is a named, deferred remainder, not converted by this slice. Also fixes a real source-map corruption bug this conversion would otherwise have introduced (caught by coverage_behaviour.rs’s own integration tests): the function body now lowers into an isolated buffer for Raw-statement embedding, so its own checkpoints are captured via a local sub-builder and merged back in at the correct splice offset, the same pattern emit_service’s handler-body lowering already established |
| v0.263.0 | Arc C, slice 11 — emit_type/emit_refined_type/emit_record_type/emit_sum_type build real bynk_ts nodes internally, fully closing step (2) of the design pass’s own decomposition order (the dispatcher and 3 object-builders deferred by slice 9, unblocked by slice 10’s emit_attached_methods signature change) — adds TsDecl::Interface.type_params/readonly TsTypeMember members, TsType::Intersection, TsType::Union’s multi-line rendering mode, and TsExpr::Arrow.generics/.return_type to bynk-ts |
| v0.262.0 | Arc C, slice 10 — emit_attached_methods/emit_forwarded_methods/emit_method build real bynk_ts nodes, closing decomposition step (3) and unblocking step (2)‘s own deferred trio |
| v0.261.0 | Arc C, slice 9 — emit_refined_checks/emit_pred_check build real bynk_ts nodes internally, correcting step (2) of the design pass’s own decomposition order (3 of 6 named functions are entangled with step (3)‘s not-yet-converted emit_attached_methods, deferred to a follow-up slice gated on a TsObjectEntry::Raw decision) and closing a real, currently-unreachable double-escaping bug in how PredKind::Matches’s own pre-escaped message text would have been carried |
| v0.260.0 | Arc C, slice 8 — emit_doc_block/ts_type_params build real bynk_ts nodes internally, closing step (1) of the design pass’s own emit.rs decomposition order and correcting its 13-function list to the real 2 (9 were never TS-emission code; the ICU cluster is real but deferred to its own future step) |
| v0.259.1 | Arc C design pass — settles emitter/lower.rs’s conversion status (a deliberate, argued, permanent exclusion from Arc C’s own scope, with ts_writes’s own non-zero floor named accordingly) and names the corrected decomposition order for emit.rs’s remaining work |
| v0.259.0 | Arc C, slice 7 — emit_commons_barrel (the multi-file commons barrel module) constructs a real bynk_ts::TsProgram instead of building TypeScript text |
| v0.258.0 | Arc C, slice 6 — emit_composition_root (Bundle-target compose.ts) constructs a real bynk_ts::TsProgram instead of building TypeScript text |
| v0.257.0 | Arc C, slice 5 — emit_test_main (tests/main.ts) constructs a real bynk_ts::TsProgram instead of building TypeScript text |
| v0.256.0 | ”Arc C, slice 4: workers_entry.rs’s emit_worker_entry and its four helpers (emit_scheduled_handler/emit_queue_handler/emit_http_route_dispatch/emit_call_handler_dispatch) construct and return a real bynk_ts::TsProgram instead of writeln!-ing a String — bynk-emit/src/project.rs’s own index.ts construction site drops its Verbatim/NotYetConverted wrap. Extends the bynk-ts node algebra with the four shapes Decision A named (TsStmt::Switch/TsSwitchCase — the first genuinely new statement-grouping construct in the tree; TsDecl::ExportDefault for a bare-expression default export; TsExpr::Conditional, with proactive parenthesisation-rule coverage in all three existing rule sites; TsType::Object’s Vec<(String, TsType, bool)> replaced by a TsTypeMember enum with readonly/method-signature members) plus real gaps found during implementation beyond that list: TsDecl::ReExport (export { A, B } from “spec”;), TsStmtKind::Blank (a bare blank line usable at any nesting depth), TsLit::Bool, TsExpr::Paren (an explicit, printer-preserved parenthesisation independent of the wrapped expression’s own precedence), TsTypeMember::Index (a TypeScript index signature, textually distinct from the semantically-equivalent Record<K,V>), TsBinaryOp::GreaterThan, TsStmt::If’s own else_branch, and TsStmtKind::InlineBlock (a braced, multi-statement block on one generated line, plus a render_block_body special case for a TryCatch body that is one). A further correction found only by the zero-diff fixture check: the As arm’s own automatic “always parenthesize a nested Await” rule (added by P7.8) was actually just one file’s own historical text, not a grammar requirement — removed, replaced by explicit TsExpr::Paren wraps at the three real sites that want it (two in this file, one in events_fanout.rs, since even within workers_entry.rs itself the two real await-as sites disagree with each other). render_binary_operand also gained a same-operator-chain-flattening refinement for non-nullish operators (a real 3-term |
| v0.255.0 | ”Arc C, slice 3: workers.rs’s emit_worker_compose and its nine private helpers construct and return a real bynk_ts::TsProgram instead of writeln!-ing a String — bynk-emit/src/project.rs’s own compose.ts construction site drops its Verbatim/NotYetConverted wrap. Extends the bynk-ts node algebra with the five shapes Decision A named (a richer TsObjectEntry — Prop/Shorthand/Method/Spread — replacing TsExpr::Object’s bare (String, TsExpr) entries; TsExpr::Arrow; TsExpr::OptionalMember/OptionalIndex; an optional flag on TsType::Object’s fields) plus real gaps found during implementation beyond that list: TsDecl::Function and TsDecl::TypeAlias (compose.ts’s own top-level function and its conditional DurableObjectNamespace fallback alias), TsDecl::ImportNamespace (the import * as X form), a parameterless TsStmt::TryCatch catch clause (ES2019 optional catch binding), and four new TsBinaryOp/one new TsUnaryOp operators (===, !==, &&, |
| v0.254.1 | ”Closes the ts_any residual for the four runtime-owned error types (ValidationError/JsonError/HttpResult/QueueResult): the two remaining as any sites in bynk-emit’s (de)serialisation codec now cast through each type’s own real, already-exported runtime type instead, bridged through unknown where tsc’s own structural-overlap check requires it. Adds a QueueResult field/type-declaration structural scan (the one of the four that had no non-handler-based import gate). Also corrects design/tracks/the-typescript-tree.md’s own Arc C ordering: serialisation.rs is a shared 10-entry-point codec library, not a single clean conversion target like events_fanout.rs was, so it’s removed from the ordering pending its own dedicated design pass; workers.rs is named as the real next slice. No other output change, verified against the full fixture corpus plus a project-form fixture exercising all four types through the real Json codec boundary, staged and passing under a real tsc —strict run (#1319).“ |
| v0.254.0 | ”Arc C, slice 1: events_fanout.rs’s emit_events_fanout_do constructs and returns a real bynk_ts::TsProgram instead of writeln!-ing a String — the first bynk-emit construction site whose output reaches Document::Ts without a TsStmt::verbatim wrap. Adds TsStmt::Comment (every generated file’s header banner needs one) and a multiline field on TsExpr::Object (the __eventRoutes table’s own real, multi-line-with-trailing-commas shape, which the existing single-line-only object literal couldn’t represent). No output change, verified against the full fixture corpus and a fresh direct test against a real fixture’s own events_fanout.ts (#1317).“ |
| v0.253.0 | ”P7.9: bynk-emit’s ts_type_ref/ts_type_ref_qualified/ts_type_ref_qualified_multi/ts_type_ref_with/ts_ty construct a real bynk_ts::TsType internally and print it through a new bynk_ts::print_type, instead of hand-format!-ing text — every function keeps its exact -> String signature, every real caller (~110-115 sites) unchanged. (ts_base itself stays an unchanged string-table lookup, now consumed by the two converted functions instead of interpolated directly.) TsType::Array gains a readonly modifier; TsType::Fn and TsType::Union added (Union closes a real gap found during implementation: ts_ty’s own Ty::ActorSum arm builds a genuine union-of-tagged-object-literals type no existing shape could represent). Zero output change, verified against the full fixture corpus and 23 new direct tests pinning every real type shape from both families (#1315).“ |
| v0.252.0 | ”P7.8: bynk-ts gains a real TsExpr/TsType/TsDecl node algebra, plus real TsStmt kinds (Const/Let/ExprStmt/Return/If/ForOf/TryCatch/Block/Continue/Assign) alongside the existing Verbatim escape hatch — the subset bynk-emit/src/emitter/events_fanout.rs concretely needs, grounded against that file’s own real shape rather than the full reference sketch. The printer learns to render every new kind, verified byte-identical against the real emitter’s own output for events_fanout.rs’s EventsFanoutDO class, including its constructor body. Also corrects the track doc’s stale Arc C schedule: contracts.rs/secrets.rs/runtime_use.rs turn out to emit JSON (or not be emission code at all), not TypeScript — events_fanout.rs is the real first conversion file. No bynk-emit behaviour change (#1313).“ |
| v0.251.1 | ”P7.7: named the bynk-ts printer’s readability guarantee (R7.5) explicitly — a # Readability policy doc block in printer.rs states the printer’s one real formatting decision today (exactly one generated line per statement) and its boundary (a statement’s own interior formatting isn’t the printer’s concern until Arc C gives it real nodes). No behaviour change (#1311).“ |
| v0.251.0 | ”P7.6: Artefacts { docs: BTreeMap<PathBuf, Document> } (Document::Ts/Toml/Json/Js/SourceMap/DebugSidecar) replaces ProjectOutput.files: Vec<CompiledFile> outright across bynk-emit, bynk-driver, bynk-strip, and bynk-wasm — every real producer and consumer now reads/writes typed documents, not pre-rendered strings. wrangler.toml’s real TomlDocument reaches the write boundary unstringified, and bynk-strip’s own main = patch for a stripped Worker’s manifest is now a structural in-tree mutation instead of a print-then-reparse. Source maps and debug sidecars become their own typed sibling documents, derived once instead of independently by two functions. No observable output change (#1309).“ |
| v0.250.0 | ”P7.5: carved a new bynk-ts crate for the TypeScript tree and printer (depends on bynk-syntax only) — TsProgram/TsStmt::verbatim (the escape hatch every future Arc C slice converts into real nodes), a printer that owns the buffer end to end, and the companion textual lint over Verbatim content (#1307). bynk-emit’s source-map machinery relocated unchanged. No bynk-emit construction site converts to the tree yet — Arc C’s own first slice starts that. No observable output change.” |
| v0.249.42 | ”P7.4: bynk deploy/bynk dev --remote and --emit js now read/write a generated wrangler.toml’s id/main fields through a real TOML parse (bynk-emit::emitter::wrangler’s new materialise_kv_namespace_id/set_wrangler_main/wrangler_needs_kv_materialisation) instead of matching the literal text those fields happen to appear as today (#1305). Immune to reformatting; no observable output change.” |
| v0.249.41 | ”P7.3: added a minimal typed TOML tree and printer (bynk-emit/src/emitter/toml_doc.rs) — emit_wrangler_toml now builds a TomlDocument instead of writing text directly, and the printer escapes every string value unconditionally rather than only the two call sites that used to remember to (#1303). No observable output change: zero-diff across the whole wrangler.toml golden corpus.” |
| v0.249.40 | ”P7.2: narrowed 24 of the 55 ts_any-counted sites (as any/bare : any) in bynk-emit to unknown, real declared/qualified types, or generated structural types (#1300) — ts_any drops from 55 to 31. The other 31 (2 residual, needing a runtime type R7.7 hasn’t exported yet, and 29 deferred — more than the proposal’s 3, several found only by the full tsc --strict fixture pass) are named at their own site with a comment recording why, not silently dropped.” |
| v0.249.39 | A changelog blurb is validated as Markdown before it is stamped — cargo xtask stamp now rejects a pending increment whose blurb reads as a link to a relative destination, the shape an unbackticked generic call such as Events.emit[E](event) silently produces. The blurb is inserted verbatim into the Book’s changelog table by the merge-time stamp on main, so the docs site’s link-checker only ever saw the row post-merge, after a bad one had already broken main; the same rule now runs pre-merge, where the author can still act on it, and again inside the stamp’s own plan, where a failure leaves the pending file intact rather than pushing a broken table. Shape only — site-root and anchor destinations are resolved by the site gate alone, which the pending-file format doc now says explicitly. Also repairs the v0.249.1 row that had been failing Docs site (astro build) on every main push since it landed. |
| v0.249.38 | ”P7.0: ts_writes and ts_any gated probes added to cargo xtask greenfield-status (#1296) — ts_writes reads 1641, ts_any reads 55. Phase 7’s own completion probe was never measured before this slice.” |
| v0.249.37 | Settle phase 7 of the compiler trajectory (design/tracks/the-typescript-tree.md) — bynk-ts is carved as a crate up front, the migration uses a statement-level Verbatim escape hatch with a companion textual lint (golden fixtures alone can’t see inside it), TsType::Any is eliminated in full with one small named residual deferred to the runtime-typing rule, and R8 splits into five rules closed by the conversion itself, two needing their own slices, and twelve already closed |
| v0.249.36 | ”P6.59: phase 6 (the-ir.md, spine #1137) retires. ast_importers reads its named floor, 5 — the bynk-emit/src/emitter{,/**} rendering subtree, argued file by file in the closing summary. design/tracks/the-ir.md deleted; its closing summary appended to design/archive/retired-tracks.md; the trajectory doc’s phase-6 row and the probe’s own doc comments re-point at the archive. Opens phase 7 (the printer).“ |
| v0.249.35 | ”P6.58: re-settles design/tracks/the-ir.md §5 at ast_importers = 5 (not 0), with a per-file structural argument for each of the five surviving files. Breaks the §5/§7 circularity between this track’s own completion criterion and phase 7’s bynk-ts entry condition. Names §12’s retirement condition. Doc-only, no AST_IMPORTER_EXCEPTIONS growth.” |
| v0.249.34 | ”P6.57: IrHttpMethod gains from_ident(); emitter/lower.rs’s two HttpMethod::from_ident sites read it instead. Corrects P6.37’s stated reason for leaving system_http_route_body’s TypeRef field alone — the resolution context does exist, the real reason is a lossy round-trip with no probe payoff. Closes Phase H of the #1137 retirement plan. ast_importers: unaffected (5).“ |
| v0.249.33 | ”P6.56: called_consumed_services reads the checker’s own resolved Callee::Cross instead of reconstructing cross-context-ness syntactically — a live shadowing-hazard defect closed. Five other emitter.rs sites this row proposed (collect_json_codec_roots, refined_or_opaque_base/emit_context_rebrands, sum_owner_of_variant, positional_field_name, ts_binop) investigated and declined, each for a traced, written reason. ast_importers: unaffected (5).“ |
| v0.249.32 | ”P6.55: emit_provider stops calling is_effectful_return(&op.return_type) twice for the same op, and its own capabilities extraction reads lower_provider_given_ir instead of re-deriving CapRef::key(). The originally-planned full ProviderShapeIr scoped back down after tracing found no real consumer for the rest. ast_importers: unaffected (5).“ |
| v0.249.31 | ”P6.54: emit_service/emit_make_surface/emit_agent/topo_order_providers/cross_context_caps_used/cross_context_cap_namespaces read the IR already in scope (or reachable via an existing lowering function) instead of re-deriving the same fact from the raw AST a second time. ast_importers: unaffected (5).“ |
| v0.249.30 | ”P6.53: emit_agent’s store-field-kind dispatch (Cell/Map/Set/Cache/Log membership, plus Cache’s @ttl and Log’s @retain values) reads state’s already-computed StoreKindIr instead of re-deriving via f.kind.head.name string comparison and a second f.annotations walk. ast_importers: unaffected (5).“ |
| v0.249.29 | ”P6.52: collect_handler_labels’s and ws_open_hosts_for’s ServiceProtocol::WebSocket matches read lower_protocol_ir_from_commons instead. WsOpenHost::{out_type,in_type} carry TyId, not &TypeRef, cascading through four renderers and deleting the resolve_ty closure’s second argument. ast_importers: unaffected (5).“ |
| v0.249.28 | ”P6.51: IrHttpMethod gains as_str(); every HandlerKind read in emitter/emit.rs (collect_handler_labels, emit_service, emit_make_surface, emit_agent, ws_open_hosts_for — 20+ sites) now reads lower_handler_kind_ir instead of matching the raw AST HandlerKind. ast_importers: unaffected (5) — emit.rs stays counted via use super::*.“ |
| v0.249.27 | ”P6.50: emitter/emit.rs’s dead build_deps_object_ty deleted (zero callers workspace-wide), and is_effectful_return moves into ir/lower.rs — fixing a backwards Ast<->Ir dependency where the lowering pass was calling up into the emitter it should only ever be called from. ast_importers: unaffected (5).“ |
| v0.249.26 | ”P6.49: project.rs’s own bynk_syntax::ast import deleted entirely — its last pre-check TypeRef walk relocates to a new ParsedFile::history_target_agent_names() (bynk-project), and TypeDecl/FnDecl/Visibility/ActorDecl are re-exported from the bynk-check modules whose own public API was already parameterised by them. ast_importers: 6 -> 5 — project.rs clears (project/diagnostics.rs cleared with it back in P6.42). Phase G of the #1137 retirement plan complete.” |
| v0.249.25 | ”P6.48: emitter::walk_unit_table_bodies (emitter.rs, already counted) replaces two hand-rolled copies of the same service/agent/provider body walk in project.rs’s unit_table_uses_emit and called_cross_context_services. ast_importers: unaffected (6) — project.rs’s Block/Expr sites cleared, but the file was already counted on other names.” |
| v0.249.24 | ”P6.47: lower_event_subscriber_shapes_ir (bynk-emit/src/ir/lower.rs) absorbs the ServiceProtocol::Events pre-filter guarding lower_service_item_ir, and EventSubscriberShape moves to ir.rs — project.rs’s per-file loop becomes one call. ast_importers: unaffected (6).“ |
| v0.249.23 | ”P6.46: two owner-side accessors replace raw AST walks in project.rs — ParsedFile::declares_messages() (bynk-project) and bynk_check::symbols::cron_and_queue_triggers() (bynk-check). ast_importers: unaffected (6).“ |
| v0.249.22 | ”P6.45: combined_types_for_unit_info moves to bynk-check::symbols, next to combined_types_for; the FnName::Method filter folds into a new lower_attached_fn_sig_ir_from_types (bynk-emit/src/ir/lower.rs). ast_importers: unaffected (6).“ |
| v0.249.21 | ”P6.44: discover_event_subscribers moves to bynk-check::symbols, next to build_cross_context_info — the sibling function its own doc comment already pointed at. ast_importers: unaffected (6).“ |
| v0.249.20 | ”P6.43: own_contract_hashes moves into bynk-check::contract, sharing one cross_context_service_for projection with build_cross_context_info — the caller-side and callee-side CrossContextService views were two hand-written copies agreeing only by convention; now one function. ast_importers: unaffected (6).“ |
| v0.249.19 | ”P6.42: project.rs’s in_memory_logical_path stops re-implementing SourceUnit::name(), and project/diagnostics.rs drops its use super::*; — two false AST coupling channels, no behavioural change. ast_importers: 7 -> 6 (project/diagnostics.rs cleared; project.rs itself still counted, six names remain).“ |
| v0.249.18 | ”P6.41: TypeShape::Refined::{base, refinement} investigated and ruled phase 7, not built. Both fields flow directly into emitter/emit.rs’s emit_refined_type/emit_refined_checks, which generate TS validation source text (export type/export const, predicate checks) — the identical “how do I render this type as TS” class of question P6.33 already ruled the JSON/wire codec renderer belongs to phase 7’s eventual printer, not phase 6’s IR construction. Building an IrRefinement/IrBaseType mirror now would mean building printer infrastructure for a consumer this track’s own §2 scope already excludes, and per the plan’s own finding the bounds (IntBound/FloatBound) deliberately keep source lexemes for byte-stable emission, so the mirror would be a near-copy rather than a clean IR abstraction — weaker grounds for building it than the four fields P6.39 already deleted, which had a real redundant-metadata case for removal. No code changes. ast_importers unaffected — invisible to the probe by construction.” |
| v0.249.17 | ”P6.40: ProtocolIr::Events::schema_dispatch flattened from Option |
| v0.249.16 | ”P6.39: deleted four of the six AST-typed ir.rs fields named as having zero production readers — GlobalRef::sum, IrExprKind::Record::def, IrItem::Type::def, IrItem::Fn::def — each verified to have no reader outside a test assertion (IrItem::Fn’s own constructor, lower_fn_item_ir, has no production call site at all). The other two, IrExprKind::RefinedCheck::{base, refinement} and IrPat::Refined::refinement, were investigated and found not to fit this slice: unlike the four deleted fields (redundant identity metadata another field or the caller’s own context already carries), these two are the dormant node’s own semantic payload — what refinement to test — and removing them would gut the variant’s meaning rather than clean up a redundancy, the same category error a naive read of “zero production readers” invites for a node no shipped emitter path has reached yet (the same shape Question/Is were in before P6.15/P6.16 wired them into a real call site). Left as-is. ast_importers unaffected (invisible to the probe — ir.rs/ir/lower.rs are excluded files).“ |
| v0.249.15 | ”P6.38: emitter/lower.rs’s idempotency_scoping_tests module reads a new TypedCommons::empty() (bynk-check) instead of hand-building an empty Commons/QualifiedName itself — this module’s own last bynk_syntax::ast dependency, per the AST-free test-constructor option P6.33 named. Does not clear emitter/lower.rs: P6.37 already found this file’s own remaining production surface (system_http_route_body’s TypeRef field, the two HttpMethod::from_ident sites) has no available IR-native alternative, so the file stays counted regardless — this slice closes only the test-residue half of R6.13’s own “known gap” paragraph for this file, not the file as a whole. ast_importers unaffected (7).“ |
| v0.249.14 | ”P6.37: BodyMode::TestCase’s test_service_handlers field (a per-service list of handler kinds a test case’s cron/queue address lowering recovers a position index from) now carries IrHandlerKind instead of the raw AST HandlerKind, mirrored in project/tests_emit.rs’s own target_service_handler_kinds builder. Investigated and left as raw AST, narrower than the plan’s own row: system_http_route_body’s TypeRef field has no resolved TyId available at its own construction site (a pre-check UnitTable walk, no checker resolution context in scope) and its sole consumer, serialise_expr_via, is P6.33’s own ruled-phase-7 codec renderer — converting it would have no real target to convert to. The two HttpMethod::from_ident sites feed http_handler_method_name, a Q7-deferred rendering-key utility taking HttpMethod by value, the same downstream-Q7-consumer shape found throughout this plan’s own Phase C/E slices. ast_importers unaffected (7) — emitter/lower.rs retains other, still-open AST surface.” |
| v0.249.13 | ”P6.36 scoping correction: not implementable as the completion plan described, per the P6.20 precedent. The plan proposed one lower_unit_items_ir enumerator serving both collect_external_references and write_header. collect_external_references genuinely needs every CommonsItem variant for correctness (a missed variant is a silently missing runtime import), and three of its nine match arms are variants IrItem has no equivalent for at all (Actor, Messages, and Event via as_type_decl()) — no enumerator, total or partial, is the right shape for it. write_header’s own remaining CommonsItem matches turned out to need no single replacement either: most are already at their real floor (bare outer kind classification, or already reading commons.callees/IrHandlerKind for the actual decision), and the few that read genuine declaration content would need either three separate small commons-only helpers or the full CheckedProgram-gated lower_agent_item_ir — the latter being the exact “expensive pass run just to discard almost all of it” shape this track already guarded against elsewhere (P6.23’s own precedent). Neither shape is what the plan proposed; neither was pursued. Named as unscoped future work. ast_importers unaffected (7).“ |
| v0.249.12 | ”P6.34: closed by investigation, no code change needed (P6.9/P6.24b precedent). The completion plan’s own P6.34 row proposed precomputing EmitProjectCtx::actors into a per-handler ActorSeamIr, describing it as a hidden dependency blocking emitter/workers.rs, emitter/workers_entry.rs, and emitter/emit.rs. Traced directly: only emit.rs reads this field (workers.rs/workers_entry.rs reach actor data through the unrelated bynk_check::symbols::UnitTable::actors, already counted independently) — the “blocking three files” framing did not hold. Even for emit.rs, lower_actor_seam_ir’s own signature requires the raw actor declarations to do its own by_clause-binder resolution; precomputing would relocate, not remove, the AST dependency (project.rs is already counted for unrelated reasons, so this would not move ast_importers even if built), while introducing a new indexing mechanism into a security-sensitive, fail-closed identity-verification path (Bearer/Oidc/Signature/Caller seam resolution) for zero probe benefit. Not pursued. ast_importers unaffected (7).“ |
| v0.249.11 | ”P6.35: plan_agent_given_deps (project.rs) no longer spells an explicit &AgentDecl type annotation — the loop body’s own a.handlers field access already pins the element type through inference, the same fully-mechanical finish the completion plan named for this function. Investigated and confirmed, no code change needed: unit_table_uses_emit/called_cross_context_services’s own remaining Block/Expr sites are walk_block_exprs’s own callback signature, the same Q7-deferred body-walking-plumbing shape as every other still-AST-typed rendering parameter in this track — no IR-native body walker exists to route through instead until a future P6.2 emitter-side cutover lands, so this is not fixable within this slice’s scope. instantiate_provider_expr confirmed already AST-free, as the plan itself already found. ast_importers unaffected (7) — project.rs retains other, still-open AST surface (own_contract_hashes, build_output’s declaration reads, etc).“ |
| v0.249.10 | ”P6.33: re-settled the #1137 completion plan’s own Phase D question — does the TypeRef-driven JSON/wire codec layer belong to phase 6 or phase 7? Ruled phase 7: emitter/serialisation.rs holds no CommonsItem-declaration-read surface at all, only the codec renderer itself, the same “how do I render this as TS source” question Q7 already settled belongs to the eventual printer. serialisation.rs joins AST_IMPORTER_EXCEPTIONS on those grounds (a phase boundary, not Q7 residue or test-only reach); emitter.rs was considered and rejected for the same treatment since it mixes real, still-open declaration-read surface in among its own codec-adjacent TypeRef sites. Also ruled: Q7 rendering-signature params were already settled by Q7 itself, not a new question; project.rs’s own_contract_hashes is a wire-format concern named as a likely future residual floor, not excluded now since project.rs has other convertible surface; IrExprKind::Call’s own Callee field is ruled entirely out of R6.13’s frame (bynk-check-internal, not bynk-emit’s business). ast_importers: 8 -> 7.” |
| v0.249.9 | ”P6.32: emitter.rs’s three near-identical file_mentions_json_error/_http_result/_connection predicates now share one marker-parameterised recursive TypeRef walk (type_ref_mentions) instead of each hand-rolling its own ~20-line copy, differing only in which wrapper variant stops the recursion. file_mentions_json_error and file_mentions_http_result’s outer CommonsItem enumerations were also byte-identical and now share one commons_mentions_type helper; file_mentions_connection keeps its own distinct outer walk (it additionally checks agent store fields, a Connection can live nowhere else) but reuses the same shared inner walk. Four new unit tests pin the marker-vs-recurse truth table explicitly, since a marker’s own wrapper variant stops the recursion (matching each original’s own unconditional true arm) rather than also searching its own inner type. Deliberately did not convert to a TyId-based walk — these are declared signature/type positions with no resolved TyId available without threading checker resolution through every call site, out of proportion to a pure deduplication. ast_importers unaffected (8) — these three functions were never why emitter.rs is counted.” |
| v0.249.8 | ”P6.31: emitter/workers_entry.rs’s eight HandlerKind matches (route collection for HTTP/cron/queue/websocket-open/event handlers) now dispatch on the checker-classified IrHandlerKind instead of the raw AST HandlerKind directly — same pattern P6.30 established in emitter/workers.rs. The HttpRoute-building arm re-derives method/path from the original AST for HttpRoute’s own still-AST-typed fields (Q7-settled); the Cron arm’s expr is a plain String in both forms, so no re-derivation was needed there. HttpRoute/QueueRoute’s own struct fields (method: HttpMethod, handler: Handler, msg_type: Option |
| v0.249.7 | ”P6.30: emit_worker_compose (emitter/workers.rs) dispatches on the checker-classified IrHandlerKind instead of matching the raw AST HandlerKind directly, for both its has_ws_open predicate and its main per-handler wrapper-selection match — the decision of which wrapper to call is now IR-driven, though each wrapper’s own body-rendering signature stays AST-parameter-driven (Q7-settled). emit_worker_compose has no TypedCommons/CheckedProgram in scope (only a table: &UnitTable), so the one remaining raw ServiceProtocol::WebSocket check inside the Message arm stays AST-typed — lower_protocol_ir_from_commons is not reachable here without threading a new parameter through, a real scoping finding narrower than the completion plan’s own estimate for this slice. ast_importers unaffected (8) — workers.rs retains other, still-open AST names.” |
| v0.249.6 | ”P6.29: cap_op_param_names (emitter/lower.rs) reads a new capability_op_sig_from_commons helper in ir/lower.rs instead of walking CommonsItem::Capability/c.ops by hand — the walk itself is unchanged (nothing IR-native replaces “find the op named op on the capability named cap” as a lookup), just relocated to an excluded file. Both current behaviours preserved exactly: first match in item order, empty result (not a panic) when the capability or op is not found. ast_importers unaffected (8) — emitter/lower.rs has other, still-open AST names remaining.” |
| v0.249.5 | ”P6.28: RuntimeUse::json_codec_roots now carries the checker’s own TyId instead of a re-derived TypeRef — both push sites in emitter/lower.rs already held the TyId before converting it, purely to satisfy this field’s old type, so the conversion moves to the single drain site (project/tests_emit.rs) immediately before collect_codec_closure, the one remaining consumer that is still genuinely TypeRef-driven. emitter/ty_to_type_ref is now pub(crate) so that drain site (a sibling module tree, not a descendant of emitter) can call it. ast_importers: 9 -> 8, emitter/runtime_use.rs cleared — the first file this completion plan has fully cleared.” |
| v0.249.4 | ”P6.27: re-exported bynk_syntax::ast::ExprId as bynk_check::checker::ExprId and retargeted bynk-emit’s two direct-ExprId sites (project.rs, emitter.rs’s sum_owner_of_variant) to it — enabling only, no behaviour change. The checker’s own public API already traffics in ExprId (expr_types and Callee are both HashMap<ExprId, _>, Q2’s own settled totality story), so this exposes an already-public dependency rather than adding one, and preserves the exact identity the checker keyed by (a bynk-emit-local id type would not). ast_importers unaffected (9) — neither retargeted site was the reason either file was counted.” |
| v0.249.3 | ”P6.26: deleted the five use bynk_syntax::ast::*; glob imports in bynk-emit (emitter.rs, project.rs, emitter/workers.rs, emitter/workers_entry.rs, emitter/serialisation.rs), adding each file its own explicit, minimal import list — a mechanical no-behaviour-change refactor. emitter/emit.rs and emitter/lower.rs each carried a module-level use super::*; inheriting emitter.rs’s own glob (Rust’s own privacy rule makes a parent module’s private use visible to descendant modules), so both also gained their own direct explicit list — but that use super::*; channel itself was narrowed, not closed: emit.rs (4,632 lines, 168 AST type references, previously zero literal occurrences of the counted string) still inherits whatever emitter.rs continues to expose. Review (#1259) found this left a durable false-zero hazard for a future slice that deletes a child’s own list while the channel and the parent’s own AST dependency both remain, so the ast_importers probe itself (xtask/src/greenfield_status.rs) now also counts any file with a module-level use super::*; whose parent module still imports the AST — catching not just emit.rs/lower.rs but project/diagnostics.rs, a third, currently AST-free file exposed to the identical latent channel. ast_importers rises 7 -> 9 as a direct, deliberate consequence of both changes — the probe becoming durably honest, not a regression.” |
| v0.249.2 | ”P6.21: emitter/lower.rs’s lower_method_call now reads Callee::Store/Callee::Query (P6.0) to detect a held store Map[K, Connection] field’s method receiver, instead of the raw !cx.is_local(&id.name) name check — another real instance of R6.5’s name-matched-receiver defect class. A held map’s entry ops (put/remove/contains/size/get) resolve as Callee::Store and its lifted query ops (e.g. parTraverse) resolve as Callee::Query, through the same checker StoreField::Map dispatch as any ordinary storage map — both variants are matched, mirroring the ordinary (non-held) Map branch’s own guard. agent_held_map_frame stays as a separate side-table lookup: it answers which kind of store field this is (held vs ordinary), not the shadowing question Callee already settles. Caught and fixed a real regression during development (a too-narrow Callee::Store-only guard silently excluded Callee::Query ops like parTraverse, producing a redundant double-wrapped IIFE for held-map broadcast fixtures) before landing — confirmed by a zero-diff bless against the entire e2e fixture corpus, including 238_websocket_inbound_workers, the fixture that caught the too-narrow guard.” |
| v0.249.1 | ”P6.21: emitter/lower.rs’s lower_method_call now reads Callee::Capability{cap:“Events”, op:“emit”} (P6.0) to detect an Events.emit[E](event) call, instead of the raw id.name == “Events” && method.name == “emit” name match — another real instance of R6.5’s name-matched-receiver defect class, the same one project.rs’s own unit_table_uses_emit already closed for the identical classification. cx.is_first_party_events() stays as a separate, still-needed guard: it answers which provider/target the emitted TS should call, a different question than whether this expression is really an Events.emit capability call at all. Verified by a full zero-diff bless against the entire e2e fixture corpus, including every Events.emit call site in the corpus. No probe movement (project.rs was already counted).“ |
| v0.249.0 | ”P6.23: project.rs’s EventSubscriberShape accumulator now reads two_param_handler/schema_dispatch off a real IrItem::Service (lower_service_item_ir, P6.11), instead of walking a matching service’s own s.handlers/s.protocol directly. The cheap ServiceProtocol::Events pre-filter stays — a structural check of which services even have a shape to capture, not a resurrected raw-AST read — before paying for a full body-lowering pass on a matching service. Safe as of #1254 (the P6.23 safety investigation this closes): a catch_unwind probe wrapping lower_service_item_ir across the entire e2e fixture corpus found zero panics, down from ~51 when the investigation began. This is the first real, shipped emitter call site for IrItem::Service/lower_service_item_ir — dormant since P6.11 (#1171). Verified by a full zero-diff bless against the entire e2e fixture corpus, including 1232_events_envelope_schema_dispatch_bare, the fixture that pins the schema_dispatch half of this exact predicate. ast_importers unaffected (project.rs was already counted)“ |
| v0.248.15 | ”P6.23: the two remaining root causes from the safety probe are root-caused and fixed, bringing lower_service_item_ir’s own panic count from ~51 across 20 services down to zero across the entire e2e fixture corpus. (1) lower_service_handler_ir called the agent-oriented lower_handler_signature_ir (which correctly panics on a resolve miss for an agent handler, whose own param type the checker does guarantee resolves) instead of its real sibling lower_service_handler_signature_ir — already graceful (falls back to Unit) since it was written, but never reached from this call site; lower_service_handler_body_ir’s own param loop carried the identical panic independently and is fixed the same way. (2) a qualified nullary sum-variant reference (Region.International) parses as ExprKind::FieldAccess, but the checker’s own check_field_access intercepts this shape — a bare-Ident receiver naming a declared sum type owning a matching variant — before ever independently type-checking the receiver, so the receiver’s own ExprId never gets a recorded type; lower_expr_ir’s FieldAccess arm now mirrors that same dispatch instead of unconditionally recursing into the receiver, producing IrExprKind::Variant directly. Verified by the full ir:: unit suite (133/133, two new pinning tests) and a full zero-diff bless against the entire e2e fixture corpus. lower_service_item_ir (and by extension a real IrItem::Service enumerator) is now safe to call on every real production service body in the corpus” |
| v0.248.14 | ”P6.23 (real progress): bynk-emit::ir::lower’s lower_ident_ir now lowers a bare HttpResult/QueueResult nullary variant reference (NotFound, Ack) to a real IrExprKind::Call{callee: Callee::Intrinsic, args: []}, the same shape lower_call_ir already produces for the call-with-args sibling (Retry(reason)), instead of panicking. lower_ident_ir gained a new expr_id: Option |
| v0.248.13 | ”P6.21 (closing the last gap): emitter/lower.rs’s remaining HttpResult/QueueResult branches now read the Callee::Intrinsic sink #1251 added, instead of a bare id.name == HTTP_RESULT check (lower_method_call’s qualified-with-args and qualified-nullary forms) or a Ty::HttpResult/Ty::QueueResult type check plus a bare http_variant/queue_variant keyword lookup (lower_ident’s bare-nullary form, lower_call’s bare-with-args form). The qualified-with-args forms close a real instance of R6.5’s name-matched-receiver defect class the same way every other P6.21 slice this session did; the two type-checked forms were already immune to name-matching and are converted for consistency with the rest of this module, not a new defect closure. Verified by a full zero-diff bless against the entire e2e fixture corpus, including test-body fixtures. This closes P6.21’s own HttpResult/QueueResult gap in full — every real dispatch site in emitter/lower.rs for these two built-in result types is now Callee-driven” |
| v0.248.12 | ”P6.21/P6.23 (checker-side): bynk-check’s checker.rs now records Callee::Intrinsic for HttpResult/QueueResult bare-variant construction at all 8 real dispatch sites (bare Ident, Call, ConstructorCall, FieldAccess for HttpResult’s nullary form, MethodCall) — the sink GlobalRef’s own doc comment in bynk-emit’s ir.rs named as the reason Decision C (#1143) left this shape out of scope entirely: the checker’s own disambiguation (expected/ctx.return_ty peeling, deciding whether NotFound resolves to HttpResult or a same-named user sum variant) is contextual/position-dependent, so a downstream reader had no recorded decision to read back without re-deriving the same logic. Purely additive — no existing Callee entry is overwritten, no control flow or diagnostic changes, so this PR is zero-diff by construction (nothing reads the new entries yet). Verified by the full bynk-check unit suite (157/157) plus a full zero-diff bless against the entire e2e fixture corpus” |
| v0.248.11 | ”P6.21 (partial, continued): emitter/lower.rs’s two agent-method-call branches (inline Agent(key).method(args) and let-bound x.method(args) after let x = Agent(key)) now read Callee::Agent (P6.0) instead of cx.local_agents.contains(&name.name)/cx.local_agent_vars.contains_key(&id.name) — a real instance of R6.5’s name-matched-receiver defect class: the checker records Callee::Agent uniformly whenever a receiver’s checked type resolves to an agent, covering both AST shapes (and the self-agent WebSocket-transfer special case nested inside the first) from one classification, where the old code re-derived the same fact twice from a bare name. cx.local_agent_vars stays consulted inside the let-bound branch’s own body — it still answers which agent a bound name refers to (needed for record_agent_call’s bookkeeping and the #908 rename resolution), a different question from whether the receiver is an agent instance at all. Verified by a full zero-diff bless against the entire e2e fixture corpus” |
| v0.248.10 | ”P6.21 (partial, continued): a real bug fix plus the Intrinsic-branch conversion #1247 previously reverted. bynk-check::test_suites::typecheck_case_body now returns its own callees map instead of discarding it — its emit-path callers (bynk-emit’s synthetic_typed_commons_for_target consumers) previously received expr_types only, so Callee classification (Callee::Intrinsic, Callee::Store, etc.) was silently never recorded for anything inside a .test.bynk case/property body, even though this function computed it correctly all along. With that fixed, emitter/lower.rs’s built-in static-constructor branches (List/Map.empty, Int/Float.parse, Duration.millis, Instant.fromEpochMillis, Bytes.fromUtf8/fromBase64/empty, Stream.of) now read Callee::Intrinsic instead of a bare id.name == X check — closing the same latent shadowing gap the previous P6.21 PRs already closed elsewhere (List/Map/Duration/Instant/Bytes/Stream never checked cx.is_local(&id.name), unlike every other converted branch; the checker’s own ctx.lookup(X).is_none() guard, present before every Callee::Intrinsic insertion, is strictly more correct). Verified by a full zero-diff bless against the entire e2e fixture corpus, including the two fixtures (814_messages_icu_date_styles, 917_bytes_in_test_case) that caught the original gap in #1247” |
| v0.248.9 | ”P6.21 (partial, continued): emitter/lower.rs’s lower_method_call now reads the checker’s own Callee::Store/Callee::Query classification (P6.0) for storage Map/Set/Cache/Log operations, instead of !cx.is_local(&id.name) — a real instance of R6.5’s name-matched-receiver defect class, the same one body_writes_state/#1196 already closed for write-detection. The per-kind side-table lookups (cx.is_agent_store_map/set, cx.agent_store_cache_ttl, cx.agent_store_log_retain) stay unchanged: they answer a different question (which store kind this field is, needed to route to the right branch) than the shadowing question Callee already settles. Held-map connection ops (a different, real-time concept) and Cell (never reaches these branches — bound into ordinary scope, not name-matched) are untouched, as are the four branches this session’s own earlier P6.21 slice already left alone (HttpResult/QueueResult bare-variant construction, Events.emit — no Callee recorded for these at all) and the rest of lower_method_call’s own ~15 remaining branches. Verified by a full zero-diff bless against the entire e2e fixture corpus” |
| v0.248.8 | ”P6.21 (partial): emitter/lower.rs’s lower_call now reads the checker’s own Callee classification (P6.0) for agent construction (Callee::AgentInit) and sum-variant construction (Callee::Ctor) instead of re-deriving them — cx.local_agents.contains(&name.name) and call_is_sum_variant’s own sum_name/call_name string comparison, both real instances of R6.5’s name-matched-receiver defect class (the same one block_writes_state carried before #1196 converted it to read Callee::Store directly). call_is_sum_variant is retired (its one call site is gone). HttpResult/QueueResult bare-variant construction and Events.emit are deliberately left on the old Ty-based path — Decision C (#1143) never gave those a Callee classification at all (GlobalRef’s own doc comment in ir.rs names why), so converting them needs a real checker-side design decision first (tracked separately), not attempted here. lower_method_call (the larger, ~900-line dispatcher) is also untouched — this is a bounded first step, not the full P6.21 cutover. Verified by a full zero-diff bless against the entire e2e fixture corpus: byte-identical generated output for every fixture, confirming the Callee-driven read produces the same dispatch decisions the name-matched code did for every real construct the corpus covers” |
| v0.248.7 | ”P6.20-pre: bynk-emit::ir::lower now lowers a bare store Map[K,V] field name used as a value (not a method-call receiver) to a new IrExprKind::StoreQuery(String), instead of panicking. Found empirically, not assumed: the completion plan for #1137 called for verifying — before P6.20 — that real fixture-corpus agent/provider bodies don’t hit lower_ident_ir’s other fallback todo!()s beyond Question/Is; a direct catch_unwind probe over every agent/provider in the full e2e fixture corpus found 4 real panics (Sales/orders via lines.joinOn(orders, ...), an argument-position bare-Map reference per ADR 0120; Inventory/items, Ledger/balances via .entries/.keys/.values, a FieldAccess-receiver-position bare-Map reference per ADR 0184) — all four now resolve cleanly, confirmed by re-running the same probe against the full corpus with zero remaining panics. Only Cell fields were ever bound into lower_handler_body_ir’s own scope (v0.81’s implicit-deref rule); Map fields are not value types (StoreField’s own doc comment) so a real fix couldn’t just widen that binding — a new LowerIrCtx.store_queryable table plus a new IrExprKind variant, mirroring checker.rs’s own ExprKind::Ident dispatch (Ty::Query(V), ADR 0120) rather than re-deriving it. Set/Cache/Log fields deliberately excluded — checker-unreachable there, not merely untested (review of #1240 found Log was wrongly included in the first draft: bynk-check never special-cases a bare Log value the way it does Map). The store-field dispatch is checked immediately after the existing cx.lookup guard, matching the checker’s own precedence (review of #1240 found the first draft checked it last, which silently mis-resolved a store field colliding with a free fn or nullary variant of the same name). Dormant as of this slice (same posture as Question/Is); verified by four unit tests (two AST shapes, a shadowing local, a colliding free fn), not bless” |
| v0.248.6 | ”P6.16: bynk-emit::ir::lower’s ExprKind::Is (value is Pattern) now lowers to a real IrExpr — a forced receiver-temp Block wrapping either a new IrExprKind::RefinedCheck (for is DeclaredRefinedType) or a recursive boolean-test walk over the pattern (Wildcard/Binding/Literal/Variant-with-nested-payloads/Or), reusing P6.4’s already-shipped IrPat/lower_pattern_ir rather than re-deriving pattern structure. Scoped narrower than R5.9/R5.10’s combined framing suggested: traced directly against the shipped desugar, lower_is itself never constructs a narrowing binding (R5.9’s own cross-site &&/if scope-propagation machinery is a separate, still-open design question this slice does not settle) — only the forced receiver temp (R5.10) is is’s own concern, and that’s what this slice builds. Dormant as of this slice; verified by unit tests (refined-type check, bare/nested variant tests, or-pattern), not bless” |
| v0.248.5 | ”Fixes for #1238’s own bot review, landed after that PR merged: LowerIrCtx::set_return_ty no longer panics on a resolve miss (it did so eagerly for every fn/handler/provider-op body lowered, including the vast majority that contain no ? at all — the same documented miss-possible case lower_service_handler_signature_ir already degrades gracefully for), embed_conversion_ir’s checker::compatible arguments now match check_question’s own (operand_err, fn_err) order, Return/HttpResultNotFound nodes carry their own returned-value type (was Unit) matching wrap_body_return’s convention, and Question/Is’s synthetic temp names now go through one shared fresh_tmp counter instead of fixed names (avoiding a duplicate-const collision if two ?/is land in one function under a future hoisting printer)“ |
| v0.248.4 | ”P6.15: bynk-emit::ir::lower’s ExprKind::Question (?) now lowers to a real IrExprKind::Match, generalising the reference document’s own Match{Ok,Err} sketch to bynk’s actual two-scrutinee-shape desugar — closing the load-bearing gap that blocked IrItem::Provider/IrItem::Service from ever getting a real emitter call site (internal only, no shipped emitter consumer of this dormant construction path exists yet — byte-identical output, confirmed by a zero-diff bless run)“ |
| v0.248.3 | ”P6.24a: IrHandler::kind is now a real IR-native mirror (IrHandlerKind/IrHttpMethod), not bynk_syntax::ast::HandlerKind reused verbatim; emitter.rs’s five purely-structural handler-kind/protocol header-import checks (has_http, has_queue, hosts_ws_open, hosts_ws_inbound, subscribed_event_type_names) now read it and the existing ProtocolIr (via a new TypedCommons-only lower_protocol_ir_from_commons sibling) instead of matching raw HandlerKind/ServiceProtocol directly — byte-identical output confirmed by zero-diff bless” |
| v0.248.2 | ”P6.18: EmitProjectCtx::imported_methods/emit_forwarded_methods (a uses-imported type’s attached-method forwarding) reads a resolved FnSig (params/return_ty as real TyIds, via a new narrow lower_fn_sig_ir_from_types reader) instead of a raw FnDecl’s unresolved TypeRefs — the first real emitter consumer of bynk-emit::ir’s signature-only lowering pattern for a fn, byte-identical output confirmed by zero-diff bless” |
| v0.248.1 | ”emit_composition_root’s event-fanout envelope-forwarding decision (wants_envelope) now reads a project-wide EventSubscriberShape accumulator, populated at each unit’s own check time, instead of reaching into a different, already-consumed unit’s raw UnitTable directly (#1232)“ |
| v0.248.0 | ”@cache’s maxAge must now resolve to an exact whole number of seconds — a positive-but-fractional value (e.g. 500.milliseconds, or 1500.milliseconds, which resolves to a real but wrong max-age=1) previously type-checked cleanly and then silently dropped the sub-second remainder at emit time rather than honouring it, with no diagnostic anywhere in the pipeline; now rejected at check time with a new bynk.http.cache_max_age_fractional_seconds diagnostic (#1230)“ |
| v0.247.71 | ”bynk-emit::emitter::workers_entry’s @cache/@limit route-annotation reading moves to bynk-emit::ir (lower_route_cache_ir/lower_route_limit_ir, #1228), so the two remaining hand-rolled ExprKind::DurationLit/Ident/IntLit matches in that file read the same already-resolved-literal IR path lower_policy_ir already established, rather than walking Handler::annotations directly (internal only, no CheckedProgram needed since neither reads a type — byte-identical output, confirmed by a zero-diff bless run against 298_http_caching/300_http_limits)“ |
| v0.247.70 | ”bynk-emit::ir’s IrExprKind::Variant no longer carries a sum: Arc |
| v0.247.69 | ”bynk-emit’s emit_consumed_context_helpers now resolves a subscribed from Events(E) header’s own event type through ProtocolIr::Events’s already-resolved TyId (via lower_protocol_ir) instead of a raw match on ServiceProtocol::Events/TypeRef::Named, and emitter/lower.rs’s cap_op_param_names (trace(Cap.op)/with-predicate observation lowering) now reads a capability op’s parameter names off lower_op_sig_ir’s already-resolved OpSig instead of a second, independent AST extraction; ast_importers stays at 7 (both files already counted for other reasons) (internal only, byte-identical output)“ |
| v0.247.68 | ”AST_IMPORTER_EXCEPTIONS now also names project/tests_emit.rs, whose test/suite case bodies call straight into emitter.rs’s Q7-settled (design/tracks/the-ir.md §3.7) body-rendering pass and read a handler’s declared signature with no TyId available, the same shape #1176 already excluded for ir.rs/ir/lower.rs; ast_importers moves 8 to 7 — emitter.rs/emitter/lower.rs stay counted, since both still hold live, in-scope AST-declaration reads distinct from that body-rendering surface” |
| v0.247.67 | ”add ActorSeamIr, a new IR type wrapping bynk-check’s five already-resolved actor-seam structs by value, and lower_actor_seam_ir, a narrow reader trying them in the one priority order that’s load-bearing (sum ahead of Bearer, since a sum’s own first peer can itself be Bearer-schemed); converts emit_service (emitter/emit.rs) and emit_worker_compose’s HTTP-dispatch match (emitter/workers.rs), the two call sites that actually branch on which of several resolvers succeeded, from independently re-deriving that branch to one shared, tested function (internal only, byte-identical output)“ |
| v0.247.66 | ”bynk-emit’s emit_agent now resolves a held store Map field’s own frame type through a new TyId-level held_frame_ty helper (recursing through Option/Effect, mirroring the checker’s own type_ref_is_held) instead of a bare TypeRef::Connection match — fixing a real, previously-uncovered gap where a Map[K, Option[Connection[F]]] value rendered the whole Option<Connection |
| v0.247.65 | ”bynk-emit’s emit_agent now derives which store fields are Sets, and each Map field’s @indexed key list, from the already-lowered StoreFieldIr/StoreKindIr rather than re-walking the raw AST store fields and their @indexed annotations a second time — the Set-derived TypeRef is still read for the rehydration boundary check (serialisation.rs’s own TypeRef-driven boundary, unconverted) (internal only, byte-identical output)“ |
| v0.247.64 | ”bynk-emit’s emit_agent reads each Cell/Map/Cache/Log state field’s TyId through the new lower_store_field_shape_ir IR reader (a shape-only sibling of lower_store_field_ir that never lowers a Cell field’s init expression) instead of ts_type_ref on the raw AST TypeRef, and resolve_store_field_ty now falls back to Ty::Unit on an unresolvable store-field type instead of an ADR 0334 panic, matching the checker’s own silent-fallback posture for a state the checker accepts but never actually validates (internal only, byte-identical output)“ |
| v0.247.63 | ”bynk-emit reads a handler’s/provider’s given clause through the new lower_handler_given_ir/lower_provider_given_ir IR readers everywhere it feeds a deps-type or cross-context decision (project.rs’s plan_agent_given_deps and EmitProjectCtx::agent_method_givens, emitter.rs’s agent_given_caps_used, emitter/emit.rs’s effective_given/build_deps_object_ty_with_surface/cap_ref_ty, emitter/workers.rs’s worker_cross_caps), instead of walking bynk_syntax::ast::CapRef directly at each site (internal only, byte-identical output)“ |
| 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.55 | P6.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.54 | P6.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.53 | The 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 |
| v0.247.52 | P6.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.51 | Redefine 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.50 | P6.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.49 | design/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 |
| v0.247.48 | P6.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 |
| v0.247.47 | bynk-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 |
| v0.247.46 | P6.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.45 | bynk-check gains TypedCommons::actor_bindings — a service handler’s own resolved by |
| v0.247.44 | P6.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.43 | P6.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.42 | P6.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.41 | P6.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.40 | P6.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 |
| v0.247.39 | P6.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.38 | P6.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 |
| v0.247.37 | P6.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.36 | P6.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.35 | P6.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.34 | P6.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.33 | Settle 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.32 | P5.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.31 | P5.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.30 | P5.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.29 | P5.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.28 | P5.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.27 | P5.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.26 | Settle 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.25 | bynk-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.24 | bynk-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.23 | Settle 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.22 | fs_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.21 | content-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.20 | content-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.19 | content-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.18 | content-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.17 | content-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.16 | content-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.15 | content-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.14 | Slice 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.13 | Settle 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.12 | Resolve #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.11 | bynk-driver reads bynk.toml through its own overlay instead of bynk-emit’s disk fallback, closing a gap #1081 (#1077) left uncaught |
| v0.247.10 | Breaking (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.9 | Resolve #1049 — decline the Positive/NonNegative → InRange fold; neither base has a writable bound that could stand for infinity |
| v0.247.8 | ”bynk-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.7 | The checker interns every Ty behind a TyId handle, making type identity a u32 comparison rather than a recursive structural walk |
| v0.247.6 | The 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.5 | The 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.4 | A ?’s propagating early return nested inside a short-circuited &&/` |
| v0.247.3 | ”cargo 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.2 | A 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.1 | The 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.0 | ”NonEmpty 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.11 | block_uses_send and bynk-lsp’s three statement-aware extract walkers enumerate every ExprKind variant explicitly instead of a wildcard arm (#1025) |
| v0.246.10 | block_writes_state’s expr walker enumerates every ExprKind variant explicitly instead of a wildcard arm (T1.7’, #1020) |
| v0.246.9 | ”fs_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.8 | The 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.7 | ”tree-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.6 | Track 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.4 | ”Breaking (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.3 | ”closes_rule: pending-file field + stamp-materialised rule ledger, closing out T0.0’s deferred Closes-Rule provenance (#1001)“ |
| v0.246.2 | ”cargo xtask greenfield-status — the T0.0 probe harness (13 probes, 9 gated against a committed table)“ |
| v0.246.1 | Settle the compiler-architecture track — the refactor acceptance gate, the emit-ABI posture, and the lowering substrate |
| v0.246.0 | Hover 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.0 | A 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.0 | An 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.0 | An 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.2 | ”bynkc fmt / bynk fmt read the project’s bynk.toml [fmt] section as the layer under their flags” |
| v0.240.1 | Event payloads and envelopes are validated at the Workers subscriber boundary |
| v0.240.0 | An 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.0 | A 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.1 | Per-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.0 | The 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.1 | The 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.0 | Idempotency.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.0 | The Idempotency capability — mechanical dedup for at-least-once delivery, slice 0 (in-memory provider) |
| v0.235.0 | A 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.0 | LocaleTag 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.7 | The 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.6 | The 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.5 | A 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.4 | An 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.3 | An 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.2 | The 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.1 | A 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.0 | A 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.0 | ”bynk.locale’s types split into a leaf commons, bynk.locale.types, closing Locale slice 2’s uses-collision gap” |
| v0.231.0 | The Cloudflare Locale provider negotiates Accept-Language against a context’s message bundle (RFC 4647 basic filtering) |
| v0.230.0 | ”messages templates gain ICU plural/select/number/date placeholders, formatted via the host Intl” |
| v0.229.2 | The 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.1 | Sequence 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.0 | A 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.0 | The 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.0 | ”bynkc 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.0 | Capability-aware quick-fixes — add a missing consumes, fill missing record fields, and auto-uses/consumes an unresolved name (#852) |
| v0.225.0 | VS Code documentation view — a file’s doc comments as a rendered reference page (bynk/documentationModel + webview) |
| v0.224.0 | Diagnostic 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.1 | The 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.0 | Doc comments resolve [Name]/[Owner.member] links against the project’s binding index, navigable via document links and hover |
| v0.221.0 | Add the Locale capability (fixed "en" on every platform) and a bynk.locale commons providing LocaleTag, Message/MessageArg, and a bundle-free render |
| v0.220.2 | ”bynk deploy reports orphaned resources and prunes them with --prune; a deleted KV namespace self-heals like a deleted queue already does” |
| v0.220.1 | ”bynk deploy --env NAME for independent multi-environment provisioning and deploy; bynk dev -- --remote reads the matching environment” |
| v0.220.0 | Refined patterns (_ where <predicate>) in match arms |
| v0.219.0 | Or-patterns (`p₁ |
| v0.218.0 | A 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.1 | bynk-lsp’s root-cache write-back is guarded against a bynk.toml/workspace-folder invalidation racing an in-flight filesystem walk |
| v0.217.0 | ”textDocument/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.5 | A system-tier case can mix a typed argument with Wire(...) in the same http address call |
| v0.216.4 | bynk-lsp memoises URI to project-root routing, so hover/completion/etc. stop re-walking the filesystem on every request |
| v0.216.3 | The 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.2 | Completion, symbols, locals-navigation, and signature-help move from bynk-lsp into bynk-ide, unblocking a future wasm-side completion entry (#808) |
| v0.216.1 | Completion, symbols, locals-navigation, and signature-help move from bynk-lsp into bynk-ide, unblocking a future wasm-side completion entry (#808) |
| v0.216.0 | ”textDocument/codeAction offers an extract-function refactor (RefactorExtract), capability-free-only” |
| v0.215.2 | LSP .-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.1 | LSP codeAction now filters its response against CodeActionParams.context.only |
| v0.215.0 | Call hierarchy now records capability-op and agent-handler-dispatch call edges, closing the under-reporting gap in #304 |
| v0.214.4 | The playground editor shows the inferred type of the expression under the cursor on hover (#397) |
| v0.214.3 | The 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.0 | textDocument/codeAction offers an extract-variable refactor (RefactorExtract) for a selected expression |
| v0.213.0 | ”bynk-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.0 | A workers context generates its own cross-context boundary codecs and imports no sibling context’s module as a value |
| v0.210.0 | LSP 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.0 | A 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.2 | The deploy ledger is written atomically and a truncated ledger is rejected rather than re-minting every namespace |
| v0.208.1 | ”fmt 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.0 | HS256 bearer verification requires an exp claim (a token with no expiry no longer verifies) |
| v0.207.0 | A 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.1 | The 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.0 | Unresolvable explicit call type arguments, Json.decode[T] targets, and lambda parameter annotations in handler bodies are now reported instead of silently swallowed |
| v0.205.1 | Project-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.0 | A 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.0 | Reject 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.0 | A 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.0 | A 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.0 | A 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.0 | A doc block containing */ can no longer terminate the emitted JSDoc comment early and inject top-level TypeScript |
| v0.197.0 | The 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.0 | A 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.0 | A 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.0 | An if/match condition ending in a bare identifier no longer swallows a single-identifier brace body as a record construction |
| v0.192.0 | The 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.0 | A 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.1 | ”bynk 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.0 | The 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.0 | A 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.1 | A 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.0 | The 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.0 | A 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.0 | Increments 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.0 | A 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.env↔makeTestState 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.0 | One 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.0 | The 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.0 | The 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.0 | A 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.0 | The 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.0 | The 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.0 | The 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.0 | A 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.0 | One 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.0 | File 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.0 | Generic 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.0 | bynk 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.0 | bynk 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.0 | bynk 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.0 | bynk 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.0 | Three 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.0 | Go-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.0 | Multi-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.0 | Hover 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.0 | Hover 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.0 | The 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.0 | Three 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.0 | Lexical 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.0 | Protocol 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.0 | One 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.0 | The 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.0 | Map 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.0 | A 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.0 | Generic 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.0 | Service-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.0 | Declared 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.0 | The 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 (mapOk ≡ map(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.0 | Oidc — 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.0 | List.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, close → consume_on_borrow). Completion/signature-help via the kernel registry; no grammar or runtime change (ADR 0156). |
| v0.149.0 | Map.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.0 | List.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.0 | List.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.0 | The 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 foldEff — uptime-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 if — if 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.0 | Editor 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.0 | Nested 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.0 | Refined 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.0 | Request 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.0 | Security 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.0 | Conditional 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.0 | HTTP 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 404→405/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.0 | bynk 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). Additive — bynkc 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.0 | Hover 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.1 | A 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.0 | bynkc 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.0 | CORS 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.0 | Literal 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.0 | Refinement-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.0 | match-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 variants — match 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.0 | Editor-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.0 | Editor-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.0 | Editor-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.0 | Editor-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.0 | Editor-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.0 | Editor-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.0 | Editor-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.0 | The 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.0 | History 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.0 | The 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.0 | Observation 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.0 | Step 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.0 | Function 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/result — stripped 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.0 | property / 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 Positive → 1/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.0 | Structural 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.0 | expect + 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 ensures — is, 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 assert→expect and test→suite/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.0 | Raw — 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.0 | Bytes — 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.0 | The 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.5 | Playground 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::Analyse — non-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.4 | The 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.3 | The 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.demo ⇒ app/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.2 | The 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.1 | A 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.108 | The 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.107 | from 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 != conn — Connection 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.106 | from 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 webSocketMessage — on 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.105 | from 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.104 | The 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.103 | The 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.102 | Held-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.101 | Streaming 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.100 | Stream[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 of→collect 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.99 | Capability 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.98 | Cell.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.97 | Storage 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 automatic — loadState 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.96 | Storage 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.95 | Storage 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.92 | Lazy 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.91 | The 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.90 | The 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 Instant↔Int 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.89 | A 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.88 | Query-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 total — first/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.87 | Storage 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.86 | The 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 Int↔Duration mix — Int + 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.85 | Storage 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.84 | Storage 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/Map — add/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.83 | Storage 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.82 | Storage 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.81 | Storage 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.80 | Agent 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.79 | Asynchronous 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.78 | Run/Debug a test from the editor (tooling; vscode-bynk only — no language or compiler change). Each test now shows a **`▷ Run Test |
| v0.77 | Quiet 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.76 | The 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 operation — GET "/", 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.75 | The 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.74 | Semantic 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.73 | Debug 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.72 | One-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.71 | Debug 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.70 | Per-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.69 | Debug 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.68 | Source 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.67 | Pre-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.66 | bynk 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.65 | bynk-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.64 | bynk-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.63 | bynk-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.62 | bynk-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.61 | bynk-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.60 | bynk-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.59 | bynkc test --format json and a VS Code Test Explorer (tooling; proposal v0.59). bynkc test gains a --format selector (rich default |
| v0.58 | bynk 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.57 | bynk 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 provisioning — wrangler 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.56 | The 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.55 | The 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.54 | Actors, 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.53 | Actors, 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 set — hasClaim("name") (present and truthy) and claimEquals("name", "value") (string equality), composed with &&/` |
| v0.52 | Actors, slice 4 — multi-actor sum dispatch (ADR 0090; actors feature track). A by clause may now name an ordered sum of peer actors — on 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.51 | Actors, 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.50 | The 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.49 | Security 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.48 | First-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.47 | Actors, 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.46 | bynk 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.45 | Actors, 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 call→Caller, 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 change — None 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.44 | Service 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.43 | String 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 scalar — String/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.42 | Numeric 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.41 | Maintenance & 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.1 | Fix: 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.40 | InRange-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.1 | Generic-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.39 | Parameter-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.1 | Project 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.38 | Extension 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.37 | Folding & 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.1 | Record 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.36 | Methods 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.35 | Implementation 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.34 | Call 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.33 | CodeLens 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.1 | Signature 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.32 | Signature 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.2 | Locals 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.1 | Locals 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.31 | Locals 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.2 | Value-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.1 | Name-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.30 | Positional 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.14 | The 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.13 | The 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_body … refined_check_as_bool: the lower_*/mock_*/emit_statement/emit_match_*/emit_if_*/kernel lowerers); (2) emitter/emit.rs — the per-declaration emission engine (emit_type … emit_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.9 | Refactor 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.10 | Refactor 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_remaining → cx.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.12 | Refactor 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.11 | Refactor 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.8 | Refactor 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_emit → emitter::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_string → emitter::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.7 | Refactor 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.5 | Refactor 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.4 | Refactor 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.3 | Refactor 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.2 | Refactor 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.1 | The 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.29 | A-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.28 | A-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.27 | The 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.26 | The 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.25 | The 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.24 | The 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.23 | The 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.22b | The 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.22a | The 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.21 | Float — 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 Int↔Float 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.20b | The 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.20a | The 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.19 | The 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.18 | Adapter 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.17 | Adapters — 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.16 | Multi-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.15 | Cross-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.13 | Refinement 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.12 | Provider composition (provides … given) — a provider may depend on other capabilities; the composition root wires the dependency graph in topological order. |
| v0.11 | Agent state-field initialisers (state { status: OrderStatus = Pending }), enabling sum-typed state machines (and opaque/refined state) — no more Option-wrapping. |
| v0.10b | Queue consumers (on queue) — message deserialisation, the Worker queue entry point with Ok/Err ack/retry, and wrangler.toml [[queues.consumers]]. |
| v0.10a | Cron handlers (on cron) — scheduled tasks compiling to the Worker scheduled entry point and wrangler.toml [triggers]. |
| v0.9.4 | Refined-literal admission (write a literal where a refined type is expected); Mock[T] value fabrication for tests. |
| v0.9.1 | assert as an expression; project-mode hardening; a tsc verification stage. |
| v0.9 | HTTP handlers (on http), HttpResult, and the Cloudflare Workers target. |
| v0.7.1 | Tail-position auto-lift of plain values into Effect. |
| v0.6 | Cross-context service calls (consumes) and composition roots. |
| v0.5 | The 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.
Deferred to v1
Section titled “Deferred to v1”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.