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