warble/lib.rs
1//! Warble front-end compiler (`warble`): parses a Warble project (profile + components + context
2//! binding), merges component defaults with profile overrides, runs the loud-fail compile checks,
3//! and emits the language-neutral IR JSON that any back-end dispatcher consumes.
4//!
5//! # Architecture
6//!
7//! ```text
8//! profile + components + context ──► warble compile ──► IR JSON ──► warble dispatch ──► native agent
9//! (declarative YAML + prompts) (front-end, Rust) (the seam) (per-target back-end)
10//! ```
11//!
12//! This crate is the first arrow: it owns the authoring types ([`ProfileFile`], [`ComponentFile`],
13//! and friends), [`ContextLoader`] (the host's semantic-layer probe), and [`compile`], which
14//! resolves a parsed project into IR. Everything downstream of the IR — the `claude-code-cli` and
15//! `vercel` back-ends (both fold into the `warble` binary), plus the separate TS
16//! `claude-agent-sdk` back-end — depends only on the IR schema, never on this crate's Rust types.
17//! See [`authoring.md`][spec-authoring] for the authoring-side contract and
18//! [`ir-schema.md`][spec-ir] for the emitted shape.
19//!
20//! # Sans-IO
21//!
22//! This crate performs **no filesystem or network access**. [`compile`] takes already-parsed
23//! authoring values, the raw step markdown, and a [`ContextLoader`] the caller has already built
24//! from the bound semantic layer — it never reads a path itself. The example below is exactly
25//! this: it deserializes YAML strings in-memory and calls [`compile`] directly, no disk involved.
26//! Keeping the core pure is what lets the same crate target native, WASM, and language bindings
27//! unchanged; the native host adapter lives in the `warble-cli` crate.
28//!
29//! # Example
30//!
31//! A minimal project — one component, one step — compiled against an empty in-memory
32//! [`ContextLoader`]. Both the profile and the component are parsed from YAML exactly as a host
33//! would read them off disk; only the reading itself is out of scope for this sans-IO crate.
34//!
35//! ```
36//! # use std::collections::HashMap;
37//! # use warble::{ContextLoader, DimensionInfo, LineageGraph, MetricInfo, ModelInfo};
38//! # struct EmptyContext(LineageGraph);
39//! # impl ContextLoader for EmptyContext {
40//! # fn is_parseable(&self) -> bool { true }
41//! # fn metrics(&self) -> &[MetricInfo] { &[] }
42//! # fn dimensions(&self) -> &[DimensionInfo] { &[] }
43//! # fn time_dimensions(&self) -> &[DimensionInfo] { &[] }
44//! # fn models(&self) -> &[ModelInfo] { &[] }
45//! # fn lineage(&self) -> &LineageGraph { &self.0 }
46//! # }
47//! use warble::{compile, BindingFile, ComponentFile, ProfileFile, SlotContents};
48//!
49//! let profile: ProfileFile = serde_yaml::from_str(concat!(
50//! "profile: demo\n",
51//! "context:\n",
52//! " project: ./context/binding.yml\n",
53//! "components:\n",
54//! " - use: hello\n",
55//! ))?;
56//! let binding: BindingFile = serde_yaml::from_str("kind: external\nproject: ./warehouse\n")?;
57//! let component: ComponentFile = serde_yaml::from_str(concat!(
58//! "id: hello\n",
59//! "verb: greet\n",
60//! "type: analytical\n",
61//! "realization_kind: skill\n",
62//! "binding_mode: runtime_selected\n",
63//! "llm_steps:\n",
64//! " - name: say_hello\n",
65//! " tier: cheap\n",
66//! " prompt_ref: steps/say_hello.md\n",
67//! "trigger:\n",
68//! " kind: one_shot\n",
69//! "guardrails:\n",
70//! " - name: read_only_execution\n",
71//! " locked: true\n",
72//! "effect:\n",
73//! " render_blocks: []\n",
74//! " outcome:\n",
75//! " kind: none\n",
76//! ))?;
77//!
78//! let mut components = HashMap::new();
79//! components.insert(component.id.clone(), component);
80//! let mut steps = HashMap::new();
81//! steps.insert("say_hello".to_string(), "Say hello to {{project_name}}.".to_string());
82//! let mut step_contents = HashMap::new();
83//! step_contents.insert("hello".to_string(), steps);
84//!
85//! let ir = compile(
86//! &profile,
87//! &components,
88//! &binding.project,
89//! &EmptyContext(LineageGraph::default()),
90//! &step_contents,
91//! &SlotContents::default(),
92//! )?;
93//! assert_eq!(ir["warble_ir_version"], "0.8");
94//! # Ok::<(), Box<dyn std::error::Error>>(())
95//! ```
96//!
97//! # Invariants
98//!
99//! These hold across the whole workspace, not just this crate — they matter to you as an API
100//! consumer too, not only to contributors:
101//!
102//! 1. **If you write your own back-end that consumes the IR `compile` returns, drive it off the
103//! three enum fields** — `realization_kind`, `outcome.kind`, `trigger.kind` — never off a
104//! component's `id` or `verb` string. Any enum value your back-end can't handle must be a hard
105//! error, not a best-effort guess or a silent no-op.
106//! 2. **Depending on `warble` alone pulls in no I/O and no `wren` dependency.** `compile` never
107//! touches a filesystem or network — you build the [`ContextLoader`] yourself and hand it in —
108//! and this crate has no dependency on any semantic-format library. That's what lets you embed
109//! it in a native binary, WASM, or another language's bindings without inheriting a dependency
110//! graph you didn't ask for. Nothing in this workspace links such a library any more: a host
111//! reads its own format and hands the projection over as a prepared-context document.
112//! 3. **No DSL in the composition layer** — there's no boolean algebra, expression language, or
113//! imperative logic in the profile/IR schema. The one form of conditionality that does exist
114//! is a closed vocabulary: a step can set `conditional: true` with a `when` guard whose
115//! `guard` is one of `on_failure` / `on_flag` / `on_missing` and whose `target` names what
116//! it's checking — never an arbitrary predicate. If you're writing a dispatcher, that means
117//! you must either realize `when` or loud-fail on it, not silently treat it as a no-op. IR
118//! growth is additive (a new optional field), never a new mechanism.
119//! 4. **The IR `compile` emits never names a specific mechanism** (no `cron`, `slack`,
120//! `subagent`, …). If you're writing a dispatcher, those names only show up later, at the
121//! capability layer via `realize-via` — so the same IR is valid input to any runtime's
122//! back-end, including ones this crate has never heard of. See
123//! [`capability-model.md`][spec-cap].
124//! 5. **This crate builds exactly one native capability: `blast_radius`** (semantic lineage — see
125//! [`blast-radius.md`][spec-blast]). If you're checking what a target back-end can support
126//! natively versus needs to borrow from its runtime, know that everything else — human
127//! approval, VCS/rollback, scheduling, subagent dispatch, schema introspection — is expected
128//! to come from the runtime (realize-via), not from `core`.
129//!
130//! [spec-authoring]: https://github.com/Canner/Warble/blob/main/docs/spec/authoring.md
131//! [spec-ir]: https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md
132//! [spec-cap]: https://github.com/Canner/Warble/blob/main/docs/spec/capability-model.md
133//! [spec-blast]: https://github.com/Canner/Warble/blob/main/docs/spec/blast-radius.md
134
135mod compile;
136mod context;
137mod error;
138mod model;
139mod raw_source;
140
141pub use compile::{compile, verify_context_preconditions};
142pub use context::{
143 prepared_document_from, Additivity, ContextLoader, DimensionInfo, ExternalContext,
144 HostAnalysis, HostConsumers, HostImpact, LineageEdge, LineageGraph, LineageKind, LineageNode,
145 MetricInfo, ModelInfo, PreparedContext, PreparedContextError, RankedSeverity,
146 PREPARED_CONTEXT_VERSION,
147};
148pub use error::CompileError;
149pub use model::{
150 AssetDecl, BindingFile, ComponentFile, Effect, Guardrail, GuardrailPatch, LlmStep, Outcome,
151 Param, ProfileComponentMount, ProfileConfig, ProfileContext, ProfileFile, RenderBlock,
152 SlotContents, SlotDecl, Trigger,
153};
154#[cfg(not(target_arch = "wasm32"))]
155pub use raw_source::read_raw_dir;
156pub use raw_source::{RawSourceContext, RawSources};