You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
design/bynk-tooling-roadmap.md §5.1 is named "The bynk driver & the project-lifecycle arc", and ADR 0083 set its posture in one sentence: "a thin orchestrator over bynkc and the Node toolchain — bynk is to bynkc what cargo is to rustc. The compiler stays pure (compile / check / fmt / test)."
The arc shipped — doctor (v0.46), dev (v0.57), new (v0.58), deploy (slices through v0.172, spine #558). But it grew around the compiler rather than above it, and the analogy the ADR asserts does not hold today. The reason is structural, not cosmetic: rustc's unit of work is one crate, and cargo exists because rustc deliberately cannot see past it — someone has to own the graph, resolve it, and hand rustc resolved flags. compile_project takes a project root and compiles the whole thing, so there is no graph left for bynk to own. It filled instead with verbs borrowed from elsewhere: new (cargo), doctor (rustup), dev/deploy (wrangler) — while the compiler kept the project model.
ADR 0083 defined the compiler's purity as a verb list. The claim this track makes is that purity should have meant not knowing what a project is. bynkc discovers projects and reads bynk.toml's [paths] (bynk-driver/src/lib.rs:23-27 → bynk-emit/src/project/paths.rs:107), which puts it at the same layer as the driver. Verb overlap is not the problem and never was — cargo fmt and rustfmt both format, and nobody is confused, because the layering is unambiguous. Same-layer duplication is the problem.
Three things have also drifted from the ADR's own text since v0.46: bynkno longer shells bynkc (it links the pipeline in-process, crate-decomposition slice 7); the two share a front-end crate (#521, bynk-driver); and bynk grew mirrors of check/fmt/test (#487, v0.138) that site/src/content/docs/docs/bynk-cli.md:12-17 pins as "drop-in equivalent". That promise is a ceiling: every improvement to bynk's interface becomes a bynkc-compat problem, so the front door can never be more human than the compiler is.
The evidence that nobody owns the manifest.bynk.toml has three divergent readings today, none of them the front door's:
[paths], read at the bottom — bynk-emit/src/project/paths.rs:107 (read_project_paths), below both binaries, reached via bynk_driver::project_options (bynk-driver/src/lib.rs:23).
[fmt], read by neither CLI — bynk_driver::run_fmt hardcodes FormatOptions::default() (bynk-driver/src/lib.rs:134), and both bynk fmt (bynk/src/fmt.rs:31) and bynkc fmt (bynkc/src/main.rs:492) route through it. Only the LSP reads [fmt]. So a project setting indent = "spaces" gets spaces on format-on-save and tabs from bynk fmt, while site/src/content/docs/docs/manifest.md:85 states the section is "consumed by bynkc fmt". That is a live defect and a false doc claim.
And a fourth key is inert by documentation: [paths] out — "Consumed by the LSP; the compiler takes its output directory from the CLI, so this key does not override bynkc" (manifest.md:63). A manifest that declares an output directory no build honours.
This is not a formatter bug or an LSP bug. It is what happens when the tool that owns the user's intent is not the tool that reads the user's config — because the front door has no seat at the manifest. bynk has no build at all: to produce an artefact you type bynkc compile -o out --target workers. Nobody has ever typed rustc to build their project.
End state at retirement:bynk is the binary a human types, and it owns the project model — the manifest is its private input, bynk build needs no flags, profiles are named rather than inferred from argv, and the on-ramp teaches bynk throughout. bynkc keeps its verbs and loses its guesses: it is told its roots and does exactly what it is told, which is what makes it a compiler rather than a second driver. The rule that decides every future command: bynk infers, bynkc is told.
Realises:design/bynk-tooling-roadmap.md §5.1 (the driver arc, whose orchestration half is complete and whose front-door half was never built), and ADR 0083 by name — this track revisits its "the compiler stays pure (compile / check / fmt / test)" clause, which defined purity as a verb list and left the project model in the compiler. It adds no language surface: no grammar, AST, checker, or emitter semantics change.
Track doc (added by the settling PR):design/tracks/front-door.md
Why a track (the ADR 0076 trigger)
Multi-increment — a new verb (bynk build), a named-profile model, a crate extraction with a publish/release-plumbing row, a breaking change to bynkc's CLI, an on-ramp rewrite (install + tutorial 01 + the /docs/cli/ URL), and a human-facing output pass. No single delete-on-merge proposal carries that, and the subtractive slices are only safe once the additive ones have landed.
Surface not yet settled — the questions below are genuinely open, and two of them (where the manifest parser lives; whether bynkc keeps discovery) are hard to reverse once other code depends on the answer.
Security/safety boundary — none. Driver-side only; no language surface, and nothing outward-facing beyond what deploy already gates under its own track.
Two triggers hold, which is the bar.
Open design questions
Where does the manifest parser live, and is the boundary structural or documentary? The proposal on the table is a new bynk-manifest leaf that bynk and bynk-lsp depend on and bynkc does not — making "the compiler cannot read the manifest" a compile error rather than a doc comment to maintain. It cannot live in bynk itself (a binary crate the LSP cannot depend on). Cost: a new published crate — seed-first publish through release-bootstrap.yml before trusted publishing, plus a row in both release workflows' hardcoded crate lists. The alternative is leaving read_project_paths in bynk-emit and enforcing the layering by policy, which is what we do now and is how it drifted.
Does bynkc keep conventional-roots discovery? Dropping [paths] still leaves the question of whether bynkc check . may infer src/ when no manifest is present. Keeping it is convenient and is the slippery slope straight back to discovery; dropping it breaks muscle memory and any CI script (pre-1.0, but examples/ are compile-checked and the drift guards will surface it).
Profile naming, and which is the default. The profile is currently a side-effect of which verb you typed: bynkc test sets .contracts(true) (bynk-emit/src/project.rs:354, per DECISION J of v0.115), bynkc compile leaves it false (:317), and --inspect silently switches import extension to .ts (bynkc/src/main.rs:116). That is three implicit profiles inferred from argv. Naming them raises a real default question: cargo defaults to dev and opts into --release, but bynk build's natural ancestor (bynkc compile) is contracts-off, i.e. release. Picking cargo's default would change behaviour; picking the ancestor's would invert cargo's ergonomics.
Does bynk test stop shelling? It is the only verb that still shells bynkc (bynk/src/cli.rs:192); everything else links in-process since slice 7. That single exception is what keeps the resolution ladder, the Skew machinery (bynk/src/compiler.rs), and the "Which bynkc?" section (bynk-cli.md:302) user-visible. If the front door is the front door, that question should never reach a human and BYNK_BYNKC becomes an internal escape hatch. Note vscode-bynk compiler resolution is narrower than the bynk driver's — a driver-first install breaks the Test Explorer #486 already reports the resolution split biting the VS Code Test Explorer.
The on-ramp inversion, and what it costs.site/src/content/docs/book/introduction/install.md:22 says cargo install --path bynkc, then invokes bynk doctor eleven lines later — a binary it never told you to install — and lists "bynkc exposes four commands" as the toolchain surface. book/tutorials/01-first-program.mdx:44 teaches bynkc compile demo.bynk --output demo.ts in the body with bynk new in a blockquote aside. /docs/cli/ is bynkc's page; bynk got the qualified /docs/bynk-cli/. Inverting the URLs is not free — the doc drift guards hardcode page paths, so it is a Rust-test change too.
What does [build] contain, and does it subsume out? Candidates: target, platform, emit, out. [paths] out already exists and is inert; a [build] out would either move it (breaking) or shadow it (confusing). Interacts with N:M context→Worker deployment grouping in bynk.toml #551 (N:M context→Worker grouping in bynk.toml), which proposes its own manifest surface.
Candidate slice decomposition
Deliberately additive-first: every slice up to the last only adds a reader at the top, so nothing below changes and each is shippable alone. The single subtractive slice lands last, once nothing depends on the old path. bynk already has toml as a dependency (bynk/Cargo.toml), so slice 0 needs no refactor beneath it.
Slice 0 — bynk build. The missing verb, no required flags. Reads [build]/out in bynk, passes explicit CompileOptions down — CompileOptions::split(root, paths) (bynk-emit/src/project.rs:328) already takes them, so nothing below moves. Makes out live. Ships alone; the first brick of the boundary.
Slice 1 — named profiles. Replace the three argv-inferred profiles with a named model across build/test/dev/deploy. Makes --inspect's "compiles a debug build" stop being a footnote.
Slice 3 — bynkc is told. The subtractive one: remove read_project_paths from bynk-emit, make bynkc take explicit roots. Gated on Q3.
Slice 4 — the on-ramp inversion. install/tutorial/URL, and retiring "drop-in equivalent" as a stated goal. Gated on Q6.
Slice 5 — human-facing output.bynk check prints nothing on success today; the cargo Compiling/Finished … in 0.5s cadence has more to say here, not less, because builds and deploys are per-context.
The bynk/bynkc contract — "bynk infers, bynkc is told", superseding ADR 0083's verb-list definition of compiler purity, and recording that verb overlap is not the boundary (the cargo fmt/rustfmt precedent). This is the ADR that stops the drift recurring, and it should land with or before slice 0 — otherwise slice 0 is just a sixth verb.
The profile model — names, the default, and which verb selects which. Hard to reverse once [build] is documented.
Threat model
None, because the security/safety trigger is not ticked. This track is driver-side and adds no language surface; it creates no resources, handles no credentials, and touches nothing outward-facing. deploy's irreversible side effects stay behind their own gate under track #558, and this track does not change them. Slice 3 is breaking (a CLI contract change) but not unsafe; slice 2's [fmt] fix will reformat files in projects that set the section, which is a migration note rather than a threat.
The theme
design/bynk-tooling-roadmap.md§5.1 is named "Thebynkdriver & the project-lifecycle arc", and ADR 0083 set its posture in one sentence: "a thin orchestrator overbynkcand the Node toolchain —bynkis tobynkcwhatcargois torustc. The compiler stays pure (compile / check / fmt / test)."The arc shipped —
doctor(v0.46),dev(v0.57),new(v0.58),deploy(slices through v0.172, spine #558). But it grew around the compiler rather than above it, and the analogy the ADR asserts does not hold today. The reason is structural, not cosmetic: rustc's unit of work is one crate, and cargo exists because rustc deliberately cannot see past it — someone has to own the graph, resolve it, and hand rustc resolved flags.compile_projecttakes a project root and compiles the whole thing, so there is no graph left forbynkto own. It filled instead with verbs borrowed from elsewhere:new(cargo),doctor(rustup),dev/deploy(wrangler) — while the compiler kept the project model.ADR 0083 defined the compiler's purity as a verb list. The claim this track makes is that purity should have meant not knowing what a project is.
bynkcdiscovers projects and readsbynk.toml's[paths](bynk-driver/src/lib.rs:23-27→bynk-emit/src/project/paths.rs:107), which puts it at the same layer as the driver. Verb overlap is not the problem and never was —cargo fmtandrustfmtboth format, and nobody is confused, because the layering is unambiguous. Same-layer duplication is the problem.Three things have also drifted from the ADR's own text since v0.46:
bynkno longer shellsbynkc(it links the pipeline in-process, crate-decomposition slice 7); the two share a front-end crate (#521,bynk-driver); andbynkgrew mirrors ofcheck/fmt/test(#487, v0.138) thatsite/src/content/docs/docs/bynk-cli.md:12-17pins as "drop-in equivalent". That promise is a ceiling: every improvement tobynk's interface becomes abynkc-compat problem, so the front door can never be more human than the compiler is.The evidence that nobody owns the manifest.
bynk.tomlhas three divergent readings today, none of them the front door's:[paths], read at the bottom —bynk-emit/src/project/paths.rs:107(read_project_paths), below both binaries, reached viabynk_driver::project_options(bynk-driver/src/lib.rs:23).[paths], read differently at the side —bynk-lsp/src/project.rs:187-194reduces the manifest to onesrc_dir(firstincludeonly;excludeexplicitly ignored). This is gap 1 of the LSP-foundations track (Track: LSP foundations — the project model, the freshness contract, and the lifecycle under the shipped surface #640): "The LSP analyses a different project from the compiler."[fmt], read by neither CLI —bynk_driver::run_fmthardcodesFormatOptions::default()(bynk-driver/src/lib.rs:134), and bothbynk fmt(bynk/src/fmt.rs:31) andbynkc fmt(bynkc/src/main.rs:492) route through it. Only the LSP reads[fmt]. So a project settingindent = "spaces"gets spaces on format-on-save and tabs frombynk fmt, whilesite/src/content/docs/docs/manifest.md:85states the section is "consumed bybynkc fmt". That is a live defect and a false doc claim.And a fourth key is inert by documentation:
[paths] out— "Consumed by the LSP; the compiler takes its output directory from the CLI, so this key does not overridebynkc" (manifest.md:63). A manifest that declares an output directory no build honours.This is not a formatter bug or an LSP bug. It is what happens when the tool that owns the user's intent is not the tool that reads the user's config — because the front door has no seat at the manifest.
bynkhas nobuildat all: to produce an artefact you typebynkc compile -o out --target workers. Nobody has ever typedrustcto build their project.End state at retirement:
bynkis the binary a human types, and it owns the project model — the manifest is its private input,bynk buildneeds no flags, profiles are named rather than inferred from argv, and the on-ramp teachesbynkthroughout.bynkckeeps its verbs and loses its guesses: it is told its roots and does exactly what it is told, which is what makes it a compiler rather than a second driver. The rule that decides every future command:bynkinfers,bynkcis told.design/bynk-tooling-roadmap.md§5.1 (the driver arc, whose orchestration half is complete and whose front-door half was never built), and ADR 0083 by name — this track revisits its "the compiler stays pure (compile / check / fmt / test)" clause, which defined purity as a verb list and left the project model in the compiler. It adds no language surface: no grammar, AST, checker, or emitter semantics change.design/tracks/front-door.mdWhy a track (the ADR 0076 trigger)
bynk build), a named-profile model, a crate extraction with a publish/release-plumbing row, a breaking change tobynkc's CLI, an on-ramp rewrite (install + tutorial 01 + the/docs/cli/URL), and a human-facing output pass. No single delete-on-merge proposal carries that, and the subtractive slices are only safe once the additive ones have landed.bynkckeeps discovery) are hard to reverse once other code depends on the answer.deployalready gates under its own track.Two triggers hold, which is the bar.
Open design questions
Where does the manifest parser live, and is the boundary structural or documentary? The proposal on the table is a new
bynk-manifestleaf thatbynkandbynk-lspdepend on andbynkcdoes not — making "the compiler cannot read the manifest" a compile error rather than a doc comment to maintain. It cannot live inbynkitself (a binary crate the LSP cannot depend on). Cost: a new published crate — seed-first publish throughrelease-bootstrap.ymlbefore trusted publishing, plus a row in both release workflows' hardcoded crate lists. The alternative is leavingread_project_pathsinbynk-emitand enforcing the layering by policy, which is what we do now and is how it drifted.Interaction with the LSP-foundations track (Track: LSP foundations — the project model, the freshness contract, and the lifecycle under the shipped surface #640) — the sequencing question this track cannot answer alone. Track: LSP foundations — the project model, the freshness contract, and the lifecycle under the shipped surface #640's gap 1 is this same problem from the LSP side, and its fix could land as "make the LSP call
read_project_paths", which would cement the reader at the bottom of the stack and make this track's slice 3 a rewrite rather than a deletion. Ifbynk-manifestlands first, Track: LSP foundations — the project model, the freshness contract, and the lifecycle under the shipped surface #640's gap 1 is solved by construction — both consumers read one parser. These two tracks must agree on the manifest's home before either ships its project-model slice. Settling should close this jointly, not in parallel.Does
bynkckeep conventional-roots discovery? Dropping[paths]still leaves the question of whetherbynkc check .may infersrc/when no manifest is present. Keeping it is convenient and is the slippery slope straight back to discovery; dropping it breaks muscle memory and any CI script (pre-1.0, butexamples/are compile-checked and the drift guards will surface it).Profile naming, and which is the default. The profile is currently a side-effect of which verb you typed:
bynkc testsets.contracts(true)(bynk-emit/src/project.rs:354, per DECISION J of v0.115),bynkc compileleaves itfalse(:317), and--inspectsilently switches import extension to.ts(bynkc/src/main.rs:116). That is three implicit profiles inferred from argv. Naming them raises a real default question: cargo defaults todevand opts into--release, butbynk build's natural ancestor (bynkc compile) is contracts-off, i.e. release. Picking cargo's default would change behaviour; picking the ancestor's would invert cargo's ergonomics.Does
bynk teststop shelling? It is the only verb that still shellsbynkc(bynk/src/cli.rs:192); everything else links in-process since slice 7. That single exception is what keeps the resolution ladder, theSkewmachinery (bynk/src/compiler.rs), and the "Whichbynkc?" section (bynk-cli.md:302) user-visible. If the front door is the front door, that question should never reach a human andBYNK_BYNKCbecomes an internal escape hatch. Note vscode-bynk compiler resolution is narrower than the bynk driver's — a driver-first install breaks the Test Explorer #486 already reports the resolution split biting the VS Code Test Explorer.The on-ramp inversion, and what it costs.
site/src/content/docs/book/introduction/install.md:22sayscargo install --path bynkc, then invokesbynk doctoreleven lines later — a binary it never told you to install — and lists "bynkcexposes four commands" as the toolchain surface.book/tutorials/01-first-program.mdx:44teachesbynkc compile demo.bynk --output demo.tsin the body withbynk newin a blockquote aside./docs/cli/isbynkc's page;bynkgot the qualified/docs/bynk-cli/. Inverting the URLs is not free — the doc drift guards hardcode page paths, so it is a Rust-test change too.What does
[build]contain, and does it subsumeout? Candidates:target,platform,emit,out.[paths] outalready exists and is inert; a[build] outwould either move it (breaking) or shadow it (confusing). Interacts with N:M context→Worker deployment grouping in bynk.toml #551 (N:M context→Worker grouping inbynk.toml), which proposes its own manifest surface.Candidate slice decomposition
Deliberately additive-first: every slice up to the last only adds a reader at the top, so nothing below changes and each is shippable alone. The single subtractive slice lands last, once nothing depends on the old path.
bynkalready hastomlas a dependency (bynk/Cargo.toml), so slice 0 needs no refactor beneath it.bynk build. The missing verb, no required flags. Reads[build]/outinbynk, passes explicitCompileOptionsdown —CompileOptions::split(root, paths)(bynk-emit/src/project.rs:328) already takes them, so nothing below moves. Makesoutlive. Ships alone; the first brick of the boundary.build/test/dev/deploy. Makes--inspect's "compiles a debug build" stop being a footnote.bynk-manifestleaf. One parser, consumed bybynkandbynk-lsp, not bybynkc. Honours[fmt]from the CLI (fixing the format-on-save divergence) and gives Track: LSP foundations — the project model, the freshness contract, and the lifecycle under the shipped surface #640's gap 1 its structural fix. Gated on Q1 + Q2.bynkcis told. The subtractive one: removeread_project_pathsfrombynk-emit, makebynkctake explicit roots. Gated on Q3.bynk checkprints nothing on success today; the cargoCompiling/Finished … in 0.5scadence has more to say here, not less, because builds and deploys are per-context.Slice status
bynk buildbynk-manifestleafbynkcis told (subtractive)Front-loaded ADR candidates
Numbers taken at merge, not here.
bynk.toml, which sections are the driver's private input versus a tool's own settings, and the crate-graph edge that enforces it. Hard to reverse: oncebynk-lspandbynkdepend on a parser, moving it again is a breaking change across three published crates. Must be settled jointly with Track: LSP foundations — the project model, the freshness contract, and the lifecycle under the shipped surface #640.bynk/bynkccontract — "bynkinfers,bynkcis told", superseding ADR 0083's verb-list definition of compiler purity, and recording that verb overlap is not the boundary (thecargo fmt/rustfmtprecedent). This is the ADR that stops the drift recurring, and it should land with or before slice 0 — otherwise slice 0 is just a sixth verb.[build]is documented.Threat model
None, because the security/safety trigger is not ticked. This track is driver-side and adds no language surface; it creates no resources, handles no credentials, and touches nothing outward-facing.
deploy's irreversible side effects stay behind their own gate under track #558, and this track does not change them. Slice 3 is breaking (a CLI contract change) but not unsafe; slice 2's[fmt]fix will reformat files in projects that set the section, which is a migration note rather than a threat.