Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
Binary size checks passed✅ 7 passed
Generated by |
Closes B-1145.
The codebase, the concept docs, the agent skill, and every runtime crate call
the generated client libraries **bridges**. The CLI never said the word:
`baml generate` was a bare top-level verb with nowhere to hang related
operations. Three things land together because they share one mechanism.
**1. `baml bridge`.**
baml bridge generate write each configured bridge
baml bridge generate --check exit non-zero if one is stale; writes nothing
baml bridge add <TARGET> write [generator.<name>], then print how to install it
baml bridge install print (never run) the pinned install command
baml bridge list configured bridges, output dirs, freshness
`baml generate` stays registered but no longer generates: it exits non-zero
naming its replacement (`baml generate add` names `baml bridge add`
specifically). It swallows every old argument so the message is what people
see, rather than clap's bare "unrecognized subcommand" — which would leave
someone with a broken CI job and no pointer to the fix.
The rename is not contained to the CLI. Sixteen in-repo callers invoked
`baml generate` — two GitHub workflows, the integ-test runners,
`scripts/smoke-go-release.py`, `tools/bump-version.py`, the C# consumer
`verify.sh` scripts, and several READMEs — and all now call `baml bridge
generate`. The Python generator banner told users to re-run `baml generate`
in every emitted file; it names the new command too.
**2. The install command is discoverable.** A bridge runtime must match the
CLI's `CANONICAL_VERSION` exactly, but that mapping lived in four
unsynchronized places and none of them printed "run exactly this".
`bridge/install.rs` is now the single table: package identity, registry
version spelling, peer packages, and ranked install candidates with the
lockfile evidence that chooses one. A contract test compares it against
`scripts/baml-language-version` across release *and* nightly versions, so a
release-time rename fails the build instead of printing an uninstallable
command.
It prints and never executes. The correct host-manifest edit is unknowable
(uv vs poetry vs pdm vs pipenv vs pip; pnpm vs yarn vs bun vs npm) and on its
own is never sufficient, because it must be followed by a lock-resolving step.
A manifest edited without its lockfile fails at import time and in CI, which
is worse than printing a line. There is no `update` verb: the version is
pinned exactly, so install and upgrade are the same string.
Peer packages are why this needs a table. The Python runtime cannot depend on
`pydantic` (that would pick v1 or v2 for the user), but generated code emits
`import pydantic`, so installing the bridge alone leaves a project that fails
at import. The requirement travels in the printed command.
**3. Staleness compares inputs, never regenerated output.** The manifest
already written into every bridge gains an input fingerprint, the toolchain
version, and the generator name. The fingerprint is a length-framed sha256
over `baml.toml`'s verbatim bytes plus every `.baml` file keyed by its
`/`-separated relative path.
Two consequences, both the point. Codegen determinism stops mattering: a
`gofmt` upgrade that reflows generated code leaves the inputs untouched, where
a `git diff` guard would false-positive. And no compile is needed, so
`--check` is viable in a pre-commit hook and the passive check stays off the
`baml check` latency budget — measured warm on an 800-file project, `baml
check` costs 1087 ms with a configured bridge vs 1117 ms without, and `baml
fmt` 582 ms vs 571 ms.
The fingerprint is taken from the `ResolvedProject` the compiler just loaded,
not re-derived from the settings root. Re-deriving names a different file set
whenever `--project` points at a source tree that is not `root/baml_src`,
which reported the bridge out of date immediately after generating it, with no
way to converge.
Excluded from the hash: generator config (already inside the hashed
`baml.toml`, embedded verbatim into every bridge, so whole-file hashing is
exact rather than over-broad); `output_dir`, which changes where a bridge
lands and not what is in it; and the toolchain version, stored as its own
field so skew reports "built by BAML 0.14.2 but this toolchain is 0.15.0"
rather than an opaque hash mismatch.
One comparison function serves `--check`, `bridge list`, and the passive
warning, so they cannot disagree about what stale means. They differ only in
depth: the manifest check is one small file read; the verify-files check also
re-hashes every recorded file, turning "assume the generated files are
untouched" into a verified fact.
A bridge is fresh or it is stale — there is no third answer. A tree that
records no provenance is one we cannot vouch for, so it reports stale rather
than "passed, but I could not verify", which is the weakest thing a CI gate
can say. Schema 2 stays readable for ownership so the first regenerate after
upgrading does not fail on an existing bridge; the schema-1 string inventories
are gone.
The passive warning follows `skill_check.rs`: an RAII guard working on a
background thread and printing in `Drop`, whitelisted to
check/run/test/pack/fmt/describe. `playground` and `lsp` are excluded as
long-running editor surfaces; `bridge` excludes itself. It also reports a
configured-but-absent bridge, which is the fresh-clone case — the default
`vcs = "ignore"` keeps the tree out of the repo, so a clone has no bridge at
all. A newly initialized project is still never nagged, because `baml init`
writes its generator lines commented out and the no-generators check returns
first. No env-var opt-out; if one is wanted it belongs in `baml.toml`.
`--check` exits 6 (`ExitCode::BridgeStale`) rather than the generic 4, so CI
can tell a stale bridge from a project that does not compile.
`bridge list` and `--check` count `[generator]` sections that failed
validation and say so, instead of silently showing fewer bridges than are
configured. Full spanned diagnostics stay with `bridge generate`, which fails
on them.
Adds `[bridge] vcs = "ignore" | "commit"`, defaulting to today's behavior. In
`commit` mode the writer emits a header-only `.gitignore` so the tree and its
manifest are both tracked — which is what lets `--check` run from a clean
checkout, the case that motivated this work.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018Jm6FCeSHxhtM2S6oTQR5B
Closes B-1145.
The codebase, the concept docs, the agent skill, and every runtime crate call the generated client libraries bridges. The CLI never said the word:
baml generatewas a bare top-level verb with nowhere to hang related operations. Three things land together because they share one mechanism.1.
baml bridgebaml bridge generatebaml generatedoes todaybaml bridge generate --checkbaml bridge add <TARGET>[generator.<name>], then print how to install itbaml bridge installbaml bridge listbaml generateandbaml generate addkeep working as hidden aliases; they appear in existing scripts and CI jobs.2. The install command is discoverable
A bridge runtime must match the CLI's
CANONICAL_VERSIONexactly, but that mapping lived in four unsynchronized places and none printed "run exactly this".bridge/install.rsis now the single table: package identity, registry version spelling, peer packages, and ranked install candidates with the lockfile evidence that chooses one. A contract test compares it againstscripts/baml-language-versionacross release and nightly versions (mutation-checked), so a release-time rename fails the build instead of printing an uninstallable command.It prints and never executes. The correct host-manifest edit is unknowable (uv vs poetry vs pdm vs pipenv vs pip; pnpm vs yarn vs bun vs npm) and on its own is never sufficient, because it must be followed by a lock-resolving step. A manifest edited without its lockfile fails at import time and in CI, which is worse than printing a line. No
updateverb: the version is pinned exactly, so install and upgrade are the same string.Peer packages are why this needs a table. The Python runtime cannot depend on
pydantic(that would pick v1 or v2 for the user), but generated code emitsimport pydantic— so installing the bridge alone leaves a project that fails at import. The requirement travels in the printed command.3. Staleness compares inputs, never regenerated output
The manifest already written into every bridge goes to schema 3, carrying an input fingerprint, the toolchain version, and the generator name. The fingerprint is a length-framed sha256 over
baml.toml's verbatim bytes plus every.bamlfile keyed by its/-separated relative path.Two consequences, both the point:
gofmtupgrade that reflows generated code leaves the inputs untouched, where agit diffguard would false-positive. (Go codegen genuinely is not byte-deterministic — it shells out to the hostgofmt.)--checkis viable in a pre-commit hook and the passive check stays off thebaml checklatency budget. Measured warm, 12 runs: 745 ms with a configured bridge vs 734 ms without.Excluded from the hash: generator config (already inside the hashed
baml.toml, embedded verbatim into every bridge, so whole-file hashing is exact rather than over-broad);output_dir, which changes where a bridge lands and not what is in it; and the toolchain version, stored as its own field so skew reports "built by BAML 0.14.2 but this toolchain is 0.15.0" rather than an opaque hash mismatch.One comparison function serves
--check,bridge list, and the passive warning, so they cannot disagree about what stale means. They differ only in depth: the manifest check is one small file read; the verify-files check also re-hashes every recorded file, turning "assume the generated files are untouched" into a verified fact.Every added manifest field is optional, so a schema-2 manifest still loads and reports provenance unknown — treated as cannot prove stale, never as stale, or every project that hadn't regenerated since upgrading would start failing CI. Likewise "never generated" is not "out of date":
--checkreports it, the passive warning does not, so a fresh project isn't nagged before it opts in.The passive warning follows
skill_check.rs: an RAII guard working on a background thread and printing inDrop, whitelisted to check/run/test/pack/fmt/describe.playgroundandlspare excluded as long-running editor surfaces;bridgeexcludes itself. No env-var opt-out; if one is wanted it belongs inbaml.toml.--checkexits 6 (ExitCode::BridgeStale) rather than the generic 4, so CI can tell a stale bridge from a project that doesn't compile.Adds
[bridge] vcs = "ignore" | "commit", defaulting to today's behavior. Incommitmode the writer emits a header-only.gitignoreso the tree and its manifest are both tracked — which is what lets--checkrun from a clean checkout, the case that motivated this work.Verification
571 tests pass (503 lib + 8 new bridge e2e + existing e2e suites); fmt and clippy clean. Manual pass in a scratch project confirmed each transition: never-generated → exit 6, generate → exit 0, source edit → passive warning on
baml check+ exit 6, hand-edited generated file → caught at verify depth only, regenerate → quiet.Third of three stacked PRs. Based on #4393 — review #4392 and #4393 first.
🤖 Generated with Claude Code
https://claude.ai/code/session_018Jm6FCeSHxhtM2S6oTQR5B