Skip to main content

submilli_engine/
packages.rs

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