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" => {
907            "Session-scoped key-value state and durable package-owned resource cleanup."
908        }
909        "submilli:skills" => "Harness skills: list(), load(name) and readFile(name, path).",
910        "submilli:url" => "URL parse/build and query-string handling. Pure compute.",
911        "submilli:variables" => "Immutable Blueprint/session configuration for packages.",
912        "submilli:uuid" => "UUID v4/v7 generation and validation.",
913        _ => "",
914    }
915}
916
917fn matches_query(defs: &PackageDeclaration, q: &str) -> bool {
918    if q.is_empty() {
919        return true;
920    }
921    let in_symbols = defs
922        .values
923        .keys()
924        .chain(defs.types.keys())
925        .any(|k| k.to_lowercase().contains(q));
926    defs.package_name.to_lowercase().contains(q)
927        || module_description(&defs.package_name)
928            .to_lowercase()
929            .contains(q)
930        || in_symbols
931}
932
933/// Render a module's exports as `.d.subm`-style declarations. Public so an
934/// embedder can render dynamically-built packages (e.g. `@mcp/<server>`) the same
935/// way the stdlib docs render.
936pub fn render_declarations(defs: &PackageDeclaration) -> String {
937    let mut out = String::new();
938    for (name, sym) in &defs.values {
939        push_doc(&mut out, value_doc(&sym.kind), "");
940        render_value(&mut out, name, &sym.kind, "");
941    }
942    for (name, sym) in &defs.types {
943        push_doc(&mut out, type_doc(&sym.kind), "");
944        render_type(&mut out, name, &sym.kind, "");
945    }
946    out.trim_end().to_string()
947}
948
949/// Render editor-facing TypeScript declarations for every stdlib module a
950/// package project can import. Each package gets a separate `declare module`
951/// block. `submilli:test` is included even though only `build test` makes it
952/// importable, and `submilli:security` even though only packages may import
953/// it — the editor should complete both; `build check` remains the gate
954/// against importing them elsewhere.
955pub fn render_stdlib_d_ts() -> String {
956    Stdlib::core().render_stdlib_d_ts()
957}
958
959/// Render editor-facing TypeScript declarations for `packages`, one
960/// `declare module` block each, in the order given.
961///
962/// A declaration names the types it borrows from another module without
963/// saying where they come from, so each block imports them. A borrowed type is
964/// matched to its module by the reference's mangled name, not by its text:
965/// many modules export a `Page` or an `Item`. `context` holds further modules
966/// those types may come from that get no block here, such as the project's
967/// own packages.
968pub fn render_packages_d_ts(
969    packages: &[&PackageDeclaration],
970    context: &[&PackageDeclaration],
971) -> String {
972    Stdlib::core().render_packages_d_ts(packages, context)
973}
974
975/// The built-in classes a package's class may extend, such as `Error`, by
976/// mangled name. They're globals, so a parent among them needs no import.
977fn global_classes() -> BTreeMap<String, String> {
978    let defs = builtin_package_declaration();
979    defs.types
980        .into_iter()
981        .filter(|(name, symbol)| {
982            matches!(symbol.kind, TypeKind::Class { .. })
983                && !is_hidden_prelude_type(&defs.package_name, name)
984        })
985        .map(|(name, symbol)| (symbol.mangled_name.as_str().to_string(), name))
986        .collect()
987}
988
989fn editor_stdlib_modules(stdlib: Stdlib) -> Vec<PackageDeclaration> {
990    let mut modules = user_modules(stdlib);
991    modules.push(crate::stdlib::test::package_declaration());
992    modules.push(crate::stdlib::security::package_declaration());
993    modules.sort_by(|a, b| a.package_name.cmp(&b.package_name));
994    modules
995}
996
997/// Where each exported type can be imported from.
998struct TypeExporters<'a> {
999    /// A type symbol's mangled name, mapped to the type and its module.
1000    by_mangled: BTreeMap<&'a str, (ExportedType<'a>, &'a str)>,
1001    /// Each module's exported types by name, for a reference whose mangled
1002    /// name is the internal-module form of a type the package root re-exports.
1003    by_module: BTreeMap<&'a str, BTreeMap<&'a str, ExportedType<'a>>>,
1004}
1005
1006#[derive(Clone, Copy)]
1007struct ExportedType<'a> {
1008    name: &'a str,
1009    /// A class or enum is a value too, and an import of one conflicts with a
1010    /// local value of the same name. An interface or alias doesn't.
1011    is_value: bool,
1012}
1013
1014impl<'a> TypeExporters<'a> {
1015    fn new(modules: impl Iterator<Item = &'a PackageDeclaration>) -> Self {
1016        let mut by_mangled = BTreeMap::new();
1017        let mut by_module: BTreeMap<&str, BTreeMap<&str, ExportedType>> = BTreeMap::new();
1018        for defs in modules {
1019            let module = defs.package_name.as_str();
1020            for (name, symbol) in &defs.types {
1021                let exported = ExportedType {
1022                    name: name.as_str(),
1023                    is_value: matches!(
1024                        symbol.kind,
1025                        TypeKind::Class { .. }
1026                            | TypeKind::NumberEnum { .. }
1027                            | TypeKind::StringEnum { .. }
1028                    ),
1029                };
1030                by_mangled.insert(symbol.mangled_name.as_str(), (exported, module));
1031                by_module
1032                    .entry(module)
1033                    .or_default()
1034                    .insert(name.as_str(), exported);
1035            }
1036        }
1037        Self {
1038            by_mangled,
1039            by_module,
1040        }
1041    }
1042
1043    /// The type a reference points at and its module, if a known module
1044    /// exports it.
1045    fn resolve(&self, reference: &TypeReference) -> Option<(ExportedType<'a>, &'a str)> {
1046        if let Some(&exported) = self.by_mangled.get(reference.mangled.as_str()) {
1047            return Some(exported);
1048        }
1049        let (module, types) = self.by_module.get_key_value(reference.package.as_str())?;
1050        Some((*types.get(reference.name.as_str())?, module))
1051    }
1052}
1053
1054/// A by-name type as a declaration refers to it: the declaring symbol's
1055/// mangled name and package, and the name the rendered text uses for it,
1056/// which is the local name an aliased import gave it.
1057struct TypeReference {
1058    mangled: String,
1059    package: String,
1060    name: String,
1061}
1062
1063/// The imports one `declare module` block needs.
1064struct BlockImports<'a> {
1065    /// Keyed by the name the block's text uses, valued by the public name and
1066    /// module to import it from.
1067    types: BTreeMap<String, (&'a str, &'a str)>,
1068    /// Each class parent's mangled name, mapped to the name the block imports
1069    /// it under.
1070    parents: BTreeMap<&'a str, String>,
1071}
1072
1073/// The imports `defs`'s block needs.
1074///
1075/// A type reference is imported under the name the text already uses for it.
1076/// That name is the package's own if it declares a type of that name, or a
1077/// value when the borrowed type is a value too (a class or enum); TypeScript
1078/// keeps a type and a value of one name apart otherwise. Two references that
1079/// use one local name for different types, which only separate source files
1080/// can produce, get the first one's import.
1081///
1082/// A class's parent records no local name, so the block picks one: the name
1083/// an import of the same type already has, else its public name, else that
1084/// name with a numeric suffix that nothing in the block uses.
1085fn block_imports<'a>(
1086    defs: &'a PackageDeclaration,
1087    exporters: &TypeExporters<'a>,
1088) -> BlockImports<'a> {
1089    let mut types = BTreeMap::new();
1090    for reference in rendered_type_references(defs) {
1091        let Some((exported, module)) = exporters.resolve(&reference) else {
1092            continue;
1093        };
1094        if module != defs.package_name && !declares(defs, &reference.name, exported.is_value) {
1095            types
1096                .entry(reference.name)
1097                .or_insert((exported.name, module));
1098        }
1099    }
1100    let mut parents = BTreeMap::new();
1101    for (mangled, exported, module) in class_parents(defs, exporters) {
1102        if module == defs.package_name {
1103            parents.insert(mangled, exported.name.to_string());
1104            continue;
1105        }
1106        let already_imported = types
1107            .iter()
1108            .find(|(_, target)| **target == (exported.name, module))
1109            .map(|(local, _)| local.clone());
1110        let local = if let Some(local) = already_imported {
1111            local
1112        } else {
1113            let local = free_local_name(defs, &types, exported.name);
1114            types.insert(local.clone(), (exported.name, module));
1115            local
1116        };
1117        parents.insert(mangled, local);
1118    }
1119    BlockImports { types, parents }
1120}
1121
1122/// `public` if the block can import a class under it, else `public` with the
1123/// first numeric suffix nothing in the block uses. Each name the block holds
1124/// rules out at most one suffix, so one past their count is always free.
1125fn free_local_name(
1126    defs: &PackageDeclaration,
1127    imports: &BTreeMap<String, (&str, &str)>,
1128    public: &str,
1129) -> String {
1130    let is_free = |name: &str| !imports.contains_key(name) && !declares(defs, name, true);
1131    if is_free(public) {
1132        return public.to_string();
1133    }
1134    let taken = imports
1135        .len()
1136        .saturating_add(defs.types.len())
1137        .saturating_add(defs.values.len())
1138        .saturating_add(defs.namespaces.len());
1139    (1..=taken.saturating_add(1))
1140        .map(|suffix| format!("{public}{suffix}"))
1141        .find(|name| is_free(name))
1142        .unwrap_or_else(|| format!("{public}{}", taken.saturating_add(1)))
1143}
1144
1145/// Whether `defs` declares `name` itself in a way that would clash with
1146/// importing a type under it.
1147fn declares(defs: &PackageDeclaration, name: &str, borrowed_is_value: bool) -> bool {
1148    defs.types.contains_key(name)
1149        || (borrowed_is_value
1150            && (defs.values.contains_key(name) || defs.namespaces.contains_key(name)))
1151}
1152
1153/// The parent of each class `defs` exports that extends a known module's
1154/// class, by mangled name, with that class and its module.
1155fn class_parents<'a>(
1156    defs: &'a PackageDeclaration,
1157    exporters: &TypeExporters<'a>,
1158) -> Vec<(&'a str, ExportedType<'a>, &'a str)> {
1159    defs.types
1160        .values()
1161        .filter_map(|symbol| match &symbol.kind {
1162            TypeKind::Class {
1163                extends: Some(extends),
1164                ..
1165            } => {
1166                let parent = extends.parent.as_str();
1167                let &(exported, module) = exporters.by_mangled.get(parent)?;
1168                Some((parent, exported, module))
1169            }
1170            _ => None,
1171        })
1172        .collect()
1173}
1174
1175/// The by-name types the rendered declarations of `defs` mention: in its
1176/// values, namespaces, and the public surface of its types, which is what
1177/// `render_ts_declarations` prints. A private function or member doesn't
1178/// appear in the text, so its types don't count.
1179///
1180/// Every by-name `Type` serializes with its declaring symbol's `mangled` name,
1181/// so walking the serialized form finds them in every signature and member
1182/// without a visitor over each declaration shape. A reference renders as its
1183/// name and type arguments only, so the walk goes into `args` and not into an
1184/// alias's body. A class's parent isn't a `Type`; `class_parents` finds
1185/// those.
1186fn rendered_type_references(defs: &PackageDeclaration) -> Vec<TypeReference> {
1187    let types: BTreeMap<&String, TypeSymbol> = defs
1188        .types
1189        .iter()
1190        .map(|(name, symbol)| (name, public_surface(symbol)))
1191        .collect();
1192    let rendered = [
1193        serde_json::to_value(&defs.values),
1194        serde_json::to_value(&types),
1195        serde_json::to_value(&defs.namespaces),
1196    ];
1197    // A part that won't serialize still renders; it just imports nothing.
1198    let mut pending: Vec<&serde_json::Value> = rendered.iter().flatten().collect();
1199    let mut references = Vec::new();
1200    while let Some(value) = pending.pop() {
1201        match value {
1202            serde_json::Value::Object(fields) => {
1203                let Some(serde_json::Value::String(mangled)) = fields.get("mangled") else {
1204                    pending.extend(fields.values());
1205                    continue;
1206                };
1207                let text = |key: &str| {
1208                    fields
1209                        .get(key)
1210                        .and_then(serde_json::Value::as_str)
1211                        .unwrap_or_default()
1212                        .to_string()
1213                };
1214                references.push(TypeReference {
1215                    mangled: mangled.clone(),
1216                    package: text("package"),
1217                    name: text("name"),
1218                });
1219                pending.extend(fields.get("args"));
1220            }
1221            serde_json::Value::Array(items) => pending.extend(items),
1222            _ => {}
1223        }
1224    }
1225    references
1226}
1227
1228/// `symbol` as `render_ts_type` prints it: a class keeps only its public
1229/// members.
1230fn public_surface(symbol: &TypeSymbol) -> TypeSymbol {
1231    let mut symbol = symbol.clone();
1232    if let TypeKind::Class {
1233        fields,
1234        narrowing_checks,
1235        methods,
1236        method_visibility,
1237        accessors,
1238        constructor,
1239        constructor_visibility,
1240        statics,
1241        static_visibility,
1242        static_fields,
1243        ..
1244    } = &mut symbol.kind
1245    {
1246        if *constructor_visibility == crate::Visibility::Private {
1247            constructor.clear();
1248        }
1249        let is_public = |visibility: Option<&crate::Visibility>| {
1250            visibility != Some(&crate::Visibility::Private)
1251        };
1252        accessors.retain(|accessor| is_public(fields.get(accessor.name()).map(|f| &f.visibility)));
1253        fields.retain(|_, field| is_public(Some(&field.visibility)));
1254        static_fields.retain(|_, field| is_public(Some(&field.visibility)));
1255        methods.retain(|name, _| is_public(method_visibility.get(name)));
1256        statics.retain(|name, _| is_public(static_visibility.get(name)));
1257        narrowing_checks.clear();
1258    }
1259    symbol
1260}
1261
1262/// Render editor-facing TypeScript ambient declarations for always-in-scope
1263/// built-ins.
1264pub fn render_lib_submilli_d_ts() -> String {
1265    let defs = builtin_package_declaration();
1266    let globals = global_classes();
1267    let parent = |extends: &ClassExtends| {
1268        let global = globals.get(extends.parent.as_str())?;
1269        Some(ts_named_type(global, &extends.args))
1270    };
1271    let mut out = String::new();
1272    render_ts_declarations(&mut out, &defs, "", "declare ", &parent);
1273    render_ts_compiler_globals(&mut out, &defs);
1274    render_ts_declarations(&mut out, &global_functions(), "", "declare ", &|_| None);
1275    let json = json_package_declaration();
1276    render_ts_declarations(&mut out, &json, "", "declare ", &|_| None);
1277    out.trim_end().to_string()
1278}
1279
1280/// The global functions declared outside the prelude's type surface: the URI
1281/// functions with the prelude's host ABI, and `parseInt`/`parseFloat` in the
1282/// number host module.
1283fn global_functions() -> PackageDeclaration {
1284    const NAMES: [&str; 6] = [
1285        "parseInt",
1286        "parseFloat",
1287        "encodeURIComponent",
1288        "encodeURI",
1289        "decodeURIComponent",
1290        "decodeURI",
1291    ];
1292    let mut globals = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
1293    let sources = crate::runtime::host_package_declarations()
1294        .into_iter()
1295        .chain([crate::runtime::prelude::package_declaration()]);
1296    for source in sources {
1297        for symbol in source.values.into_values() {
1298            if NAMES.contains(&symbol.name.as_str()) {
1299                globals.values.insert(symbol.name.clone(), symbol);
1300            }
1301        }
1302    }
1303    globals
1304}
1305
1306fn render_ts_compiler_globals(out: &mut String, defs: &PackageDeclaration) {
1307    if !defs.types.contains_key("Function") {
1308        out.push_str(
1309            "interface Function {\n  /** The parameters before the first optional, defaulted or rest one. */\n  readonly length: number;\n}\n\n",
1310        );
1311    }
1312    out.push_str("interface CallableFunction extends Function {}\n\n");
1313    out.push_str("interface NewableFunction extends Function {}\n\n");
1314    out.push_str("interface IArguments {}\n\n");
1315    out.push_str(
1316        "/** Throw `Error(message)` if `condition` is false. Compiler intrinsic — always in scope, not imported. */\n\
1317         declare function assert(condition: boolean, message?: string): void;\n\n",
1318    );
1319    render_ts_checker_plumbing(out, defs);
1320}
1321
1322// Members the TS checker requires structurally but submilli handles
1323// intrinsically, so the prelude declarations lack them: with `target` ≥
1324// es2015 `for-of` needs `[Symbol.iterator]()` (submilli's protocol is a
1325// plain `iterator()` method; arrays iterate at the language level), and
1326// `arr[i]` needs an index signature. Merged into `Array` and into every
1327// prelude interface declaring `iterator()`. Users never write these; only
1328// the checker reads them.
1329fn render_ts_checker_plumbing(out: &mut String, defs: &PackageDeclaration) {
1330    out.push_str("declare const Symbol: { readonly iterator: unique symbol };\n\n");
1331    out.push_str(
1332        "interface Array<T> {\n  [index: number]: T;\n  [Symbol.iterator](): Iterator<T>;\n}\n\n",
1333    );
1334    for (name, sym) in &defs.types {
1335        if let TypeKind::Interface {
1336            generics, methods, ..
1337        } = &sym.kind
1338            && let Some(m) = methods.get("iterator")
1339        {
1340            let _ = writeln!(
1341                out,
1342                "interface {name}{} {{\n  [Symbol.iterator](): {};\n}}\n",
1343                ts_interface_generics(name, generics, "declare "),
1344                ts_type(&m.ret)
1345            );
1346        }
1347    }
1348}
1349
1350fn render_declare_module(out: &mut String, defs: &PackageDeclaration) {
1351    let _ = writeln!(out, "declare module \"{}\" {{", defs.package_name);
1352    render_ts_declarations(out, defs, "  ", "export ", &|_| None);
1353    let _ = writeln!(out, "}}\n");
1354}
1355
1356/// `class_parent` renders a class's `extends` target, or declines, in which
1357/// case the class renders without one.
1358fn render_ts_declarations(
1359    out: &mut String,
1360    defs: &PackageDeclaration,
1361    indent: &str,
1362    export_prefix: &str,
1363    class_parent: &dyn Fn(&ClassExtends) -> Option<String>,
1364) {
1365    for (name, sym) in &defs.values {
1366        if is_hidden_prelude_value(&defs.package_name, name) {
1367            continue;
1368        }
1369        push_doc(out, value_doc(&sym.kind), indent);
1370        render_ts_value(out, name, &sym.kind, indent, export_prefix);
1371    }
1372    // Only the runtime's own modules pair a type with an `XConstructor`
1373    // interface for its static side; in a package they're two ordinary types.
1374    let pairs_constructors = defs.package_name.starts_with("submilli:");
1375    for (name, sym) in &defs.types {
1376        if is_hidden_prelude_type(&defs.package_name, name) {
1377            continue;
1378        }
1379        if pairs_constructors && name.ends_with("Constructor") {
1380            continue;
1381        }
1382        if pairs_constructors && defs.types.contains_key(&format!("{name}Constructor")) {
1383            render_ts_type_with_ctor(
1384                out,
1385                &defs.types,
1386                name,
1387                indent,
1388                export_prefix,
1389                !defs.values.contains_key(name),
1390            );
1391        } else {
1392            push_doc(out, type_doc(&sym.kind), indent);
1393            let parent = match &sym.kind {
1394                TypeKind::Class {
1395                    extends: Some(extends),
1396                    ..
1397                } => class_parent(extends),
1398                _ => None,
1399            };
1400            render_ts_type(
1401                out,
1402                name,
1403                &sym.kind,
1404                indent,
1405                export_prefix,
1406                parent.as_deref(),
1407            );
1408        }
1409    }
1410    for (name, ns) in &defs.namespaces {
1411        render_ts_namespace(out, name, ns, indent, export_prefix);
1412    }
1413}
1414
1415fn is_hidden_prelude_value(package_name: &str, name: &str) -> bool {
1416    package_name == crate::mangle::PRELUDE_PACKAGE
1417        && (name.starts_with("string_")
1418            || matches!(
1419                name,
1420                "string_concat" | "string_eq" | "string_length" | "string_cmp"
1421            ))
1422}
1423
1424fn is_hidden_prelude_type(package_name: &str, name: &str) -> bool {
1425    package_name == crate::mangle::PRELUDE_PACKAGE && name.contains('#')
1426}
1427
1428fn render_ts_value(
1429    out: &mut String,
1430    name: &str,
1431    kind: &ValueKind,
1432    indent: &str,
1433    export_prefix: &str,
1434) {
1435    let reserved_export = export_prefix == "export " && is_ts_reserved_word(name);
1436    let declared_name = if reserved_export {
1437        format!("{name}_")
1438    } else {
1439        name.to_string()
1440    };
1441    let decl_export_prefix = if reserved_export { "" } else { export_prefix };
1442    match kind {
1443        ValueKind::Function {
1444            generics,
1445            params,
1446            ret,
1447            type_predicate,
1448            ..
1449        } => {
1450            let ret = ts_return_type(
1451                params,
1452                ret,
1453                type_predicate.as_ref(),
1454                value_doc(kind).as_ref(),
1455            );
1456            let _ = writeln!(
1457                out,
1458                "{indent}{decl_export_prefix}function {declared_name}{}({}): {ret};",
1459                generics_str(generics),
1460                ts_params_str(params, value_doc(kind).as_ref())
1461            );
1462        }
1463        ValueKind::Let { ty, .. } => {
1464            let _ = writeln!(
1465                out,
1466                "{indent}{decl_export_prefix}let {declared_name}: {};",
1467                ts_type(ty)
1468            );
1469        }
1470        ValueKind::Const { ty, .. } => {
1471            let _ = writeln!(
1472                out,
1473                "{indent}{decl_export_prefix}const {declared_name}: {};",
1474                ts_type(ty)
1475            );
1476        }
1477    }
1478    if reserved_export {
1479        let _ = writeln!(out, "{indent}export {{ {declared_name} as {name} }};");
1480    }
1481    out.push('\n');
1482}
1483
1484/// `parent` is the rendered `extends` target of a class, if it has one.
1485fn render_ts_type(
1486    out: &mut String,
1487    name: &str,
1488    kind: &TypeKind,
1489    indent: &str,
1490    export_prefix: &str,
1491    parent: Option<&str>,
1492) {
1493    let inner = format!("{indent}  ");
1494    match kind {
1495        TypeKind::Interface {
1496            generics,
1497            methods,
1498            properties,
1499            index,
1500            ..
1501        } => {
1502            let _ = writeln!(
1503                out,
1504                "{indent}{export_prefix}interface {name}{} {{",
1505                ts_interface_generics(name, generics, export_prefix)
1506            );
1507            if let Some(index) = index {
1508                let ro = if index.readonly { "readonly " } else { "" };
1509                let _ = writeln!(out, "{inner}{ro}[key: string]: {};", ts_type(&index.value));
1510            }
1511            for (pname, prop) in properties {
1512                push_doc(out, &prop.doc, &inner);
1513                let ro = if prop.readonly { "readonly " } else { "" };
1514                let opt = if prop.optional { "?" } else { "" };
1515                let pname = member_name(pname);
1516                let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", ts_type(&prop.ty));
1517            }
1518            for (mname, m) in methods {
1519                push_doc(out, &m.doc, &inner);
1520                let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1521                let optional = if m.optional { "?" } else { "" };
1522                let mname = method_prefix(mname);
1523                let _ = writeln!(
1524                    out,
1525                    "{inner}{mname}{optional}{}({}): {ret};",
1526                    generics_str(&m.generics),
1527                    ts_params_str(&m.params, m.doc.as_ref())
1528                );
1529            }
1530            let _ = writeln!(out, "{indent}}}\n");
1531        }
1532        TypeKind::NumberEnum { variants, .. } => {
1533            let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1534            for (v, value) in variants {
1535                let _ = writeln!(out, "{inner}{v} = {value},");
1536            }
1537            let _ = writeln!(out, "{indent}}}\n");
1538        }
1539        TypeKind::StringEnum { variants, .. } => {
1540            let _ = writeln!(out, "{indent}{export_prefix}enum {name} {{");
1541            for (v, value) in variants {
1542                let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
1543            }
1544            let _ = writeln!(out, "{indent}}}\n");
1545        }
1546        TypeKind::Alias { generics, ty, .. } => {
1547            let _ = writeln!(
1548                out,
1549                "{indent}{export_prefix}type {name}{} = {};\n",
1550                generics_str(generics),
1551                ts_type(ty)
1552            );
1553        }
1554        TypeKind::Class {
1555            generics,
1556            fields,
1557            methods,
1558            method_visibility,
1559            statics,
1560            static_visibility,
1561            static_fields,
1562            accessors,
1563            constructor,
1564            constructor_visibility,
1565            ..
1566        } => {
1567            // Private members are part of the in-memory class but never the public
1568            // API an agent can call, so they're omitted from the rendered surface
1569            // (`packages.docs` and the generated `.d.ts`). Privacy itself is enforced
1570            // by the typechecker; this only keeps private names/types out of the docs.
1571            let extends = parent.map(|parent| format!(" extends {parent}"));
1572            let _ = writeln!(
1573                out,
1574                "{indent}{export_prefix}class {name}{}{} {{",
1575                generics_str(generics),
1576                extends.unwrap_or_default()
1577            );
1578            for (fname, field) in static_fields {
1579                if field.visibility == crate::Visibility::Private {
1580                    continue;
1581                }
1582                push_doc(out, &field.doc, &inner);
1583                let ro = if field.readonly { "readonly " } else { "" };
1584                let fname = member_name(fname);
1585                let _ = writeln!(out, "{inner}static {ro}{fname}: {};", ts_type(&field.ty));
1586            }
1587            for (mname, m) in statics {
1588                if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
1589                    continue;
1590                }
1591                push_doc(out, &m.doc, &inner);
1592                let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1593                let mname = member_name(mname);
1594                let _ = writeln!(
1595                    out,
1596                    "{inner}static {mname}{}({}): {ret};",
1597                    generics_str(&m.generics),
1598                    ts_params_str(&m.params, m.doc.as_ref())
1599                );
1600            }
1601            for (fname, field) in fields {
1602                if field.visibility == crate::Visibility::Private
1603                    || accessors.iter().any(|a| a.name() == fname)
1604                {
1605                    continue;
1606                }
1607                push_doc(out, &field.doc, &inner);
1608                let ro = if field.readonly { "readonly " } else { "" };
1609                let opt = if field.optional { "?" } else { "" };
1610                let fname = member_name(fname);
1611                let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", ts_type(&field.ty));
1612            }
1613            render_class_accessors(out, fields, accessors, &inner, ts_type);
1614            // A private constructor still renders, so a TypeScript consumer can't
1615            // call an implicit public one, but as `protected`: a subclass in the
1616            // class's own module is legal here, and tsc rejects `extends` of a
1617            // class whose constructor is `private`. Its parameters stay hidden. The
1618            // cost: tsc lets a consumer extend it, which Submilli rejects.
1619            if *constructor_visibility == crate::Visibility::Private {
1620                let _ = writeln!(out, "{inner}protected constructor();");
1621            } else {
1622                let _ = writeln!(
1623                    out,
1624                    "{inner}constructor({});",
1625                    ts_params_str(constructor, None)
1626                );
1627            }
1628            for (mname, m) in methods {
1629                if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
1630                    continue;
1631                }
1632                push_doc(out, &m.doc, &inner);
1633                let ret = ts_return_type(&m.params, &m.ret, m.predicate.as_ref(), m.doc.as_ref());
1634                let optional = if m.optional { "?" } else { "" };
1635                let mname = member_name(mname);
1636                let _ = writeln!(
1637                    out,
1638                    "{inner}{mname}{optional}{}({}): {ret};",
1639                    generics_str(&m.generics),
1640                    ts_params_str(&m.params, m.doc.as_ref())
1641                );
1642            }
1643            let _ = writeln!(out, "{indent}}}\n");
1644        }
1645    }
1646}
1647
1648/// Render a class's accessor properties for the public surface (docs / `.d.ts`).
1649/// Same-typed get+set renders as a writable property, get-only as `readonly`,
1650/// and a write-only or differently-typed pair as explicit `get`/`set` lines.
1651/// Private accessors (visibility lives on the property's `FieldSig`) are skipped.
1652fn render_class_accessors(
1653    out: &mut String,
1654    fields: &std::collections::BTreeMap<String, crate::FieldSig>,
1655    accessors: &[crate::AccessorSig],
1656    inner: &str,
1657    fmt_ty: impl Fn(&Type) -> String,
1658) {
1659    use crate::AccessorSig;
1660    let mut by_name: std::collections::BTreeMap<&str, (Option<&Type>, Option<&crate::Param>)> =
1661        std::collections::BTreeMap::new();
1662    for acc in accessors {
1663        let entry = by_name.entry(acc.name()).or_insert((None, None));
1664        match acc {
1665            AccessorSig::Getter { ret_ty, .. } => entry.0 = Some(ret_ty),
1666            AccessorSig::Setter { param, .. } => entry.1 = Some(param),
1667        }
1668    }
1669    for (name, (get, set)) in by_name {
1670        if fields.get(name).map(|f| f.visibility) == Some(crate::Visibility::Private) {
1671            continue;
1672        }
1673        let name = member_name(name);
1674        match (get, set) {
1675            (Some(r), Some(p)) if r == &p.ty => {
1676                let _ = writeln!(out, "{inner}{name}: {};", fmt_ty(r));
1677            }
1678            (Some(r), Some(p)) => {
1679                let _ = writeln!(out, "{inner}get {name}(): {};", fmt_ty(r));
1680                let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1681            }
1682            (Some(r), None) => {
1683                let _ = writeln!(out, "{inner}readonly {name}: {};", fmt_ty(r));
1684            }
1685            (None, Some(p)) => {
1686                let _ = writeln!(out, "{inner}set {name}({}: {});", p.name, fmt_ty(&p.ty));
1687            }
1688            (None, None) => {}
1689        }
1690    }
1691}
1692
1693fn render_ts_type_with_ctor(
1694    out: &mut String,
1695    types: &BTreeMap<String, TypeSymbol>,
1696    name: &str,
1697    indent: &str,
1698    export_prefix: &str,
1699    synthesize_binding: bool,
1700) {
1701    if let Some(sym) = types.get(name) {
1702        push_doc(out, type_doc(&sym.kind), indent);
1703        render_ts_type(out, name, &sym.kind, indent, export_prefix, None);
1704    }
1705    let ctor = format!("{name}Constructor");
1706    if let Some(sym) = types.get(&ctor) {
1707        push_doc(out, type_doc(&sym.kind), indent);
1708        render_ts_type(out, &ctor, &sym.kind, indent, export_prefix, None);
1709        if synthesize_binding {
1710            let _ = writeln!(out, "{indent}{export_prefix}const {name}: {ctor};\n");
1711        }
1712    }
1713}
1714
1715fn render_ts_namespace(
1716    out: &mut String,
1717    name: &str,
1718    ns: &NamespaceSymbol,
1719    indent: &str,
1720    export_prefix: &str,
1721) {
1722    push_doc(out, &ns.doc, indent);
1723    let _ = writeln!(out, "{indent}{export_prefix}namespace {name} {{");
1724    let inner = format!("{indent}  ");
1725    for (vname, sym) in &ns.values {
1726        push_doc(out, value_doc(&sym.kind), &inner);
1727        render_ts_value(out, vname, &sym.kind, &inner, "");
1728    }
1729    for (tname, sym) in &ns.types {
1730        push_doc(out, type_doc(&sym.kind), &inner);
1731        render_ts_type(out, tname, &sym.kind, &inner, "", None);
1732    }
1733    for (nname, sub) in &ns.namespaces {
1734        render_ts_namespace(out, nname, sub, &inner, "");
1735    }
1736    let _ = writeln!(out, "{indent}}}\n");
1737}
1738
1739fn ts_params_str(params: &[Param], doc: Option<&DocComment>) -> String {
1740    rendered_params(params, doc, ts_type)
1741}
1742
1743fn ts_return_type(
1744    params: &[Param],
1745    ret: &Type,
1746    predicate: Option<&TypePredicate>,
1747    doc: Option<&DocComment>,
1748) -> String {
1749    let Some(predicate) = predicate else {
1750        return ts_type(ret);
1751    };
1752    let Some(param) = params.get(predicate.parameter_index as usize) else {
1753        return ts_type(ret);
1754    };
1755    let names = parameter_display_names(params, doc);
1756    let Some(name) = names.get(predicate.parameter_index as usize) else {
1757        return ts_type(ret);
1758    };
1759    let asserted = ts_type(&predicate.asserted_type);
1760    match param.ty {
1761        Type::TypeVar(_) | Type::GenericParam { .. } => {
1762            format!("{name} is {} & {asserted}", ts_type(&param.ty))
1763        }
1764        _ => format!("{name} is {asserted}"),
1765    }
1766}
1767
1768fn ts_type(ty: &Type) -> String {
1769    match ty {
1770        Type::Refined { original, ty } => format!("({} & {})", ts_type(original), ts_type(ty)),
1771        Type::Number => "number".to_string(),
1772        Type::NumberLiteral(n) => n.0.to_string(),
1773        Type::BigInt => "bigint".to_string(),
1774        Type::BigIntLiteral(digits) => format!("{digits}n"),
1775        Type::String => "string".to_string(),
1776        Type::StringLiteral(s) => format!("\"{}\"", escape_string_literal(s)),
1777        Type::Uint8Array => "Uint8Array".to_string(),
1778        Type::Boolean => "boolean".to_string(),
1779        Type::BooleanLiteral(value) => value.to_string(),
1780        Type::Null => "null".to_string(),
1781        Type::Undefined => "undefined".to_string(),
1782        Type::Void => "void".to_string(),
1783        Type::Unknown => "unknown".to_string(),
1784        Type::NonPrimitive => "object".to_string(),
1785        Type::Function {
1786            params,
1787            ret,
1788            predicate,
1789            has_rest,
1790            optional,
1791        } => {
1792            let params = params
1793                .iter()
1794                .enumerate()
1795                .map(|(i, p)| {
1796                    let prefix = if *has_rest && i == params.len().saturating_sub(1) {
1797                        "..."
1798                    } else {
1799                        ""
1800                    };
1801                    let fixed = params.len().saturating_sub(usize::from(*has_rest));
1802                    if i < fixed && i >= fixed.saturating_sub(*optional) {
1803                        let shown = crate::types::shown_beside_optional_marker(p);
1804                        format!("{prefix}arg{i}?: {}", ts_type(&shown))
1805                    } else {
1806                        format!("{prefix}arg{i}: {}", ts_type(p))
1807                    }
1808                })
1809                .collect::<Vec<_>>()
1810                .join(", ");
1811            let ret = predicate.as_deref().map_or_else(
1812                || ts_type(ret),
1813                |p| format!("arg{} is {}", p.parameter_index, ts_type(&p.asserted_type)),
1814            );
1815            format!("({params}) => {ret}")
1816        }
1817        Type::Object { fields, index } => {
1818            let mut members: Vec<String> = fields
1819                .iter()
1820                .map(|(name, field)| {
1821                    let opt = if field.optional { "?" } else { "" };
1822                    let ro = if field.readonly { "readonly " } else { "" };
1823                    let name = member_name(name);
1824                    format!("{ro}{name}{opt}: {}", ts_type(&field.ty))
1825                })
1826                .collect();
1827            if let Some(index) = index {
1828                let ro = if index.readonly { "readonly " } else { "" };
1829                members.push(format!("{ro}[key: string]: {}", ts_type(&index.value)));
1830            }
1831            if members.is_empty() {
1832                "{}".into()
1833            } else {
1834                format!("{{ {} }}", members.join("; "))
1835            }
1836        }
1837        Type::Array(elem) => format!("{}[]", ts_type_array_element(elem)),
1838        Type::Tuple(elements) => {
1839            let elements = elements
1840                .iter()
1841                .enumerate()
1842                .map(|(index, ty)| {
1843                    if index >= elements.len().saturating_sub(elements.optional) {
1844                        format!("({})?", ts_type(ty))
1845                    } else {
1846                        ts_type(ty)
1847                    }
1848                })
1849                .collect::<Vec<_>>()
1850                .join(", ");
1851            format!("[{elements}]")
1852        }
1853        Type::Readonly(inner) if matches!(inner.as_ref(), Type::InterfaceRef { .. }) => {
1854            format!("Readonly{}", ts_type(inner))
1855        }
1856        Type::Readonly(inner) => format!("readonly {}", ts_type(inner)),
1857        Type::Error => "never".to_string(),
1858        Type::Never => "never".to_string(),
1859        Type::TypeVar(name) | Type::GenericParam { name, .. } => name.clone(),
1860        Type::InterfaceRef { name, args, .. }
1861        | Type::ClassRef { name, args, .. }
1862        | Type::Alias { name, args, .. } => ts_named_type(name, args),
1863        Type::NumberEnum { name, .. } | Type::StringEnum { name, .. } => name.clone(),
1864        Type::Union(members) => crate::types::union_display_order(members)
1865            .into_iter()
1866            .map(ts_type_union_member)
1867            .collect::<Vec<_>>()
1868            .join(" | "),
1869        Type::AliasRef { name, args, .. } => ts_named_type(name, args),
1870    }
1871}
1872
1873fn ts_named_type(name: &str, args: &[Type]) -> String {
1874    if args.is_empty() {
1875        return name.to_string();
1876    }
1877    let args = args.iter().map(ts_type).collect::<Vec<_>>().join(", ");
1878    format!("{name}<{args}>")
1879}
1880
1881fn ts_type_array_element(ty: &Type) -> String {
1882    match ty {
1883        Type::Readonly(inner) if matches!(inner.as_ref(), Type::InterfaceRef { .. }) => ts_type(ty),
1884        Type::Function { .. } | Type::Union(_) | Type::Readonly(_) => format!("({})", ts_type(ty)),
1885        _ => ts_type(ty),
1886    }
1887}
1888
1889fn ts_type_union_member(ty: &Type) -> String {
1890    match ty {
1891        Type::Function { .. } => format!("({})", ts_type(ty)),
1892        _ => ts_type(ty),
1893    }
1894}
1895
1896fn is_ts_reserved_word(name: &str) -> bool {
1897    matches!(
1898        name,
1899        "break"
1900            | "case"
1901            | "catch"
1902            | "class"
1903            | "const"
1904            | "continue"
1905            | "debugger"
1906            | "default"
1907            | "delete"
1908            | "do"
1909            | "else"
1910            | "enum"
1911            | "export"
1912            | "extends"
1913            | "false"
1914            | "finally"
1915            | "for"
1916            | "function"
1917            | "if"
1918            | "import"
1919            | "in"
1920            | "instanceof"
1921            | "new"
1922            | "null"
1923            | "return"
1924            | "super"
1925            | "switch"
1926            | "this"
1927            | "throw"
1928            | "true"
1929            | "try"
1930            | "typeof"
1931            | "var"
1932            | "void"
1933            | "while"
1934            | "with"
1935            | "as"
1936            | "async"
1937            | "await"
1938            | "from"
1939            | "get"
1940            | "let"
1941            | "of"
1942            | "set"
1943            | "static"
1944            | "using"
1945            | "yield"
1946    )
1947}
1948
1949fn json_package_declaration() -> PackageDeclaration {
1950    let mut defs = PackageDeclaration::with_package(crate::mangle::PRELUDE_PACKAGE);
1951    let json_prefix = crate::mangle::prelude(JSON_BUILTIN);
1952    let mut json = NamespaceSymbol {
1953        name: JSON_BUILTIN.to_string(),
1954        mangled_prefix: json_prefix.clone(),
1955        declaration_span: Span::at(FileId::JSON),
1956        values: BTreeMap::new(),
1957        types: BTreeMap::new(),
1958        namespaces: BTreeMap::new(),
1959        doc: None,
1960    };
1961    json.values.insert(
1962        "stringify".to_string(),
1963        ValueSymbol {
1964            name: "stringify".to_string(),
1965            mangled_name: crate::mangle::extend(&json_prefix, "stringify"),
1966            declaration_span: Span::at(FileId::JSON),
1967            kind: ValueKind::Function {
1968                generics: vec!["T".to_string()],
1969                params: vec![
1970                    Param::new("value", Type::TypeVar("T".to_string())),
1971                    Param {
1972                        name: "replacer".to_string(),
1973                        ty: Type::union(vec![Type::Null, Type::Undefined]),
1974                        default: Some(crate::DefaultValue::Undefined),
1975                        optional: false,
1976                        rest: false,
1977                    },
1978                    Param {
1979                        name: "space".to_string(),
1980                        ty: Type::union(vec![Type::Number, Type::String, Type::Null, Type::Undefined]),
1981                        default: Some(crate::DefaultValue::Undefined),
1982                        optional: false,
1983                        rest: false,
1984                    },
1985                ],
1986                ret: Type::union(vec![Type::String, Type::Undefined]),
1987                type_predicate: None,
1988                doc: crate::doc(FileId::JSON, "/** Serialize a value to a JSON string, or return undefined when it has no JSON representation. */"),
1989            },
1990        },
1991    );
1992    json.values.insert(
1993        "parse".to_string(),
1994        ValueSymbol {
1995            name: "parse".to_string(),
1996            mangled_name: crate::mangle::extend(&json_prefix, "parse"),
1997            declaration_span: Span::at(FileId::JSON),
1998            kind: ValueKind::Function {
1999                generics: Vec::new(),
2000                params: vec![Param::new("text", Type::String)],
2001                ret: Type::Unknown,
2002                type_predicate: None,
2003                doc: crate::doc(
2004                    FileId::JSON,
2005                    "/** Parse a JSON string as unknown; use `JSON.parse(s) as T` to validate a target type. */",
2006                ),
2007            },
2008        },
2009    );
2010    defs.namespaces.insert(JSON_BUILTIN.to_string(), json);
2011    defs
2012}
2013
2014fn render_value(out: &mut String, name: &str, kind: &ValueKind, indent: &str) {
2015    match kind {
2016        ValueKind::Function {
2017            generics,
2018            params,
2019            ret,
2020            ..
2021        } => {
2022            let _ = writeln!(
2023                out,
2024                "{indent}function {name}{}({}): {ret};\n",
2025                generics_str(generics),
2026                params_str(params, value_doc(kind).as_ref())
2027            );
2028        }
2029        ValueKind::Let { ty, .. } => {
2030            let _ = writeln!(out, "{indent}let {name}: {ty};\n");
2031        }
2032        ValueKind::Const { ty, .. } => {
2033            let _ = writeln!(out, "{indent}const {name}: {ty};\n");
2034        }
2035    }
2036}
2037
2038fn render_type(out: &mut String, name: &str, kind: &TypeKind, indent: &str) {
2039    let inner = format!("{indent}  ");
2040    match kind {
2041        TypeKind::Interface {
2042            generics,
2043            methods,
2044            properties,
2045            ..
2046        } => {
2047            let _ = writeln!(out, "{indent}interface {name}{} {{", generics_str(generics));
2048            for (pname, prop) in properties {
2049                push_doc(out, &prop.doc, &inner);
2050                let ro = if prop.readonly { "readonly " } else { "" };
2051                let opt = if prop.optional { "?" } else { "" };
2052                let pname = member_name(pname);
2053                let _ = writeln!(out, "{inner}{ro}{pname}{opt}: {};", prop.ty);
2054            }
2055            for (mname, m) in methods {
2056                push_doc(out, &m.doc, &inner);
2057                let mname = method_prefix(mname);
2058                let _ = writeln!(
2059                    out,
2060                    "{inner}{mname}{}{}({}): {};",
2061                    if m.optional { "?" } else { "" },
2062                    generics_str(&m.generics),
2063                    params_str(&m.params, m.doc.as_ref()),
2064                    m.ret
2065                );
2066            }
2067            let _ = writeln!(out, "{indent}}}\n");
2068        }
2069        TypeKind::NumberEnum { variants, .. } => {
2070            let _ = writeln!(out, "{indent}enum {name} {{");
2071            for (v, value) in variants {
2072                let _ = writeln!(out, "{inner}{v} = {value},");
2073            }
2074            let _ = writeln!(out, "{indent}}}\n");
2075        }
2076        TypeKind::StringEnum { variants, .. } => {
2077            let _ = writeln!(out, "{indent}enum {name} {{");
2078            for (v, value) in variants {
2079                let _ = writeln!(out, "{inner}{v} = \"{}\",", escape_string_literal(value));
2080            }
2081            let _ = writeln!(out, "{indent}}}\n");
2082        }
2083        TypeKind::Alias { generics, ty, .. } => {
2084            let _ = writeln!(
2085                out,
2086                "{indent}type {name}{} = {ty};\n",
2087                generics_str(generics)
2088            );
2089        }
2090        TypeKind::Class {
2091            generics,
2092            fields,
2093            methods,
2094            method_visibility,
2095            statics,
2096            static_visibility,
2097            static_fields,
2098            accessors,
2099            constructor,
2100            constructor_visibility,
2101            ..
2102        } => {
2103            // Private members exist in the class but are never callable from a
2104            // consumer, so they're omitted from the rendered API surface
2105            // (`packages.docs`). Privacy is enforced by the typechecker; this only
2106            // keeps private names/types out of the agent-facing docs.
2107            let _ = writeln!(out, "{indent}class {name}{} {{", generics_str(generics));
2108            for (fname, field) in static_fields {
2109                if field.visibility == crate::Visibility::Private {
2110                    continue;
2111                }
2112                push_doc(out, &field.doc, &inner);
2113                let ro = if field.readonly { "readonly " } else { "" };
2114                let fname = member_name(fname);
2115                let _ = writeln!(out, "{inner}static {ro}{fname}: {};", field.ty);
2116            }
2117            for (mname, m) in statics {
2118                if static_visibility.get(mname) == Some(&crate::Visibility::Private) {
2119                    continue;
2120                }
2121                push_doc(out, &m.doc, &inner);
2122                let mname = member_name(mname);
2123                let _ = writeln!(
2124                    out,
2125                    "{inner}static {mname}{}({}): {};",
2126                    generics_str(&m.generics),
2127                    params_str(&m.params, m.doc.as_ref()),
2128                    m.ret
2129                );
2130            }
2131            for (fname, field) in fields {
2132                if field.visibility == crate::Visibility::Private
2133                    || accessors.iter().any(|a| a.name() == fname)
2134                {
2135                    continue;
2136                }
2137                push_doc(out, &field.doc, &inner);
2138                let ro = if field.readonly { "readonly " } else { "" };
2139                let opt = if field.optional { "?" } else { "" };
2140                let fname = member_name(fname);
2141                let _ = writeln!(out, "{inner}{ro}{fname}{opt}: {};", field.ty);
2142            }
2143            render_class_accessors(out, fields, accessors, &inner, ToString::to_string);
2144            if *constructor_visibility != crate::Visibility::Private {
2145                let _ = writeln!(
2146                    out,
2147                    "{inner}constructor({});",
2148                    params_str(constructor, None)
2149                );
2150            }
2151            for (mname, m) in methods {
2152                if method_visibility.get(mname) == Some(&crate::Visibility::Private) {
2153                    continue;
2154                }
2155                push_doc(out, &m.doc, &inner);
2156                let mname = member_name(mname);
2157                let _ = writeln!(
2158                    out,
2159                    "{inner}{mname}{}{}({}): {};",
2160                    if m.optional { "?" } else { "" },
2161                    generics_str(&m.generics),
2162                    params_str(&m.params, m.doc.as_ref()),
2163                    m.ret
2164                );
2165            }
2166            let _ = writeln!(out, "{indent}}}\n");
2167        }
2168    }
2169}
2170
2171/// Render `name`'s interface followed by its `{name}Constructor` (the `new` /
2172/// static side, e.g. `Array.isArray`) and the `const name: nameConstructor`
2173/// binding that ties them — the lib.d.ts shape an LLM expects. Either half may
2174/// be absent.
2175fn render_type_with_ctor(
2176    out: &mut String,
2177    types: &BTreeMap<String, TypeSymbol>,
2178    name: &str,
2179    indent: &str,
2180) {
2181    if let Some(sym) = types.get(name) {
2182        push_doc(out, type_doc(&sym.kind), indent);
2183        render_type(out, name, &sym.kind, indent);
2184    }
2185    let ctor = format!("{name}Constructor");
2186    if let Some(sym) = types.get(&ctor) {
2187        push_doc(out, type_doc(&sym.kind), indent);
2188        render_type(out, &ctor, &sym.kind, indent);
2189        let _ = writeln!(out, "{indent}const {name}: {ctor};\n");
2190    }
2191}
2192
2193fn render_namespace(out: &mut String, name: &str, ns: &NamespaceSymbol, indent: &str) {
2194    let _ = writeln!(out, "{indent}namespace {name} {{");
2195    let inner = format!("{indent}  ");
2196    for (vname, sym) in &ns.values {
2197        push_doc(out, value_doc(&sym.kind), &inner);
2198        render_value(out, vname, &sym.kind, &inner);
2199    }
2200    for (tname, sym) in &ns.types {
2201        // The namespace's own values already bind each `XConstructor` to `X`,
2202        // so render every type (constructors included) plainly — no merge.
2203        push_doc(out, type_doc(&sym.kind), &inner);
2204        render_type(out, tname, &sym.kind, &inner);
2205    }
2206    for (nname, sub) in &ns.namespaces {
2207        render_namespace(out, nname, sub, &inner);
2208    }
2209    let _ = writeln!(out, "{indent}}}\n");
2210}
2211
2212fn generics_str(generics: &[String]) -> String {
2213    if generics.is_empty() {
2214        String::new()
2215    } else {
2216        format!("<{}>", generics.join(", "))
2217    }
2218}
2219
2220// TS ≥5.6 hard-codes that the global `Iterable`/`Iterator` types take three
2221// type parameters; phantom defaults satisfy the checker while keeping
2222// single-argument usage (`Iterable<T>`) valid.
2223fn ts_interface_generics(name: &str, generics: &[String], export_prefix: &str) -> String {
2224    if export_prefix == "declare "
2225        && matches!(name, "Iterable" | "Iterator")
2226        && let [t] = generics
2227    {
2228        return format!("<{t}, TReturn = any, TNext = any>");
2229    }
2230    generics_str(generics)
2231}
2232
2233fn params_str(params: &[Param], doc: Option<&DocComment>) -> String {
2234    rendered_params(params, doc, ToString::to_string)
2235}
2236
2237fn rendered_params(
2238    params: &[Param],
2239    doc: Option<&DocComment>,
2240    format_type: impl Fn(&Type) -> String,
2241) -> String {
2242    let names = parameter_display_names(params, doc);
2243    params
2244        .iter()
2245        .zip(names)
2246        .enumerate()
2247        .map(|(index, (p, name))| {
2248            let prefix = if p.rest { "..." } else { "" };
2249            // A parameter a required one follows can't be written `x?`.
2250            let later_required = params
2251                .iter()
2252                .skip(index.saturating_add(1))
2253                .any(|later| !later.is_omittable() && !later.rest);
2254            let opt = if p.is_omittable() && !later_required {
2255                "?"
2256            } else {
2257                ""
2258            };
2259            // `x?: T` already admits `undefined`; printing it again is noise.
2260            let ty = if opt.is_empty() {
2261                p.ty.clone()
2262            } else {
2263                crate::types::shown_beside_optional_marker(&p.ty)
2264            };
2265            format!("{prefix}{name}{opt}: {}", format_type(&ty))
2266        })
2267        .collect::<Vec<_>>()
2268        .join(", ")
2269}
2270
2271/// Binding patterns are lowered to compiler-only names. Documentation uses a
2272/// positional JSDoc name when valid, or a unique positional placeholder.
2273fn parameter_display_names(params: &[Param], doc: Option<&DocComment>) -> Vec<String> {
2274    let tags: Vec<_> = doc
2275        .into_iter()
2276        .flat_map(|doc| &doc.params)
2277        .filter(|tag| !tag.name.contains('.'))
2278        .collect();
2279    let mut used: BTreeSet<String> = params
2280        .iter()
2281        .filter(|p| !crate::lower_patterns::is_pattern_param(&p.name))
2282        .map(|p| p.name.clone())
2283        .collect();
2284    params
2285        .iter()
2286        .enumerate()
2287        .map(|(index, param)| {
2288            if !crate::lower_patterns::is_pattern_param(&param.name) {
2289                return param.name.clone();
2290            }
2291            if let Some(tag) = tags.get(index)
2292                && valid_parameter_name(&tag.name)
2293                && used.insert(tag.name.clone())
2294            {
2295                return tag.name.clone();
2296            }
2297            let mut name = format!("arg{index}");
2298            while !used.insert(name.clone()) {
2299                name.push('_');
2300            }
2301            name
2302        })
2303        .collect()
2304}
2305
2306fn valid_parameter_name(name: &str) -> bool {
2307    let mut chars = name.chars();
2308    chars
2309        .next()
2310        .is_some_and(|c| c.is_ascii_alphabetic() || matches!(c, '_' | '$'))
2311        && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '$'))
2312        && !is_ts_reserved_word(name)
2313        && !matches!(
2314            name,
2315            "arguments"
2316                | "eval"
2317                | "implements"
2318                | "interface"
2319                | "package"
2320                | "private"
2321                | "protected"
2322                | "public"
2323        )
2324}
2325
2326fn push_doc(out: &mut String, doc: &Option<DocComment>, indent: &str) {
2327    let Some(doc) = doc else { return };
2328    let mut lines: Vec<String> = doc.summary.lines().map(str::to_string).collect();
2329    for p in &doc.params {
2330        lines.push(format!("@param {} {}", p.name, p.description));
2331    }
2332    for cap in &doc.capabilities {
2333        let mut line = format!("@capability {}", cap.capability);
2334        if !cap.bindings.is_empty() {
2335            let bindings = cap
2336                .bindings
2337                .iter()
2338                .map(|b| {
2339                    let value = match &b.kind {
2340                        DocCapabilityBindingKind::Parameter { param, path, .. }
2341                            if *param == b.field && path.is_empty() =>
2342                        {
2343                            String::new()
2344                        }
2345                        DocCapabilityBindingKind::Parameter { param, path, .. } => {
2346                            let suffix = path
2347                                .iter()
2348                                .map(|part| format!(".{part}"))
2349                                .collect::<String>();
2350                            format!(": ${param}{suffix}")
2351                        }
2352                        DocCapabilityBindingKind::Type { name, .. } => format!(": {name}"),
2353                        DocCapabilityBindingKind::Literal { value, .. } => {
2354                            format!(": {}", literal_str(value))
2355                        }
2356                    };
2357                    format!("{}{}", b.field, value)
2358                })
2359                .collect::<Vec<_>>()
2360                .join(", ");
2361            line.push_str(&format!(" {{ {bindings} }}"));
2362        }
2363        if !cap.description.is_empty() {
2364            line.push_str(" - ");
2365            line.push_str(&cap.description);
2366        }
2367        lines.push(line);
2368    }
2369    if let Some(r) = &doc.returns {
2370        lines.push(format!("@returns {}", r.description));
2371    }
2372    if lines.is_empty() {
2373        return;
2374    }
2375    let _ = writeln!(out, "{indent}/**");
2376    for line in lines {
2377        let _ = writeln!(out, "{indent} * {line}");
2378    }
2379    let _ = writeln!(out, "{indent} */");
2380}
2381
2382fn literal_str(value: &DocCapabilityLiteral) -> String {
2383    match value {
2384        DocCapabilityLiteral::String(s) => {
2385            format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
2386        }
2387        DocCapabilityLiteral::Number(n) => n.clone(),
2388        DocCapabilityLiteral::Boolean(v) => v.to_string(),
2389        DocCapabilityLiteral::Null => "null".to_string(),
2390    }
2391}
2392
2393fn value_doc(kind: &ValueKind) -> &Option<DocComment> {
2394    match kind {
2395        ValueKind::Function { doc, .. }
2396        | ValueKind::Let { doc, .. }
2397        | ValueKind::Const { doc, .. } => doc,
2398    }
2399}
2400
2401fn type_doc(kind: &TypeKind) -> &Option<DocComment> {
2402    match kind {
2403        TypeKind::Interface { doc, .. }
2404        | TypeKind::Class { doc, .. }
2405        | TypeKind::NumberEnum { doc, .. }
2406        | TypeKind::StringEnum { doc, .. }
2407        | TypeKind::Alias { doc, .. } => doc,
2408    }
2409}
2410
2411/// What a method's declaration line starts with: nothing for a call signature
2412/// (stored as the method `@call`), `new ` for a construct signature, else its
2413/// member name.
2414fn method_prefix(name: &str) -> Cow<'_, str> {
2415    match name {
2416        "@call" => Cow::Borrowed(""),
2417        "new" => Cow::Borrowed("new "),
2418        _ => member_name(name),
2419    }
2420}
2421
2422/// A member name as declaration source: bare when it is an ASCII identifier,
2423/// else quoted and escaped, as `"a b"` or `"\uD800"` must be. A non-ASCII
2424/// identifier is quoted too, since a TypeScript built on older Unicode tables
2425/// may not read it.
2426fn member_name(name: &str) -> Cow<'_, str> {
2427    if name.is_ascii() && crate::type_rendering::is_identifier_name(name) {
2428        return Cow::Borrowed(name);
2429    }
2430    Cow::Owned(format!("\"{}\"", escape_string_literal(name)))
2431}
2432
2433#[cfg(test)]
2434mod tests {
2435    use super::*;
2436
2437    #[test]
2438    fn member_names_are_quoted_unless_identifiers() {
2439        let mut lone = String::new();
2440        crate::literal_units::push_lone_surrogate(&mut lone, 0xD800);
2441        assert_eq!(member_name("count"), "count");
2442        assert_eq!(member_name("$el_2"), "$el_2");
2443        assert_eq!(member_name("café"), "\"café\"");
2444        assert_eq!(member_name("a b"), "\"a b\"");
2445        assert_eq!(member_name("0"), "\"0\"");
2446        assert_eq!(member_name(&lone), "\"\\uD800\"");
2447    }
2448
2449    #[test]
2450    fn destructured_display_names_are_valid_and_distinct() {
2451        let params = [
2452            Param::new("#pattern_p_0", Type::Number),
2453            Param::new("arg0", Type::Number),
2454            Param::new("", Type::Number),
2455            Param::new("#pattern_p_3", Type::Number),
2456        ];
2457        let doc = crate::doc(
2458            FileId(0),
2459            "/** Names.\n * @param class Reserved.\n * @param arg0 Existing.\n * @param arg0.x Property.\n * @param pair Pair.\n * @param pair Duplicate.\n */",
2460        );
2461        assert_eq!(
2462            parameter_display_names(&params, doc.as_ref()),
2463            ["arg0_", "arg0", "pair", "arg3"]
2464        );
2465        assert_eq!(
2466            params_str(&params, doc.as_ref()),
2467            "arg0_: number, arg0: number, pair: number, arg3: number"
2468        );
2469        assert_eq!(
2470            ts_params_str(&params, doc.as_ref()),
2471            "arg0_: number, arg0: number, pair: number, arg3: number"
2472        );
2473    }
2474
2475    #[test]
2476    fn catalog_filters_before_counting_omitted_entries() {
2477        let extra = (0..CATALOG_LIMIT)
2478            .map(|index| CatalogEntry {
2479                name: format!("@acme/package{index}"),
2480                source: "registry".into(),
2481                description: String::new(),
2482            })
2483            .collect();
2484        let catalog = catalog_filtered(extra, |name| !name.starts_with("submilli:"));
2485        assert_eq!(catalog.entries.len(), CATALOG_LIMIT);
2486        assert_eq!(catalog.remaining, 0);
2487        assert!(
2488            catalog
2489                .entries
2490                .iter()
2491                .all(|entry| entry.name.starts_with("@acme/"))
2492        );
2493    }
2494
2495    #[test]
2496    fn http_docs_render_signatures() {
2497        let doc = docs("submilli:http").expect("http module");
2498        assert_eq!(doc.name, "submilli:http");
2499        assert!(!doc.description.is_empty());
2500        assert!(doc.declarations.contains("function get("));
2501        assert!(doc.declarations.contains("function post("));
2502        assert!(doc.declarations.contains("@capability http.get"));
2503        assert!(doc.declarations.contains("@capability http.download"));
2504    }
2505
2506    #[test]
2507    fn fs_and_package_configuration_docs_render_capabilities() {
2508        let fs = docs("submilli:fs").expect("fs module");
2509        assert!(fs.declarations.contains("@capability fs.read"));
2510        assert!(fs.declarations.contains("@capability fs.write"));
2511        assert!(fs.declarations.contains("@capability fs.list"));
2512
2513        let secrets = docs("submilli:secrets").expect("secrets module");
2514        assert!(secrets.declarations.contains("@capability secrets.get"));
2515        let variables = docs("submilli:variables").expect("variables module");
2516        assert!(variables.declarations.contains("@capability variables.get"));
2517        assert!(
2518            variables
2519                .declarations
2520                .contains("get(name: string): string | undefined")
2521        );
2522    }
2523
2524    #[test]
2525    fn security_is_not_user_facing() {
2526        assert!(docs("submilli:security").is_none());
2527        assert!(!search("").iter().any(|m| m.name == INTERNAL_MODULE));
2528    }
2529
2530    #[test]
2531    fn search_matches_symbol_names() {
2532        let hits = search("sha256");
2533        assert!(
2534            hits.iter().any(|m| m.name == "submilli:crypto"),
2535            "{:?}",
2536            names(&hits)
2537        );
2538    }
2539
2540    #[test]
2541    fn empty_query_lists_all_user_modules() {
2542        let names = names(&search(""));
2543        assert!(names.contains(&"submilli:crypto".to_string()));
2544        assert!(names.contains(&"submilli:http".to_string()));
2545        assert!(!names.contains(&INTERNAL_MODULE.to_string()));
2546    }
2547
2548    fn names(hits: &[ModuleSummary]) -> Vec<String> {
2549        hits.iter().map(|m| m.name.clone()).collect()
2550    }
2551
2552    #[test]
2553    fn builtins_lists_headline_types_and_namespaces() {
2554        let b = builtins();
2555        for expected in ["Array", "Map", "Set", "String", "Number", "Error"] {
2556            assert!(
2557                b.types.contains(&expected.to_string()),
2558                "missing {expected}"
2559            );
2560        }
2561        // Plumbing and flattened namespace members stay out of the catalog.
2562        for hidden in [
2563            "ArrayConstructor",
2564            "Console",
2565            "Iterator",
2566            "Temporal#Instant",
2567        ] {
2568            assert!(!b.types.contains(&hidden.to_string()), "leaked {hidden}");
2569        }
2570        for ns in ["Math", "Temporal", "JSON"] {
2571            assert!(
2572                b.namespaces.contains(&ns.to_string()),
2573                "missing namespace {ns}"
2574            );
2575        }
2576    }
2577
2578    #[test]
2579    fn every_builtin_has_renderable_docs() {
2580        let b = builtins();
2581        for name in b.types.iter().chain(b.namespaces.iter()) {
2582            assert!(
2583                builtin_docs(name).is_some_and(|d| !d.is_empty()),
2584                "no docs for built-in {name}"
2585            );
2586        }
2587    }
2588
2589    #[test]
2590    fn dotted_path_returns_the_member_slice() {
2591        let slice = builtin_docs("Temporal.Instant").expect("Temporal.Instant");
2592        // The binding, the interface, and the constructor side — the whole
2593        // slice, not whichever map is walked first.
2594        assert!(slice.contains("const Instant: Temporal.InstantConstructor;"));
2595        assert!(slice.contains("interface Instant {"));
2596        assert!(slice.contains("interface InstantConstructor {"));
2597        // Wrapped in its namespace, so the reader learns `Temporal.Instant.from`.
2598        assert!(slice.starts_with("namespace Temporal {"), "{slice}");
2599        // Doc comments survive the slice.
2600        assert!(slice.contains("A point in time, to nanosecond precision"));
2601        // Members of sibling types stay out.
2602        assert!(!slice.contains("interface ZonedDateTime {"), "{slice}");
2603
2604        let full = builtin_docs("Temporal").expect("Temporal");
2605        assert!(
2606            slice.len() * 4 < full.len(),
2607            "slice {} vs full {}",
2608            slice.len(),
2609            full.len()
2610        );
2611    }
2612
2613    #[test]
2614    fn dotted_path_resolves_nested_namespaces_and_values() {
2615        let now = builtin_docs("Temporal.Now").expect("Temporal.Now");
2616        assert!(now.starts_with("namespace Temporal {"));
2617        assert!(now.contains("namespace Now {"));
2618        assert!(now.contains("function instant(): Temporal.Instant;"));
2619
2620        let one = builtin_docs("Temporal.Now.instant").expect("Temporal.Now.instant");
2621        assert!(one.contains("function instant(): Temporal.Instant;"));
2622        assert!(!one.contains("plainDateISO"), "{one}");
2623
2624        let max = builtin_docs("Math.max").expect("Math.max");
2625        assert!(max.contains("function max("), "{max}");
2626        assert!(!max.contains("function min("), "{max}");
2627
2628        let parse = builtin_docs("JSON.parse").expect("JSON.parse");
2629        assert!(parse.contains("function parse("), "{parse}");
2630        assert!(!parse.contains("function stringify("), "{parse}");
2631    }
2632
2633    #[test]
2634    fn dotted_path_resolves_members_of_a_type_head() {
2635        // `isArray` lives on the constructor side; the stub names that owner
2636        // rather than pretending the member sits on `Array` itself.
2637        let is_array = builtin_docs("Array.isArray").expect("Array.isArray");
2638        assert!(
2639            is_array.starts_with("interface ArrayConstructor"),
2640            "{is_array}"
2641        );
2642        assert!(is_array.contains("isArray"));
2643
2644        let repeat = builtin_docs("String.repeat").expect("String.repeat");
2645        assert!(repeat.starts_with("interface String"), "{repeat}");
2646        assert!(
2647            repeat.contains("repeat(count: number): string;"),
2648            "{repeat}"
2649        );
2650
2651        // A type reached through a namespace is not itself a namespace, so it
2652        // must not gain a `namespace Instant {` wrapper.
2653        let from = builtin_docs("Temporal.Instant.from").expect("Temporal.Instant.from");
2654        assert!(from.starts_with("namespace Temporal {"), "{from}");
2655        assert!(from.contains("interface InstantConstructor {"), "{from}");
2656        assert!(!from.contains("namespace Instant"), "{from}");
2657    }
2658
2659    #[test]
2660    fn every_member_a_miss_names_is_itself_resolvable() {
2661        // The miss message is a repair instruction: if it names a member, that
2662        // member must resolve. This closes the loop over every TypeKind, so a
2663        // kind the member renderer does not handle cannot pass unnoticed.
2664        let b = builtins();
2665        for name in b.types.iter().chain(b.namespaces.iter()) {
2666            let BuiltinLookup::UnknownMember { members, .. } =
2667                builtin_lookup(&format!("{name}.__definitely_not_a_member"))
2668            else {
2669                continue;
2670            };
2671            for member in members {
2672                let path = format!("{name}.{member}");
2673                assert!(
2674                    matches!(builtin_lookup(&path), BuiltinLookup::Found(_)),
2675                    "`{path}` is offered as a member of `{name}` but does not resolve"
2676                );
2677            }
2678        }
2679    }
2680
2681    #[test]
2682    fn unknown_member_lists_the_members_that_exist() {
2683        let BuiltinLookup::UnknownMember {
2684            path,
2685            member,
2686            members,
2687        } = builtin_lookup("Temporal.Foo")
2688        else {
2689            panic!("expected an unknown-member miss");
2690        };
2691        assert_eq!(path, "Temporal");
2692        assert_eq!(member, "Foo");
2693        assert!(members.contains(&"Instant".to_string()));
2694        assert!(members.contains(&"Now".to_string()));
2695        // Constructor plumbing is bound to its base name already.
2696        assert!(
2697            !members.contains(&"InstantConstructor".to_string()),
2698            "{members:?}"
2699        );
2700
2701        // A value is a leaf: it reports having no members rather than the
2702        // enclosing namespace's.
2703        let BuiltinLookup::UnknownMember { path, members, .. } = builtin_lookup("Math.max.foo")
2704        else {
2705            panic!("expected an unknown-member miss");
2706        };
2707        assert_eq!(path, "Math.max");
2708        assert!(members.is_empty());
2709    }
2710
2711    #[test]
2712    fn a_bad_member_is_reported_against_its_full_path() {
2713        for (query, expected_path) in [
2714            ("Temporal.Instant.nope", "Temporal.Instant"),
2715            ("Temporal.Now.nope", "Temporal.Now"),
2716            ("Array.nope", "Array"),
2717            ("Temporal.Instant.from.x", "Temporal.Instant.from"),
2718            // A deep path rooted at a top-level type reports the member that
2719            // actually failed, not the one that resolved.
2720            ("Array.isArray.foo", "Array.isArray"),
2721        ] {
2722            let BuiltinLookup::UnknownMember { path, member, .. } = builtin_lookup(query) else {
2723                panic!("expected an unknown-member miss for {query}");
2724            };
2725            assert_eq!(path, expected_path, "for {query}");
2726            assert!(!member.is_empty(), "for {query}");
2727        }
2728    }
2729
2730    #[test]
2731    fn unknown_head_is_not_an_unknown_member() {
2732        assert_eq!(builtin_lookup("Bogus.Thing"), BuiltinLookup::Unknown);
2733        // The internal flattening separator stays rejected.
2734        assert_eq!(builtin_lookup("Temporal#Instant"), BuiltinLookup::Unknown);
2735        assert_eq!(builtin_lookup(""), BuiltinLookup::Unknown);
2736    }
2737
2738    #[test]
2739    fn plain_names_are_unchanged_and_segments_are_case_insensitive() {
2740        // Every catalog name still renders, and a namespace still renders whole.
2741        let full = builtin_docs("Temporal").expect("Temporal");
2742        assert!(full.starts_with("namespace Temporal {"));
2743        for member in ["Instant", "ZonedDateTime", "PlainDate", "Duration"] {
2744            assert!(
2745                full.contains(&format!("interface {member} {{")),
2746                "missing {member}"
2747            );
2748        }
2749        assert_eq!(
2750            builtin_docs("temporal.instant"),
2751            builtin_docs("Temporal.Instant")
2752        );
2753        // A trailing dot is absorbed rather than rejected.
2754        assert_eq!(builtin_docs("Temporal."), builtin_docs("Temporal"));
2755    }
2756
2757    #[test]
2758    fn resolve_prefers_a_module_then_falls_back_to_builtins() {
2759        assert!(matches!(resolve("submilli:http"), Resolution::Module(_)));
2760        assert!(matches!(resolve("Temporal"), Resolution::Builtin { .. }));
2761        assert!(matches!(
2762            resolve("Temporal.Instant"),
2763            Resolution::Builtin { .. }
2764        ));
2765        assert!(matches!(
2766            resolve("Temporal.Foo"),
2767            Resolution::UnknownMember { .. }
2768        ));
2769        assert!(matches!(resolve("nope"), Resolution::Unknown));
2770        assert!(matches!(resolve("submilli:security"), Resolution::Unknown));
2771    }
2772
2773    #[test]
2774    fn suggest_reaches_past_bare_edit_distance() {
2775        let extra = vec!["@mcp/linear".to_string()];
2776        // A name written without its scheme is the likeliest package typo and
2777        // sits 9 edits from the full name, far outside any threshold.
2778        assert_eq!(suggest("http", &extra).as_deref(), Some("submilli:http"));
2779        assert_eq!(suggest("htp", &extra).as_deref(), Some("submilli:http"));
2780        assert_eq!(suggest("linear", &extra).as_deref(), Some("@mcp/linear"));
2781        // A dotted miss is matched on its head alone.
2782        assert_eq!(suggest("Temporel", &extra).as_deref(), Some("Temporal"));
2783        assert_eq!(
2784            suggest("Temporel.Instant", &extra).as_deref(),
2785            Some("Temporal")
2786        );
2787        assert_eq!(suggest("Aray", &extra).as_deref(), Some("Array"));
2788        assert_eq!(suggest("zzzzzzzzz", &extra), None);
2789        // Internal plumbing never surfaces as a candidate, even when the query
2790        // is its exact name — the public `secrets` module is offered instead.
2791        assert_ne!(
2792            suggest(INTERNAL_MODULE, &[]).as_deref(),
2793            Some(INTERNAL_MODULE)
2794        );
2795    }
2796
2797    #[test]
2798    fn a_deliberately_omitted_global_points_at_its_replacement() {
2799        // `Date` is two edits from `Math` and eight from `Temporal`, so edit
2800        // distance alone sends the model to arithmetic.
2801        assert_eq!(suggest("Date", &[]).as_deref(), Some("Temporal"));
2802        assert_eq!(suggest("date", &[]).as_deref(), Some("Temporal"));
2803    }
2804
2805    #[test]
2806    fn suggest_declines_degenerate_and_self_queries() {
2807        // `closest_match` floors its threshold at 2 edits, so without a length
2808        // guard any short query lands on an unrelated name.
2809        for q in ["", ".", "a", "xy"] {
2810            assert_eq!(suggest(q, &[]), None, "for {q:?}");
2811        }
2812        // An empty leading segment is skipped rather than matched on.
2813        assert_eq!(suggest(".Temporel", &[]).as_deref(), Some("Temporal"));
2814        // A name that resolved nowhere is never its own repair.
2815        let declared = vec!["@acme/mypkg".to_string()];
2816        assert_eq!(suggest("@acme/mypkg", &declared), None);
2817    }
2818
2819    #[test]
2820    fn catalog_is_summary_only_and_bounded() {
2821        let stdlib = catalog(Vec::new());
2822        assert_eq!(stdlib.remaining, 0);
2823        assert!(stdlib.entries.iter().all(|e| e.source == SOURCE_STDLIB));
2824        assert!(stdlib.entries.iter().any(|e| e.name == "submilli:http"));
2825        assert!(stdlib.entries.iter().all(|e| !e.description.is_empty()));
2826        assert!(!stdlib.entries.iter().any(|e| e.name == INTERNAL_MODULE));
2827
2828        let extra: Vec<CatalogEntry> = (0..CATALOG_LIMIT)
2829            .map(|i| CatalogEntry {
2830                name: format!("@acme/p{i}"),
2831                source: "registry".to_string(),
2832                description: "x".to_string(),
2833            })
2834            .collect();
2835        let big = catalog(extra);
2836        assert_eq!(big.entries.len(), CATALOG_LIMIT);
2837        assert_eq!(big.remaining, stdlib.entries.len());
2838    }
2839
2840    #[test]
2841    fn miss_messages_name_the_repair() {
2842        let msg = unknown_member_message("Temporal", "Foo", &["Instant".into(), "Now".into()]);
2843        assert!(msg.contains("Temporal"), "{msg}");
2844        assert!(msg.contains("Instant, Now"), "{msg}");
2845        let leaf = unknown_member_message("Math.max", "foo", &[]);
2846        assert!(leaf.contains("no members"), "{leaf}");
2847        // A `source` tag is a machine field; the prose must say it out loud.
2848        assert!(builtin_no_import_note("Temporal").contains("import"));
2849    }
2850
2851    /// Every package any embedder can enable, so the declaration checks below
2852    /// cover the optional packages too.
2853    fn every_package() -> Stdlib {
2854        crate::stdlib::OptionalPackage::ALL
2855            .iter()
2856            .fold(Stdlib::core(), |set, package| set.with(*package))
2857    }
2858
2859    #[test]
2860    fn stdlib_capability_tags_parse_cleanly() {
2861        for module in user_modules(every_package()) {
2862            for symbol in module.values.values() {
2863                let Some(doc) = value_doc(&symbol.kind) else {
2864                    continue;
2865                };
2866                for cap in &doc.capabilities {
2867                    assert!(
2868                        cap.diagnostics.is_empty(),
2869                        "{}.{}: {:?}",
2870                        module.package_name,
2871                        symbol.name,
2872                        cap.diagnostics
2873                    );
2874                }
2875            }
2876        }
2877    }
2878
2879    #[test]
2880    fn every_importable_stdlib_symbol_has_docs() {
2881        for module in user_modules(every_package()) {
2882            for symbol in module.values.values() {
2883                assert!(
2884                    value_doc(&symbol.kind).is_some(),
2885                    "{}.{} is missing docs",
2886                    module.package_name,
2887                    symbol.name
2888                );
2889            }
2890            for symbol in module.types.values() {
2891                assert!(
2892                    type_doc(&symbol.kind).is_some(),
2893                    "{}.{} is missing docs",
2894                    module.package_name,
2895                    symbol.name
2896                );
2897            }
2898        }
2899    }
2900
2901    #[test]
2902    fn builtin_docs_print_a_call_signature_without_a_name() {
2903        for name in ["Number", "Number.@call"] {
2904            let docs = builtin_docs(name).expect("Number built-in");
2905            assert!(docs.contains("  (value: "), "{docs}");
2906            assert!(!docs.contains("@call"), "{docs}");
2907        }
2908    }
2909
2910    #[test]
2911    fn builtin_docs_folds_constructor_into_array() {
2912        let docs = builtin_docs("Array").expect("Array built-in");
2913        assert!(docs.contains("interface Array<"));
2914        assert!(docs.contains("interface ArrayConstructor"));
2915        assert!(docs.contains("const Array: ArrayConstructor;"));
2916    }
2917
2918    #[test]
2919    fn builtin_docs_renders_temporal_namespace() {
2920        let docs = builtin_docs("Temporal").expect("Temporal built-in");
2921        assert!(docs.starts_with("namespace Temporal {"));
2922        assert!(docs.contains("interface Instant"));
2923        assert!(docs.contains("namespace Now {"));
2924        // Nested members are indented under the namespace.
2925        assert!(docs.contains("  interface Instant"));
2926        // The constructor binding comes from the namespace's own values — not
2927        // also synthesized by the constructor-merge (which would duplicate it).
2928        assert_eq!(docs.matches("const Instant").count(), 1, "{docs}");
2929    }
2930
2931    #[test]
2932    fn builtin_docs_handles_json_intrinsic() {
2933        let docs = builtin_docs("JSON").expect("JSON built-in");
2934        assert!(docs.contains(
2935            "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
2936        ));
2937        assert!(docs.contains("function parse(text: string): unknown;"));
2938    }
2939
2940    #[test]
2941    fn builtin_docs_is_case_insensitive() {
2942        // A lowercase name resolves to the canonically-cased built-in.
2943        assert_eq!(builtin_docs("array"), builtin_docs("Array"));
2944        assert_eq!(builtin_docs("temporal"), builtin_docs("Temporal"));
2945        assert_eq!(builtin_docs("json"), builtin_docs("JSON"));
2946        // Constructor-suffixed lookup folds in case-insensitively too.
2947        assert_eq!(builtin_docs("ARRAY"), builtin_docs("Array"));
2948    }
2949
2950    #[test]
2951    fn builtin_docs_unknown_is_none() {
2952        assert!(builtin_docs("Promise").is_none());
2953        assert!(builtin_docs("Temporal#Instant").is_none());
2954    }
2955
2956    #[test]
2957    fn packages_d_ts_imports_the_types_a_package_borrows() {
2958        let mut package = PackageDeclaration::with_package("@acme/files");
2959        let download = crate::stdlib::http::package_declaration()
2960            .values
2961            .get("download")
2962            .expect("submilli:http declares download")
2963            .clone();
2964        package.values.insert("fetch".to_string(), download);
2965
2966        let docs = render_packages_d_ts(&[&package], &[]);
2967
2968        assert!(docs.starts_with("declare module \"@acme/files\" {\n"));
2969        assert!(docs.contains("  import type { DownloadResult } from \"submilli:http\";\n"));
2970        assert!(docs.contains("  import type { DownloadOptions } from \"submilli:http\";\n"));
2971        assert!(!docs.contains("import type { Response }"));
2972    }
2973
2974    #[test]
2975    fn a_borrowed_type_is_imported_from_its_own_module_when_others_share_its_name() {
2976        // `submilli:session` and `@acme/other` export a `Page` too; the
2977        // reference's mangled name picks the leaf's.
2978        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
2979        leaf.types
2980            .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
2981        let mut other = PackageDeclaration::with_package("@acme/other");
2982        other
2983            .types
2984            .insert("Page".to_string(), alias_symbol("@acme/other", "Page"));
2985        let mut mid = PackageDeclaration::with_package("@acme/mid");
2986        let page = reference(
2987            "@acme/leaf",
2988            crate::mangle::package_symbol("@acme/leaf", "Page"),
2989            "Page",
2990        );
2991        mid.values
2992            .insert("page".to_string(), const_symbol("@acme/mid", "page", page));
2993
2994        let docs = render_packages_d_ts(&[&leaf, &other, &mid], &[]);
2995
2996        assert!(docs.contains("  import type { Page } from \"@acme/leaf\";\n"));
2997        assert!(!docs.contains("from \"submilli:session\""));
2998        assert!(!docs.contains("from \"@acme/other\""));
2999    }
3000
3001    fn alias_symbol(package: &str, name: &str) -> crate::TypeSymbol {
3002        crate::TypeSymbol {
3003            name: name.to_string(),
3004            mangled_name: crate::mangle::package_symbol(package, name),
3005            declaration_span: crate::Span::at(crate::FileId(0)),
3006            kind: crate::TypeKind::Alias {
3007                generics: Vec::new(),
3008                ty: Type::Number,
3009                doc: None,
3010            },
3011        }
3012    }
3013
3014    fn const_symbol(package: &str, name: &str, ty: Type) -> ValueSymbol {
3015        ValueSymbol {
3016            name: name.to_string(),
3017            mangled_name: crate::mangle::package_symbol(package, name),
3018            declaration_span: crate::Span::at(crate::FileId(0)),
3019            kind: ValueKind::Const { ty, doc: None },
3020        }
3021    }
3022
3023    fn reference(package: &str, mangled: crate::MangledName, local_name: &str) -> Type {
3024        Type::alias_ref(
3025            crate::types::Package(package.to_string()),
3026            local_name,
3027            mangled,
3028            Vec::new(),
3029        )
3030    }
3031
3032    #[test]
3033    fn an_aliased_type_is_imported_under_the_name_the_text_uses() {
3034        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3035        leaf.types
3036            .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
3037        let mut mid = PackageDeclaration::with_package("@acme/mid");
3038        let page = reference(
3039            "@acme/leaf",
3040            crate::mangle::package_symbol("@acme/leaf", "Page"),
3041            "LeafPage",
3042        );
3043        mid.values
3044            .insert("page".to_string(), const_symbol("@acme/mid", "page", page));
3045
3046        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3047
3048        assert!(docs.contains("  import type { Page as LeafPage } from \"@acme/leaf\";\n"));
3049        assert!(!docs.contains("declare module \"@acme/leaf\""));
3050    }
3051
3052    #[test]
3053    fn a_type_reexported_from_an_internal_module_is_imported_from_the_package() {
3054        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3055        leaf.types
3056            .insert("Status".to_string(), alias_symbol("@acme/leaf", "Status"));
3057        let mut mid = PackageDeclaration::with_package("@acme/mid");
3058        let status = reference(
3059            "@acme/leaf",
3060            crate::mangle::package_module_symbol("@acme/leaf", "model", "Status"),
3061            "Status",
3062        );
3063        mid.values.insert(
3064            "status".to_string(),
3065            const_symbol("@acme/mid", "status", status),
3066        );
3067
3068        let docs = render_packages_d_ts(&[&leaf, &mid], &[]);
3069
3070        assert!(docs.contains("  import type { Status } from \"@acme/leaf\";\n"));
3071    }
3072
3073    #[test]
3074    fn only_the_rendered_declarations_bring_imports() {
3075        let mut mid = PackageDeclaration::with_package("@acme/mid");
3076        let response = reference(
3077            "submilli:http",
3078            crate::mangle::package_symbol("submilli:http", "Response"),
3079            "Response",
3080        );
3081        mid.runtime_globals.insert(
3082            crate::mangle::package_symbol("@acme/mid", "hidden"),
3083            response.clone(),
3084        );
3085
3086        let docs = render_packages_d_ts(&[&mid], &[]);
3087
3088        assert!(!docs.contains("import type"), "{docs}");
3089    }
3090
3091    #[test]
3092    fn a_value_named_like_a_borrowed_interface_leaves_its_import() {
3093        // TypeScript keeps a type and a value of one name apart, so the
3094        // interface still needs its import next to the package's own value.
3095        let download = reference(
3096            "submilli:http",
3097            crate::mangle::package_symbol("submilli:http", "DownloadResult"),
3098            "DownloadResult",
3099        );
3100        let mut mid = PackageDeclaration::with_package("@acme/mid");
3101        mid.values.insert(
3102            "DownloadResult".to_string(),
3103            const_symbol("@acme/mid", "DownloadResult", download),
3104        );
3105
3106        let docs = render_packages_d_ts(&[&mid], &[]);
3107
3108        assert!(
3109            docs.contains("  import type { DownloadResult } from \"submilli:http\";\n"),
3110            "{docs}"
3111        );
3112    }
3113
3114    #[test]
3115    fn type_arguments_bring_imports_and_alias_bodies_do_not() {
3116        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3117        leaf.types
3118            .insert("Page".to_string(), alias_symbol("@acme/leaf", "Page"));
3119        leaf.types
3120            .insert("Item".to_string(), alias_symbol("@acme/leaf", "Item"));
3121        let mut mid = PackageDeclaration::with_package("@acme/mid");
3122        let item = reference(
3123            "@acme/leaf",
3124            crate::mangle::package_symbol("@acme/leaf", "Item"),
3125            "Item",
3126        );
3127        let page_of_items = Type::alias_ref(
3128            crate::types::Package("@acme/leaf".to_string()),
3129            "Page",
3130            crate::mangle::package_symbol("@acme/leaf", "Page"),
3131            vec![Type::Array(Box::new(item))],
3132        );
3133        mid.values.insert(
3134            "page".to_string(),
3135            const_symbol("@acme/mid", "page", page_of_items),
3136        );
3137        // A resolved alias renders as its name; what it stands for doesn't
3138        // appear in the text.
3139        let response = reference(
3140            "submilli:http",
3141            crate::mangle::package_symbol("submilli:http", "Response"),
3142            "Response",
3143        );
3144        let wrapped = Type::Alias {
3145            mangled: crate::mangle::package_symbol("@acme/leaf", "Item"),
3146            package: crate::types::Package("@acme/leaf".to_string()),
3147            name: "Item".to_string(),
3148            args: Vec::new(),
3149            ty: Box::new(response),
3150        };
3151        mid.values.insert(
3152            "item".to_string(),
3153            const_symbol("@acme/mid", "item", wrapped),
3154        );
3155
3156        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3157
3158        assert!(docs.contains("  import type { Page } from \"@acme/leaf\";\n"));
3159        assert!(docs.contains("  import type { Item } from \"@acme/leaf\";\n"));
3160        assert!(!docs.contains("Response"), "{docs}");
3161    }
3162
3163    fn class_symbol(
3164        package: &str,
3165        name: &str,
3166        extends: Option<crate::ClassExtends>,
3167    ) -> crate::TypeSymbol {
3168        crate::TypeSymbol {
3169            name: name.to_string(),
3170            mangled_name: crate::mangle::package_symbol(package, name),
3171            declaration_span: crate::Span::at(crate::FileId(0)),
3172            kind: TypeKind::Class {
3173                generics: Vec::new(),
3174                fields: BTreeMap::new(),
3175                narrowing_checks: BTreeMap::new(),
3176                methods: BTreeMap::new(),
3177                method_visibility: BTreeMap::new(),
3178                accessors: Vec::new(),
3179                constructor: Vec::new(),
3180                constructor_visibility: crate::Visibility::Public,
3181                statics: BTreeMap::new(),
3182                static_visibility: BTreeMap::new(),
3183                static_fields: BTreeMap::new(),
3184                extends,
3185                implements: Vec::new(),
3186                doc: None,
3187            },
3188        }
3189    }
3190
3191    #[test]
3192    fn a_class_renders_and_imports_its_parent() {
3193        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3194        leaf.types
3195            .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3196        leaf.types
3197            .insert("Item".to_string(), alias_symbol("@acme/leaf", "Item"));
3198        let item = reference(
3199            "@acme/leaf",
3200            crate::mangle::package_symbol("@acme/leaf", "Item"),
3201            "Item",
3202        );
3203        let extends = crate::ClassExtends {
3204            parent: crate::mangle::package_symbol("@acme/leaf", "Base"),
3205            args: vec![item],
3206        };
3207        let mut sub = class_symbol("@acme/mid", "Sub", Some(extends));
3208        if let TypeKind::Class { fields, .. } = &mut sub.kind {
3209            // A private member isn't rendered, so its type isn't imported.
3210            let response = reference(
3211                "submilli:http",
3212                crate::mangle::package_symbol("submilli:http", "Response"),
3213                "Response",
3214            );
3215            fields.insert(
3216                "hidden".to_string(),
3217                crate::FieldSig {
3218                    ty: response,
3219                    visibility: crate::Visibility::Private,
3220                    readonly: false,
3221                    optional: false,
3222                    method: false,
3223                    doc: None,
3224                },
3225            );
3226        }
3227        let mut mid = PackageDeclaration::with_package("@acme/mid");
3228        mid.types.insert("Sub".to_string(), sub);
3229
3230        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3231
3232        assert!(
3233            docs.contains("  import type { Base } from \"@acme/leaf\";\n"),
3234            "{docs}"
3235        );
3236        assert!(
3237            docs.contains("  import type { Item } from \"@acme/leaf\";\n"),
3238            "{docs}"
3239        );
3240        assert!(
3241            docs.contains("  export class Sub extends Base<Item> {"),
3242            "{docs}"
3243        );
3244        assert!(!docs.contains("Response"), "{docs}");
3245    }
3246
3247    fn subclass_of(package: &str, name: &str, parent: crate::MangledName) -> crate::TypeSymbol {
3248        class_symbol(package, name, Some(crate::ClassExtends::plain(parent)))
3249    }
3250
3251    /// `@acme/leaf` and `@acme/other`, which each export a class `Base`.
3252    fn two_bases() -> (PackageDeclaration, PackageDeclaration) {
3253        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3254        leaf.types
3255            .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3256        let mut other = PackageDeclaration::with_package("@acme/other");
3257        other.types.insert(
3258            "Base".to_string(),
3259            class_symbol("@acme/other", "Base", None),
3260        );
3261        (leaf, other)
3262    }
3263
3264    #[test]
3265    fn a_parent_named_like_a_local_value_is_imported_under_another_name() {
3266        let (leaf, _) = two_bases();
3267        let mut package = PackageDeclaration::with_package("@acme/shadowed");
3268        package.values.insert(
3269            "Base".to_string(),
3270            const_symbol("@acme/shadowed", "Base", Type::Number),
3271        );
3272        let base = crate::mangle::package_symbol("@acme/leaf", "Base");
3273        package.types.insert(
3274            "Sub".to_string(),
3275            subclass_of("@acme/shadowed", "Sub", base),
3276        );
3277
3278        let docs = render_packages_d_ts(&[&package], &[&leaf]);
3279
3280        assert!(
3281            docs.contains("  import type { Base as Base1 } from \"@acme/leaf\";\n"),
3282            "{docs}"
3283        );
3284        assert!(
3285            docs.contains("  export class Sub extends Base1 {"),
3286            "{docs}"
3287        );
3288    }
3289
3290    #[test]
3291    fn parents_that_share_a_public_name_get_different_local_names() {
3292        let (leaf, other) = two_bases();
3293        let mut package = PackageDeclaration::with_package("@acme/both");
3294        package.types.insert(
3295            "A".to_string(),
3296            subclass_of(
3297                "@acme/both",
3298                "A",
3299                crate::mangle::package_symbol("@acme/leaf", "Base"),
3300            ),
3301        );
3302        package.types.insert(
3303            "B".to_string(),
3304            subclass_of(
3305                "@acme/both",
3306                "B",
3307                crate::mangle::package_symbol("@acme/other", "Base"),
3308            ),
3309        );
3310
3311        let docs = render_packages_d_ts(&[&package], &[&leaf, &other]);
3312
3313        assert!(
3314            docs.contains("  import type { Base } from \"@acme/leaf\";\n"),
3315            "{docs}"
3316        );
3317        assert!(
3318            docs.contains("  import type { Base as Base1 } from \"@acme/other\";\n"),
3319            "{docs}"
3320        );
3321        assert!(docs.contains("  export class A extends Base {"), "{docs}");
3322        assert!(docs.contains("  export class B extends Base1 {"), "{docs}");
3323    }
3324
3325    #[test]
3326    fn a_parent_already_imported_under_an_alias_keeps_it() {
3327        let (leaf, _) = two_bases();
3328        let base = crate::mangle::package_symbol("@acme/leaf", "Base");
3329        let mut package = PackageDeclaration::with_package("@acme/aliased");
3330        package.values.insert(
3331            "base".to_string(),
3332            const_symbol(
3333                "@acme/aliased",
3334                "base",
3335                reference("@acme/leaf", base.clone(), "LB"),
3336            ),
3337        );
3338        package
3339            .types
3340            .insert("Sub".to_string(), subclass_of("@acme/aliased", "Sub", base));
3341
3342        let docs = render_packages_d_ts(&[&package], &[&leaf]);
3343
3344        assert!(
3345            docs.contains("  import type { Base as LB } from \"@acme/leaf\";\n"),
3346            "{docs}"
3347        );
3348        assert!(docs.contains("  export class Sub extends LB {"), "{docs}");
3349        assert!(!docs.contains("import type { Base }"), "{docs}");
3350    }
3351
3352    #[test]
3353    fn a_class_extending_a_builtin_error_renders_its_parent_as_the_global() {
3354        let error = builtin_package_declaration()
3355            .types
3356            .get("Error")
3357            .expect("the prelude declares Error")
3358            .mangled_name
3359            .clone();
3360        let mut package = PackageDeclaration::with_package("@acme/errors");
3361        package.types.insert(
3362            "BillingError".to_string(),
3363            subclass_of("@acme/errors", "BillingError", error),
3364        );
3365
3366        let docs = render_packages_d_ts(&[&package], &[]);
3367
3368        assert!(
3369            docs.contains("  export class BillingError extends Error {"),
3370            "{docs}"
3371        );
3372        assert!(!docs.contains("import type"), "{docs}");
3373    }
3374
3375    #[test]
3376    fn a_package_value_named_like_the_global_parent_leaves_the_class_without_it() {
3377        let error = builtin_package_declaration()
3378            .types
3379            .get("Error")
3380            .expect("the prelude declares Error")
3381            .mangled_name
3382            .clone();
3383        let mut package = PackageDeclaration::with_package("@acme/errors");
3384        package.values.insert(
3385            "Error".to_string(),
3386            const_symbol("@acme/errors", "Error", Type::Number),
3387        );
3388        package.types.insert(
3389            "BillingError".to_string(),
3390            subclass_of("@acme/errors", "BillingError", error),
3391        );
3392
3393        let docs = render_packages_d_ts(&[&package], &[]);
3394
3395        assert!(docs.contains("  export class BillingError {"), "{docs}");
3396    }
3397
3398    #[test]
3399    fn global_functions_are_in_the_editor_declarations() {
3400        let lib = render_lib_submilli_d_ts();
3401        for function in [
3402            "parseInt(",
3403            "parseFloat(",
3404            "encodeURIComponent(",
3405            "encodeURI(",
3406            "decodeURIComponent(",
3407            "decodeURI(",
3408            "isNaN(",
3409            "isFinite(",
3410        ] {
3411            assert!(
3412                lib.contains(&format!("declare function {function}")),
3413                "{function}"
3414            );
3415        }
3416    }
3417
3418    #[test]
3419    fn builtin_errors_extend_error_in_the_editor_declarations() {
3420        let lib = render_lib_submilli_d_ts();
3421        for class in [
3422            "QuotaExceededError",
3423            "RangeError",
3424            "TypeError",
3425            "SyntaxError",
3426            "URIError",
3427            "ReferenceError",
3428            "PermissionDeniedError",
3429        ] {
3430            assert!(
3431                lib.contains(&format!("declare class {class} extends Error {{")),
3432                "{class}"
3433            );
3434        }
3435        assert!(lib.contains("declare class Error {"));
3436    }
3437
3438    #[test]
3439    fn a_value_named_like_a_borrowed_class_keeps_the_class_out() {
3440        // A class is a value too, so importing it would clash with the const.
3441        let mut leaf = PackageDeclaration::with_package("@acme/leaf");
3442        leaf.types
3443            .insert("Base".to_string(), class_symbol("@acme/leaf", "Base", None));
3444        let base = Type::class_ref(
3445            crate::types::Package("@acme/leaf".to_string()),
3446            "Base",
3447            crate::mangle::package_symbol("@acme/leaf", "Base"),
3448            Vec::new(),
3449        );
3450        let mut mid = PackageDeclaration::with_package("@acme/mid");
3451        mid.values
3452            .insert("Base".to_string(), const_symbol("@acme/mid", "Base", base));
3453
3454        let docs = render_packages_d_ts(&[&mid], &[&leaf]);
3455
3456        assert!(!docs.contains("import type"), "{docs}");
3457    }
3458
3459    #[test]
3460    fn a_package_type_named_like_a_constructor_is_an_ordinary_type() {
3461        let mut package = PackageDeclaration::with_package("@acme/leaf");
3462        package
3463            .types
3464            .insert("Client".to_string(), alias_symbol("@acme/leaf", "Client"));
3465        package.types.insert(
3466            "ClientConstructor".to_string(),
3467            alias_symbol("@acme/leaf", "ClientConstructor"),
3468        );
3469
3470        let docs = render_packages_d_ts(&[&package], &[]);
3471
3472        assert!(docs.contains("  export type Client = number;"), "{docs}");
3473        assert!(
3474            docs.contains("  export type ClientConstructor = number;"),
3475            "{docs}"
3476        );
3477        assert!(!docs.contains("const Client"), "{docs}");
3478    }
3479
3480    #[test]
3481    fn stdlib_d_ts_declares_each_user_module() {
3482        let docs = every_package().render_stdlib_d_ts();
3483        for module in user_modules(every_package()) {
3484            assert!(
3485                docs.contains(&format!("declare module \"{}\" {{", module.package_name)),
3486                "missing declare module for {}",
3487                module.package_name
3488            );
3489        }
3490        assert!(docs.contains("declare module \"submilli:security\" {\n"));
3491        assert!(docs.contains("  export function check<T>("));
3492        assert!(docs.contains("declare module \"submilli:uuid\" {\n"));
3493        assert!(docs.contains("  export function v4("));
3494        assert!(docs.contains("  function delete_("));
3495        assert!(docs.contains("  export { delete_ as delete };"));
3496        assert!(!docs.contains("export function delete("));
3497    }
3498
3499    #[test]
3500    fn lib_submilli_d_ts_renders_ambient_builtins() {
3501        let docs = render_lib_submilli_d_ts();
3502        assert!(docs.contains("interface Array<"));
3503        assert!(docs.contains("interface ArrayConstructor"));
3504        assert!(docs.contains("declare const Array: ArrayConstructor;"));
3505        assert_eq!(
3506            docs.matches("declare const Array: ArrayConstructor;")
3507                .count(),
3508            1
3509        );
3510        assert!(docs.contains("declare const console: Console;"));
3511        assert!(docs.contains("declare namespace Math {"));
3512        assert!(docs.contains("declare namespace Temporal {"));
3513        assert!(docs.contains("declare namespace JSON {"));
3514        assert!(docs.contains(
3515            "function stringify<T>(value: T, replacer?: null, space?: number | string | null): string | undefined;"
3516        ));
3517        assert!(docs.contains("interface Function {\n  /** The parameters before the first optional, defaulted or rest one. */\n  readonly length: number;\n}"));
3518        assert!(docs.contains("interface IArguments {}"));
3519        assert!(docs.contains("declare class Error {"));
3520        assert!(docs.contains("static isError(value?: unknown): value is Error;"));
3521        assert!(!docs.contains("ErrorConstructor"));
3522        assert!(!docs.contains("declare module"));
3523        assert!(!docs.contains("function string_concat"));
3524    }
3525
3526    #[test]
3527    fn d_ts_renderer_uses_typescript_call_and_predicate_signatures() {
3528        let docs = render_lib_submilli_d_ts();
3529        assert!(docs.contains("(value: string | bigint | undefined): number;"));
3530        assert!(docs.contains("isArray<T>(value: T): value is T & unknown[];"));
3531        assert!(!docs.contains("@call"));
3532    }
3533
3534    fn class_with_constructor(
3535        package: &str,
3536        name: &str,
3537        constructor: Vec<Param>,
3538        constructor_visibility: crate::Visibility,
3539        extends: Option<ClassExtends>,
3540    ) -> TypeSymbol {
3541        TypeSymbol {
3542            name: name.to_string(),
3543            mangled_name: crate::mangle::package_symbol(package, name),
3544            declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3545            kind: TypeKind::Class {
3546                generics: Vec::new(),
3547                fields: BTreeMap::new(),
3548                narrowing_checks: BTreeMap::new(),
3549                methods: BTreeMap::new(),
3550                method_visibility: BTreeMap::new(),
3551                accessors: Vec::new(),
3552                constructor,
3553                constructor_visibility,
3554                statics: BTreeMap::new(),
3555                static_visibility: BTreeMap::new(),
3556                static_fields: BTreeMap::new(),
3557                extends,
3558                implements: Vec::new(),
3559                doc: None,
3560            },
3561        }
3562    }
3563
3564    #[test]
3565    fn a_private_constructor_stays_out_of_the_docs_and_is_protected_in_d_ts() {
3566        let package = "@acme/shapes";
3567        let mut defs = PackageDeclaration::with_package(package);
3568        let point = class_with_constructor(
3569            package,
3570            "Point",
3571            vec![Param::new("secret", Type::String)],
3572            crate::Visibility::Private,
3573            None,
3574        );
3575        let labeled = class_with_constructor(
3576            package,
3577            "Labeled",
3578            vec![Param::new("secret", Type::String)],
3579            crate::Visibility::Private,
3580            Some(ClassExtends {
3581                parent: point.mangled_name.clone(),
3582                args: Vec::new(),
3583            }),
3584        );
3585        defs.types.insert("Point".to_string(), point);
3586        defs.types.insert("Labeled".to_string(), labeled);
3587
3588        let docs = render_declarations(&defs);
3589        assert!(docs.contains("class Point"), "{docs}");
3590        assert!(!docs.contains("constructor"), "{docs}");
3591
3592        // `protected`, not `private`: tsc rejects `Labeled extends Point` when
3593        // `Point`'s constructor is private, though Submilli allows it here.
3594        let d_ts = render_packages_d_ts(&[&defs], &[]);
3595        assert!(d_ts.contains("class Labeled extends Point"), "{d_ts}");
3596        assert_eq!(
3597            d_ts.matches("protected constructor();").count(),
3598            2,
3599            "{d_ts}"
3600        );
3601        assert!(!d_ts.contains("secret"), "{d_ts}");
3602    }
3603
3604    #[test]
3605    fn constructor_visibility_round_trips_and_a_public_one_is_not_written() {
3606        let package = "@acme/shapes";
3607        for visibility in [crate::Visibility::Private, crate::Visibility::Public] {
3608            let symbol = class_with_constructor(package, "Point", Vec::new(), visibility, None);
3609            let json = serde_json::to_string(&symbol.kind).unwrap();
3610            assert_eq!(
3611                json.contains("constructor_visibility"),
3612                !visibility.is_public()
3613            );
3614            let TypeKind::Class {
3615                constructor_visibility,
3616                ..
3617            } = serde_json::from_str::<TypeKind>(&json).unwrap()
3618            else {
3619                panic!("not a class: {json}");
3620            };
3621            assert_eq!(constructor_visibility, visibility);
3622        }
3623    }
3624
3625    #[test]
3626    fn class_declarations_omit_private_members() {
3627        use crate::{FieldSig, MethodSig, Visibility};
3628
3629        let field = |ty, visibility| FieldSig {
3630            ty,
3631            visibility,
3632            readonly: false,
3633            optional: false,
3634            method: false,
3635            doc: None,
3636        };
3637        let method = |ret| MethodSig {
3638            optional: false,
3639            generics: Vec::new(),
3640            params: Vec::new(),
3641            ret,
3642            predicate: None,
3643            doc: None,
3644        };
3645
3646        let mut fields = BTreeMap::new();
3647        fields.insert("name".to_string(), field(Type::String, Visibility::Public));
3648        fields.insert(
3649            "sound".to_string(),
3650            field(Type::String, Visibility::Private),
3651        );
3652        let mut methods = BTreeMap::new();
3653        methods.insert("speak".to_string(), method(Type::String));
3654        methods.insert("secret".to_string(), method(Type::Number));
3655        let mut method_visibility = BTreeMap::new();
3656        method_visibility.insert("speak".to_string(), Visibility::Public);
3657        method_visibility.insert("secret".to_string(), Visibility::Private);
3658
3659        let mut defs = PackageDeclaration::with_package("@acme/zoo");
3660        defs.types.insert(
3661            "Animal".to_string(),
3662            TypeSymbol {
3663                name: "Animal".to_string(),
3664                mangled_name: crate::mangle::package_symbol("@acme/zoo", "Animal"),
3665                declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3666                kind: TypeKind::Class {
3667                    generics: Vec::new(),
3668                    fields,
3669                    narrowing_checks: BTreeMap::new(),
3670                    methods,
3671                    method_visibility,
3672                    accessors: Vec::new(),
3673                    constructor: vec![Param::new("name", Type::String)],
3674                    constructor_visibility: crate::Visibility::Public,
3675                    statics: BTreeMap::new(),
3676                    static_visibility: BTreeMap::new(),
3677                    static_fields: BTreeMap::new(),
3678                    extends: None,
3679                    implements: Vec::new(),
3680                    doc: None,
3681                },
3682            },
3683        );
3684
3685        let rendered = render_declarations(&defs);
3686        // Public surface and the constructor stay.
3687        assert!(rendered.contains("class Animal {"), "{rendered}");
3688        assert!(rendered.contains("name: string;"), "{rendered}");
3689        assert!(rendered.contains("speak(): string;"), "{rendered}");
3690        assert!(
3691            rendered.contains("constructor(name: string);"),
3692            "{rendered}"
3693        );
3694        // Private members and the `private` keyword never reach the docs.
3695        assert!(!rendered.contains("private "), "{rendered}");
3696        assert!(!rendered.contains("sound"), "{rendered}");
3697        assert!(!rendered.contains("secret"), "{rendered}");
3698    }
3699
3700    #[test]
3701    fn class_declarations_render_accessors_faithfully() {
3702        use crate::{AccessorSig, FieldSig, Param, Visibility};
3703
3704        // Accessors are not methods on the public surface — they render as
3705        // properties (or explicit get/set when read/write types differ).
3706        let prop = |ty: Type, readonly| FieldSig {
3707            ty,
3708            visibility: Visibility::Public,
3709            readonly,
3710            optional: false,
3711            method: false,
3712            doc: None,
3713        };
3714        let mut fields = BTreeMap::new();
3715        fields.insert("area".to_string(), prop(Type::Number, true)); // get-only
3716        fields.insert("size".to_string(), prop(Type::Number, false)); // get+set, same type
3717        fields.insert("label".to_string(), prop(Type::String, false)); // get/set, diff types
3718        fields.insert("secret".to_string(), prop(Type::Number, false)); // set-only
3719
3720        let setter = |name: &str, ty: Type| AccessorSig::Setter {
3721            name: name.to_string(),
3722            param: Param::new("v", ty),
3723        };
3724        let getter = |name: &str, ty: Type| AccessorSig::Getter {
3725            name: name.to_string(),
3726            ret_ty: ty,
3727        };
3728        let accessors = vec![
3729            getter("area", Type::Number),
3730            getter("size", Type::Number),
3731            setter("size", Type::Number),
3732            getter("label", Type::String),
3733            setter("label", Type::Number),
3734            setter("secret", Type::Number),
3735        ];
3736
3737        let mut defs = PackageDeclaration::with_package("@acme/geo");
3738        defs.types.insert(
3739            "Shape".to_string(),
3740            TypeSymbol {
3741                name: "Shape".to_string(),
3742                mangled_name: crate::mangle::package_symbol("@acme/geo", "Shape"),
3743                declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3744                kind: TypeKind::Class {
3745                    generics: Vec::new(),
3746                    fields,
3747                    narrowing_checks: BTreeMap::new(),
3748                    methods: BTreeMap::new(),
3749                    method_visibility: BTreeMap::new(),
3750                    accessors,
3751                    constructor: Vec::new(),
3752                    constructor_visibility: crate::Visibility::Public,
3753                    statics: BTreeMap::new(),
3754                    static_visibility: BTreeMap::new(),
3755                    static_fields: BTreeMap::new(),
3756                    extends: None,
3757                    implements: Vec::new(),
3758                    doc: None,
3759                },
3760            },
3761        );
3762
3763        let docs = render_declarations(&defs);
3764        let shape = &defs.types.get("Shape").unwrap().kind;
3765        let mut dts = String::new();
3766        render_ts_type(&mut dts, "Shape", shape, "", "export ", None);
3767        for rendered in [&docs, &dts] {
3768            assert!(rendered.contains("readonly area: number;"), "{rendered}"); // get-only
3769            assert!(rendered.contains("size: number;"), "{rendered}"); // get+set same type
3770            assert!(rendered.contains("get label(): string;"), "{rendered}"); // diff types
3771            assert!(rendered.contains("set label(v: number);"), "{rendered}");
3772            assert!(rendered.contains("set secret(v: number);"), "{rendered}"); // write-only
3773            // Never the synthetic `get x`/`set x` *method* form.
3774            assert!(!rendered.contains("get area"), "{rendered}");
3775            assert!(!rendered.contains("get size("), "{rendered}");
3776        }
3777    }
3778
3779    #[test]
3780    fn class_declarations_render_public_statics_only() {
3781        use crate::{FieldSig, MethodSig, Param, Visibility};
3782
3783        let method = |ret: Type| MethodSig {
3784            optional: false,
3785            generics: Vec::new(),
3786            params: vec![Param::new("x", Type::Number)],
3787            ret,
3788            predicate: None,
3789            doc: None,
3790        };
3791        let mut statics = BTreeMap::new();
3792        statics.insert("make".to_string(), method(Type::Number));
3793        statics.insert("hidden".to_string(), method(Type::Number));
3794        let mut static_visibility = BTreeMap::new();
3795        static_visibility.insert("make".to_string(), Visibility::Public);
3796        static_visibility.insert("hidden".to_string(), Visibility::Private);
3797        let mut static_fields = BTreeMap::new();
3798        static_fields.insert(
3799            "MAX".to_string(),
3800            FieldSig {
3801                ty: Type::Number,
3802                visibility: Visibility::Public,
3803                readonly: true,
3804                optional: false,
3805                method: false,
3806                doc: None,
3807            },
3808        );
3809        static_fields.insert(
3810            "KEY".to_string(),
3811            FieldSig {
3812                ty: Type::Number,
3813                visibility: Visibility::Private,
3814                readonly: true,
3815                optional: false,
3816                method: false,
3817                doc: None,
3818            },
3819        );
3820        static_fields.insert(
3821            "count".to_string(),
3822            FieldSig {
3823                ty: Type::Number,
3824                visibility: Visibility::Public,
3825                readonly: false,
3826                optional: false,
3827                method: false,
3828                doc: None,
3829            },
3830        );
3831        static_fields.insert(
3832            "seed".to_string(),
3833            FieldSig {
3834                ty: Type::Number,
3835                visibility: Visibility::Private,
3836                readonly: false,
3837                optional: false,
3838                method: false,
3839                doc: None,
3840            },
3841        );
3842
3843        let mut defs = PackageDeclaration::with_package("@acme/calc");
3844        defs.types.insert(
3845            "Calc".to_string(),
3846            TypeSymbol {
3847                name: "Calc".to_string(),
3848                mangled_name: crate::mangle::package_symbol("@acme/calc", "Calc"),
3849                declaration_span: Span::new(FileId(0), 0, 0).unwrap(),
3850                kind: TypeKind::Class {
3851                    generics: Vec::new(),
3852                    fields: BTreeMap::new(),
3853                    narrowing_checks: BTreeMap::new(),
3854                    methods: BTreeMap::new(),
3855                    method_visibility: BTreeMap::new(),
3856                    accessors: Vec::new(),
3857                    constructor: Vec::new(),
3858                    constructor_visibility: crate::Visibility::Public,
3859                    statics,
3860                    static_visibility,
3861                    static_fields,
3862                    extends: None,
3863                    implements: Vec::new(),
3864                    doc: None,
3865                },
3866            },
3867        );
3868
3869        let docs = render_declarations(&defs);
3870        let calc = &defs.types.get("Calc").unwrap().kind;
3871        let mut dts = String::new();
3872        render_ts_type(&mut dts, "Calc", calc, "", "export ", None);
3873        for rendered in [&docs, &dts] {
3874            assert!(
3875                rendered.contains("static make(x: number): number;"),
3876                "{rendered}"
3877            );
3878            assert!(
3879                rendered.contains("static readonly MAX: number;"),
3880                "{rendered}"
3881            );
3882            assert!(rendered.contains("static count: number;"), "{rendered}");
3883            assert!(!rendered.contains("hidden"), "{rendered}");
3884            assert!(!rendered.contains("KEY"), "{rendered}");
3885            assert!(!rendered.contains("seed"), "{rendered}");
3886        }
3887    }
3888}