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