Skip to main content

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};