lattice_plugin_api/lib.rs
1//! The plugin-API catalog: the `wit/` package parsed at build time into typed
2//! data, and the reference, JSON and agent bundle rendered from it (PI.1).
3//!
4//! The `wit/` package at the workspace root IS the canonical plugin API
5//! (plugin-host.md §5). This crate exposes a [`PluginApiCatalog`] *derived from
6//! that WIT at build time* (`build.rs` → `wit-parser` → `$OUT_DIR/catalog.rs`),
7//! so the catalog can never drift from the interface it documents. It answers
8//! the "what CAN a plugin do" facet of the introspection layer (design §5.11);
9//! `:describe-plugin-api` / `:list-plugin-apis` / `:apropos` (PI.2) render it,
10//! and plugin authors export it (JSON/markdown).
11//!
12//! This crate is deliberately **wasmtime-free** — its only build input is the
13//! WIT text and its only runtime dep is `std`. `lattice-host` can therefore dep
14//! it for the introspection ex-commands WITHOUT pulling the WASM runtime into
15//! the host, keeping the no-per-frame-WASM invariant (plugin-host.md PH7.5).
16//!
17//! Two things the catalog carries that the parser can't infer:
18//! - **direction** — world-derived (does a guest *export* the interface, i.e.
19//! implement it, or *import* it, i.e. call into the host); a descriptive
20//! hint, since `use`-for-types also registers an import edge.
21//! - **capability** — a host-authored annotation the WIT can't express (which
22//! OS capability a seam requires); see [`CAPABILITY_ANNOTATIONS`]. Every
23//! parsed interface MUST have an entry (enforced by a test), so a new WIT
24//! interface forces a deliberate capability decision before it ships.
25
26#![warn(missing_docs)]
27
28use std::sync::OnceLock;
29
30/// The whole plugin-API surface, derived from `wit/`.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct PluginApiCatalog {
33 /// Every named interface in the package, sorted by name.
34 pub interfaces: Vec<ApiInterface>,
35 /// Every plugin world (the test-only `trampoline-fixture` excluded), sorted
36 /// by name.
37 pub worlds: Vec<ApiWorld>,
38}
39
40/// One WIT interface — a namespace of functions a plugin implements or calls.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ApiInterface {
43 /// Kebab-case interface name, e.g. `host-services`.
44 pub name: String,
45 /// The interface's `///` doc comment, if any.
46 pub doc: Option<String>,
47 /// World-derived direction relative to a guest plugin.
48 pub direction: Direction,
49 /// Host-authored capability requirement (the WIT can't carry it).
50 pub capability: Capability,
51 /// The interface's functions, sorted by name.
52 pub functions: Vec<ApiFunction>,
53 /// The types this interface DEFINES, in WIT source order (authors order a
54 /// seam's types so each is read after what it depends on).
55 pub types: Vec<ApiType>,
56 /// The types this interface pulls in from another with `use`, in source
57 /// order. Listed apart from [`types`](Self::types) so a reference links to
58 /// the definition instead of repeating it.
59 pub uses: Vec<ApiUse>,
60}
61
62/// One function within an interface.
63///
64/// Resource methods are functions too: WIT names them `[method]<resource>.<name>`
65/// (`[static]…`, `[constructor]<resource>` likewise) and [`kind`](Self::kind)
66/// says which resource they belong to.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct ApiFunction {
69 /// The WIT function name, e.g. `walk` or `[method]document.line`.
70 pub name: String,
71 /// The function's `///` doc comment, if any.
72 pub doc: Option<String>,
73 /// Freestanding, or which resource it is a method / static / constructor of.
74 pub kind: ApiFunctionKind,
75 /// Declared `async` in the WIT.
76 pub is_async: bool,
77 /// Parameters in order. A method's first parameter is `self`.
78 pub params: Vec<ApiParam>,
79 /// The result type in WIT syntax, or `None` for a function returning nothing.
80 pub result: Option<String>,
81}
82
83impl ApiFunction {
84 /// The name a reader calls it by: `walk`, `document.line`,
85 /// `document.new` for a constructor. Strips WIT's `[method]` /
86 /// `[static]` / `[constructor]` mangling.
87 pub fn display_name(&self) -> String {
88 match &self.kind {
89 ApiFunctionKind::Freestanding => self.name.clone(),
90 ApiFunctionKind::Constructor(r) => format!("{r}.new"),
91 ApiFunctionKind::Method(_) | ApiFunctionKind::Static(_) => self
92 .name
93 .split_once(']')
94 .map(|(_, rest)| rest.to_string())
95 .unwrap_or_else(|| self.name.clone()),
96 }
97 }
98
99 /// The function's signature in WIT syntax, as it would be declared inside
100 /// its interface (or its resource block): `walk: func(opts: walk-options)
101 /// -> result<list<string>, string>`. A method's implicit `self` is omitted,
102 /// as WIT source omits it.
103 pub fn signature(&self) -> String {
104 let params = |skip: usize| {
105 self.params
106 .iter()
107 .skip(skip)
108 .map(|p| format!("{}: {}", p.name, p.ty))
109 .collect::<Vec<_>>()
110 .join(", ")
111 };
112 let result = self
113 .result
114 .as_ref()
115 .map(|r| format!(" -> {r}"))
116 .unwrap_or_default();
117 let asyncness = if self.is_async { "async " } else { "" };
118 let short = self.display_name();
119 let short = short.rsplit('.').next().unwrap_or(&self.name);
120 match &self.kind {
121 ApiFunctionKind::Freestanding => {
122 format!("{}: {asyncness}func({}){result}", self.name, params(0))
123 }
124 ApiFunctionKind::Method(_) => {
125 format!("{short}: {asyncness}func({}){result}", params(1))
126 }
127 ApiFunctionKind::Static(_) => {
128 format!("{short}: static {asyncness}func({}){result}", params(0))
129 }
130 // A constructor's declared result is the resource itself; WIT
131 // source does not spell it.
132 ApiFunctionKind::Constructor(_) => format!("constructor({})", params(0)),
133 }
134 }
135}
136
137/// Whether a function stands alone or belongs to a resource.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum ApiFunctionKind {
140 /// A plain interface function.
141 Freestanding,
142 /// A method on the named resource (first parameter is `self`).
143 Method(String),
144 /// A static function on the named resource.
145 Static(String),
146 /// The named resource's constructor.
147 Constructor(String),
148}
149
150/// One function parameter.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ApiParam {
153 /// Parameter name as declared.
154 pub name: String,
155 /// Parameter type in WIT syntax.
156 pub ty: String,
157}
158
159/// One type an interface defines.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct ApiType {
162 /// Kebab-case type name, e.g. `raw-candidate`.
163 pub name: String,
164 /// The type's `///` doc comment, if any.
165 pub doc: Option<String>,
166 /// What kind of type it is, with its fields or cases.
167 pub kind: ApiTypeKind,
168}
169
170/// The shape of a type definition.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub enum ApiTypeKind {
173 /// A `record`: every field, in order, each with a type.
174 Record(Vec<ApiMember>),
175 /// A `variant`: every case, in order; a case may carry a payload type.
176 Variant(Vec<ApiMember>),
177 /// An `enum`: every case, in order (no payloads).
178 Enum(Vec<ApiMember>),
179 /// A `flags`: every flag, in order.
180 Flags(Vec<ApiMember>),
181 /// A `resource`: a host- or guest-owned handle. Its methods are the
182 /// interface's functions whose [`ApiFunction::kind`] names it.
183 Resource,
184 /// `type name = <expr>`: the aliased type in WIT syntax.
185 Alias(String),
186}
187
188impl ApiTypeKind {
189 /// The WIT keyword for this kind: `record`, `variant`, `enum`, `flags`,
190 /// `resource`, or `type` for an alias.
191 pub fn keyword(&self) -> &'static str {
192 match self {
193 ApiTypeKind::Record(_) => "record",
194 ApiTypeKind::Variant(_) => "variant",
195 ApiTypeKind::Enum(_) => "enum",
196 ApiTypeKind::Flags(_) => "flags",
197 ApiTypeKind::Resource => "resource",
198 ApiTypeKind::Alias(_) => "type",
199 }
200 }
201
202 /// The fields / cases / flags, empty for a resource or alias.
203 pub fn members(&self) -> &[ApiMember] {
204 match self {
205 ApiTypeKind::Record(m)
206 | ApiTypeKind::Variant(m)
207 | ApiTypeKind::Enum(m)
208 | ApiTypeKind::Flags(m) => m,
209 ApiTypeKind::Resource | ApiTypeKind::Alias(_) => &[],
210 }
211 }
212}
213
214/// A record field, variant case, enum case or flag.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct ApiMember {
217 /// Kebab-case member name.
218 pub name: String,
219 /// The member's type in WIT syntax: always present for a record field,
220 /// the payload for a variant case that has one, `None` otherwise.
221 pub ty: Option<String>,
222 /// The member's `///` doc comment, if any.
223 pub doc: Option<String>,
224}
225
226/// A type an interface imports from another with `use`.
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct ApiUse {
229 /// The name it is known by in this interface (after any `as` rename).
230 pub name: String,
231 /// The interface that defines it.
232 pub from: String,
233 /// Its name in the defining interface.
234 pub original: String,
235}
236
237/// One WIT world — a bundle of imported/exported interfaces a component targets.
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct ApiWorld {
240 /// Kebab-case world name, e.g. `picker-source-plugin`.
241 pub name: String,
242 /// The world's `///` doc comment, if any.
243 pub doc: Option<String>,
244 /// Interface names the world imports (guest → host), sorted.
245 pub imports: Vec<String>,
246 /// Interface names the world exports (guest implements), sorted.
247 pub exports: Vec<String>,
248 /// Freestanding functions the world exports — the guest's entry points
249 /// (`register-grammar`, `register-picker-sources`, …), in WIT source
250 /// order. Not part of any interface, so they appear nowhere else in the
251 /// catalog.
252 pub export_functions: Vec<ApiFunction>,
253 /// Freestanding functions the world imports from the host, in source
254 /// order.
255 pub import_functions: Vec<ApiFunction>,
256}
257
258/// World-derived direction of an interface relative to a guest plugin.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum Direction {
261 /// Guest *implements* it (a world exports it): `grammar`, `picker-source`, …
262 GuestExport,
263 /// Guest *calls into the host* through it (a world imports it): `host-services`.
264 GuestImport,
265 /// Both an export and an import edge exist across worlds.
266 Both,
267 /// Neither — a shared type bag (`types`) or a still-stub interface,
268 /// referenced only via `use` for its types.
269 TypesOnly,
270}
271
272/// A host-authored capability annotation the WIT can't itself carry.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum Capability {
275 /// Filesystem access (e.g. `host-services::walk`).
276 Fs,
277 /// Network access.
278 Net,
279 /// Subprocess spawn.
280 Proc,
281 /// No OS capability — a pure data / dispatch seam.
282 None,
283}
284
285/// The host-authored capability annotation, one row per WIT interface.
286///
287/// A test asserts this covers EVERY parsed interface, so adding a WIT interface
288/// without a deliberate capability decision fails the build's test gate. Most
289/// seams are pure data/dispatch (`None`). Three reach the OS: `host-services`
290/// (`Fs` — its `walk`), `project` (`Fs` — resolution walks on a cache miss),
291/// and `plugin-manager` (`Proc` — the host builds what the guest declared; see
292/// the row's own comment for why one variant cannot describe it).
293pub const CAPABILITY_ANNOTATIONS: &[(&str, Capability)] = &[
294 // OM.A1. `None` and not `Fs`, which is the interesting part: the host
295 // walks the project and reads every file, and hands the guest `text`. The
296 // guest touches no filesystem — no preopens, no `walk` — so the capability
297 // this seam requires *of a plugin* is nothing. Keeping the read host-side
298 // is what makes that true, and annotating it `Fs` would misreport a
299 // deliberate design property as a permission.
300 ("scanned-excerpt-source", Capability::None),
301 // The three REGISTRY-shaped seams, `None` for `scanned-excerpt-source`'s
302 // reason immediately above and not by default: in each the host owns the
303 // mechanism — the walk, the view, the picker — and the guest declares what
304 // to make of it or receives what the host already read. The capability
305 // belongs to the host's I/O, not to the seam that describes it. A seam
306 // where the GUEST reaches the filesystem would be annotated `Fs` here even
307 // though its WIT looks the same, which is why this table is hand-written.
308 ("multibuffer-view-registry", Capability::None),
309 ("multibuffer-view-source", Capability::None),
310 ("picker-registry", Capability::None),
311 // MV.3: the host owns the view, the walk and every file read; a guest
312 // declares a view spec and receives excerpts. `None` for
313 // `scanned-excerpt-source`'s reason, immediately above — the capability
314 // belongs to the host's walk, not to the seam that describes what to make
315 // of it.
316 ("multibuffer-view-registry", Capability::None),
317 ("multibuffer-view-source", Capability::None),
318 ("buffer", Capability::None),
319 ("command", Capability::None),
320 ("completion-source", Capability::None),
321 ("config", Capability::None),
322 ("context", Capability::None),
323 ("dashboard", Capability::None),
324 ("decorations", Capability::None),
325 ("error-parser", Capability::None),
326 ("events", Capability::None),
327 ("grammar", Capability::None),
328 ("grammar-callbacks", Capability::None),
329 ("help", Capability::None),
330 ("host-services", Capability::Fs),
331 ("keymap", Capability::None),
332 // IM.6. Same shape as `scanned-excerpt-source` above: the guest names a file and
333 // never sends pixels, and the host resolves + reads it. `media.wit` calls
334 // that out as deliberate — "the `fs:read` capability decision stays with
335 // the HOST, which is what stops a plugin putting arbitrary bytes on screen
336 // regardless of its grant". So the seam demands nothing of the plugin.
337 ("media", Capability::None),
338 // LG.3c. The guest hands over grammar BYTES and query source; the host
339 // compiles and runs them. No OS reach: the grammar is wasm the host
340 // executes inside tree-sitter's own sandboxed store, not native code and
341 // not a file the guest names. Fetching a grammar from git is the plugin
342 // MANAGER's job (`Proc`/net, that row), not this seam's.
343 ("language", Capability::None),
344 ("logging", Capability::None),
345 ("modes", Capability::None),
346 ("picker-source", Capability::None),
347 // The `require` seam. The GUEST does nothing but declare a list; the HOST
348 // then clones or downloads the source (net), stages it into the user
349 // plugin root (fs), and runs cargo-component over it (proc). `Capability`
350 // is single-valued, so this row cannot say "all three" — it names the
351 // widest blast radius and the doc above says why. If a second seam ever
352 // needs a union, that is the point to make `Capability` a set rather than
353 // keep picking a representative.
354 ("plugin-manager", Capability::Proc),
355 // Resolution walks the filesystem on a cache miss. This is not a
356 // formality: `error-parser-plugin` deliberately does NOT import `project`
357 // for exactly this reason (see `wit/error-parser.wit`), because that world
358 // shares the sync linker with `grammar` and a directory walk one guest
359 // call from a keystroke is a paramount-#1 violation. Annotating this
360 // `None` would contradict the decision that WIT already records.
361 ("project", Capability::Fs),
362 // SG.3a. `None`: declaring a sign is pure data in one direction — the
363 // guest names a glyph, a fallback glyph, a theme element and a priority,
364 // and the host writes its OWN registry. No filesystem, no network, and the
365 // name is namespaced host-side so a guest cannot reach another plugin's
366 // signs or a native producer's.
367 ("signs", Capability::None),
368 ("theme", Capability::None),
369 // TR.2b. `None`: a keyed menu is pure data in both directions — the host
370 // projects where the menu was opened from, the guest answers rows naming
371 // commands. The guest touches no filesystem, no network, and cannot forge
372 // a `CommandId` (names are resolved host-side).
373 ("transient-source", Capability::None),
374 ("tree-sitter", Capability::None),
375 ("types", Capability::None),
376 ("ui", Capability::None),
377];
378
379/// The capability annotation for an interface, or `None` if unannotated (which
380/// the coverage test forbids for any parsed interface).
381pub fn capability_for(name: &str) -> Option<Capability> {
382 CAPABILITY_ANNOTATIONS
383 .iter()
384 .find(|(n, _)| *n == name)
385 .map(|(_, c)| *c)
386}
387
388// The parsed catalog data (`generated_interfaces()` / `generated_worlds()`),
389// emitted by build.rs from wit/. Private free functions in this module scope.
390include!(concat!(env!("OUT_DIR"), "/catalog.rs"));
391
392pub mod examples;
393pub mod json;
394pub mod render;
395
396/// The plugin-API catalog, derived from `wit/` at build time and merged with
397/// the host-authored capability annotation. Computed once, then cached.
398pub fn catalog() -> &'static PluginApiCatalog {
399 static CATALOG: OnceLock<PluginApiCatalog> = OnceLock::new();
400 CATALOG.get_or_init(|| {
401 let mut interfaces = generated_interfaces();
402 for iface in &mut interfaces {
403 iface.capability = capability_for(&iface.name).unwrap_or(Capability::None);
404 }
405 PluginApiCatalog {
406 interfaces,
407 worlds: generated_worlds(),
408 }
409 })
410}
411
412impl PluginApiCatalog {
413 /// The interface with this exact name, if present.
414 pub fn interface(&self, name: &str) -> Option<&ApiInterface> {
415 self.interfaces.iter().find(|i| i.name == name)
416 }
417
418 /// The world with this exact name, if present.
419 pub fn world(&self, name: &str) -> Option<&ApiWorld> {
420 self.worlds.iter().find(|w| w.name == name)
421 }
422
423 /// The interface that DEFINES the type `name`, with the definition.
424 /// Interfaces that merely `use` it are not answers.
425 pub fn type_def(&self, name: &str) -> Option<(&ApiInterface, &ApiType)> {
426 self.interfaces
427 .iter()
428 .find_map(|i| i.types.iter().find(|t| t.name == name).map(|t| (i, t)))
429 }
430}