Skip to main content

amql_engine/
lib.rs

1//! AQL engine: parse, index, query, and validate code annotations.
2//!
3//! All domain logic lives here. Surface crates (CLI, MCP server, WASM)
4//! are thin shims that call into this crate.
5
6#[cfg(all(feature = "resolver", feature = "fs"))]
7mod bench;
8#[cfg(feature = "fs")]
9mod code_cache;
10mod compression;
11#[cfg(all(feature = "resolver", feature = "fs"))]
12mod coverage;
13mod declare;
14mod diff;
15mod error;
16#[cfg(feature = "fs")]
17mod extractor;
18mod file_lock;
19#[cfg(feature = "fs")]
20mod init;
21mod manifest;
22mod matcher;
23pub mod meta;
24#[cfg(feature = "resolver")]
25mod navigate;
26mod paths;
27mod pattern;
28#[cfg(all(feature = "resolver", feature = "fs"))]
29mod pipeline;
30#[cfg(all(feature = "resolver", feature = "fs"))]
31mod query;
32mod query_options;
33mod repair;
34#[cfg(feature = "resolver")]
35mod resolver;
36mod selector;
37mod sidecar;
38#[cfg(all(feature = "resolver", feature = "fs"))]
39mod stats;
40mod store;
41mod sync;
42mod transact;
43mod types;
44mod validator;
45#[cfg(feature = "wasm")]
46mod index;
47mod xml;
48
49#[cfg(all(feature = "resolver", feature = "fs"))]
50pub use bench::{
51    auto_detect_cases, count_source_files, execute_bench_request, format_bench_table,
52    parse_bench_config, project_name, run_bench, AqlDef, BaselineDef, BenchCase, BenchConfig,
53    BenchRequest, BenchResponse, BenchResult, MeasuredOutput,
54};
55#[cfg(feature = "fs")]
56pub use code_cache::{glob_source_files, CodeCache};
57pub use compression::{measure_compression, CompressionResult, FileCompression, SourceEntry};
58#[cfg(all(feature = "resolver", feature = "fs"))]
59pub use coverage::{
60    diff_file_coverage, file_coverage, file_summary, spot_check_bindings, FileCoverage,
61    FileDiffCoverage, FileSummary, SpotCheckFailure, SpotCheckResult,
62};
63pub use declare::{declare_config, DeclareAttr, DeclareConfig, DeclareExtractor, DeclareTag};
64pub use diff::{diff_annotations, DiffChange, DiffEntry, DiffResult};
65pub use error::AqlError;
66#[cfg(feature = "fs")]
67pub use extractor::{run_all_extractors, run_extractor, ExtractorResult};
68#[cfg(feature = "fs")]
69pub use extractor::{BuiltinExtractor, ExtractorRegistry};
70#[cfg(all(feature = "fs", feature = "resolver"))]
71pub use extractor::{
72    ExpressExtractor, GoHttpExtractor, GoStructureExtractor, GoTestExtractor, ReactExtractor,
73    RustStructureExtractor, TestExtractor, TypeScriptStructureExtractor,
74};
75pub use file_lock::FileLockManager;
76#[cfg(feature = "fs")]
77pub use init::init_project;
78#[cfg(feature = "fs")]
79pub use init::{detect_stacks, generate_schema, DetectedStacks, Stack};
80pub use manifest::ExtractorConfig;
81#[cfg(feature = "fs")]
82pub use manifest::{find_project_root, load_manifest};
83pub use manifest::{
84    parse_manifest, AttrDefinition, AttrType, Manifest, TagDefinition, BUILTIN_ATTRS,
85    NON_GENERATED_BUILTINS, SCHEMA_VERSION,
86};
87pub use matcher::{filter_by_selector, filter_by_selector_indexed, matches_compound, Matchable};
88#[cfg(feature = "resolver")]
89pub use navigate::{
90    batch_mutate,
91    expand,
92    expand_node,
93    insert,
94    insert_source,
95    move_node,
96    move_node_in_source,
97    next,
98    next_node,
99    prev,
100    prev_node,
101    read_source,
102    remove,
103    remove_node,
104    replace,
105    replace_node,
106    select as nav_select,
107    // Pure functions (no file I/O) — for direct use by consumers
108    select_nodes,
109    shrink,
110    shrink_node,
111    BatchMutationResult,
112    InsertPosition,
113    MutationOp,
114    MutationRequest,
115    MutationResult,
116    NavResult,
117    NodeRef,
118};
119pub use pattern::{
120    diff_from_pattern, extract_pattern, pattern_with_deltas, AttrSummary, Delta, Pattern,
121    PatternResult,
122};
123#[cfg(all(feature = "resolver", feature = "fs"))]
124pub use pipeline::{QueryContext, QueryMiddleware, QueryPipeline, QueryPipelineBuilder};
125#[cfg(all(feature = "resolver", feature = "fs"))]
126pub use query::{batch_query, unified_query, AnnotationSummary, CodeElementSummary, QueryResult};
127pub use query_options::QueryOptions;
128pub use repair::{suggest_repairs, suggest_repairs_from_annotations, RepairSuggestion};
129#[cfg(feature = "resolver")]
130pub use resolver::{
131    CodeElement, CodeResolver, GoResolver, ResolverRegistry, RustResolver, SourceLocation,
132    TypeScriptResolver,
133};
134pub use selector::{
135    parse_selector, AttrOp, AttrPredicate, Combinator, CompoundSelector, Predicate, PredicateOp,
136    PredicateValue, SelectorAst,
137};
138pub use sidecar::{
139    extract_aql_symbols, extract_bind_from_line, resolve_bind_name, sidecar_for_colocated,
140    source_candidates, AqlSymbol, SidecarLocator, SOURCE_EXTENSIONS,
141};
142#[cfg(feature = "fs")]
143pub use sidecar::{ColocatedLocator, DirectoryLocator};
144#[cfg(all(feature = "resolver", feature = "fs"))]
145pub use stats::{project_stats, ProjectStats};
146#[cfg(feature = "fs")]
147pub use store::{glob_annotation_files, glob_aql_files};
148pub use store::{Annotation, AnnotationStore, FileEntry};
149pub use sync::{apply_edits, sync_sidecar, SidecarEdit};
150pub use transact::{execute_transaction, TransactionCondition, TransactionOp, TransactionResult};
151pub use types::{
152    AttrName, Binding, CodeElementName, NodeKind, ProjectRoot, RelativePath, Scope, SelectorStr,
153    TagName,
154};
155pub use validator::{validate, ValidationLevel, ValidationResult};
156
157pub use rustc_hash::FxHashMap;
158
159/// Operations that MUST be exposed by both `amql-cli` (as a `Command` variant) and
160/// `amql-mcp-server` (as an `aql_{op}` tool function). Names are lowercase snake_case.
161///
162/// Adding an entry here causes both shim surface-coverage tests to fail until the
163/// operation is implemented in both shims.
164pub const SURFACE: &[&str] = &[
165    "bench",
166    "diff",
167    "extract",
168    "init",
169    "locate",
170    "nav_insert",
171    "nav_read",
172    "nav_remove",
173    "nav_replace",
174    "nav_select",
175    "query",
176    "repair",
177    "schema",
178    "select",
179    "stats",
180    "transact",
181    "validate",
182];
183
184/// Operations exposed only by `amql-cli` (no MCP equivalent required).
185/// `mcp` is a transport command that starts the MCP server itself.
186pub const CLI_ONLY: &[&str] = &["mcp"];
187
188/// Operations exposed only by `amql-mcp-server` (no CLI equivalent required).
189/// Entries here must NOT appear in `SURFACE`.
190pub const MCP_ONLY: &[&str] = &[
191    "batch_mutate",
192    "batch_query",
193    "coverage",
194    "diff_coverage",
195    "file_summary",
196    "nav_expand",
197    "nav_move",
198    "nav_next",
199    "nav_prev",
200    "nav_shrink",
201    "pattern",
202    "refresh",
203    "spot_check",
204];
205
206use serde_json::Value as JsonValue;
207use std::borrow::Cow;
208
209/// Shared utility: convert a JSON value to a string representation.
210/// Returns `Cow::Borrowed` for the `String` variant to avoid allocation.
211pub fn json_value_to_string(v: &JsonValue) -> Cow<'_, str> {
212    match v {
213        JsonValue::String(s) => Cow::Borrowed(s.as_str()),
214        JsonValue::Bool(b) => Cow::Owned(b.to_string()),
215        JsonValue::Number(n) => Cow::Owned(n.to_string()),
216        JsonValue::Null => Cow::Borrowed(""),
217        other => Cow::Owned(other.to_string()),
218    }
219}
220
221// ---------------------------------------------------------------------------
222// TypeScript/Flow codegen — generates wasm.d.ts + wasm.js.flow for the npm package
223// ---------------------------------------------------------------------------
224
225#[cfg(all(test, feature = "codegen"))]
226mod ts_codegen {
227    use wasm_js_bridge::{generate_index_dts, generate_index_flow, OpaqueType, TypeAlias, WasmFn};
228
229    /// Collect both TS and Flow declarations from a single type list.
230    /// Each entry is a Rust type path — the macro calls `TS::decl` and `Flow::decl` once each.
231    macro_rules! collect_decls {
232        ($ts_cfg:expr, $flow_cfg:expr, [ $($ty:ty),* $(,)? ]) => {{
233            use ts_rs::TS;
234            use flowjs_rs::Flow;
235            let ts: Vec<String> = vec![ $( <$ty as TS>::decl($ts_cfg) ),* ];
236            let flow: Vec<String> = vec![ $( <$ty as Flow>::decl($flow_cfg) ),* ];
237            (ts, flow)
238        }};
239    }
240
241    const ALIASES: &[TypeAlias] = &[
242        TypeAlias {
243            name: "AttrPredicate",
244            target: "Predicate",
245        },
246        TypeAlias {
247            name: "AttrOp",
248            target: "PredicateOp",
249        },
250    ];
251
252    // All engine WASM exports — fn-pointer descriptors auto-generated by #[wasm_export] in wasm.rs.
253    const WASM_FNS: &[WasmFn] = &[
254        crate::index::_WASM_JS_BRIDGE_DECLARE,
255        crate::index::_WASM_JS_BRIDGE_PARSE_MANIFEST,
256        crate::index::_WASM_JS_BRIDGE_PARSE_SELECTOR,
257        crate::index::_WASM_JS_BRIDGE_SELECT,
258        crate::index::_WASM_JS_BRIDGE_VALIDATE,
259        crate::index::_WASM_JS_BRIDGE_DIFF_ANNOTATIONS,
260        crate::index::_WASM_JS_BRIDGE_EXTRACT_PATTERN,
261        crate::index::_WASM_JS_BRIDGE_SUGGEST_REPAIRS,
262        crate::index::_WASM_JS_BRIDGE_SIDECAR_FOR,
263        crate::index::_WASM_JS_BRIDGE_SOURCE_CANDIDATES,
264        crate::index::_WASM_JS_BRIDGE_EXTRACT_BIND,
265        crate::index::_WASM_JS_BRIDGE_RESOLVE_BIND_NAME,
266        crate::index::_WASM_JS_BRIDGE_EXTRACT_AQL_SYMBOLS,
267        crate::index::_WASM_JS_BRIDGE_MEASURE_COMPRESSION,
268    ];
269
270    #[test]
271    fn generate_npm_files() {
272        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
273        let ts_cfg = ts_rs::Config::default();
274        let flow_cfg = flowjs_rs::Config::default();
275
276        // Single type list → both TS and Flow declarations
277        let (ts_decls, flow_decls) = collect_decls!(
278            &ts_cfg,
279            &flow_cfg,
280            [
281                // Newtypes (must come first — referenced by other types)
282                amql_selector::TagName,
283                amql_selector::AttrName,
284                amql_mutate::NodeKind,
285                amql_mutate::RelativePath,
286                crate::Binding,
287                crate::Scope,
288                crate::CodeElementName,
289                crate::SelectorStr,
290                // serde_json::Value → JsonValue
291                serde_json::Value,
292                // Predicate types
293                amql_predicates::PredicateOp,
294                amql_predicates::PredicateValue,
295                amql_predicates::Predicate,
296                amql_predicates::Token,
297                // Selector types
298                amql_selector::Combinator,
299                amql_selector::CompoundSelector,
300                amql_selector::SelectorAst,
301                // Mutation types
302                amql_mutate::NodeRef,
303                amql_mutate::InsertPosition,
304                amql_mutate::MutationResult,
305                amql_mutate::RemoveResult,
306                // Engine types
307                crate::Annotation,
308                crate::FileEntry,
309                crate::DeclareConfig,
310                crate::DeclareTag,
311                crate::DeclareAttr,
312                crate::DeclareExtractor,
313                crate::Manifest,
314                crate::TagDefinition,
315                crate::AttrDefinition,
316                crate::AttrType,
317                crate::ExtractorConfig,
318                crate::DiffResult,
319                crate::DiffEntry,
320                crate::DiffChange,
321                crate::Pattern,
322                crate::PatternResult,
323                crate::AttrSummary,
324                crate::Delta,
325                crate::ValidationResult,
326                crate::ValidationLevel,
327                crate::RepairSuggestion,
328                crate::CompressionResult,
329                crate::FileCompression,
330                crate::SourceEntry,
331                crate::AqlSymbol,
332            ]
333        );
334
335        // Stem derived from src/wasm.rs → "wasm"
336        let stem = wasm_js_bridge::file_to_stem(WASM_FNS[0].file);
337
338        let dts = generate_index_dts(&ts_decls, ALIASES, &[], WASM_FNS);
339        std::fs::write(dir.join(format!("{stem}.d.ts")), dts)
340            .expect("Failed to write .d.ts");
341
342        let opaque = &[
343            OpaqueType { name: "TagName", bound: Some("string") },
344            OpaqueType { name: "AttrName", bound: Some("string") },
345            OpaqueType { name: "NodeKind", bound: Some("string") },
346            OpaqueType { name: "RelativePath", bound: Some("string") },
347            OpaqueType { name: "Binding", bound: Some("string") },
348            OpaqueType { name: "Scope", bound: Some("string") },
349            OpaqueType { name: "CodeElementName", bound: Some("string") },
350            OpaqueType { name: "SelectorStr", bound: Some("string") },
351            OpaqueType { name: "Manifest", bound: None },
352        ];
353        let flow = generate_index_flow(&flow_decls, ALIASES, &[], WASM_FNS, opaque);
354        std::fs::write(dir.join(format!("{stem}.js.flow")), flow)
355            .expect("Failed to write .js.flow");
356    }
357}