Skip to main content

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}