waggle_lens_code/lib.rs
1//! # waggle-lens-code — the symbol lens (design doc `20`)
2//!
3//! Source-code structure, computed **once at mint** where the artifact is
4//! at hand, stored content-addressed beside the snapshot, served
5//! everywhere as plain data. This crate is the extraction half:
6//! tree-sitter parses the snapshot, each grammar's `tags.scm` query marks
7//! the definitions, and the result is a flat, compact [`SymbolOutline`] —
8//! never an AST, never an index, never retained.
9//!
10//! The performance discipline (doc `20 §4`) is structural here:
11//! compiled queries are cached once per process ([`std::sync::OnceLock`]),
12//! parsers are reused per thread, the outline is an arena (one `Vec`, one
13//! shared name buffer, parent links by index), and the parse tree drops
14//! before extraction returns. `extract` is a pure function of its inputs —
15//! no clock, no I/O — so the sans-I/O law holds even though this crate
16//! lives outside `waggle-core` (it must: grammars are C, and this crate is
17//! **never compiled to wasm**; the edge serves precomputed outlines).
18
19mod extract;
20mod outline;
21
22pub use extract::extract;
23pub use outline::{SymbolOutline, WIRE_VERSION};
24
25/// The languages the v1 lens ships (doc `20 §9`): chosen by agent
26/// traffic; each addition pays its binary-size cost consciously.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Lang {
29 /// Rust (`.rs`).
30 Rust,
31 /// Python (`.py`, `.pyi`).
32 Python,
33 /// TypeScript (`.ts`, `.mts`, `.cts`).
34 TypeScript,
35 /// TSX (`.tsx`).
36 Tsx,
37 /// JavaScript (`.js`, `.mjs`, `.cjs`, `.jsx`).
38 JavaScript,
39 /// Go (`.go`).
40 Go,
41}
42
43/// Detect a supported language from a path — extension first, then the
44/// well-known extension-less basenames have no grammar here (they are
45/// text-sniffed upstream); a `None` simply means "no outline", never an
46/// error: the lens degrades to the plain text loop (doc `20 §9`).
47#[must_use]
48pub fn detect(path: &str) -> Option<Lang> {
49 let basename = path.rsplit(['/', '\\']).next().unwrap_or(path);
50 let ext = basename.rsplit_once('.').map(|(_, e)| e)?;
51 match ext.to_ascii_lowercase().as_str() {
52 "rs" => Some(Lang::Rust),
53 "py" | "pyi" => Some(Lang::Python),
54 "ts" | "mts" | "cts" => Some(Lang::TypeScript),
55 "tsx" => Some(Lang::Tsx),
56 "js" | "mjs" | "cjs" | "jsx" => Some(Lang::JavaScript),
57 "go" => Some(Lang::Go),
58 _ => None,
59 }
60}
61
62/// The outline blob's content type (doc `20 §3`).
63pub const OUTLINE_CONTENT_TYPE: &str = "application/waggle-outline+json";
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn detection_is_extension_driven_and_total() {
71 assert_eq!(detect("crates/waggle-core/src/mint.rs"), Some(Lang::Rust));
72 assert_eq!(detect("a/b/app.test.TSX"), Some(Lang::Tsx));
73 assert_eq!(detect("script.mjs"), Some(Lang::JavaScript));
74 assert_eq!(detect("cmd/main.go"), Some(Lang::Go));
75 assert_eq!(detect("README.md"), None, "no grammar is not an error");
76 assert_eq!(detect("Makefile"), None, "extension-less: text loop only");
77 assert_eq!(detect(".hidden"), None);
78 }
79}