Skip to main content

submilli_engine/
packages.rs

1//! Package discovery over the standard library, plus the prelude built-in
2//! catalog (`builtins` / `builtin_docs`).
3//!
4//! In MVP the resolvable set is the stdlib — there is no registry yet. Each
5//! module's exported declarations render to `.d.subm`-style text directly from
6//! its [`PackageDeclaration`] (the public symbol model). Shared by the MCP
7//! `packages.*` / `builtins.docs` tools, the server's REST surface, and the
8//! `submilli docs` / `search` / `builtins` CLI commands.
9
10use std::borrow::Cow;
11use std::collections::{BTreeMap, BTreeSet};
12use std::fmt::Write;
13
14use crate::runtime::prelude::declaration::prelude_package_declaration;
15use crate::stdlib::Stdlib;
16use crate::types::escape_string_literal;
17use crate::{
18    ClassExtends, DocCapabilityBindingKind, DocCapabilityLiteral, DocComment, FileId,
19    NamespaceSymbol, PackageDeclaration, Param, Span, Type, TypeKind, TypePredicate, TypeSymbol,
20    ValueKind, ValueSymbol,
21};
22
23/// Internal plumbing imported by the fs/http shims; never user-facing.
24const INTERNAL_MODULE: &str = "submilli:security";
25
26/// A module's full documentation: its one-line description and `.d.subm`
27/// declarations.
28pub struct ModuleDoc {
29    pub name: String,
30    pub description: String,
31    pub declarations: String,
32}
33
34/// A search result — name + one-line description.
35pub struct ModuleSummary {
36    pub name: String,
37    pub description: String,
38}
39
40/// Documentation for a stdlib module, or `None` if `name` isn't one (callers
41/// handle `@mcp/*` and unknown names). Every module resolves, opt-in ones such
42/// as `submilli:git` included; what a caller's scope shows is its own decision.
43pub fn docs(name: &str) -> Option<ModuleDoc> {
44    Stdlib::core().docs(name)
45}
46
47/// Modules whose name, description, or an exported symbol contains `query`
48/// (case-insensitive). An empty query lists every module.
49pub fn search(query: &str) -> Vec<ModuleSummary> {
50    Stdlib::core().search(query)
51}
52
53/// Discovery over the modules of a [`Stdlib`]: what the free functions above do
54/// for [`Stdlib::core`], for any set.
55impl Stdlib {
56    /// [`docs`] over the modules of this set.
57    pub fn docs(self, name: &str) -> Option<ModuleDoc> {
58        user_modules(self)
59            .into_iter()
60            .find(|d| d.package_name == name)
61            .map(|defs| ModuleDoc {
62                name: defs.package_name.clone(),
63                description: module_description(&defs.package_name).to_string(),
64                declarations: render_declarations(&defs),
65            })
66    }
67
68    /// [`search`] over the modules of this set.
69    pub fn search(self, query: &str) -> Vec<ModuleSummary> {
70        let q = query.trim().to_lowercase();
71        user_modules(self)
72            .iter()
73            .filter(|defs| matches_query(defs, &q))
74            .map(|defs| ModuleSummary {
75                name: defs.package_name.clone(),
76                description: module_description(&defs.package_name).to_string(),
77            })
78            .collect()
79    }
80
81    /// [`resolve`] over the modules of this set.
82    pub fn resolve(self, name: &str) -> Resolution {
83        if let Some(doc) = self.docs(name) {
84            return Resolution::Module(doc);
85        }
86        match builtin_lookup(name) {
87            BuiltinLookup::Found(declarations) => Resolution::Builtin {
88                name: name.to_string(),
89                declarations,
90            },
91            BuiltinLookup::UnknownMember {
92                path,
93                member,
94                members,
95            } => Resolution::UnknownMember {
96                path,
97                member,
98                members,
99            },
100            BuiltinLookup::Unknown => Resolution::Unknown,
101        }
102    }
103
104    /// [`suggest_filtered`] over the modules of this set.
105    pub fn suggest_filtered(
106        self,
107        name: &str,
108        extra: &[String],
109        visible: impl Fn(&str) -> bool,
110    ) -> Option<String> {
111        let mut candidates: Vec<String> = self.search("").into_iter().map(|m| m.name).collect();
112        let builtins = builtins();
113        candidates.extend(builtins.types);
114        candidates.extend(builtins.namespaces);
115        candidates.extend(extra.iter().cloned());
116        candidates.retain(|candidate| visible(candidate));
117
118        // Only the head segment is in question: `Temporel.Instant` misses because
119        // of `Temporel`, and the tail only inflates the distance past threshold.
120        let query = name.split('.').find(|s| !s.is_empty()).unwrap_or(name);
121        // `closest_match` floors its threshold at 2 edits, so a one- or two-character
122        // query sits within reach of an unrelated name. Below that length there is no
123        // signal to match on.
124        if query.len() < 3 {
125            return None;
126        }
127
128        if let Some((_, replacement)) = OMITTED_GLOBALS
129            .iter()
130            .find(|(omitted, _)| omitted.eq_ignore_ascii_case(name))
131        {
132            return Some((*replacement).to_string());
133        }
134
135        // Never offer the query back to the caller: a name that resolved nowhere is
136        // not its own repair, however it was spelled.
137        let is_self = |c: &String| c.eq_ignore_ascii_case(name);
138        if let Some(hit) = candidates
139            .iter()
140            .find(|c| !is_self(c) && name_tail(c).eq_ignore_ascii_case(query))
141        {
142            return Some(hit.clone());
143        }
144        // Match full names and scheme-stripped tails in one pass, so the globally
145        // closest key wins: `htp` should reach `submilli:http` (one edit from its
146        // tail) rather than whichever unrelated full name lands inside threshold.
147        let mut keys: Vec<(&str, &str)> = Vec::new();
148        for c in candidates.iter().filter(|c| !is_self(c)) {
149            keys.push((c.as_str(), c.as_str()));
150            let tail = name_tail(c);
151            if tail != c.as_str() {
152                keys.push((tail, c.as_str()));
153            }
154        }
155        let hit = crate::did_you_mean::closest_match(query, keys.iter().map(|(k, _)| *k))?;
156        keys.iter()
157            .find(|(k, _)| *k == hit)
158            .map(|(_, owner)| (*owner).to_string())
159    }
160
161    /// [`catalog_filtered`] over the modules of this set.
162    pub fn catalog_filtered(
163        self,
164        extra: Vec<CatalogEntry>,
165        visible: impl Fn(&str) -> bool,
166    ) -> Catalog {
167        let mut entries: Vec<CatalogEntry> = self
168            .search("")
169            .into_iter()
170            .map(|m| CatalogEntry {
171                name: m.name,
172                source: SOURCE_STDLIB.to_string(),
173                description: m.description,
174            })
175            .collect();
176        entries.extend(extra);
177        entries.retain(|entry| visible(&entry.name));
178        let remaining = entries.len().saturating_sub(CATALOG_LIMIT);
179        entries.truncate(CATALOG_LIMIT);
180        Catalog { entries, remaining }
181    }
182
183    /// [`render_stdlib_d_ts`] for the modules of this set.
184    pub fn render_stdlib_d_ts(self) -> String {
185        let mut out = String::new();
186        for defs in &editor_stdlib_modules(self) {
187            render_declare_module(&mut out, defs);
188        }
189        out.trim_end().to_string()
190    }
191
192    /// [`render_packages_d_ts`] against the modules of this set.
193    pub fn render_packages_d_ts(
194        self,
195        packages: &[&PackageDeclaration],
196        context: &[&PackageDeclaration],
197    ) -> String {
198        let stdlib = editor_stdlib_modules(self);
199        let exporters = TypeExporters::new(
200            stdlib
201                .iter()
202                .chain(packages.iter().copied())
203                .chain(context.iter().copied()),
204        );
205        let globals = global_classes();
206        let mut out = String::new();
207        for defs in packages {
208            let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
209            let imports = block_imports(defs, &exporters);
210            for (local, (public, module)) in &imports.types {
211                if local == public {
212                    let _ = writeln!(out, "  import type {{ {local} }} from \"{module}\";");
213                } else {
214                    let _ = writeln!(
215                        out,
216                        "  import type {{ {public} as {local} }} from \"{module}\";"
217                    );
218                }
219            }
220            let parent = |extends: &ClassExtends| {
221                if let Some(local) = imports.parents.get(extends.parent.as_str()) {
222                    return Some(ts_named_type(local, &extends.args));
223                }
224                // A package value of the global's name would hide it; the class
225                // then renders without its parent rather than extend the value.
226                let global = globals.get(extends.parent.as_str())?;
227                if declares(defs, global, true) {
228                    return None;
229                }
230                Some(ts_named_type(global, &extends.args))
231            };
232            render_ts_declarations(&mut out, defs, "  ", "export ", &parent);
233            let _ = writeln!(out, "}}\n");
234        }
235        out.trim_end().to_string()
236    }
237}
238
239/// The language built-ins always in scope without an `import`: headline prelude
240/// types (`Array`, `Map`, `String`, …) and the namespace globals (`Math`,
241/// `Temporal`, and the `JSON` compiler intrinsic). Backs the `{builtins}` prompt
242/// placeholder and the `builtins.docs` tool.
243pub struct Builtins {
244    pub types: Vec<String>,
245    pub namespaces: Vec<String>,
246}
247
248/// `JSON` is a compiler intrinsic (special-cased in the typechecker, not a
249/// prelude `PackageDeclaration` entry), so it's catalogued and rendered by hand.
250const JSON_BUILTIN: &str = "JSON";
251
252/// Prelude types that exist for the type system but that an agent never writes
253/// by name: the receiver type of the `console` global, an options bag, regex
254/// match results, and the iteration protocol. Filtered out of the catalog.
255const SUPPORTING_TYPES: &[&str] = &[
256    "Console",
257    "Base64Options",
258    "RegExpMatch",
259    "Iterator",
260    "Iterable",
261    "IteratorResult",
262    "IteratorYieldResult",
263    "IteratorReturnResult",
264];
265
266fn is_headline_type(name: &str) -> bool {
267    !name.contains('#')                   // flattened namespace member (`Temporal#Instant`)
268        && !name.ends_with("Constructor") // `new`/static plumbing, folded into its base
269        && !SUPPORTING_TYPES.contains(&name)
270}
271
272/// The catalog of built-ins an agent can reference — see [`Builtins`].
273pub fn builtins() -> Builtins {
274    let defs = builtin_package_declaration();
275    // `defs.types`/`namespaces` are `BTreeMap`s, so keys arrive sorted.
276    let mut types: Vec<String> = defs
277        .types
278        .keys()
279        .filter(|n| is_headline_type(n))
280        .cloned()
281        .collect();
282    types.push("Record".into());
283    types.sort();
284    let mut namespaces: Vec<String> = defs.namespaces.keys().cloned().collect();
285    namespaces.push(JSON_BUILTIN.to_string());
286    namespaces.sort();
287    Builtins { types, namespaces }
288}
289
290/// `.d.subm` declarations for a single built-in (`Array`, `Temporal`, `JSON`, …),
291/// or `None` if `name` isn't one. Accepts a dotted member path
292/// (`Temporal.Instant`) as well as a plain name — see [`builtin_lookup`].
293pub fn builtin_docs(name: &str) -> Option<String> {
294    match builtin_lookup(name) {
295        BuiltinLookup::Found(declarations) => Some(declarations),
296        BuiltinLookup::UnknownMember { .. } | BuiltinLookup::Unknown => None,
297    }
298}
299
300/// Why resolving a built-in name succeeded or failed. The distinction matters
301/// to callers: an unresolvable *member* can name the members that do exist,
302/// which turns a dead end into a one-edit repair.
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub enum BuiltinLookup {
305    /// Rendered `.d.subm` declarations for the resolved name.
306    Found(String),
307    /// `path` resolved, but it has no member called `member`.
308    UnknownMember {
309        path: String,
310        member: String,
311        members: Vec<String>,
312    },
313    /// The leading segment names no built-in at all.
314    Unknown,
315}
316
317/// Resolve a built-in name, which may be a dotted path into a namespace
318/// (`Temporal.Instant`, `Temporal.Now.instant`, `Math.max`, `Array.isArray`).
319///
320/// A plain name renders exactly as it always has. A dotted path renders only
321/// the member slice, wrapped in its enclosing `namespace` chain — the
322/// declarations refer to each other by qualified name (`Temporal.Instant`), so
323/// an unwrapped slice would teach `Instant.from(...)` instead of
324/// `Temporal.Instant.from(...)`.
325pub fn builtin_lookup(name: &str) -> BuiltinLookup {
326    // `#` is the internal namespace-flattening separator (`Temporal#Instant`);
327    // it's never part of the surface an agent names.
328    if name.contains('#') {
329        return BuiltinLookup::Unknown;
330    }
331    // A trailing or doubled dot is a typo the path walk can absorb rather than
332    // reject (see the forgiveness principle in AGENTS.md).
333    let segments: Vec<&str> = name.split('.').filter(|s| !s.is_empty()).collect();
334    let Some((head, rest)) = segments.split_first() else {
335        return BuiltinLookup::Unknown;
336    };
337
338    if head.eq_ignore_ascii_case("Record") && rest.is_empty() {
339        return BuiltinLookup::Found("/** Record<K, V> accepts string keys. With K = string, reads return V | undefined and writes require V. Finite string-literal keys are all required. Equivalent open syntax: { [key: string]: V }. */\ntype Record<K extends string, V> = { [P in K]: V };\n".into());
340    }
341    if head.eq_ignore_ascii_case(JSON_BUILTIN) {
342        let defs = json_package_declaration();
343        return walk_namespace(JSON_BUILTIN, &defs.namespaces[JSON_BUILTIN], rest);
344    }
345
346    let defs = builtin_package_declaration();
347    if let Some(canonical) = resolve_ignore_case(defs.namespaces.keys(), head) {
348        return walk_namespace(&canonical, &defs.namespaces[&canonical], rest);
349    }
350
351    let Some(canonical) = resolve_type_key(&defs.types, head) else {
352        return BuiltinLookup::Unknown;
353    };
354    match rest {
355        [] => {
356            let mut out = String::new();
357            render_type_with_ctor(&mut out, &defs.types, &canonical, "");
358            BuiltinLookup::Found(out.trim_end().to_string())
359        }
360        [member, deeper @ ..] => resolve_type_member(&defs.types, &canonical, member, deeper),
361    }
362}
363
364/// Walk `rest` down from the namespace `head`, which is already resolved.
365fn walk_namespace<'a>(head: &str, head_ns: &'a NamespaceSymbol, rest: &[&str]) -> BuiltinLookup {
366    let mut chain: Vec<String> = vec![head.to_string()];
367    let mut ns: &'a NamespaceSymbol = head_ns;
368
369    for (idx, seg) in rest.iter().enumerate() {
370        let tail = &rest[idx + 1..];
371
372        if let Some(canonical) = resolve_ignore_case(ns.namespaces.keys(), seg) {
373            let sub = &ns.namespaces[&canonical];
374            if tail.is_empty() {
375                let mut body = String::new();
376                render_namespace(&mut body, &canonical, sub, "");
377                return found_in(&chain, &body);
378            }
379            chain.push(canonical);
380            ns = sub;
381            continue;
382        }
383
384        if let Some(canonical) = resolve_type_key(&ns.types, seg) {
385            if tail.is_empty() {
386                let mut body = String::new();
387                render_namespace_member(&mut body, ns, &canonical);
388                return found_in(&chain, &body);
389            }
390            // The type name stays off `chain`: a type is not a namespace, and
391            // the rendered stub already names the interface the member hangs off.
392            return match resolve_type_member(&ns.types, &canonical, tail[0], &tail[1..]) {
393                BuiltinLookup::Found(body) => found_in(&chain, &body),
394                miss => qualify(&chain, miss),
395            };
396        }
397
398        if let Some(canonical) = resolve_ignore_case(ns.values.keys(), seg) {
399            if tail.is_empty() {
400                let sym = &ns.values[&canonical];
401                let mut body = String::new();
402                push_doc(&mut body, value_doc(&sym.kind), "");
403                render_value(&mut body, &canonical, &sym.kind, "");
404                return found_in(&chain, &body);
405            }
406            // A value is a leaf: it has no members to walk into.
407            chain.push(canonical);
408            return BuiltinLookup::UnknownMember {
409                path: chain.join("."),
410                member: tail.join("."),
411                members: Vec::new(),
412            };
413        }
414
415        return BuiltinLookup::UnknownMember {
416            path: chain.join("."),
417            member: (*seg).to_string(),
418            members: namespace_member_names(ns),
419        };
420    }
421
422    let mut out = String::new();
423    render_namespace(&mut out, head, head_ns, "");
424    BuiltinLookup::Found(out.trim_end().to_string())
425}
426
427/// Resolve `member` on the type `type_name`, checking the interface itself and
428/// then its `*Constructor` (the `new`/static side, where `Array.isArray` lives).
429fn resolve_type_member(
430    types: &BTreeMap<String, TypeSymbol>,
431    type_name: &str,
432    member: &str,
433    deeper: &[&str],
434) -> BuiltinLookup {
435    if !deeper.is_empty() {
436        return BuiltinLookup::UnknownMember {
437            path: format!("{type_name}.{member}"),
438            member: deeper.join("."),
439            members: Vec::new(),
440        };
441    }
442    let ctor = format!("{type_name}Constructor");
443    for owner in [type_name, ctor.as_str()] {
444        let Some(sym) = types.get(owner) else {
445            continue;
446        };
447        if let Some(body) = render_type_member(owner, &sym.kind, member) {
448            return BuiltinLookup::Found(body.trim_end().to_string());
449        }
450    }
451    BuiltinLookup::UnknownMember {
452        path: type_name.to_string(),
453        member: member.to_string(),
454        members: type_member_names(types, type_name),
455    }
456}
457
458/// Render the slice of `ns` named `canonical`: the namespace's own binding for
459/// the name, the interface, and its `*Constructor`.
460///
461/// Deliberately not [`render_type_with_ctor`] — that synthesises its own
462/// `const <name>: <name>Constructor;` line, which inside a namespace both
463/// duplicates the namespace's existing binding and drops the qualification
464/// (`Temporal.InstantConstructor`) that binding carries.
465fn render_namespace_member(out: &mut String, ns: &NamespaceSymbol, canonical: &str) {
466    if let Some(sym) = ns.values.get(canonical) {
467        push_doc(out, value_doc(&sym.kind), "");
468        render_value(out, canonical, &sym.kind, "");
469    }
470    if let Some(sym) = ns.types.get(canonical) {
471        push_doc(out, type_doc(&sym.kind), "");
472        render_type(out, canonical, &sym.kind, "");
473    }
474    let ctor = format!("{canonical}Constructor");
475    if let Some(sym) = ns.types.get(&ctor) {
476        push_doc(out, type_doc(&sym.kind), "");
477        render_type(out, &ctor, &sym.kind, "");
478    }
479}
480
481/// One method or property of an interface, rendered inside a stub of its owner
482/// so the reader sees which type the member hangs off.
483fn render_type_member(owner: &str, kind: &TypeKind, member: &str) -> Option<String> {
484    if let TypeKind::Class { .. } = kind {
485        return render_class_member(owner, kind, member);
486    }
487    let TypeKind::Interface {
488        generics,
489        methods,
490        properties,
491        ..
492    } = kind
493    else {
494        return None;
495    };
496    let mut body = String::new();
497    if let Some(name) = resolve_ignore_case(properties.keys(), member) {
498        let prop = &properties[&name];
499        push_doc(&mut body, &prop.doc, "  ");
500        let ro = if prop.readonly { "readonly " } else { "" };
501        let opt = if prop.optional { "?" } else { "" };
502        let name = member_name(&name);
503        let _ = writeln!(body, "  {ro}{name}{opt}: {};", prop.ty);
504    } else {
505        let name = resolve_ignore_case(methods.keys(), member)?;
506        let m = &methods[&name];
507        push_doc(&mut body, &m.doc, "  ");
508        let optional = if m.optional { "?" } else { "" };
509        let name = method_prefix(&name);
510        let _ = writeln!(
511            body,
512            "  {name}{optional}{}({}): {};",
513            generics_str(&m.generics),
514            params_str(&m.params, m.doc.as_ref()),
515            m.ret
516        );
517    }
518    Some(format!(
519        "interface {owner}{} {{\n{body}}}\n",
520        generics_str(generics)
521    ))
522}
523
524/// One public member of a class, rendered inside a stub of its owner. Mirrors
525/// the visibility filtering of the full class rendering: a private member is
526/// not part of the surface an agent can call, so it stays unresolvable.
527fn render_class_member(owner: &str, kind: &TypeKind, member: &str) -> Option<String> {
528    let TypeKind::Class {
529        generics,
530        fields,
531        methods,
532        method_visibility,
533        statics,
534        static_visibility,
535        static_fields,
536        ..
537    } = kind
538    else {
539        return None;
540    };
541    let mut body = String::new();
542    if let Some(name) = resolve_ignore_case(static_fields.keys(), member)
543        .filter(|n| static_fields[n].visibility != crate::Visibility::Private)
544    {
545        let field = &static_fields[&name];
546        push_doc(&mut body, &field.doc, "  ");
547        let ro = if field.readonly { "readonly " } else { "" };
548        let name = member_name(&name);
549        let _ = writeln!(body, "  static {ro}{name}: {};", field.ty);
550    } else if let Some(name) = resolve_ignore_case(statics.keys(), member)
551        .filter(|n| static_visibility.get(n) != Some(&crate::Visibility::Private))
552    {
553        let m = &statics[&name];
554        push_doc(&mut body, &m.doc, "  ");
555        let name = member_name(&name);
556        let _ = writeln!(
557            body,
558            "  static {name}{}({}): {};",
559            generics_str(&m.generics),
560            params_str(&m.params, m.doc.as_ref()),
561            m.ret
562        );
563    } else if let Some(name) = resolve_ignore_case(fields.keys(), member)
564        .filter(|n| fields[n].visibility != crate::Visibility::Private)
565    {
566        let field = &fields[&name];
567        push_doc(&mut body, &field.doc, "  ");
568        let ro = if field.readonly { "readonly " } else { "" };
569        let opt = if field.optional { "?" } else { "" };
570        let name = member_name(&name);
571        let _ = writeln!(body, "  {ro}{name}{opt}: {};", field.ty);
572    } else {
573        let name = resolve_ignore_case(methods.keys(), member)
574            .filter(|n| method_visibility.get(n) != Some(&crate::Visibility::Private))?;
575        let m = &methods[&name];
576        push_doc(&mut body, &m.doc, "  ");
577        let optional = if m.optional { "?" } else { "" };
578        let name = member_name(&name);
579        let _ = writeln!(
580            body,
581            "  {name}{optional}{}({}): {};",
582            generics_str(&m.generics),
583            params_str(&m.params, m.doc.as_ref()),
584            m.ret
585        );
586    }
587    Some(format!(
588        "class {owner}{} {{\n{body}}}\n",
589        generics_str(generics)
590    ))
591}
592
593/// Members an agent can name on `type_name`, including its `*Constructor` side.
594fn type_member_names(types: &BTreeMap<String, TypeSymbol>, type_name: &str) -> Vec<String> {
595    let ctor = format!("{type_name}Constructor");
596    let mut names: Vec<String> =
597        [type_name, ctor.as_str()]
598            .iter()
599            .filter_map(|owner| types.get(*owner))
600            .flat_map(|sym| match &sym.kind {
601                TypeKind::Interface {
602                    methods,
603                    properties,
604                    ..
605                } => properties
606                    .keys()
607                    .chain(methods.keys())
608                    .cloned()
609                    .collect::<Vec<_>>(),
610                // Private members are not part of the surface an agent can call, so
611                // naming them here would offer a repair the typechecker rejects.
612                TypeKind::Class {
613                    fields,
614                    methods,
615                    method_visibility,
616                    statics,
617                    static_visibility,
618                    static_fields,
619                    ..
620                } => {
621                    static_fields
622                        .iter()
623                        .filter(|(_, f)| f.visibility != crate::Visibility::Private)
624                        .map(|(n, _)| n)
625                        .chain(statics.keys().filter(|n| {
626                            static_visibility.get(*n) != Some(&crate::Visibility::Private)
627                        }))
628                        .chain(
629                            fields
630                                .iter()
631                                .filter(|(_, f)| f.visibility != crate::Visibility::Private)
632                                .map(|(n, _)| n),
633                        )
634                        .chain(methods.keys().filter(|n| {
635                            method_visibility.get(*n) != Some(&crate::Visibility::Private)
636                        }))
637                        .cloned()
638                        .collect::<Vec<_>>()
639                }
640                _ => Vec::new(),
641            })
642            .collect();
643    names.sort();
644    names.dedup();
645    names
646}
647
648/// Members an agent can name on `ns`, with the `*Constructor` plumbing folded
649/// away — the namespace binds each constructor to its base name already.
650fn namespace_member_names(ns: &NamespaceSymbol) -> Vec<String> {
651    let mut names: Vec<String> = ns
652        .values
653        .keys()
654        .chain(ns.types.keys())
655        .chain(ns.namespaces.keys())
656        .filter(|n| !n.ends_with("Constructor"))
657        .cloned()
658        .collect();
659    names.sort();
660    names.dedup();
661    names
662}
663
664/// Find the type key matching `name`, accepting the base name of a
665/// `*Constructor` entry so `Instant` resolves when only `InstantConstructor`
666/// is declared.
667fn resolve_type_key(types: &BTreeMap<String, TypeSymbol>, name: &str) -> Option<String> {
668    resolve_ignore_case(types.keys(), name).or_else(|| {
669        resolve_ignore_case(types.keys(), &format!("{name}Constructor"))
670            .map(|c| c.trim_end_matches("Constructor").to_string())
671    })
672}
673
674/// Wrap a rendered member slice in its enclosing `namespace` chain.
675fn found_in(chain: &[String], body: &str) -> BuiltinLookup {
676    let mut out = body.trim_end().to_string();
677    for name in chain.iter().rev() {
678        out = format!("namespace {name} {{\n{}\n}}", indent_block(&out, "  "));
679    }
680    BuiltinLookup::Found(out)
681}
682
683/// Re-anchor a miss reported against a bare type name onto its full path.
684fn qualify(chain: &[String], miss: BuiltinLookup) -> BuiltinLookup {
685    match miss {
686        BuiltinLookup::UnknownMember {
687            path,
688            member,
689            members,
690        } if !chain.is_empty() => BuiltinLookup::UnknownMember {
691            path: format!("{}.{path}", chain.join(".")),
692            member,
693            members,
694        },
695        other => other,
696    }
697}
698
699/// How a discovery surface should answer one name, once the stdlib and
700/// built-in catalogs have both been consulted.
701///
702/// The server layers `@mcp/*` and registry packages on top; those live in
703/// crates that depend on this one, so they cannot be resolved from here.
704pub enum Resolution {
705    /// An importable stdlib module.
706    Module(ModuleDoc),
707    /// A language built-in, always in scope without an `import`.
708    Builtin { name: String, declarations: String },
709    /// The head resolved but the member did not.
710    UnknownMember {
711        path: String,
712        member: String,
713        members: Vec<String>,
714    },
715    /// Neither catalog knows the name.
716    Unknown,
717}
718
719/// Try `name` as a stdlib module, then as a built-in. Every discovery surface
720/// runs this same ladder so MCP, REST, and the CLI reach the same answer.
721pub fn resolve(name: &str) -> Resolution {
722    Stdlib::core().resolve(name)
723}
724
725/// A did-you-mean candidate for a name that resolved nowhere, drawn from the
726/// stdlib and built-in catalogs plus any `extra` names the caller can see
727/// (`@mcp/*`, registry packages).
728///
729/// Two things bare edit distance cannot do on its own: a dotted name fails on
730/// its head segment, and a package named without its scheme (`http` for
731/// `submilli:http`) sits far outside any sane threshold.
732/// Globals this language deliberately omits, each pointing at what replaces it.
733/// Edit distance cannot find these — `Date` is nearer `Math` than `Temporal` —
734/// and an LLM carrying JS habits reaches for them by name, so the redirect is
735/// the difference between a dead end and the right answer.
736const OMITTED_GLOBALS: &[(&str, &str)] = &[("Date", "Temporal")];
737
738pub fn suggest(name: &str, extra: &[String]) -> Option<String> {
739    suggest_filtered(name, extra, |_| true)
740}
741
742/// Suggest only names visible to the caller, retaining built-in corrections.
743pub fn suggest_filtered(
744    name: &str,
745    extra: &[String],
746    visible: impl Fn(&str) -> bool,
747) -> Option<String> {
748    Stdlib::core().suggest_filtered(name, extra, visible)
749}
750
751/// The part of a package name after its scheme or scope: `submilli:http` and
752/// `@mcp/linear` reduce to `http` and `linear`.
753fn name_tail(name: &str) -> &str {
754    let after_scheme = name.rsplit_once(':').map_or(name, |(_, t)| t);
755    after_scheme
756        .rsplit_once('/')
757        .map_or(after_scheme, |(_, t)| t)
758}
759
760/// A summary-only entry in the "here is what exists" listing a zero-hit
761/// discovery response carries. Never declarations — this is a pointer, not a
762/// payload.
763#[derive(Debug, Clone, PartialEq, Eq)]
764pub struct CatalogEntry {
765    pub name: String,
766    pub source: String,
767    pub description: String,
768}
769
770/// A bounded available-package listing.
771pub struct Catalog {
772    pub entries: Vec<CatalogEntry>,
773    /// Entries dropped past [`CATALOG_LIMIT`], reported as a count so the
774    /// response stays bounded as the registry grows.
775    pub remaining: usize,
776}
777
778/// Upper bound on listed entries; the remainder is reported as a count.
779pub const CATALOG_LIMIT: usize = 50;
780
781/// Where the language globals live. A zero-hit package search says this rather
782/// than folding built-ins into its results, which would teach an agent to
783/// write an `import` for a global.
784///
785/// `fetch_with` names the call the asking surface actually accepts — the tool
786/// (`builtins.docs`) or the CLI command (`submilli builtins`). Pointing a CLI
787/// user at a tool name they cannot run would be its own dead end.
788pub fn builtins_pointer(fetch_with: &str) -> String {
789    let head = "Language built-ins (Array, Map, String, Temporal, JSON, …)";
790    format!("{head} are always in scope without an import — fetch them with {fetch_with}.")
791}
792
793/// A package name asked of a built-ins surface. The forgiveness here is
794/// asymmetric on purpose: a built-in asked of `packages.docs` is simply served,
795/// because it needs no `import`, while a package needs an `import` the caller
796/// still has to write — so this names the call to make rather than hiding that
797/// step behind the declarations.
798///
799/// `fetch_with` is the call the asking surface actually accepts, exactly as in
800/// [`builtins_pointer`].
801pub fn package_correcting_message(name: &str, fetch_with: &str) -> String {
802    format!(
803        "`{name}` is a package, not a language built-in. {fetch_with} for its declarations, \
804         and write `import ... from \"{name}\"` to use it."
805    )
806}
807
808/// Source tag for the stdlib modules this crate can see.
809pub const SOURCE_STDLIB: &str = "stdlib";
810
811/// The stdlib catalog plus whatever `extra` sources the caller can see, capped.
812pub fn catalog(extra: Vec<CatalogEntry>) -> Catalog {
813    catalog_filtered(extra, |_| true)
814}
815
816/// Filter before applying the catalog limit so omitted counts reflect visibility.
817pub fn catalog_filtered(extra: Vec<CatalogEntry>, visible: impl Fn(&str) -> bool) -> Catalog {
818    Stdlib::core().catalog_filtered(extra, visible)
819}
820
821/// Name the head, then list what it actually has, so the repair is one edit.
822pub fn unknown_member_message(path: &str, member: &str, members: &[String]) -> String {
823    if members.is_empty() {
824        return format!("`{path}` has no member `{member}` — `{path}` has no members.");
825    }
826    format!(
827        "`{path}` has no member `{member}`. Available members of `{path}`: {}.",
828        members.join(", ")
829    )
830}
831
832/// Prose to accompany a built-in served through a package-shaped surface. A
833/// `source` tag is a machine field; an agent reading the message still has to
834/// be told not to write an `import`.
835pub fn builtin_no_import_note(name: &str) -> String {
836    format!("`{name}` is a language built-in, always in scope — do not write an `import` for it.")
837}
838
839fn indent_block(body: &str, pad: &str) -> String {
840    body.lines()
841        .map(|l| {
842            if l.is_empty() {
843                String::new()
844            } else {
845                format!("{pad}{l}")
846            }
847        })
848        .collect::<Vec<_>>()
849        .join("\n")
850}
851
852fn builtin_package_declaration() -> PackageDeclaration {
853    let mut defs = prelude_package_declaration();
854    let prelude_decl = crate::runtime::prelude::package_declaration();
855    for (name, namespace) in prelude_decl.namespaces {
856        defs.namespaces.entry(name).or_insert(namespace);
857    }
858    defs
859}
860
861/// Find the key that matches `name` case-insensitively, preferring an exact
862/// match so canonical casing always wins when both exist.
863fn resolve_ignore_case<'a>(keys: impl Iterator<Item = &'a String>, name: &str) -> Option<String> {
864    let mut fallback = None;
865    for key in keys {
866        if key == name {
867            return Some(key.clone());
868        }
869        if fallback.is_none() && key.eq_ignore_ascii_case(name) {
870            fallback = Some(key.clone());
871        }
872    }
873    fallback
874}
875
876/// The stdlib modules an agent may import, minus internal plumbing.
877fn user_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
878    stdlib
879        .package_declarations()
880        .into_iter()
881        .filter(|d| d.package_name != INTERNAL_MODULE)
882        .collect()
883}
884
885/// One-line module summaries — the single source for the stdlib tour shown in
886/// `packages.search`/`docs` and (manually mirrored) MVP.md §I.
887fn module_description(name: &str) -> &'static str {
888    match name {
889        "submilli:agents" => {
890            "Harness sub-agents: run<T>(agent, input), and list() to discover them."
891        }
892        "submilli:crypto" => "Hashing, HMAC, and random bytes.",
893        "submilli:code" => {
894            "Workspace tools: numbered reads, search, glob, tree, anchored edits and unified diffs."
895        }
896        "submilli:embedding" => {
897            "Remote text embeddings: embed batches into sealed vectors, and models() to discover aliases."
898        }
899        "submilli:fs" => "Sandbox filesystem: read/write/list/stat/remove/exists/info.",
900        "submilli:git" => {
901            "Capability-controlled VFS repositories: history, staging, commits, branches and HTTPS fetch."
902        }
903        "submilli:http" => "Outbound HTTP: get/post/put/patch/delete/head.",
904        "submilli:llm" => "Gated model calls: call/batch, and models() to discover them.",
905        "submilli:secrets" => "Policy-gated access to Blueprint-declared secrets.",
906        "submilli:session" => "Session-scoped key-value state: get/has/set/remove/list.",
907        "submilli:skills" => "Harness skills: list(), load(name) and readFile(name, path).",
908        "submilli:url" => "URL parse/build and query-string handling. Pure compute.",
909        "submilli:uuid" => "UUID v4/v7 generation and validation.",
910        _ => "",
911    }
912}
913
914fn matches_query(defs: &PackageDeclaration, q: &str) -> bool {
915    if q.is_empty() {
916        return true;
917    }
918    let in_symbols = defs
919        .values
920        .keys()
921        .chain(defs.types.keys())
922        .any(|k| k.to_lowercase().contains(q));
923    defs.package_name.to_lowercase().contains(q)
924        || module_description(&defs.package_name)
925            .to_lowercase()
926            .contains(q)
927        || in_symbols
928}
929
930/// Render a module's exports as `.d.subm`-style declarations. Public so an
931/// embedder can render dynamically-built packages (e.g. `@mcp/<server>`) the same
932/// way the stdlib docs render.
933pub fn render_declarations(defs: &PackageDeclaration) -> String {
934    let mut out = String::new();
935    for (name, sym) in &defs.values {
936        push_doc(&mut out, value_doc(&sym.kind), "");
937        render_value(&mut out, name, &sym.kind, "");
938    }
939    for (name, sym) in &defs.types {
940        push_doc(&mut out, type_doc(&sym.kind), "");
941        render_type(&mut out, name, &sym.kind, "");
942    }
943    out.trim_end().to_string()
944}
945
946/// Render editor-facing TypeScript declarations for every stdlib module a
947/// package project can import. Each package gets a separate `declare module`
948/// block. `submilli:test` is included even though only `build test` makes it
949/// importable, and `submilli:security` even though only packages may import
950/// it — the editor should complete both; `build check` remains the gate
951/// against importing them elsewhere.
952pub fn render_stdlib_d_ts() -> String {
953    Stdlib::core().render_stdlib_d_ts()
954}
955
956/// Render editor-facing TypeScript declarations for `packages`, one
957/// `declare module` block each, in the order given.
958///
959/// A declaration names the types it borrows from another module without
960/// saying where they come from, so each block imports them. A borrowed type is
961/// matched to its module by the reference's mangled name, not by its text:
962/// many modules export a `Page` or an `Item`. `context` holds further modules
963/// those types may come from that get no block here, such as the project's
964/// own packages.
965pub fn render_packages_d_ts(
966    packages: &[&PackageDeclaration],
967    context: &[&PackageDeclaration],
968) -> String {
969    Stdlib::core().render_packages_d_ts(packages, context)
970}
971
972/// The built-in classes a package's class may extend, such as `Error`, by
973/// mangled name. They're globals, so a parent among them needs no import.
974fn global_classes() -> BTreeMap<String, String> {
975    let defs = builtin_package_declaration();
976    defs.types
977        .into_iter()
978        .filter(|(name, symbol)| {
979            matches!(symbol.kind, TypeKind::Class { .. })
980                && !is_hidden_prelude_type(&defs.package_name, name)
981        })
982        .map(|(name, symbol)| (symbol.mangled_name.as_str().to_string(), name))
983        .collect()
984}
985
986fn editor_stdlib_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
987    let mut modules = user_modules(stdlib);
988    modules.push(crate::stdlib::test::package_declaration());
989    modules.push(crate::stdlib::security::package_declaration());
990    modules.sort_by(|a, b| a.package_name.cmp(&b.package_name));
991    modules
992}
993
994/// Where each exported type can be imported from.
995struct TypeExporters<'a> {
996    /// A type symbol's mangled name, mapped to the type and its module.
997    by_mangled: BTreeMap<&'a str, (ExportedType<'a>, &'a str)>,
998    /// Each module's exported types by name, for a reference whose mangled
999    /// name is the internal-module form of a type the package root re-exports.
1000    by_module: BTreeMap<&'a str, BTreeMap<&'a str, ExportedType<'a>>>,
1001}
1002
1003#[derive(Clone, Copy)]
1004struct ExportedType<'a> {
1005    name: &'a str,
1006    /// A class or enum is a value too, and an import of one conflicts with a
1007    /// local value of the same name. An interface or alias doesn't.
1008    is_value: bool,
1009}
1010
1011impl<'a> TypeExporters<'a> {
1012    fn new(modules: impl Iterator<Item = &'a PackageDeclaration>) -> Self {
1013        let mut by_mangled = BTreeMap::new();
1014        let mut by_module: BTreeMap<&str, BTreeMap<&str, ExportedType>> = BTreeMap::new();
1015        for defs in modules {
1016            let module = defs.package_name.as_str();
1017            for (name, symbol) in &defs.types {
1018                let exported = ExportedType {
1019                    name: name.as_str(),
1020                    is_value: matches!(
1021                        symbol.kind,
1022                        TypeKind::Class { .. }
1023                            | TypeKind::NumberEnum { .. }
1024                            | TypeKind::StringEnum { .. }
1025                    ),
1026                };
1027                by_mangled.insert(symbol.mangled_name.as_str(), (exported, module));
1028                by_module
1029                    .entry(module)
1030                    .or_default()
1031                    .insert(name.as_str(), exported);
1032            }
1033        }
1034        Self {
1035            by_mangled,
1036            by_module,
1037        }
1038    }
1039
1040    /// The type a reference points at and its module, if a known module
1041    /// exports it.
1042    fn resolve(&self, reference: &TypeReference) -> Option<(ExportedType<'a>, &'a str)> {
1043        if let Some(&exported) = self.by_mangled.get(reference.mangled.as_str()) {
1044            return Some(exported);
1045        }
1046        let (module, types) = self.by_module.get_key_value(reference.package.as_str())?;
1047        Some((*types.get(reference.name.as_str())?, module))
1048    }
1049}
1050
1051/// A by-name type as a declaration refers to it: the declaring symbol's
1052/// mangled name and package, and the name the rendered text uses for it,
1053/// which is the local name an aliased import gave it.
1054struct TypeReference {
1055    mangled: String,
1056    package: String,
1057    name: String,
1058}
1059
1060/// The imports one `declare module` block needs.
1061struct BlockImports<'a> {
1062    /// Keyed by the name the block's text uses, valued by the public name and
1063    /// module to import it from.
1064    types: BTreeMap<String, (&'a str, &'a str)>,
1065    /// Each class parent's mangled name, mapped to the name the block imports
1066    /// it under.
1067    parents: BTreeMap<&'a str, String>,
1068}
1069
1070/// The imports `defs`'s block needs.
1071///
1072/// A type reference is imported under the name the text already uses for it.
1073/// That name is the package's own if it declares a type of that name, or a
1074/// value when the borrowed type is a value too (a class or enum); TypeScript
1075/// keeps a type and a value of one name apart otherwise. Two references that
1076/// use one local name for different types, which only separate source files
1077/// can produce, get the first one's import.
1078///
1079/// A class's parent records no local name, so the block picks one: the name
1080/// an import of the same type already has, else its public name, else that
1081/// name with a numeric suffix that nothing in the block uses.
1082fn block_imports<'a>(
1083    defs: &'a PackageDeclaration,
1084    exporters: &TypeExporters<'a>,
1085) -> BlockImports<'a> {
1086    let mut types = BTreeMap::new();
1087    for reference in rendered_type_references(defs) {
1088        let Some((exported, module)) = exporters.resolve(&reference) else {
1089            continue;
1090        };
1091        if module != defs.package_name && !declares(defs, &reference.name, exported.is_value) {
1092            types
1093                .entry(reference.name)
1094                .or_insert((exported.name, module));
1095        }
1096    }
1097    let mut parents = BTreeMap::new();
1098    for (mangled, exported, module) in class_parents(defs, exporters) {
1099        if module == defs.package_name {
1100            parents.insert(mangled, exported.name.to_string());
1101            continue;
1102        }
1103        let already_imported = types
1104            .iter()
1105            .find(|(_, target)| **target == (exported.name, module))
1106            .map(|(local, _)| local.clone());
1107        let local = if let Some(local) = already_imported {
1108            local
1109        } else {
1110            let local = free_local_name(defs, &types, exported.name);
1111            types.insert(local.clone(), (exported.name, module));
1112            local
1113        };
1114        parents.insert(mangled, local);
1115    }
1116    BlockImports { types, parents }
1117}
1118
1119/// `public` if the block can import a class under it, else `public` with the
1120/// first numeric suffix nothing in the block uses. Each name the block holds
1121/// rules out at most one suffix, so one past their count is always free.
1122fn free_local_name(
1123    defs: &PackageDeclaration,
1124    imports: &BTreeMap<String, (&str, &str)>,
1125    public: &str,
1126) -> String {
1127    let is_free = |name: &str| !imports.contains_key(name) && !declares(defs, name, true);
1128    if is_free(public) {
1129        return public.to_string();
1130    }
1131    let taken = imports
1132        .len()
1133        .saturating_add(defs.types.len())
1134        .saturating_add(defs.values.len())
1135        .saturating_add(defs.namespaces.len());
1136    (1..=taken.saturating_add(1))
1137        .map(|suffix| format!("{public}{suffix}"))
1138        .find(|name| is_free(name))
1139        .unwrap_or_else(|| format!("{public}{}", taken.saturating_add(1)))
1140}
1141
1142/// Whether `defs` declares `name` itself in a way that would clash with
1143/// importing a type under it.
1144fn declares(defs: &PackageDeclaration, name: &str, borrowed_is_value: bool) -> bool {
1145    defs.types.contains_key(name)
1146        || (borrowed_is_value
1147            && (defs.values.contains_key(name) || defs.namespaces.contains_key(name)))
1148}
1149
1150/// The parent of each class `defs` exports that extends a known module's
1151/// class, by mangled name, with that class and its module.
1152fn class_parents<'a>(
1153    defs: &'a PackageDeclaration,
1154    exporters: &TypeExporters<'a>,
1155) -> Vec<(&'a str, ExportedType<'a>, &'a str)> {
1156    defs.types
1157        .values()
1158        .filter_map(|symbol| match &symbol.kind {
1159            TypeKind::Class {
1160                extends: Some(extends),
1161                ..
1162            } => {
1163                let parent = extends.parent.as_str();
1164                let &(exported, module) = exporters.by_mangled.get(parent)?;
1165                Some((parent, exported, module))
1166            }
1167            _ => None,
1168        })
1169        .collect()
1170}
1171
1172/// The by-name types the rendered declarations of `defs` mention: in its
1173/// values, namespaces, and the public surface of its types, which is what
1174/// `render_ts_declarations` prints. A private function or member doesn't
1175/// appear in the text, so its types don't count.
1176///
1177/// Every by-name `Type` serializes with its declaring symbol's `mangled` name,
1178/// so walking the serialized form finds them in every signature and member
1179/// without a visitor over each declaration shape. A reference renders as its
1180/// name and type arguments only, so the walk goes into `args` and not into an
1181/// alias's body. A class's parent isn't a `Type`; `class_parents` finds
1182/// those.
1183fn rendered_type_references(defs: &PackageDeclaration) -> Vec<TypeReference> {
1184    let types: BTreeMap<&String, TypeSymbol> = defs
1185        .types
1186        .iter()
1187        .map(|(name, symbol)| (name, public_surface(symbol)))
1188        .collect();
1189    let rendered = [
1190        serde_json::to_value(&defs.values),
1191        serde_json::to_value(&types),
1192        serde_json::to_value(&defs.namespaces),
1193    ];
1194    // A part that won't serialize still renders; it just imports nothing.
1195    let mut pending: Vec<&serde_json::Value> = rendered.iter().flatten().collect();
1196    let mut references = Vec::new();
1197    while let Some(value) = pending.pop() {
1198        match value {
1199            serde_json::Value::Object(fields) => {
1200                let Some(serde_json::Value::String(mangled)) = fields.get("mangled") else {
1201                    pending.extend(fields.values());
1202                    continue;
1203                };
1204                let text = |key: &str| {
1205                    fields
1206                        .get(key)
1207                        .and_then(serde_json::Value::as_str)
1208                        .unwrap_or_default()
1209                        .to_string()
1210                };
1211                references.push(TypeReference {
1212                    mangled: mangled.clone(),
1213                    package: text("package"),
1214                    name: text("name"),
1215                });
1216                pending.extend(fields.get("args"));
1217            }
1218            serde_json::Value::Array(items) => pending.extend(items),
1219            _ => {}
1220        }
1221    }
1222    references
1223}
1224
1225/// `symbol` as `render_ts_type` prints it: a class keeps only its public
1226/// members.
1227fn public_surface(symbol: &TypeSymbol) -> TypeSymbol {
1228    let mut symbol = symbol.clone();
1229    if let TypeKind::Class {
1230        fields,
1231        narrowing_checks,
1232        methods,
1233        method_visibility,
1234        accessors,
1235        constructor,
1236        constructor_visibility,
1237        statics,
1238        static_visibility,
1239        static_fields,
1240        ..
1241    } = &mut symbol.kind
1242    {
1243        if *constructor_visibility == crate::Visibility::Private {
1244            constructor.clear();
1245        }
1246        let is_public = |visibility: Option<&crate::Visibility>| {
1247            visibility != Some(&crate::Visibility::Private)
1248        };
1249        accessors.retain(|accessor| is_public(fields.get(accessor.name()).map(|f| &f.visibility)));
1250        fields.retain(|_, field| is_public(Some(&field.visibility)));
1251        static_fields.retain(|_, field| is_public(Some(&field.visibility)));
1252        methods.retain(|name, _| is_public(method_visibility.get(name)));
1253        statics.retain(|name, _| is_public(static_visibility.get(name)));
1254        narrowing_checks.clear();
1255    }
1256    symbol
1257}
1258
1259/// Render editor-facing TypeScript ambient declarations for always-in-scope
1260/// built-ins.
1261pub fn render_lib_submilli_d_ts() -> String {
1262    let defs = builtin_package_declaration();
1263    let globals = global_classes();
1264    let parent = |extends: &ClassExtends| {
1265        let global = globals.get(extends.parent.as_str())?;
1266        Some(ts_named_type(global, &extends.args))
1267    };
1268    let mut out = String::new();
1269    render_ts_declarations(&mut out, &defs, "", "declare ", &parent);
1270    render_ts_compiler_globals(&mut out, &defs);
1271    let json = json_package_declaration();
1272    render_ts_declarations(&mut out, &json, "", "declare ", &|_| None);
1273    out.trim_end().to_string()
1274}
1275
1276fn render_ts_compiler_globals(out: &mut String, defs: &PackageDeclaration) {
1277    if !defs.types.contains_key("Function") {
1278        out.push_str("interface Function {}\n\n");
1279    }
1280    out.push_str("interface CallableFunction extends Function {}\n\n");
1281    out.push_str("interface NewableFunction extends Function {}\n\n");
1282    out.push_str("interface IArguments {}\n\n");
1283    out.push_str(
1284        "/** Throw `Error(message)` if `condition` is false. Compiler intrinsic — always in scope, not imported. */\n\
1285         declare function assert(condition: boolean, message?: string): void;\n\n",
1286    );
1287    render_ts_checker_plumbing(out, defs);
1288}
1289
1290// Members the TS checker requires structurally but submilli handles
1291// intrinsically, so the prelude declarations lack them: with `target` ≥
1292// es2015 `for-of` needs `[Symbol.iterator]()` (submilli's protocol is a
1293// plain `iterator()` method; arrays iterate at the language level), and
1294// `arr[i]` needs an index signature. Merged into `Array` and into every
1295// prelude interface declaring `iterator()`. Users never write these; only
1296// the checker reads them.
1297fn render_ts_checker_plumbing(out: &mut String, defs: &PackageDeclaration) {
1298    out.push_str("declare const Symbol: { readonly iterator: unique symbol };\n\n");
1299    out.push_str(
1300        "interface Array<T> {\n  [index: number]: T;\n  [Symbol.iterator](): Iterator<T>;\n}\n\n",
1301    );
1302    for (name, sym) in &defs.types {
1303        if let TypeKind::Interface {
1304            generics, methods, ..
1305        } = &sym.kind
1306            && let Some(m) = methods.get("iterator")
1307        {
1308            let _ = writeln!(
1309                out,
1310                "interface {name}{} {{\n  [Symbol.iterator](): {};\n}}\n",
1311                ts_interface_generics(name, generics, "declare "),
1312                ts_type(&m.ret)
1313            );
1314        }
1315    }
1316}
1317
1318fn render_declare_module(out: &mut String, defs: &PackageDeclaration) {
1319    let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
1320    render_ts_declarations(out, defs, "  ", "export ", &|_| None);
1321    let _ = writeln!(out, "}}\n");
1322}
1323
1324/// `class_parent` renders a class's `extends` target, or declines, in which
1325/// case the class renders without one.
1326fn render_ts_declarations(
1327    out: &mut String,
1328    defs: &PackageDeclaration,
1329    indent: &str,
1330    export_prefix: &str,
1331    class_parent: &dyn Fn(&ClassExtends) -> Option<String>,
1332) {
1333    for (name, sym) in &defs.values {
1334        if is_hidden_prelude_value(&defs.package_name, name) {
1335            continue;
1336        }
1337        push_doc(out, value_doc(&sym.kind), indent);
1338        render_ts_value(out, name, &sym.kind, indent, export_prefix);
1339    }
1340    // Only the runtime's own modules pair a type with an `XConstructor`
1341    // interface for its static side; in a package they're two ordinary types.
1342    let pairs_constructors = defs.package_name.starts_with("submilli:");
1343    for (name, sym) in &defs.types {
1344        if is_hidden_prelude_type(&defs.package_name, name) {
1345            continue;
1346        }
1347        if pairs_constructors && name.ends_with("Constructor") {
1348            continue;
1349        }
1350        if pairs_constructors && defs.types.contains_key(&format!("{name}Constructor")) {
1351            render_ts_type_with_ctor(
1352                out,
1353                &defs.types,
1354                name,
1355                indent,
1356                export_prefix,
1357                !defs.values.contains_key(name),
1358            );
1359        } else {
1360            push_doc(out, type_doc(&sym.kind), indent);
1361            let parent = match &sym.kind {
1362                TypeKind::Class {
1363                    extends: Some(extends),
1364                    ..
1365                } => class_parent(extends),
1366                _ => None,
1367            };
1368            render_ts_type(
1369                out,
1370                name,
1371                &sym.kind,
1372                indent,
1373                export_prefix,
1374                parent.as_deref(),
1375            );
1376        }
1377    }
1378    for (name, ns) in &defs.namespaces {
1379        render_ts_namespace(out, name, ns, indent, export_prefix);
1380    }
1381}
1382
1383fn is_hidden_prelude_value(package_name: &str, name: &str) -> bool {
1384    package_name == crate::mangle::PRELUDE_PACKAGE
1385        && (name.starts_with("string_")
1386            || matches!(
1387                name,
1388                "string_concat" | "string_eq" | "string_length" | "string_cmp"
1389            ))
1390}
1391
1392fn is_hidden_prelude_type(package_name: &str, name: &str) -> bool {
1393    package_name == crate::mangle::PRELUDE_PACKAGE && name.contains('#')
1394}
1395
1396fn render_ts_value(
1397    out: &mut String,
1398    name: &str,
1399    kind: &ValueKind,
1400    indent: &str,
1401    export_prefix: &str,
1402) {
1403    let reserved_export = export_prefix == "export " && is_ts_reserved_word(name);
1404    let declared_name = if reserved_export {
1405        format!("{name}_")
1406    } else {
1407        name.to_string()
1408    };
1409    let decl_export_prefix = if reserved_export { "" } else { export_prefix };
1410    match kind {
1411        ValueKind::Function {
1412            generics,
1413            params,
1414            ret,
1415            type_predicate,
1416            ..
1417        } => {
1418            let ret = ts_return_type(
1419                params,
1420                ret,
1421                type_predicate.as_ref(),
1422                value_doc(kind).as_ref(),
1423            );
1424            let _ = writeln!(
1425                out,
1426                "{indent}{decl_export_prefix}function {declared_name}{}({}): {ret};",
1427                generics_str(generics),
1428                ts_params_str(params, value_doc(kind).as_ref())
1429            );
1430        }
1431        ValueKind::Let { ty, .. } => {
1432            let _ = writeln!(
1433                out,
1434                "{indent}{decl_export_prefix}let {declared_name}: {};",
1435                ts_type(ty)
1436            );
1437        }
1438        ValueKind::Const { ty, .. } => {
1439            let _ = writeln!(
1440                out,
1441                "{indent}{decl_export_prefix}const {declared_name}: {};",
1442                ts_type(ty)
1443            );
1444        }
1445    }
1446    if reserved_export {
1447        let _ = writeln!(out, "{indent}export {{ {declared_name} as {name} }};");
1448    }
1449    out.push('\n');
1450}
1451
1452/// `parent` is the rendered `extends` target of a class, if it has one.
1453fn render_ts_type(
1454    out: &mut String,
1455    name: &str,
1456    kind: &TypeKind,
1457    indent: &str,
1458    export_prefix: &str,
1459    parent: Option<&str>,
1460) {
1461    let inner = format!("{indent}  ");
1462    match kind {
1463        TypeKind::Interface {
1464            generics,
1465            methods,
1466            properties,
1467            index,
1468            ..
1469        } => {
1470            let _ = writeln!(
1471                out,
1472                "{indent}{export_prefix}interface {name}{} {{",
1473                ts_interface_generics(name, generics, export_prefix)
1474            );
1475            if let Some(index) = index {
1476                let ro = if index.readonly { "readonly " } else { "" };
1477                let _ = writeln!(out, "{inner}{ro}[key: string]: {};", ts_type(&index.value));
1478            }
1479            for (pname, prop) in properties {
1480                push_doc(out, &prop.doc, &inner);
1481                let ro = if prop.readonly { "readonly " } else { "" };
1482                let opt = if prop.optional { "?" } else { "" };
1483                let pname = member_name(pname);
1484                let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", ts_type(&prop.ty));
1485            }
1486            for (mname, m) in methods {
1487                push_doc(out, &m.doc, &inner);
1488                let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1489                let optional = if m.optional { "?" } else { "" };
1490                let mname = method_prefix(mname);
1491                let _ = writeln!(
1492                    out,
1493                    "{inner}{mname}{optional}{}({}): {ret};",
1494                    generics_str(&m.generics),
1495                    ts_params_str(&m.params, m.doc.as_ref())
1496                );
1497            }
1498            let _ = writeln!(out, "{indent}}}\n");
1499        }
1500        TypeKind::NumberEnum { variants, .. } => {
1501            let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1502            for (v, value) in variants {
1503                let _ = writeln!(out, "{inner}{v} = {value},");
1504            }
1505            let _ = writeln!(out, "{indent}}}\n");
1506        }
1507        TypeKind::StringEnum { variants, .. } => {
1508            let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1509            for (v, value) in variants {
1510                let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
1511            }
1512            let _ = writeln!(out, "{indent}}}\n");
1513        }
1514        TypeKind::Alias { generics, ty, .. } => {
1515            let _ = writeln!(
1516                out,
1517                "{indent}{export_prefix}type {name}{} = {};\n",
1518                generics_str(generics),
1519                ts_type(ty)
1520            );
1521        }
1522        TypeKind::Class {
1523            generics,
1524            fields,
1525            methods,
1526            method_visibility,
1527            statics,
1528            static_visibility,
1529            static_fields,
1530            accessors,
1531            constructor,
1532            constructor_visibility,
1533            ..
1534        } => {
1535            // Private members are part of the in-memory class but never the public
1536            // API an agent can call, so they're omitted from the rendered surface
1537            // (`packages.docs` and the generated `.d.ts`). Privacy itself is enforced
1538            // by the typechecker; this only keeps private names/types out of the docs.
1539            let extends = parent.map(|parent| format!(" extends {parent}"));
1540            let _ = writeln!(
1541                out,
1542                "{indent}{export_prefix}class {name}{}{} {{",
1543                generics_str(generics),
1544                extends.unwrap_or_default()
1545            );
1546            for (fname, field) in static_fields {
1547                if field.visibility == crate::Visibility::Private {
1548                    continue;
1549                }
1550                push_doc(out, &field.doc, &inner);
1551                let ro = if field.readonly { "readonly " } else { "" };
1552                let fname = member_name(fname);
1553                let _ = writeln!(out, "{inner}static {ro}{fname}: {};", ts_type(&field.ty));
1554            }
1555            for (mname, m) in statics {
1556                if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
1557                    continue;
1558                }
1559                push_doc(out, &m.doc, &inner);
1560                let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1561                let mname = member_name(mname);
1562                let _ = writeln!(
1563                    out,
1564                    "{inner}static {mname}{}({}): {ret};",
1565                    generics_str(&m.generics),
1566                    ts_params_str(&m.params, m.doc.as_ref())
1567                );
1568            }
1569            for (fname, field) in fields {
1570                if field.visibility == crate::Visibility::Private
1571                    || accessors.iter().any(|a| a.name() == fname)
1572                {
1573                    continue;
1574                }
1575                push_doc(out, &field.doc, &inner);
1576                let ro = if field.readonly { "readonly " } else { "" };
1577                let opt = if field.optional { "?" } else { "" };
1578                let fname = member_name(fname);
1579                let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", ts_type(&field.ty));
1580            }
1581            render_class_accessors(out, fields, accessors, &inner, ts_type);
1582            // A private constructor still renders, so a TypeScript consumer can't
1583            // call an implicit public one, but as `protected`: a subclass in the
1584            // class's own module is legal here, and tsc rejects `extends` of a
1585            // class whose constructor is `private`. Its parameters stay hidden. The
1586            // cost: tsc lets a consumer extend it, which Submilli rejects.
1587            if *constructor_visibility == crate::Visibility::Private {
1588                let _ = writeln!(out, "{inner}protected constructor();");
1589            } else {
1590                let _ = writeln!(
1591                    out,
1592                    "{inner}constructor({});",
1593                    ts_params_str(constructor, None)
1594                );
1595            }
1596            for (mname, m) in methods {
1597                if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
1598                    continue;
1599                }
1600                push_doc(out, &m.doc, &inner);
1601                let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1602                let optional = if m.optional { "?" } else { "" };
1603                let mname = member_name(mname);
1604                let _ = writeln!(
1605                    out,
1606                    "{inner}{mname}{optional}{}({}): {ret};",
1607                    generics_str(&m.generics),
1608                    ts_params_str(&m.params, m.doc.as_ref())
1609                );
1610            }
1611            let _ = writeln!(out, "{indent}}}\n");
1612        }
1613    }
1614}
1615
1616/// Render a class's accessor properties for the public surface (docs / `.d.ts`).
1617/// Same-typed get+set renders as a writable property, get-only as `readonly`,
1618/// and a write-only or differently-typed pair as explicit `get`/`set` lines.
1619/// Private accessors (visibility lives on the property's `FieldSig`) are skipped.
1620fn render_class_accessors(
1621    out: &mut String,
1622    fields: &std::collections::BTreeMap<String, crate::FieldSig>,
1623    accessors: &[crate::AccessorSig],
1624    inner: &str,
1625    fmt_ty: impl Fn(&Type) -> String,
1626) {
1627    use crate::AccessorSig;
1628    let mut by_name: std::collections::BTreeMap<&str, (Option<&Type>, Option<&crate::Param>)> =
1629        std::collections::BTreeMap::new();
1630    for acc in accessors {
1631        let entry = by_name.entry(acc.name()).or_insert((None, None));
1632        match acc {
1633            AccessorSig::Getter { ret_ty, .. } => entry.0 = Some(ret_ty),
1634            AccessorSig::Setter { param, .. } => entry.1 = Some(param),
1635        }
1636    }
1637    for (name, (get, set)) in by_name {
1638        if fields.get(name).map(|f| f.visibility) == Some(crate::Visibility::Private) {
1639            continue;
1640        }
1641        let name = member_name(name);
1642        match (get, set) {
1643            (Some(r), Some(p)) if r == &p.ty => {
1644                let _ = writeln!(out, "{inner}{name}: {};", fmt_ty(r));
1645            }
1646            (Some(r), Some(p)) => {
1647                let _ = writeln!(out, "{inner}get {name}(): {};", fmt_ty(r));
1648                let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1649            }
1650            (Some(r), None) => {
1651                let _ = writeln!(out, "{inner}readonly {name}: {};", fmt_ty(r));
1652            }
1653            (None, Some(p)) => {
1654                let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1655            }
1656            (None, None) => {}
1657        }
1658    }
1659}
1660
1661fn render_ts_type_with_ctor(
1662    out: &mut String,
1663    types: &BTreeMap<String, TypeSymbol>,
1664    name: &str,
1665    indent: &str,
1666    export_prefix: &str,
1667    synthesize_binding: bool,
1668) {
1669    if let Some(sym) = types.get(name) {
1670        push_doc(out, type_doc(&sym.kind), indent);
1671        render_ts_type(out, name, &sym.kind, indent, export_prefix, None);
1672    }
1673    let ctor = format!("{name}Constructor");
1674    if let Some(sym) = types.get(&ctor) {
1675        push_doc(out, type_doc(&sym.kind), indent);
1676        render_ts_type(out, &ctor, &sym.kind, indent, export_prefix, None);
1677        if synthesize_binding {
1678            let _ = writeln!(out, "{indent}{export_prefix}const {name}: {ctor};\n");
1679        }
1680    }
1681}
1682
1683fn render_ts_namespace(
1684    out: &mut String,
1685    name: &str,
1686    ns: &NamespaceSymbol,
1687    indent: &str,
1688    export_prefix: &str,
1689) {
1690    push_doc(out, &ns.doc, indent);
1691    let _ = writeln!(out, "{indent}{export_prefix}namespace {name} {{");
1692    let inner = format!("{indent}  ");
1693    for (vname, sym) in &ns.values {
1694        push_doc(out, value_doc(&sym.kind), &inner);
1695        render_ts_value(out, vname, &sym.kind, &inner, "");
1696    }
1697    for (tname, sym) in &ns.types {
1698        push_doc(out, type_doc(&sym.kind), &inner);
1699        render_ts_type(out, tname, &sym.kind, &inner, "", None);
1700    }
1701    for (nname, sub) in &ns.namespaces {
1702        render_ts_namespace(out, nname, sub, &inner, "");
1703    }
1704    let _ = writeln!(out, "{indent}}}\n");
1705}
1706
1707fn ts_params_str(params: &[Param], doc: Option<&DocComment>) -> String {
1708    rendered_params(params, doc, ts_type)
1709}
1710
1711fn ts_return_type(
1712    params: &[Param],
1713    ret: &Type,
1714    predicate: Option<&TypePredicate>,
1715    doc: Option<&DocComment>,
1716) -> String {
1717    let Some(predicate) = predicate else {
1718        return ts_type(ret);
1719    };
1720    let Some(param) = params.get(predicate.parameter_index as usize) else {
1721        return ts_type(ret);
1722    };
1723    let names = parameter_display_names(params, doc);
1724    let Some(name) = names.get(predicate.parameter_index as usize) else {
1725        return ts_type(ret);
1726    };
1727    let asserted = ts_type(&predicate.asserted_type);
1728    match param.ty {
1729        Type::TypeVar(_) | Type::GenericParam { .. } => {
1730            format!("{name} is {} & {asserted}", ts_type(&param.ty))
1731        }
1732        _ => format!("{name} is {asserted}"),
1733    }
1734}
1735
1736fn ts_type(ty: &Type) -> String {
1737    match ty {
1738        Type::Refined { original, ty } => format!("({} & {})", ts_type(original), ts_type(ty)),
1739        Type::Number => "number".to_string(),
1740        Type::NumberLiteral(n) => n.0.to_string(),
1741        Type::BigInt => "bigint".to_string(),
1742        Type::BigIntLiteral(digits) => format!("{digits}n"),
1743        Type::String => "string".to_string(),
1744        Type::StringLiteral(s) => format!("\"{}\"", escape_string_literal(s)),
1745        Type::Uint8Array => "Uint8Array".to_string(),
1746        Type::Boolean => "boolean".to_string(),
1747        Type::BooleanLiteral(value) => value.to_string(),
1748        Type::Null => "null".to_string(),
1749        Type::Undefined => "undefined".to_string(),
1750        Type::Void => "void".to_string(),
1751        Type::Unknown => "unknown".to_string(),
1752        Type::Function {
1753            params,
1754            ret,
1755            predicate,
1756            has_rest,
1757            optional,
1758        } => {
1759            let params = params
1760                .iter()
1761                .enumerate()
1762                .map(|(i, p)| {
1763                    let prefix = if *has_rest && i == params.len().saturating_sub(1) {
1764                        "..."
1765                    } else {
1766                        ""
1767                    };
1768                    let fixed = params.len().saturating_sub(usize::from(*has_rest));
1769                    if i < fixed && i >= fixed.saturating_sub(*optional) {
1770                        let shown = crate::types::shown_beside_optional_marker(p);
1771                        format!("{prefix}arg{i}?: {}", ts_type(&shown))
1772                    } else {
1773                        format!("{prefix}arg{i}: {}", ts_type(p))
1774                    }
1775                })
1776                .collect::<Vec<_>>()
1777                .join(", ");
1778            let ret = predicate.as_deref().map_or_else(
1779                || ts_type(ret),
1780                |p| format!("arg{} is {}", p.parameter_index, ts_type(&p.asserted_type)),
1781            );
1782            format!("({params}) => {ret}")
1783        }
1784        Type::Object { fields, index } => {
1785            let mut members: Vec<String> = fields
1786                .iter()
1787                .map(|(name, field)| {
1788                    let opt = if field.optional { "?" } else { "" };
1789                    let ro = if field.readonly { "readonly " } else { "" };
1790                    let name = member_name(name);
1791                    format!("{ro}{name}{opt}: {}", ts_type(&field.ty))
1792                })
1793                .collect();
1794            if let Some(index) = index {
1795                let ro = if index.readonly { "readonly " } else { "" };
1796                members.push(format!("{ro}[key: string]: {}", ts_type(&index.value)));
1797            }
1798            if members.is_empty() {
1799                "{}".into()
1800            } else {
1801                format!("{{ {} }}", members.join("; "))
1802            }
1803        }
1804        Type::Array(elem) => format!("{}[]", ts_type_array_element(elem)),
1805        Type::Tuple(elements) => {
1806            let elements = elements
1807                .iter()
1808                .enumerate()
1809                .map(|(index, ty)| {
1810                    if index >= elements.len().saturating_sub(elements.optional) {
1811                        format!("({})?", ts_type(ty))
1812                    } else {
1813                        ts_type(ty)
1814                    }
1815                })
1816                .collect::<Vec<_>>()
1817                .join(", ");
1818            format!("[{elements}]")
1819        }
1820        Type::Readonly(inner) => format!("readonly {}", ts_type(inner)),
1821        Type::Error => "never".to_string(),
1822        Type::Never => "never".to_string(),
1823        Type::TypeVar(name) | Type::GenericParam { name, .. } => name.clone(),
1824        Type::InterfaceRef { name, args, .. }
1825        | Type::ClassRef { name, args, .. }
1826        | Type::Alias { name, args, .. } => ts_named_type(name, args),
1827        Type::NumberEnum { name, .. } | Type::StringEnum { name, .. } => name.clone(),
1828        Type::Union(members) => crate::types::union_display_order(members)
1829            .into_iter()
1830            .map(ts_type_union_member)
1831            .collect::<Vec<_>>()
1832            .join(" | "),
1833        Type::AliasRef { name, args, .. } => ts_named_type(name, args),
1834    }
1835}
1836
1837fn ts_named_type(name: &str, args: &[Type]) -> String {
1838    if args.is_empty() {
1839        return name.to_string();
1840    }
1841    let args = args.iter().map(ts_type).collect::<Vec<_>>().join(", ");
1842    format!("{name}<{args}>")
1843}
1844
1845fn ts_type_array_element(ty: &Type) -> String {
1846    match ty {
1847        Type::Function { .. } | Type::Union(_) | Type::Readonly(_) => format!("({})", ts_type(ty)),
1848        _ => ts_type(ty),
1849    }
1850}
1851
1852fn ts_type_union_member(ty: &Type) -> String {
1853    match ty {
1854        Type::Function { .. } => format!("({})", ts_type(ty)),
1855        _ => ts_type(ty),
1856    }
1857}
1858
1859fn is_ts_reserved_word(name: &str) -> bool {
1860    matches!(
1861        name,
1862        "break"
1863            | "case"
1864            | "catch"
1865            | "class"
1866            | "const"
1867            | "continue"
1868            | "debugger"
1869            | "default"
1870            | "delete"
1871            | "do"
1872            | "else"
1873            | "enum"
1874            | "export"
1875            | "extends"
1876            | "false"
1877            | "finally"
1878            | "for"
1879            | "function"
1880            | "if"
1881            | "import"
1882            | "in"
1883            | "instanceof"
1884            | "new"
1885            | "null"
1886            | "return"
1887            | "super"
1888            | "switch"
1889            | "this"
1890            | "throw"
1891            | "true"
1892            | "try"
1893            | "typeof"
1894            | "var"
1895            | "void"
1896            | "while"
1897            | "with"
1898            | "as"
1899            | "async"
1900            | "await"
1901            | "from"
1902            | "get"
1903            | "let"
1904            | "of"
1905            | "set"
1906            | "static"
1907            | "using"
1908            | "yield"
1909    )
1910}
1911
1912fn json_package_declaration() -> PackageDeclaration {
1913    let mut defs = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
1914    let json_prefix = crate::mangle::prelude(JSON_BUILTIN);
1915    let mut json = NamespaceSymbol {
1916        name: JSON_BUILTIN.to_string(),
1917        mangled_prefix: json_prefix.clone(),
1918        declaration_span: Span::at(FileId::JSON),
1919        values: BTreeMap::new(),
1920        types: BTreeMap::new(),
1921        namespaces: BTreeMap::new(),
1922        doc: None,
1923    };
1924    json.values.insert(
1925        "stringify".to_string(),
1926        ValueSymbol {
1927            name: "stringify".to_string(),
1928            mangled_name: crate::mangle::extend(&json_prefix, "stringify"),
1929            declaration_span: Span::at(FileId::JSON),
1930            kind: ValueKind::Function {
1931                generics: vec!["T".to_string()],
1932                params: vec![
1933                    Param::new("value", Type::TypeVar("T".to_string())),
1934                    Param {
1935                        name: "replacer".to_string(),
1936                        ty: Type::union(vec![Type::Null, Type::Undefined]),
1937                        default: Some(crate::DefaultValue::Undefined),
1938                        optional: false,
1939                        rest: false,
1940                    },
1941                    Param {
1942                        name: "space".to_string(),
1943                        ty: Type::union(vec![Type::Number, Type::String, Type::Null, Type::Undefined]),
1944                        default: Some(crate::DefaultValue::Undefined),
1945                        optional: false,
1946                        rest: false,
1947                    },
1948                ],
1949                ret: Type::union(vec![Type::String, Type::Undefined]),
1950                type_predicate: None,
1951                doc: crate::doc(FileId::JSON, "/** Serialize a value to a JSON string, or return undefined when it has no JSON representation. */"),
1952            },
1953        },
1954    );
1955    json.values.insert(
1956        "parse".to_string(),
1957        ValueSymbol {
1958            name: "parse".to_string(),
1959            mangled_name: crate::mangle::extend(&json_prefix, "parse"),
1960            declaration_span: Span::at(FileId::JSON),
1961            kind: ValueKind::Function {
1962                generics: Vec::new(),
1963                params: vec![Param::new("text", Type::String)],
1964                ret: Type::Unknown,
1965                type_predicate: None,
1966                doc: crate::doc(
1967                    FileId::JSON,
1968                    "/** Parse a JSON string as unknown; use `JSON.parse(s) as T` to validate a target type. */",
1969                ),
1970            },
1971        },
1972    );
1973    defs.namespaces.insert(JSON_BUILTIN.to_string(), json);
1974    defs
1975}
1976
1977fn render_value(out: &mut String, name: &str, kind: &ValueKind, indent: &str) {
1978    match kind {
1979        ValueKind::Function {
1980            generics,
1981            params,
1982            ret,
1983            ..
1984        } => {
1985            let _ = writeln!(
1986                out,
1987                "{indent}function {name}{}({}): {ret};\n",
1988                generics_str(generics),
1989                params_str(params, value_doc(kind).as_ref())
1990            );
1991        }
1992        ValueKind::Let { ty, .. } => {
1993            let _ = writeln!(out, "{indent}let {name}: {ty};\n");
1994        }
1995        ValueKind::Const { ty, .. } => {
1996            let _ = writeln!(out, "{indent}const {name}: {ty};\n");
1997        }
1998    }
1999}
2000
2001fn render_type(out: &mut String, name: &str, kind: &TypeKind, indent: &str) {
2002    let inner = format!("{indent}  ");
2003    match kind {
2004        TypeKind::Interface {
2005            generics,
2006            methods,
2007            properties,
2008            ..
2009        } => {
2010            let _ = writeln!(out, "{indent}interface {name}{} {{", generics_str(generics));
2011            for (pname, prop) in properties {
2012                push_doc(out, &prop.doc, &inner);
2013                let ro = if prop.readonly { "readonly " } else { "" };
2014                let opt = if prop.optional { "?" } else { "" };
2015                let pname = member_name(pname);
2016                let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", prop.ty);
2017            }
2018            for (mname, m) in methods {
2019                push_doc(out, &m.doc, &inner);
2020                let mname = method_prefix(mname);
2021                let _ = writeln!(
2022                    out,
2023                    "{inner}{mname}{}{}({}): {};",
2024                    if m.optional { "?" } else { "" },
2025                    generics_str(&m.generics),
2026                    params_str(&m.params, m.doc.as_ref()),
2027                    m.ret
2028                );
2029            }
2030            let _ = writeln!(out, "{indent}}}\n");
2031        }
2032        TypeKind::NumberEnum { variants, .. } => {
2033            let _ = writeln!(out, "{indent}enum {name} {{");
2034            for (v, value) in variants {
2035                let _ = writeln!(out, "{inner}{v} = {value},");
2036            }
2037            let _ = writeln!(out, "{indent}}}\n");
2038        }
2039        TypeKind::StringEnum { variants, .. } => {
2040            let _ = writeln!(out, "{indent}enum {name} {{");
2041            for (v, value) in variants {
2042                let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
2043            }
2044            let _ = writeln!(out, "{indent}}}\n");
2045        }
2046        TypeKind::Alias { generics, ty, .. } => {
2047            let _ = writeln!(
2048                out,
2049                "{indent}type {name}{} = {ty};\n",
2050                generics_str(generics)
2051            );
2052        }
2053        TypeKind::Class {
2054            generics,
2055            fields,
2056            methods,
2057            method_visibility,
2058            statics,
2059            static_visibility,
2060            static_fields,
2061            accessors,
2062            constructor,
2063            constructor_visibility,
2064            ..
2065        } => {
2066            // Private members exist in the class but are never callable from a
2067            // consumer, so they're omitted from the rendered API surface
2068            // (`packages.docs`). Privacy is enforced by the typechecker; this only
2069            // keeps private names/types out of the agent-facing docs.
2070            let _ = writeln!(out, "{indent}class {name}{} {{", generics_str(generics));
2071            for (fname, field) in static_fields {
2072                if field.visibility == crate::Visibility::Private {
2073                    continue;
2074                }
2075                push_doc(out, &field.doc, &inner);
2076                let ro = if field.readonly { "readonly " } else { "" };
2077                let fname = member_name(fname);
2078                let _ = writeln!(out, "{inner}static {ro}{fname}: {};", field.ty);
2079            }
2080            for (mname, m) in statics {
2081                if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
2082                    continue;
2083                }
2084                push_doc(out, &m.doc, &inner);
2085                let mname = member_name(mname);
2086                let _ = writeln!(
2087                    out,
2088                    "{inner}static {mname}{}({}): {};",
2089                    generics_str(&m.generics),
2090                    params_str(&m.params, m.doc.as_ref()),
2091                    m.ret
2092                );
2093            }
2094            for (fname, field) in fields {
2095                if field.visibility == crate::Visibility::Private
2096                    || accessors.iter().any(|a| a.name() == fname)
2097                {
2098                    continue;
2099                }
2100                push_doc(out, &field.doc, &inner);
2101                let ro = if field.readonly { "readonly " } else { "" };
2102                let opt = if field.optional { "?" } else { "" };
2103                let fname = member_name(fname);
2104                let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", field.ty);
2105            }
2106            render_class_accessors(out, fields, accessors, &inner, ToString::to_string);
2107            if *constructor_visibility != crate::Visibility::Private {
2108                let _ = writeln!(
2109                    out,
2110                    "{inner}constructor({});",
2111                    params_str(constructor, None)
2112                );
2113            }
2114            for (mname, m) in methods {
2115                if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
2116                    continue;
2117                }
2118                push_doc(out, &m.doc, &inner);
2119                let mname = member_name(mname);
2120                let _ = writeln!(
2121                    out,
2122                    "{inner}{mname}{}{}({}): {};",
2123                    if m.optional { "?" } else { "" },
2124                    generics_str(&m.generics),
2125                    params_str(&m.params, m.doc.as_ref()),
2126                    m.ret
2127                );
2128            }
2129            let _ = writeln!(out, "{indent}}}\n");
2130        }
2131    }
2132}
2133
2134/// Render `name`'s interface followed by its `{name}Constructor` (the `new` /
2135/// static side, e.g. `Array.isArray`) and the `const name: nameConstructor`
2136/// binding that ties them — the lib.d.ts shape an LLM expects. Either half may
2137/// be absent.
2138fn render_type_with_ctor(
2139    out: &mut String,
2140    types: &BTreeMap<String, TypeSymbol>,
2141    name: &str,
2142    indent: &str,
2143) {
2144    if let Some(sym) = types.get(name) {
2145        push_doc(out, type_doc(&sym.kind), indent);
2146        render_type(out, name, &sym.kind, indent);
2147    }
2148    let ctor = format!("{name}Constructor");
2149    if let Some(sym) = types.get(&ctor) {
2150        push_doc(out, type_doc(&sym.kind), indent);
2151        render_type(out, &ctor, &sym.kind, indent);
2152        let _ = writeln!(out, "{indent}const {name}: {ctor};\n");
2153    }
2154}
2155
2156fn render_namespace(out: &mut String, name: &str, ns: &NamespaceSymbol, indent: &str) {
2157    let _ = writeln!(out, "{indent}namespace {name} {{");
2158    let inner = format!("{indent}  ");
2159    for (vname, sym) in &ns.values {
2160        push_doc(out, value_doc(&sym.kind), &inner);
2161        render_value(out, vname, &sym.kind, &inner);
2162    }
2163    for (tname, sym) in &ns.types {
2164        // The namespace's own values already bind each `XConstructor` to `X`,
2165        // so render every type (constructors included) plainly — no merge.
2166        push_doc(out, type_doc(&sym.kind), &inner);
2167        render_type(out, tname, &sym.kind, &inner);
2168    }
2169    for (nname, sub) in &ns.namespaces {
2170        render_namespace(out, nname, sub, &inner);
2171    }
2172    let _ = writeln!(out, "{indent}}}\n");
2173}
2174
2175fn generics_str(generics: &[String]) -> String {
2176    if generics.is_empty() {
2177        String::new()
2178    } else {
2179        format!("<{}>", generics.join(", "))
2180    }
2181}
2182
2183// TS ≥5.6 hard-codes that the global `Iterable`/`Iterator` types take three
2184// type parameters; phantom defaults satisfy the checker while keeping
2185// single-argument usage (`Iterable<T>`) valid.
2186fn ts_interface_generics(name: &str, generics: &[String], export_prefix: &str) -> String {
2187    if export_prefix == "declare "
2188        && matches!(name, "Iterable" | "Iterator")
2189        && let [t] = generics
2190    {
2191        return format!("<{t}, TReturn = any, TNext = any>");
2192    }
2193    generics_str(generics)
2194}
2195
2196fn params_str(params: &[Param], doc: Option<&DocComment>) -> String {
2197    rendered_params(params, doc, ToString::to_string)
2198}
2199
2200fn rendered_params(
2201    params: &[Param],
2202    doc: Option<&DocComment>,
2203    format_type: impl Fn(&Type) -> String,
2204) -> String {
2205    let names = parameter_display_names(params, doc);
2206    params
2207        .iter()
2208        .zip(names)
2209        .enumerate()
2210        .map(|(index, (p, name))| {
2211            let prefix = if p.rest { "..." } else { "" };
2212            // A parameter a required one follows can't be written `x?`.
2213            let later_required = params
2214                .iter()
2215                .skip(index.saturating_add(1))
2216                .any(|later| !later.is_omittable() && !later.rest);
2217            let opt = if p.is_omittable() && !later_required {
2218                "?"
2219            } else {
2220                ""
2221            };
2222            // `x?: T` already admits `undefined`; printing it again is noise.
2223            let ty = if opt.is_empty() {
2224                p.ty.clone()
2225            } else {
2226                crate::types::shown_beside_optional_marker(&p.ty)
2227            };
2228            format!("{prefix}{name}{opt}: {}", format_type(&ty))
2229        })
2230        .collect::<Vec<_>>()
2231        .join(", ")
2232}
2233
2234/// Binding patterns are lowered to compiler-only names. Documentation uses a
2235/// positional JSDoc name when valid, or a unique positional placeholder.
2236fn parameter_display_names(params: &[Param], doc: Option<&DocComment>) -> Vec<String> {
2237    let tags: Vec<_> = doc
2238        .into_iter()
2239        .flat_map(|doc| &doc.params)
2240        .filter(|tag| !tag.name.contains('.'))
2241        .collect();
2242    let mut used: BTreeSet<String> = params
2243        .iter()
2244        .filter(|p| !crate::lower_patterns::is_pattern_param(&p.name))
2245        .map(|p| p.name.clone())
2246        .collect();
2247    params
2248        .iter()
2249        .enumerate()
2250        .map(|(index, param)| {
2251            if !crate::lower_patterns::is_pattern_param(&param.name) {
2252                return param.name.clone();
2253            }
2254            if let Some(tag) = tags.get(index)
2255                && valid_parameter_name(&tag.name)
2256                && used.insert(tag.name.clone())
2257            {
2258                return tag.name.clone();
2259            }
2260            let mut name = format!("arg{index}");
2261            while !used.insert(name.clone()) {
2262                name.push('_');
2263            }
2264            name
2265        })
2266        .collect()
2267}
2268
2269fn valid_parameter_name(name: &str) -> bool {
2270    let mut chars = name.chars();
2271    chars
2272        .next()
2273        .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '_' | '$'))
2274        && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '$'))
2275        && !is_ts_reserved_word(name)
2276        && !matches!(
2277            name,
2278            "arguments"
2279                | "eval"
2280                | "implements"
2281                | "interface"
2282                | "package"
2283                | "private"
2284                | "protected"
2285                | "public"
2286        )
2287}
2288
2289fn push_doc(out: &mut String, doc: &Option<DocComment>, indent: &str) {
2290    let Some(doc) = doc else { return };
2291    let mut lines: Vec<String> = doc.summary.lines().map(str::to_string).collect();
2292    for p in &doc.params {
2293        lines.push(format!("@param {} {}", p.name, p.description));
2294    }
2295    for cap in &doc.capabilities {
2296        let mut line = format!("@capability {}", cap.capability);
2297        if !cap.bindings.is_empty() {
2298            let bindings = cap
2299                .bindings
2300                .iter()
2301                .map(|b| {
2302                    let value = match &b.kind {
2303                        DocCapabilityBindingKind::Parameter { param, path, .. }
2304                            if *param == b.field && path.is_empty() =>
2305                        {
2306                            String::new()
2307                        }
2308                        DocCapabilityBindingKind::Parameter { param, path, .. } => {
2309                            let suffix = path
2310                                .iter()
2311                                .map(|part| format!(".{part}"))
2312                                .collect::<String>();
2313                            format!(": ${param}{suffix}")
2314                        }
2315                        DocCapabilityBindingKind::Type { name, .. } => format!(": {name}"),
2316                        DocCapabilityBindingKind::Literal { value, .. } => {
2317                            format!(": {}", literal_str(value))
2318                        }
2319                    };
2320                    format!("{}{}", b.field, value)
2321                })
2322                .collect::<Vec<_>>()
2323                .join(", ");
2324            line.push_str(&format!(" {{ {bindings} }}"));
2325        }
2326        if !cap.description.is_empty() {
2327            line.push_str(" - ");
2328            line.push_str(&cap.description);
2329        }
2330        lines.push(line);
2331    }
2332    if let Some(r) = &doc.returns {
2333        lines.push(format!("@returns {}", r.description));
2334    }
2335    if lines.is_empty() {
2336        return;
2337    }
2338    let _ = writeln!(out, "{indent}/**");
2339    for line in lines {
2340        let _ = writeln!(out, "{indent} * {line}");
2341    }
2342    let _ = writeln!(out, "{indent} */");
2343}
2344
2345fn literal_str(value: &DocCapabilityLiteral) -> String {
2346    match value {
2347        DocCapabilityLiteral::String(s) => {
2348            format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
2349        }
2350        DocCapabilityLiteral::Number(n) => n.clone(),
2351        DocCapabilityLiteral::Boolean(v) => v.to_string(),
2352        DocCapabilityLiteral::Null => "null".to_string(),
2353    }
2354}
2355
2356fn value_doc(kind: &ValueKind) -> &Option<DocComment> {
2357    match kind {
2358        ValueKind::Function { doc, .. }
2359        | ValueKind::Let { doc, .. }
2360        | ValueKind::Const { doc, .. } => doc,
2361    }
2362}
2363
2364fn type_doc(kind: &TypeKind) -> &Option<DocComment> {
2365    match kind {
2366        TypeKind::Interface { doc, .. }
2367        | TypeKind::Class { doc, .. }
2368        | TypeKind::NumberEnum { doc, .. }
2369        | TypeKind::StringEnum { doc, .. }
2370        | TypeKind::Alias { doc, .. } => doc,
2371    }
2372}
2373
2374/// What a method's declaration line starts with: nothing for a call signature
2375/// (stored as the method `@call`), `new ` for a construct signature, else its
2376/// member name.
2377fn method_prefix(name: &str) -> Cow<'_, str> {
2378    match name {
2379        "@call" => Cow::Borrowed(""),
2380        "new" => Cow::Borrowed("new "),
2381        _ => member_name(name),
2382    }
2383}
2384
2385/// A member name as declaration source: bare when it is an ASCII identifier,
2386/// else quoted and escaped, as `"a b"` or `"\uD800"` must be. A non-ASCII
2387/// identifier is quoted too, since a TypeScript built on older Unicode tables
2388/// may not read it.
2389fn member_name(name: &str) -> Cow<'_, str> {
2390    if name.is_ascii() && crate::type_rendering::is_identifier_name(name) {
2391        return Cow::Borrowed(name);
2392    }
2393    Cow::Owned(format!("\"{}\"", escape_string_literal(name)))
2394}
2395
2396#[cfg(test)]
2397mod tests {
2398    use super::*;
2399
2400    #[test]
2401    fn member_names_are_quoted_unless_identifiers() {
2402        let mut lone = String::new();
2403        crate::literal_units::push_lone_surrogate(&mut lone, 0xD800);
2404        assert_eq!(member_name("count"), "count");
2405        assert_eq!(member_name("$el_2"), "$el_2");
2406        assert_eq!(member_name("café"), "\"café\"");
2407        assert_eq!(member_name("a b"), "\"a b\"");
2408        assert_eq!(member_name("0"), "\"0\"");
2409        assert_eq!(member_name(&lone), "\"\\uD800\"");
2410    }
2411
2412    #[test]
2413    fn destructured_display_names_are_valid_and_distinct() {
2414        let params = [
2415            Param::new("#pattern_p_0", Type::Number),
2416            Param::new("arg0", Type::Number),
2417            Param::new("", Type::Number),
2418            Param::new("#pattern_p_3", Type::Number),
2419        ];
2420        let doc = crate::doc(
2421            FileId(0),
2422            "/** Names.\n * @param class Reserved.\n * @param arg0 Existing.\n * @param arg0.x Property.\n * @param pair Pair.\n * @param pair Duplicate.\n */",
2423        );
2424        assert_eq!(
2425            parameter_display_names(&params, doc.as_ref()),
2426            ["arg0_", "arg0", "pair", "arg3"]
2427        );
2428        assert_eq!(
2429            params_str(&params, doc.as_ref()),
2430            "arg0_: number, arg0: number, pair: number, arg3: number"
2431        );
2432        assert_eq!(
2433            ts_params_str(&params, doc.as_ref()),
2434            "arg0_: number, arg0: number, pair: number, arg3: number"
2435        );
2436    }
2437
2438    #[test]
2439    fn catalog_filters_before_counting_omitted_entries() {
2440        let extra = (0..CATALOG_LIMIT)
2441            .map(|index| CatalogEntry {
2442                name: format!("@acme/package{index}"),
2443                source: "registry".into(),
2444                description: String::new(),
2445            })
2446            .collect();
2447        let catalog = catalog_filtered(extra, |name| !name.starts_with("submilli:"));
2448        assert_eq!(catalog.entries.len(), CATALOG_LIMIT);
2449        assert_eq!(catalog.remaining, 0);
2450        assert!(
2451            catalog
2452                .entries
2453                .iter()
2454                .all(|entry| entry.name.starts_with("@acme/"))
2455        );
2456    }
2457
2458    #[test]
2459    fn http_docs_render_signatures() {
2460        let doc = docs("submilli:http").expect("http module");
2461        assert_eq!(doc.name, "submilli:http");
2462        assert!(!doc.description.is_empty());
2463        assert!(doc.declarations.contains("function get("));
2464        assert!(doc.declarations.contains("function post("));
2465        assert!(doc.declarations.contains("@capability http.get"));
2466        assert!(doc.declarations.contains("@capability http.download"));
2467    }
2468
2469    #[test]
2470    fn fs_and_secrets_docs_render_capabilities() {
2471        let fs = docs("submilli:fs").expect("fs module");
2472        assert!(fs.declarations.contains("@capability fs.read"));
2473        assert!(fs.declarations.contains("@capability fs.write"));
2474        assert!(fs.declarations.contains("@capability fs.list"));
2475
2476        let secrets = docs("submilli:secrets").expect("secrets module");
2477        assert!(secrets.declarations.contains("@capability secrets.get"));
2478    }
2479
2480    #[test]
2481    fn security_is_not_user_facing() {
2482        assert!(docs("submilli:security").is_none());
2483        assert!(!search("").iter().any(|m| m.name == INTERNAL_MODULE));
2484    }
2485
2486    #[test]
2487    fn search_matches_symbol_names() {
2488        let hits = search("sha256");
2489        assert!(
2490            hits.iter().any(|m| m.name == "submilli:crypto"),
2491            "{:?}",
2492            names(&hits)
2493        );
2494    }
2495
2496    #[test]
2497    fn empty_query_lists_all_user_modules() {
2498        let names = names(&search(""));
2499        assert!(names.contains(&"submilli:crypto".to_string()));
2500        assert!(names.contains(&"submilli:http".to_string()));
2501        assert!(!names.contains(&INTERNAL_MODULE.to_string()));
2502    }
2503
2504    fn names(hits: &[ModuleSummary]) -> Vec<String> {
2505        hits.iter().map(|m| m.name.clone()).collect()
2506    }
2507
2508    #[test]
2509    fn builtins_lists_headline_types_and_namespaces() {
2510        let b = builtins();
2511        for expected in ["Array", "Map", "Set", "String", "Number", "Error"] {
2512            assert!(
2513                b.types.contains(&expected.to_string()),
2514                "missing {expected}"
2515            );
2516        }
2517        // Plumbing and flattened namespace members stay out of the catalog.
2518        for hidden in [
2519            "ArrayConstructor",
2520            "Console",
2521            "Iterator",
2522            "Temporal#Instant",
2523        ] {
2524            assert!(!b.types.contains(&hidden.to_string()), "leaked {hidden}");
2525        }
2526        for ns in ["Math", "Temporal", "JSON"] {
2527            assert!(
2528                b.namespaces.contains(&ns.to_string()),
2529                "missing namespace {ns}"
2530            );
2531        }
2532    }
2533
2534    #[test]
2535    fn every_builtin_has_renderable_docs() {
2536        let b = builtins();
2537        for name in b.types.iter().chain(b.namespaces.iter()) {
2538            assert!(
2539                builtin_docs(name).is_some_and(|d| !d.is_empty()),
2540                "no docs for built-in {name}"
2541            );
2542        }
2543    }
2544
2545    #[test]
2546    fn dotted_path_returns_the_member_slice() {
2547        let slice = builtin_docs("Temporal.Instant").expect("Temporal.Instant");
2548        // The binding, the interface, and the constructor side — the whole
2549        // slice, not whichever map is walked first.
2550        assert!(slice.contains("const Instant: Temporal.InstantConstructor;"));
2551        assert!(slice.contains("interface Instant {"));
2552        assert!(slice.contains("interface InstantConstructor {"));
2553        // Wrapped in its namespace, so the reader learns `Temporal.Instant.from`.
2554        assert!(slice.starts_with("namespace Temporal {"), "{slice}");
2555        // Doc comments survive the slice.
2556        assert!(slice.contains("A point in time, to nanosecond precision"));
2557        // Members of sibling types stay out.
2558        assert!(!slice.contains("interface ZonedDateTime {"), "{slice}");
2559
2560        let full = builtin_docs("Temporal").expect("Temporal");
2561        assert!(
2562            slice.len() * 4 < full.len(),
2563            "slice {} vs full {}",
2564            slice.len(),
2565            full.len()
2566        );
2567    }
2568
2569    #[test]
2570    fn dotted_path_resolves_nested_namespaces_and_values() {
2571        let now = builtin_docs("Temporal.Now").expect("Temporal.Now");
2572        assert!(now.starts_with("namespace Temporal {"));
2573        assert!(now.contains("namespace Now {"));
2574        assert!(now.contains("function instant(): Temporal.Instant;"));
2575
2576        let one = builtin_docs("Temporal.Now.instant").expect("Temporal.Now.instant");
2577        assert!(one.contains("function instant(): Temporal.Instant;"));
2578        assert!(!one.contains("plainDateISO"), "{one}");
2579
2580        let max = builtin_docs("Math.max").expect("Math.max");
2581        assert!(max.contains("function max("), "{max}");
2582        assert!(!max.contains("function min("), "{max}");
2583
2584        let parse = builtin_docs("JSON.parse").expect("JSON.parse");
2585        assert!(parse.contains("function parse("), "{parse}");
2586        assert!(!parse.contains("function stringify("), "{parse}");
2587    }
2588
2589    #[test]
2590    fn dotted_path_resolves_members_of_a_type_head() {
2591        // `isArray` lives on the constructor side; the stub names that owner
2592        // rather than pretending the member sits on `Array` itself.
2593        let is_array = builtin_docs("Array.isArray").expect("Array.isArray");
2594        assert!(
2595            is_array.starts_with("interface ArrayConstructor"),
2596            "{is_array}"
2597        );
2598        assert!(is_array.contains("isArray"));
2599
2600        let repeat = builtin_docs("String.repeat").expect("String.repeat");
2601        assert!(repeat.starts_with("interface String"), "{repeat}");
2602        assert!(
2603            repeat.contains("repeat(count: number): string;"),
2604            "{repeat}"
2605        );
2606
2607        // A type reached through a namespace is not itself a namespace, so it
2608        // must not gain a `namespace Instant {` wrapper.
2609        let from = builtin_docs("Temporal.Instant.from").expect("Temporal.Instant.from");
2610        assert!(from.starts_with("namespace Temporal {"), "{from}");
2611        assert!(from.contains("interface InstantConstructor {"), "{from}");
2612        assert!(!from.contains("namespace Instant"), "{from}");
2613    }
2614
2615    #[test]
2616    fn every_member_a_miss_names_is_itself_resolvable() {
2617        // The miss message is a repair instruction: if it names a member, that
2618        // member must resolve. This closes the loop over every TypeKind, so a
2619        // kind the member renderer does not handle cannot pass unnoticed.
2620        let b = builtins();
2621        for name in b.types.iter().chain(b.namespaces.iter()) {
2622            let BuiltinLookup::UnknownMember { members, .. } =
2623                builtin_lookup(&format!("{name}.__definitely_not_a_member"))
2624            else {
2625                continue;
2626            };
2627            for member in members {
2628                let path = format!("{name}.{member}");
2629                assert!(
2630                    matches!(builtin_lookup(&path), BuiltinLookup::Found(_)),
2631                    "`{path}` is offered as a member of `{name}` but does not resolve"
2632                );
2633            }
2634        }
2635    }
2636
2637    #[test]
2638    fn unknown_member_lists_the_members_that_exist() {
2639        let BuiltinLookup::UnknownMember {
2640            path,
2641            member,
2642            members,
2643        } = builtin_lookup("Temporal.Foo")
2644        else {
2645            panic!("expected an unknown-member miss");
2646        };
2647        assert_eq!(path, "Temporal");
2648        assert_eq!(member, "Foo");
2649        assert!(members.contains(&"Instant".to_string()));
2650        assert!(members.contains(&"Now".to_string()));
2651        // Constructor plumbing is bound to its base name already.
2652        assert!(
2653            !members.contains(&"InstantConstructor".to_string()),
2654            "{members:?}"
2655        );
2656
2657        // A value is a leaf: it reports having no members rather than the
2658        // enclosing namespace's.
2659        let BuiltinLookup::UnknownMember { path, members, .. } = builtin_lookup("Math.max.foo")
2660        else {
2661            panic!("expected an unknown-member miss");
2662        };
2663        assert_eq!(path, "Math.max");
2664        assert!(members.is_empty());
2665    }
2666
2667    #[test]
2668    fn a_bad_member_is_reported_against_its_full_path() {
2669        for (query, expected_path) in [
2670            ("Temporal.Instant.nope", "Temporal.Instant"),
2671            ("Temporal.Now.nope", "Temporal.Now"),
2672            ("Array.nope", "Array"),
2673            ("Temporal.Instant.from.x", "Temporal.Instant.from"),
2674            // A deep path rooted at a top-level type reports the member that
2675            // actually failed, not the one that resolved.
2676            ("Array.isArray.foo", "Array.isArray"),
2677        ] {
2678            let BuiltinLookup::UnknownMember { path, member, .. } = builtin_lookup(query) else {
2679                panic!("expected an unknown-member miss for {query}");
2680            };
2681            assert_eq!(path, expected_path, "for {query}");
2682            assert!(!member.is_empty(), "for {query}");
2683        }
2684    }
2685
2686    #[test]
2687    fn unknown_head_is_not_an_unknown_member() {
2688        assert_eq!(builtin_lookup("Bogus.Thing"), BuiltinLookup::Unknown);
2689        // The internal flattening separator stays rejected.
2690        assert_eq!(builtin_lookup("Temporal#Instant"), BuiltinLookup::Unknown);
2691        assert_eq!(builtin_lookup(""), BuiltinLookup::Unknown);
2692    }
2693
2694    #[test]
2695    fn plain_names_are_unchanged_and_segments_are_case_insensitive() {
2696        // Every catalog name still renders, and a namespace still renders whole.
2697        let full = builtin_docs("Temporal").expect("Temporal");
2698        assert!(full.starts_with("namespace Temporal {"));
2699        for member in ["Instant", "ZonedDateTime", "PlainDate", "Duration"] {
2700            assert!(
2701                full.contains(&format!("interface {member} {{")),
2702                "missing {member}"
2703            );
2704        }
2705        assert_eq!(
2706            builtin_docs("temporal.instant"),
2707            builtin_docs("Temporal.Instant")
2708        );
2709        // A trailing dot is absorbed rather than rejected.
2710        assert_eq!(builtin_docs("Temporal."), builtin_docs("Temporal"));
2711    }
2712
2713    #[test]
2714    fn resolve_prefers_a_module_then_falls_back_to_builtins() {
2715        assert!(matches!(resolve("submilli:http"), Resolution::Module(_)));
2716        assert!(matches!(resolve("Temporal"), Resolution::Builtin { .. }));
2717        assert!(matches!(
2718            resolve("Temporal.Instant"),
2719            Resolution::Builtin { .. }
2720        ));
2721        assert!(matches!(
2722            resolve("Temporal.Foo"),
2723            Resolution::UnknownMember { .. }
2724        ));
2725        assert!(matches!(resolve("nope"), Resolution::Unknown));
2726        assert!(matches!(resolve("submilli:security"), Resolution::Unknown));
2727    }
2728
2729    #[test]
2730    fn suggest_reaches_past_bare_edit_distance() {
2731        let extra = vec!["@mcp/linear".to_string()];
2732        // A name written without its scheme is the likeliest package typo and
2733        // sits 9 edits from the full name, far outside any threshold.
2734        assert_eq!(suggest("http", &extra).as_deref(), Some("submilli:http"));
2735        assert_eq!(suggest("htp", &extra).as_deref(), Some("submilli:http"));
2736        assert_eq!(suggest("linear", &extra).as_deref(), Some("@mcp/linear"));
2737        // A dotted miss is matched on its head alone.
2738        assert_eq!(suggest("Temporel", &extra).as_deref(), Some("Temporal"));
2739        assert_eq!(
2740            suggest("Temporel.Instant", &extra).as_deref(),
2741            Some("Temporal")
2742        );
2743        assert_eq!(suggest("Aray", &extra).as_deref(), Some("Array"));
2744        assert_eq!(suggest("zzzzzzzzz", &extra), None);
2745        // Internal plumbing never surfaces as a candidate, even when the query
2746        // is its exact name — the public `secrets` module is offered instead.
2747        assert_ne!(
2748            suggest(INTERNAL_MODULE, &[]).as_deref(),
2749            Some(INTERNAL_MODULE)
2750        );
2751    }
2752
2753    #[test]
2754    fn a_deliberately_omitted_global_points_at_its_replacement() {
2755        // `Date` is two edits from `Math` and eight from `Temporal`, so edit
2756        // distance alone sends the model to arithmetic.
2757        assert_eq!(suggest("Date", &[]).as_deref(), Some("Temporal"));
2758        assert_eq!(suggest("date", &[]).as_deref(), Some("Temporal"));
2759    }
2760
2761    #[test]
2762    fn suggest_declines_degenerate_and_self_queries() {
2763        // `closest_match` floors its threshold at 2 edits, so without a length
2764        // guard any short query lands on an unrelated name.
2765        for q in ["", ".", "a", "xy"] {
2766            assert_eq!(suggest(q, &[]), None, "for {q:?}");
2767        }
2768        // An empty leading segment is skipped rather than matched on.
2769        assert_eq!(suggest(".Temporel", &[]).as_deref(), Some("Temporal"));
2770        // A name that resolved nowhere is never its own repair.
2771        let declared = vec!["@acme/mypkg".to_string()];
2772        assert_eq!(suggest("@acme/mypkg", &declared), None);
2773    }
2774
2775    #[test]
2776    fn catalog_is_summary_only_and_bounded() {
2777        let stdlib = catalog(Vec::new());
2778        assert_eq!(stdlib.remaining, 0);
2779        assert!(stdlib.entries.iter().all(|e| e.source == SOURCE_STDLIB));
2780        assert!(stdlib.entries.iter().any(|e| e.name == "submilli:http"));
2781        assert!(stdlib.entries.iter().all(|e| !e.description.is_empty()));
2782        assert!(!stdlib.entries.iter().any(|e| e.name == INTERNAL_MODULE));
2783
2784        let extra: Vec<CatalogEntry> = (0..CATALOG_LIMIT)
2785            .map(|i| CatalogEntry {
2786                name: format!("@acme/p{i}"),
2787                source: "registry".to_string(),
2788                description: "x".to_string(),
2789            })
2790            .collect();
2791        let big = catalog(extra);
2792        assert_eq!(big.entries.len(), CATALOG_LIMIT);
2793        assert_eq!(big.remaining, stdlib.entries.len());
2794    }
2795
2796    #[test]
2797    fn miss_messages_name_the_repair() {
2798        let msg = unknown_member_message("Temporal", "Foo", &["Instant".into(), "Now".into()]);
2799        assert!(msg.contains("Temporal"), "{msg}");
2800        assert!(msg.contains("Instant, Now"), "{msg}");
2801        let leaf = unknown_member_message("Math.max", "foo", &[]);
2802        assert!(leaf.contains("no members"), "{leaf}");
2803        // A `source` tag is a machine field; the prose must say it out loud.
2804        assert!(builtin_no_import_note("Temporal").contains("import"));
2805    }
2806
2807    /// Every package any embedder can enable, so the declaration checks below
2808    /// cover the optional packages too.
2809    fn every_package() -> Stdlib {
2810        crate::stdlib::OptionalPackage::ALL
2811            .iter()
2812            .fold(Stdlib::core(), |set, package| set.with(*package))
2813    }
2814
2815    #[test]
2816    fn stdlib_capability_tags_parse_cleanly() {
2817        for module in user_modules(every_package()) {
2818            for symbol in module.values.values() {
2819                let Some(doc) = value_doc(&symbol.kind) else {
2820                    continue;
2821                };
2822                for cap in &doc.capabilities {
2823                    assert!(
2824                        cap.diagnostics.is_empty(),
2825                        "{}.{}: {:?}",
2826                        module.package_name,
2827                        symbol.name,
2828                        cap.diagnostics
2829                    );
2830                }
2831            }
2832        }
2833    }
2834
2835    #[test]
2836    fn every_importable_stdlib_symbol_has_docs() {
2837        for module in user_modules(every_package()) {
2838            for symbol in module.values.values() {
2839                assert!(
2840                    value_doc(&symbol.kind).is_some(),
2841                    "{}.{} is missing docs",
2842                    module.package_name,
2843                    symbol.name
2844                );
2845            }
2846            for symbol in module.types.values() {
2847                assert!(
2848                    type_doc(&symbol.kind).is_some(),
2849                    "{}.{} is missing docs",
2850                    module.package_name,
2851                    symbol.name
2852                );
2853            }
2854        }
2855    }
2856
2857    #[test]
2858    fn builtin_docs_print_a_call_signature_without_a_name() {
2859        for name in ["Number", "Number.@call"] {
2860            let docs = builtin_docs(name).expect("Number built-in");
2861            assert!(docs.contains("  (value: "), "{docs}");
2862            assert!(!docs.contains("@call"), "{docs}");
2863        }
2864    }
2865
2866    #[test]
2867    fn builtin_docs_folds_constructor_into_array() {
2868        let docs = builtin_docs("Array").expect("Array built-in");
2869        assert!(docs.contains("interface Array<"));
2870        assert!(docs.contains("interface ArrayConstructor"));
2871        assert!(docs.contains("const Array: ArrayConstructor;"));
2872    }
2873
2874    #[test]
2875    fn builtin_docs_renders_temporal_namespace() {
2876        let docs = builtin_docs("Temporal").expect("Temporal built-in");
2877        assert!(docs.starts_with("namespace Temporal {"));
2878        assert!(docs.contains("interface Instant"));
2879        assert!(docs.contains("namespace Now {"));
2880        // Nested members are indented under the namespace.
2881        assert!(docs.contains("  interface Instant"));
2882        // The constructor binding comes from the namespace's own values — not
2883        // also synthesized by the constructor-merge (which would duplicate it).
2884        assert_eq!(docs.matches("const Instant").count(), 1, "{docs}");
2885    }
2886
2887    #[test]
2888    fn builtin_docs_handles_json_intrinsic() {
2889        let docs = builtin_docs("JSON").expect("JSON built-in");
2890        assert!(docs.contains(
2891            "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
2892        ));
2893        assert!(docs.contains("function parse(text: string): unknown;"));
2894    }
2895
2896    #[test]
2897    fn builtin_docs_is_case_insensitive() {
2898        // A lowercase name resolves to the canonically-cased built-in.
2899        assert_eq!(builtin_docs("array"), builtin_docs("Array"));
2900        assert_eq!(builtin_docs("temporal"), builtin_docs("Temporal"));
2901        assert_eq!(builtin_docs("json"), builtin_docs("JSON"));
2902        // Constructor-suffixed lookup folds in case-insensitively too.
2903        assert_eq!(builtin_docs("ARRAY"), builtin_docs("Array"));
2904    }
2905
2906    #[test]
2907    fn builtin_docs_unknown_is_none() {
2908        assert!(builtin_docs("Promise").is_none());
2909        assert!(builtin_docs("Temporal#Instant").is_none());
2910    }
2911
2912    #[test]
2913    fn packages_d_ts_imports_the_types_a_package_borrows() {
2914        let mut package = PackageDeclaration::with_package("@acme/files");
2915        let download = crate::stdlib::http::package_declaration()
2916            .values
2917            .get("download")
2918            .expect("submilli:http declares download")
2919            .clone();
2920        package.values.insert("fetch".to_string(), download);
2921
2922        let docs = render_packages_d_ts(&[&package], &[]);
2923
2924        assert!(docs.starts_with("declare module \"@acme/files\" {\n"));
2925        assert!(docs.contains("  import type { DownloadResult } from \"submilli:http\";\n"));
2926        assert!(docs.contains("  import type { DownloadOptions } from \"submilli:http\";\n"));
2927        assert!(!docs.contains("import type { Response }"));
2928    }
2929
2930    #[test]
2931    fn a_borrowed_type_is_imported_from_its_own_module_when_others_share_its_name() {
2932        // `submilli:session` and `@acme/other` export a `Page` too; the
2933        // reference's mangled name picks the leaf's.
2934        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
2935        leaf.types
2936            .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
2937        let mut other = PackageDeclaration::with_package("@acme/other");
2938        other
2939            .types
2940            .insert("Page".to_string(), alias_symbol("@acme/other", "Page"));
2941        let mut mid = PackageDeclaration::with_package("@acme/mid");
2942        let page = reference(
2943            "@acme/leaf",
2944            crate::mangle::package_symbol("@acme/leaf", "Page"),
2945            "Page",
2946        );
2947        mid.values
2948            .insert("page".to_string(), const_symbol("@acme/mid", "page", page));
2949
2950        let docs = render_packages_d_ts(&[&leaf, &other, &mid], &[]);
2951
2952        assert!(docs.contains("  import type { Page } from \"@acme/leaf\";\n"));
2953        assert!(!docs.contains("from \"submilli:session\""));
2954        assert!(!docs.contains("from \"@acme/other\""));
2955    }
2956
2957    fn alias_symbol(package: &str, name: &str) -> crate::TypeSymbol {
2958        crate::TypeSymbol {
2959            name: name.to_string(),
2960            mangled_name: crate::mangle::package_symbol(package, name),
2961            declaration_span: crate::Span::at(crate::FileId(0)),
2962            kind: crate::TypeKind::Alias {
2963                generics: Vec::new(),
2964                ty: Type::Number,
2965                doc: None,
2966            },
2967        }
2968    }
2969
2970    fn const_symbol(package: &str, name: &str, ty: Type) -> ValueSymbol {
2971        ValueSymbol {
2972            name: name.to_string(),
2973            mangled_name: crate::mangle::package_symbol(package, name),
2974            declaration_span: crate::Span::at(crate::FileId(0)),
2975            kind: ValueKind::Const { ty, doc: None },
2976        }
2977    }
2978
2979    fn reference(package: &str, mangled: crate::MangledName, local_name: &str) -> Type {
2980        Type::alias_ref(
2981            crate::types::Package(package.to_string()),
2982            local_name,
2983            mangled,
2984            Vec::new(),
2985        )
2986    }
2987
2988    #[test]
2989    fn an_aliased_type_is_imported_under_the_name_the_text_uses() {
2990        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
2991        leaf.types
2992            .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
2993        let mut mid = PackageDeclaration::with_package("@acme/mid");
2994        let page = reference(
2995            "@acme/leaf",
2996            crate::mangle::package_symbol("@acme/leaf", "Page"),
2997            "LeafPage",
2998        );
2999        mid.values
3000            .insert("page".to_string(), const_symbol("@acme/mid", "page", page));
3001
3002        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3003
3004        assert!(docs.contains("  import type { Page as LeafPage } from \"@acme/leaf\";\n"));
3005        assert!(!docs.contains("declare module \"@acme/leaf\""));
3006    }
3007
3008    #[test]
3009    fn a_type_reexported_from_an_internal_module_is_imported_from_the_package() {
3010        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3011        leaf.types
3012            .insert("Status".to_string(), alias_symbol("@acme/leaf", "Status"));
3013        let mut mid = PackageDeclaration::with_package("@acme/mid");
3014        let status = reference(
3015            "@acme/leaf",
3016            crate::mangle::package_module_symbol("@acme/leaf", "model", "Status"),
3017            "Status",
3018        );
3019        mid.values.insert(
3020            "status".to_string(),
3021            const_symbol("@acme/mid", "status", status),
3022        );
3023
3024        let docs = render_packages_d_ts(&[&leaf, &mid], &[]);
3025
3026        assert!(docs.contains("  import type { Status } from \"@acme/leaf\";\n"));
3027    }
3028
3029    #[test]
3030    fn only_the_rendered_declarations_bring_imports() {
3031        let mut mid = PackageDeclaration::with_package("@acme/mid");
3032        let response = reference(
3033            "submilli:http",
3034            crate::mangle::package_symbol("submilli:http", "Response"),
3035            "Response",
3036        );
3037        mid.runtime_globals.insert(
3038            crate::mangle::package_symbol("@acme/mid", "hidden"),
3039            response.clone(),
3040        );
3041
3042        let docs = render_packages_d_ts(&[&mid], &[]);
3043
3044        assert!(!docs.contains("import type"), "{docs}");
3045    }
3046
3047    #[test]
3048    fn a_value_named_like_a_borrowed_interface_leaves_its_import() {
3049        // TypeScript keeps a type and a value of one name apart, so the
3050        // interface still needs its import next to the package's own value.
3051        let download = reference(
3052            "submilli:http",
3053            crate::mangle::package_symbol("submilli:http", "DownloadResult"),
3054            "DownloadResult",
3055        );
3056        let mut mid = PackageDeclaration::with_package("@acme/mid");
3057        mid.values.insert(
3058            "DownloadResult".to_string(),
3059            const_symbol("@acme/mid", "DownloadResult", download),
3060        );
3061
3062        let docs = render_packages_d_ts(&[&mid], &[]);
3063
3064        assert!(
3065            docs.contains("  import type { DownloadResult } from \"submilli:http\";\n"),
3066            "{docs}"
3067        );
3068    }
3069
3070    #[test]
3071    fn type_arguments_bring_imports_and_alias_bodies_do_not() {
3072        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3073        leaf.types
3074            .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
3075        leaf.types
3076            .insert("Item".to_string(), alias_symbol("@acme/leaf", "Item"));
3077        let mut mid = PackageDeclaration::with_package("@acme/mid");
3078        let item = reference(
3079            "@acme/leaf",
3080            crate::mangle::package_symbol("@acme/leaf", "Item"),
3081            "Item",
3082        );
3083        let page_of_items = Type::alias_ref(
3084            crate::types::Package("@acme/leaf".to_string()),
3085            "Page",
3086            crate::mangle::package_symbol("@acme/leaf", "Page"),
3087            vec![Type::Array(Box::new(item))],
3088        );
3089        mid.values.insert(
3090            "page".to_string(),
3091            const_symbol("@acme/mid", "page", page_of_items),
3092        );
3093        // A resolved alias renders as its name; what it stands for doesn't
3094        // appear in the text.
3095        let response = reference(
3096            "submilli:http",
3097            crate::mangle::package_symbol("submilli:http", "Response"),
3098            "Response",
3099        );
3100        let wrapped = Type::Alias {
3101            mangled: crate::mangle::package_symbol("@acme/leaf", "Item"),
3102            package: crate::types::Package("@acme/leaf".to_string()),
3103            name: "Item".to_string(),
3104            args: Vec::new(),
3105            ty: Box::new(response),
3106        };
3107        mid.values.insert(
3108            "item".to_string(),
3109            const_symbol("@acme/mid", "item", wrapped),
3110        );
3111
3112        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3113
3114        assert!(docs.contains("  import type { Page } from \"@acme/leaf\";\n"));
3115        assert!(docs.contains("  import type { Item } from \"@acme/leaf\";\n"));
3116        assert!(!docs.contains("Response"), "{docs}");
3117    }
3118
3119    fn class_symbol(
3120        package: &str,
3121        name: &str,
3122        extends: Option<crate::ClassExtends>,
3123    ) -> crate::TypeSymbol {
3124        crate::TypeSymbol {
3125            name: name.to_string(),
3126            mangled_name: crate::mangle::package_symbol(package, name),
3127            declaration_span: crate::Span::at(crate::FileId(0)),
3128            kind: TypeKind::Class {
3129                generics: Vec::new(),
3130                fields: BTreeMap::new(),
3131                narrowing_checks: BTreeMap::new(),
3132                methods: BTreeMap::new(),
3133                method_visibility: BTreeMap::new(),
3134                accessors: Vec::new(),
3135                constructor: Vec::new(),
3136                constructor_visibility: crate::Visibility::Public,
3137                statics: BTreeMap::new(),
3138                static_visibility: BTreeMap::new(),
3139                static_fields: BTreeMap::new(),
3140                extends,
3141                implements: Vec::new(),
3142                doc: None,
3143            },
3144        }
3145    }
3146
3147    #[test]
3148    fn a_class_renders_and_imports_its_parent() {
3149        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3150        leaf.types
3151            .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3152        leaf.types
3153            .insert("Item".to_string(), alias_symbol("@acme/leaf", "Item"));
3154        let item = reference(
3155            "@acme/leaf",
3156            crate::mangle::package_symbol("@acme/leaf", "Item"),
3157            "Item",
3158        );
3159        let extends = crate::ClassExtends {
3160            parent: crate::mangle::package_symbol("@acme/leaf", "Base"),
3161            args: vec![item],
3162        };
3163        let mut sub = class_symbol("@acme/mid", "Sub", Some(extends));
3164        if let TypeKind::Class { fields, .. } = &mut sub.kind {
3165            // A private member isn't rendered, so its type isn't imported.
3166            let response = reference(
3167                "submilli:http",
3168                crate::mangle::package_symbol("submilli:http", "Response"),
3169                "Response",
3170            );
3171            fields.insert(
3172                "hidden".to_string(),
3173                crate::FieldSig {
3174                    ty: response,
3175                    visibility: crate::Visibility::Private,
3176                    readonly: false,
3177                    optional: false,
3178                    doc: None,
3179                },
3180            );
3181        }
3182        let mut mid = PackageDeclaration::with_package("@acme/mid");
3183        mid.types.insert("Sub".to_string(), sub);
3184
3185        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3186
3187        assert!(
3188            docs.contains("  import type { Base } from \"@acme/leaf\";\n"),
3189            "{docs}"
3190        );
3191        assert!(
3192            docs.contains("  import type { Item } from \"@acme/leaf\";\n"),
3193            "{docs}"
3194        );
3195        assert!(
3196            docs.contains("  export class Sub extends Base<Item> {"),
3197            "{docs}"
3198        );
3199        assert!(!docs.contains("Response"), "{docs}");
3200    }
3201
3202    fn subclass_of(package: &str, name: &str, parent: crate::MangledName) -> crate::TypeSymbol {
3203        class_symbol(package, name, Some(crate::ClassExtends::plain(parent)))
3204    }
3205
3206    /// `@acme/leaf` and `@acme/other`, which each export a class `Base`.
3207    fn two_bases() -> (PackageDeclaration, PackageDeclaration) {
3208        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3209        leaf.types
3210            .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3211        let mut other = PackageDeclaration::with_package("@acme/other");
3212        other.types.insert(
3213            "Base".to_string(),
3214            class_symbol("@acme/other", "Base", None),
3215        );
3216        (leaf, other)
3217    }
3218
3219    #[test]
3220    fn a_parent_named_like_a_local_value_is_imported_under_another_name() {
3221        let (leaf, _) = two_bases();
3222        let mut package = PackageDeclaration::with_package("@acme/shadowed");
3223        package.values.insert(
3224            "Base".to_string(),
3225            const_symbol("@acme/shadowed", "Base", Type::Number),
3226        );
3227        let base = crate::mangle::package_symbol("@acme/leaf", "Base");
3228        package.types.insert(
3229            "Sub".to_string(),
3230            subclass_of("@acme/shadowed", "Sub", base),
3231        );
3232
3233        let docs = render_packages_d_ts(&[&package], &[&leaf]);
3234
3235        assert!(
3236            docs.contains("  import type { Base as Base1 } from \"@acme/leaf\";\n"),
3237            "{docs}"
3238        );
3239        assert!(
3240            docs.contains("  export class Sub extends Base1 {"),
3241            "{docs}"
3242        );
3243    }
3244
3245    #[test]
3246    fn parents_that_share_a_public_name_get_different_local_names() {
3247        let (leaf, other) = two_bases();
3248        let mut package = PackageDeclaration::with_package("@acme/both");
3249        package.types.insert(
3250            "A".to_string(),
3251            subclass_of(
3252                "@acme/both",
3253                "A",
3254                crate::mangle::package_symbol("@acme/leaf", "Base"),
3255            ),
3256        );
3257        package.types.insert(
3258            "B".to_string(),
3259            subclass_of(
3260                "@acme/both",
3261                "B",
3262                crate::mangle::package_symbol("@acme/other", "Base"),
3263            ),
3264        );
3265
3266        let docs = render_packages_d_ts(&[&package], &[&leaf, &other]);
3267
3268        assert!(
3269            docs.contains("  import type { Base } from \"@acme/leaf\";\n"),
3270            "{docs}"
3271        );
3272        assert!(
3273            docs.contains("  import type { Base as Base1 } from \"@acme/other\";\n"),
3274            "{docs}"
3275        );
3276        assert!(docs.contains("  export class A extends Base {"), "{docs}");
3277        assert!(docs.contains("  export class B extends Base1 {"), "{docs}");
3278    }
3279
3280    #[test]
3281    fn a_parent_already_imported_under_an_alias_keeps_it() {
3282        let (leaf, _) = two_bases();
3283        let base = crate::mangle::package_symbol("@acme/leaf", "Base");
3284        let mut package = PackageDeclaration::with_package("@acme/aliased");
3285        package.values.insert(
3286            "base".to_string(),
3287            const_symbol(
3288                "@acme/aliased",
3289                "base",
3290                reference("@acme/leaf", base.clone(), "LB"),
3291            ),
3292        );
3293        package
3294            .types
3295            .insert("Sub".to_string(), subclass_of("@acme/aliased", "Sub", base));
3296
3297        let docs = render_packages_d_ts(&[&package], &[&leaf]);
3298
3299        assert!(
3300            docs.contains("  import type { Base as LB } from \"@acme/leaf\";\n"),
3301            "{docs}"
3302        );
3303        assert!(docs.contains("  export class Sub extends LB {"), "{docs}");
3304        assert!(!docs.contains("import type { Base }"), "{docs}");
3305    }
3306
3307    #[test]
3308    fn a_class_extending_a_builtin_error_renders_its_parent_as_the_global() {
3309        let error = builtin_package_declaration()
3310            .types
3311            .get("Error")
3312            .expect("the prelude declares Error")
3313            .mangled_name
3314            .clone();
3315        let mut package = PackageDeclaration::with_package("@acme/errors");
3316        package.types.insert(
3317            "BillingError".to_string(),
3318            subclass_of("@acme/errors", "BillingError", error),
3319        );
3320
3321        let docs = render_packages_d_ts(&[&package], &[]);
3322
3323        assert!(
3324            docs.contains("  export class BillingError extends Error {"),
3325            "{docs}"
3326        );
3327        assert!(!docs.contains("import type"), "{docs}");
3328    }
3329
3330    #[test]
3331    fn a_package_value_named_like_the_global_parent_leaves_the_class_without_it() {
3332        let error = builtin_package_declaration()
3333            .types
3334            .get("Error")
3335            .expect("the prelude declares Error")
3336            .mangled_name
3337            .clone();
3338        let mut package = PackageDeclaration::with_package("@acme/errors");
3339        package.values.insert(
3340            "Error".to_string(),
3341            const_symbol("@acme/errors", "Error", Type::Number),
3342        );
3343        package.types.insert(
3344            "BillingError".to_string(),
3345            subclass_of("@acme/errors", "BillingError", error),
3346        );
3347
3348        let docs = render_packages_d_ts(&[&package], &[]);
3349
3350        assert!(docs.contains("  export class BillingError {"), "{docs}");
3351    }
3352
3353    #[test]
3354    fn builtin_errors_extend_error_in_the_editor_declarations() {
3355        let lib = render_lib_submilli_d_ts();
3356        for class in [
3357            "QuotaExceededError",
3358            "RangeError",
3359            "TypeError",
3360            "SyntaxError",
3361            "URIError",
3362            "ReferenceError",
3363            "PermissionDeniedError",
3364        ] {
3365            assert!(
3366                lib.contains(&format!("declare class {class} extends Error {{")),
3367                "{class}"
3368            );
3369        }
3370        assert!(lib.contains("declare class Error {"));
3371    }
3372
3373    #[test]
3374    fn a_value_named_like_a_borrowed_class_keeps_the_class_out() {
3375        // A class is a value too, so importing it would clash with the const.
3376        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3377        leaf.types
3378            .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3379        let base = Type::class_ref(
3380            crate::types::Package("@acme/leaf".to_string()),
3381            "Base",
3382            crate::mangle::package_symbol("@acme/leaf", "Base"),
3383            Vec::new(),
3384        );
3385        let mut mid = PackageDeclaration::with_package("@acme/mid");
3386        mid.values
3387            .insert("Base".to_string(), const_symbol("@acme/mid", "Base", base));
3388
3389        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3390
3391        assert!(!docs.contains("import type"), "{docs}");
3392    }
3393
3394    #[test]
3395    fn a_package_type_named_like_a_constructor_is_an_ordinary_type() {
3396        let mut package = PackageDeclaration::with_package("@acme/leaf");
3397        package
3398            .types
3399            .insert("Client".to_string(), alias_symbol("@acme/leaf", "Client"));
3400        package.types.insert(
3401            "ClientConstructor".to_string(),
3402            alias_symbol("@acme/leaf", "ClientConstructor"),
3403        );
3404
3405        let docs = render_packages_d_ts(&[&package], &[]);
3406
3407        assert!(docs.contains("  export type Client = number;"), "{docs}");
3408        assert!(
3409            docs.contains("  export type ClientConstructor = number;"),
3410            "{docs}"
3411        );
3412        assert!(!docs.contains("const Client"), "{docs}");
3413    }
3414
3415    #[test]
3416    fn stdlib_d_ts_declares_each_user_module() {
3417        let docs = every_package().render_stdlib_d_ts();
3418        for module in user_modules(every_package()) {
3419            assert!(
3420                docs.contains(&format!("declare module \"{}\" {{", module.package_name)),
3421                "missing declare module for {}",
3422                module.package_name
3423            );
3424        }
3425        assert!(docs.contains("declare module \"submilli:security\" {\n"));
3426        assert!(docs.contains("  export function check<T>("));
3427        assert!(docs.contains("declare module \"submilli:uuid\" {\n"));
3428        assert!(docs.contains("  export function v4("));
3429        assert!(docs.contains("  function delete_("));
3430        assert!(docs.contains("  export { delete_ as delete };"));
3431        assert!(!docs.contains("export function delete("));
3432    }
3433
3434    #[test]
3435    fn lib_submilli_d_ts_renders_ambient_builtins() {
3436        let docs = render_lib_submilli_d_ts();
3437        assert!(docs.contains("interface Array<"));
3438        assert!(docs.contains("interface ArrayConstructor"));
3439        assert!(docs.contains("declare const Array: ArrayConstructor;"));
3440        assert_eq!(
3441            docs.matches("declare const Array: ArrayConstructor;")
3442                .count(),
3443            1
3444        );
3445        assert!(docs.contains("declare const console: Console;"));
3446        assert!(docs.contains("declare namespace Math {"));
3447        assert!(docs.contains("declare namespace Temporal {"));
3448        assert!(docs.contains("declare namespace JSON {"));
3449        assert!(docs.contains(
3450            "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
3451        ));
3452        assert!(docs.contains("interface Function {}"));
3453        assert!(docs.contains("interface IArguments {}"));
3454        assert!(docs.contains("declare class Error {"));
3455        assert!(docs.contains("static isError(value?: unknown): value is Error;"));
3456        assert!(!docs.contains("ErrorConstructor"));
3457        assert!(!docs.contains("declare module"));
3458        assert!(!docs.contains("function string_concat"));
3459    }
3460
3461    #[test]
3462    fn d_ts_renderer_uses_typescript_call_and_predicate_signatures() {
3463        let docs = render_lib_submilli_d_ts();
3464        assert!(docs.contains("(value: string | bigint | undefined): number;"));
3465        assert!(docs.contains("isArray<T>(value: T): value is T & unknown[];"));
3466        assert!(!docs.contains("@call"));
3467    }
3468
3469    fn class_with_constructor(
3470        package: &str,
3471        name: &str,
3472        constructor: Vec<Param>,
3473        constructor_visibility: crate::Visibility,
3474        extends: Option<ClassExtends>,
3475    ) -> TypeSymbol {
3476        TypeSymbol {
3477            name: name.to_string(),
3478            mangled_name: crate::mangle::package_symbol(package, name),
3479            declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3480            kind: TypeKind::Class {
3481                generics: Vec::new(),
3482                fields: BTreeMap::new(),
3483                narrowing_checks: BTreeMap::new(),
3484                methods: BTreeMap::new(),
3485                method_visibility: BTreeMap::new(),
3486                accessors: Vec::new(),
3487                constructor,
3488                constructor_visibility,
3489                statics: BTreeMap::new(),
3490                static_visibility: BTreeMap::new(),
3491                static_fields: BTreeMap::new(),
3492                extends,
3493                implements: Vec::new(),
3494                doc: None,
3495            },
3496        }
3497    }
3498
3499    #[test]
3500    fn a_private_constructor_stays_out_of_the_docs_and_is_protected_in_d_ts() {
3501        let package = "@acme/shapes";
3502        let mut defs = PackageDeclaration::with_package(package);
3503        let point = class_with_constructor(
3504            package,
3505            "Point",
3506            vec![Param::new("secret", Type::String)],
3507            crate::Visibility::Private,
3508            None,
3509        );
3510        let labeled = class_with_constructor(
3511            package,
3512            "Labeled",
3513            vec![Param::new("secret", Type::String)],
3514            crate::Visibility::Private,
3515            Some(ClassExtends {
3516                parent: point.mangled_name.clone(),
3517                args: Vec::new(),
3518            }),
3519        );
3520        defs.types.insert("Point".to_string(), point);
3521        defs.types.insert("Labeled".to_string(), labeled);
3522
3523        let docs = render_declarations(&defs);
3524        assert!(docs.contains("class Point"), "{docs}");
3525        assert!(!docs.contains("constructor"), "{docs}");
3526
3527        // `protected`, not `private`: tsc rejects `Labeled extends Point` when
3528        // `Point`'s constructor is private, though Submilli allows it here.
3529        let d_ts = render_packages_d_ts(&[&defs], &[]);
3530        assert!(d_ts.contains("class Labeled extends Point"), "{d_ts}");
3531        assert_eq!(
3532            d_ts.matches("protected constructor();").count(),
3533            2,
3534            "{d_ts}"
3535        );
3536        assert!(!d_ts.contains("secret"), "{d_ts}");
3537    }
3538
3539    #[test]
3540    fn constructor_visibility_round_trips_and_a_public_one_is_not_written() {
3541        let package = "@acme/shapes";
3542        for visibility in [crate::Visibility::Private, crate::Visibility::Public] {
3543            let symbol = class_with_constructor(package, "Point", Vec::new(), visibility, None);
3544            let json = serde_json::to_string(&symbol.kind).unwrap();
3545            assert_eq!(
3546                json.contains("constructor_visibility"),
3547                !visibility.is_public()
3548            );
3549            let TypeKind::Class {
3550                constructor_visibility,
3551                ..
3552            } = serde_json::from_str::<TypeKind>(&json).unwrap()
3553            else {
3554                panic!("not a class: {json}");
3555            };
3556            assert_eq!(constructor_visibility, visibility);
3557        }
3558    }
3559
3560    #[test]
3561    fn class_declarations_omit_private_members() {
3562        use crate::{FieldSig, MethodSig, Visibility};
3563
3564        let field = |ty, visibility| FieldSig {
3565            ty,
3566            visibility,
3567            readonly: false,
3568            optional: false,
3569            doc: None,
3570        };
3571        let method = |ret| MethodSig {
3572            optional: false,
3573            generics: Vec::new(),
3574            params: Vec::new(),
3575            ret,
3576            predicate: None,
3577            doc: None,
3578        };
3579
3580        let mut fields = BTreeMap::new();
3581        fields.insert("name".to_string(), field(Type::String, Visibility::Public));
3582        fields.insert(
3583            "sound".to_string(),
3584            field(Type::String, Visibility::Private),
3585        );
3586        let mut methods = BTreeMap::new();
3587        methods.insert("speak".to_string(), method(Type::String));
3588        methods.insert("secret".to_string(), method(Type::Number));
3589        let mut method_visibility = BTreeMap::new();
3590        method_visibility.insert("speak".to_string(), Visibility::Public);
3591        method_visibility.insert("secret".to_string(), Visibility::Private);
3592
3593        let mut defs = PackageDeclaration::with_package("@acme/zoo");
3594        defs.types.insert(
3595            "Animal".to_string(),
3596            TypeSymbol {
3597                name: "Animal".to_string(),
3598                mangled_name: crate::mangle::package_symbol("@acme/zoo", "Animal"),
3599                declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3600                kind: TypeKind::Class {
3601                    generics: Vec::new(),
3602                    fields,
3603                    narrowing_checks: BTreeMap::new(),
3604                    methods,
3605                    method_visibility,
3606                    accessors: Vec::new(),
3607                    constructor: vec![Param::new("name", Type::String)],
3608                    constructor_visibility: crate::Visibility::Public,
3609                    statics: BTreeMap::new(),
3610                    static_visibility: BTreeMap::new(),
3611                    static_fields: BTreeMap::new(),
3612                    extends: None,
3613                    implements: Vec::new(),
3614                    doc: None,
3615                },
3616            },
3617        );
3618
3619        let rendered = render_declarations(&defs);
3620        // Public surface and the constructor stay.
3621        assert!(rendered.contains("class Animal {"), "{rendered}");
3622        assert!(rendered.contains("name: string;"), "{rendered}");
3623        assert!(rendered.contains("speak(): string;"), "{rendered}");
3624        assert!(
3625            rendered.contains("constructor(name: string);"),
3626            "{rendered}"
3627        );
3628        // Private members and the `private` keyword never reach the docs.
3629        assert!(!rendered.contains("private "), "{rendered}");
3630        assert!(!rendered.contains("sound"), "{rendered}");
3631        assert!(!rendered.contains("secret"), "{rendered}");
3632    }
3633
3634    #[test]
3635    fn class_declarations_render_accessors_faithfully() {
3636        use crate::{AccessorSig, FieldSig, Param, Visibility};
3637
3638        // Accessors are not methods on the public surface — they render as
3639        // properties (or explicit get/set when read/write types differ).
3640        let prop = |ty: Type, readonly| FieldSig {
3641            ty,
3642            visibility: Visibility::Public,
3643            readonly,
3644            optional: false,
3645            doc: None,
3646        };
3647        let mut fields = BTreeMap::new();
3648        fields.insert("area".to_string(), prop(Type::Number, true)); // get-only
3649        fields.insert("size".to_string(), prop(Type::Number, false)); // get+set, same type
3650        fields.insert("label".to_string(), prop(Type::String, false)); // get/set, diff types
3651        fields.insert("secret".to_string(), prop(Type::Number, false)); // set-only
3652
3653        let setter = |name: &str, ty: Type| AccessorSig::Setter {
3654            name: name.to_string(),
3655            param: Param::new("v", ty),
3656        };
3657        let getter = |name: &str, ty: Type| AccessorSig::Getter {
3658            name: name.to_string(),
3659            ret_ty: ty,
3660        };
3661        let accessors = vec![
3662            getter("area", Type::Number),
3663            getter("size", Type::Number),
3664            setter("size", Type::Number),
3665            getter("label", Type::String),
3666            setter("label", Type::Number),
3667            setter("secret", Type::Number),
3668        ];
3669
3670        let mut defs = PackageDeclaration::with_package("@acme/geo");
3671        defs.types.insert(
3672            "Shape".to_string(),
3673            TypeSymbol {
3674                name: "Shape".to_string(),
3675                mangled_name: crate::mangle::package_symbol("@acme/geo", "Shape"),
3676                declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3677                kind: TypeKind::Class {
3678                    generics: Vec::new(),
3679                    fields,
3680                    narrowing_checks: BTreeMap::new(),
3681                    methods: BTreeMap::new(),
3682                    method_visibility: BTreeMap::new(),
3683                    accessors,
3684                    constructor: Vec::new(),
3685                    constructor_visibility: crate::Visibility::Public,
3686                    statics: BTreeMap::new(),
3687                    static_visibility: BTreeMap::new(),
3688                    static_fields: BTreeMap::new(),
3689                    extends: None,
3690                    implements: Vec::new(),
3691                    doc: None,
3692                },
3693            },
3694        );
3695
3696        let docs = render_declarations(&defs);
3697        let shape = &defs.types.get("Shape").unwrap().kind;
3698        let mut dts = String::new();
3699        render_ts_type(&mut dts, "Shape", shape, "", "export ", None);
3700        for rendered in [&docs, &dts] {
3701            assert!(rendered.contains("readonly area: number;"), "{rendered}"); // get-only
3702            assert!(rendered.contains("size: number;"), "{rendered}"); // get+set same type
3703            assert!(rendered.contains("get label(): string;"), "{rendered}"); // diff types
3704            assert!(rendered.contains("set label(v: number);"), "{rendered}");
3705            assert!(rendered.contains("set secret(v: number);"), "{rendered}"); // write-only
3706            // Never the synthetic `get x`/`set x` *method* form.
3707            assert!(!rendered.contains("get area"), "{rendered}");
3708            assert!(!rendered.contains("get size("), "{rendered}");
3709        }
3710    }
3711
3712    #[test]
3713    fn class_declarations_render_public_statics_only() {
3714        use crate::{FieldSig, MethodSig, Param, Visibility};
3715
3716        let method = |ret: Type| MethodSig {
3717            optional: false,
3718            generics: Vec::new(),
3719            params: vec![Param::new("x", Type::Number)],
3720            ret,
3721            predicate: None,
3722            doc: None,
3723        };
3724        let mut statics = BTreeMap::new();
3725        statics.insert("make".to_string(), method(Type::Number));
3726        statics.insert("hidden".to_string(), method(Type::Number));
3727        let mut static_visibility = BTreeMap::new();
3728        static_visibility.insert("make".to_string(), Visibility::Public);
3729        static_visibility.insert("hidden".to_string(), Visibility::Private);
3730        let mut static_fields = BTreeMap::new();
3731        static_fields.insert(
3732            "MAX".to_string(),
3733            FieldSig {
3734                ty: Type::Number,
3735                visibility: Visibility::Public,
3736                readonly: true,
3737                optional: false,
3738                doc: None,
3739            },
3740        );
3741        static_fields.insert(
3742            "KEY".to_string(),
3743            FieldSig {
3744                ty: Type::Number,
3745                visibility: Visibility::Private,
3746                readonly: true,
3747                optional: false,
3748                doc: None,
3749            },
3750        );
3751        static_fields.insert(
3752            "count".to_string(),
3753            FieldSig {
3754                ty: Type::Number,
3755                visibility: Visibility::Public,
3756                readonly: false,
3757                optional: false,
3758                doc: None,
3759            },
3760        );
3761        static_fields.insert(
3762            "seed".to_string(),
3763            FieldSig {
3764                ty: Type::Number,
3765                visibility: Visibility::Private,
3766                readonly: false,
3767                optional: false,
3768                doc: None,
3769            },
3770        );
3771
3772        let mut defs = PackageDeclaration::with_package("@acme/calc");
3773        defs.types.insert(
3774            "Calc".to_string(),
3775            TypeSymbol {
3776                name: "Calc".to_string(),
3777                mangled_name: crate::mangle::package_symbol("@acme/calc", "Calc"),
3778                declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3779                kind: TypeKind::Class {
3780                    generics: Vec::new(),
3781                    fields: BTreeMap::new(),
3782                    narrowing_checks: BTreeMap::new(),
3783                    methods: BTreeMap::new(),
3784                    method_visibility: BTreeMap::new(),
3785                    accessors: Vec::new(),
3786                    constructor: Vec::new(),
3787                    constructor_visibility: crate::Visibility::Public,
3788                    statics,
3789                    static_visibility,
3790                    static_fields,
3791                    extends: None,
3792                    implements: Vec::new(),
3793                    doc: None,
3794                },
3795            },
3796        );
3797
3798        let docs = render_declarations(&defs);
3799        let calc = &defs.types.get("Calc").unwrap().kind;
3800        let mut dts = String::new();
3801        render_ts_type(&mut dts, "Calc", calc, "", "export ", None);
3802        for rendered in [&docs, &dts] {
3803            assert!(
3804                rendered.contains("static make(x: number): number;"),
3805                "{rendered}"
3806            );
3807            assert!(
3808                rendered.contains("static readonly MAX: number;"),
3809                "{rendered}"
3810            );
3811            assert!(rendered.contains("static count: number;"), "{rendered}");
3812            assert!(!rendered.contains("hidden"), "{rendered}");
3813            assert!(!rendered.contains("KEY"), "{rendered}");
3814            assert!(!rendered.contains("seed"), "{rendered}");
3815        }
3816    }
3817}