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