@defold-typescript/types 0.8.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/api-targets.json +18 -0
  3. package/generated/b2d_body.d.ts +348 -0
  4. package/generated/buffer.d.ts +5 -2
  5. package/generated/camera.d.ts +236 -1
  6. package/generated/collectionfactory.d.ts +4 -0
  7. package/generated/factory.d.ts +4 -0
  8. package/generated/font.d.ts +81 -0
  9. package/generated/go.d.ts +53 -0
  10. package/generated/gui.d.ts +265 -50
  11. package/generated/html5.d.ts +17 -13
  12. package/generated/http.d.ts +16 -0
  13. package/generated/image.d.ts +24 -0
  14. package/generated/json.d.ts +2 -0
  15. package/generated/kinds/gui-script.d.ts +2 -0
  16. package/generated/kinds/render-script.d.ts +2 -0
  17. package/generated/kinds/script.d.ts +2 -0
  18. package/generated/model.d.ts +14 -0
  19. package/generated/msg.d.ts +17 -11
  20. package/generated/particlefx.d.ts +6 -0
  21. package/generated/physics.d.ts +32 -0
  22. package/generated/profiler.d.ts +14 -0
  23. package/generated/render.d.ts +109 -0
  24. package/generated/resource.d.ts +223 -0
  25. package/generated/socket.d.ts +640 -11
  26. package/generated/sound.d.ts +6 -0
  27. package/generated/sprite.d.ts +5 -0
  28. package/generated/sys.d.ts +136 -0
  29. package/generated/tilemap.d.ts +2 -0
  30. package/generated/timer.d.ts +3 -0
  31. package/generated/vmath.d.ts +109 -93
  32. package/generated/window.d.ts +23 -0
  33. package/index.d.ts +8 -0
  34. package/package.json +4 -1
  35. package/scripts/fidelity-audit.ts +26 -3
  36. package/scripts/fidelity-baseline.json +18 -2
  37. package/scripts/materialize-version.ts +23 -0
  38. package/scripts/regen.ts +5 -1
  39. package/scripts/signature-store-io.ts +18 -0
  40. package/scripts/sync-api-docs.ts +208 -12
  41. package/src/core-types.ts +13 -6
  42. package/src/doc-comment.ts +42 -5
  43. package/src/emit-dts.ts +134 -5
  44. package/src/engine-globals.d.ts +2 -0
  45. package/src/example-store.ts +11 -7
  46. package/src/index.ts +18 -1
  47. package/src/lifecycle.ts +383 -4
  48. package/src/msg-overloads.d.ts +3 -0
  49. package/src/signature-store.ts +20 -0
  50. package/src/socket-types.d.ts +48 -0
@@ -51,8 +51,11 @@ declare global {
51
51
  * On platforms that does not support dimming, `window.DIMMING_UNKNOWN` is always returned.
52
52
  *
53
53
  * @returns The mode for screen dimming
54
+ *
54
55
  * - `window.DIMMING_UNKNOWN`
56
+ *
55
57
  * - `window.DIMMING_ON`
58
+ *
56
59
  * - `window.DIMMING_OFF`
57
60
  */
58
61
  function get_dim_mode(): Opaque<"constant">;
@@ -74,15 +77,24 @@ declare global {
74
77
  * this returns the full window size and zero insets.
75
78
  *
76
79
  * @returns safe area data
80
+ *
77
81
  * `safe_area`
78
82
  * table table containing these keys:
83
+ *
79
84
  * - number `x`
85
+ *
80
86
  * - number `y`
87
+ *
81
88
  * - number `width`
89
+ *
82
90
  * - number `height`
91
+ *
83
92
  * - number `inset_left`
93
+ *
84
94
  * - number `inset_top`
95
+ *
85
96
  * - number `inset_right`
97
+ *
86
98
  * - number `inset_bottom`
87
99
  */
88
100
  function get_safe_area(): { safe_area: { x: number; y: number; width: number; height: number; inset_left: number; inset_top: number; inset_right: number; inset_bottom: number } };
@@ -96,7 +108,9 @@ declare global {
96
108
  * This function has no effect on platforms that does not support dimming.
97
109
  *
98
110
  * @param mode - The mode for screen dimming
111
+ *
99
112
  * - `window.DIMMING_ON`
113
+ *
100
114
  * - `window.DIMMING_OFF`
101
115
  */
102
116
  function set_dim_mode(mode: Opaque<"constant">): void;
@@ -104,18 +118,27 @@ declare global {
104
118
  * Sets a window event listener. Only one window event listener can be set at a time.
105
119
  *
106
120
  * @param callback - A callback which receives info about window events. Pass an empty function or `nil` if you no longer wish to receive callbacks.
121
+ *
107
122
  * `self`
108
123
  * object The calling script
109
124
  * `event`
110
125
  * constant The type of event. Can be one of these:
126
+ *
111
127
  * - `window.WINDOW_EVENT_FOCUS_LOST`
128
+ *
112
129
  * - `window.WINDOW_EVENT_FOCUS_GAINED`
130
+ *
113
131
  * - `window.WINDOW_EVENT_RESIZED`
132
+ *
114
133
  * - `window.WINDOW_EVENT_ICONIFIED`
134
+ *
115
135
  * - `window.WINDOW_EVENT_DEICONIFIED`
136
+ *
116
137
  * `data`
117
138
  * table The callback value `data` is a table which currently holds these values
139
+ *
118
140
  * - number `width`: The width of a resize event. nil otherwise.
141
+ *
119
142
  * - number `height`: The height of a resize event. nil otherwise.
120
143
  * @example
121
144
  * ```ts
package/index.d.ts CHANGED
@@ -34,6 +34,7 @@ import "./generated/push";
34
34
  import "./generated/render";
35
35
  import "./generated/resource";
36
36
  import "./generated/socket";
37
+ import "./src/socket-types";
37
38
  import "./generated/sound";
38
39
  import "./generated/sprite";
39
40
  import "./generated/sys";
@@ -57,7 +58,14 @@ export {
57
58
  type Vector3,
58
59
  type Vector4,
59
60
  } from "./src/core-types";
61
+ export { examplesHtmlToMarkdown, htmlToCodeText, htmlToDocText } from "./src/doc-comment";
60
62
  export { type EmitOptions, emitDeclarations } from "./src/emit-dts";
63
+ export {
64
+ hashExampleSource,
65
+ lookupTranslation,
66
+ type Translation,
67
+ type TranslationStore,
68
+ } from "./src/example-store";
61
69
  export {
62
70
  defineGuiScript,
63
71
  defineRenderScript,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/types",
3
- "version": "0.8.4",
3
+ "version": "0.10.0",
4
4
  "description": "TypeScript types for the Defold engine's Lua APIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,6 +30,9 @@
30
30
  "./core-types": {
31
31
  "types": "./src/core-types.ts"
32
32
  },
33
+ "./lifecycle": {
34
+ "types": "./src/lifecycle.ts"
35
+ },
33
36
  "./timers": {
34
37
  "types": "./src/timers.d.ts"
35
38
  },
@@ -4,6 +4,7 @@ import {
4
4
  ARBITRARY_TABLE_SLOTS,
5
5
  applyNestedFieldCurations,
6
6
  buildTableDocResolver,
7
+ HANDLE_METHOD_LOCAL,
7
8
  HOMOGENEOUS_ARRAY_SLOTS,
8
9
  MAPPING_TABLE_SLOTS,
9
10
  type NestedMapping,
@@ -76,6 +77,26 @@ function trailingOptionalCutoff(params: readonly Record<string, unknown>[]): num
76
77
  return cutoff;
77
78
  }
78
79
 
80
+ // Colon `<receiver>:<method>` FUNCTION elements are emitted as method-bearing
81
+ // interfaces (see emit-dts collectHandleMethodGroups). Before that recovery they
82
+ // emitted nothing yet were never counted as a droppedMembers loss — a blind spot
83
+ // that let `socket` read `droppedMembers: 0` over an incomplete surface. Count
84
+ // them honestly: with emission on, only a malformed colon name (non-identifier
85
+ // segment) is still a loss; with emission off, every colon method is dropped.
86
+ export function countDroppedHandleMethods(doc: unknown, namespace: string, emit = true): number {
87
+ const prefix = `${namespace}.`;
88
+ let dropped = 0;
89
+ for (const element of elementsOf(doc)) {
90
+ if (element.type !== "FUNCTION" || typeof element.name !== "string") continue;
91
+ const local = element.name.startsWith(prefix)
92
+ ? element.name.slice(prefix.length)
93
+ : element.name;
94
+ if (!local.includes(":")) continue;
95
+ if (!emit || !HANDLE_METHOD_LOCAL.test(local)) dropped += 1;
96
+ }
97
+ return dropped;
98
+ }
99
+
79
100
  function auditEntry(
80
101
  entry: ModuleManifestEntry,
81
102
  knownConstantFqns: ReadonlySet<string>,
@@ -356,9 +377,11 @@ function auditEntry(
356
377
  unknownTokens: [...unknown].sort(),
357
378
  recordTables,
358
379
  multiReturn,
359
- droppedMembers: generateModuleDeclaration(entry, {
360
- knownConstantFqns: NO_KNOWN_CONSTANTS,
361
- }).dropped.filter((name) => !OVERLOAD_COVERED_SKIPS.has(name)).length,
380
+ droppedMembers:
381
+ generateModuleDeclaration(entry, {
382
+ knownConstantFqns: NO_KNOWN_CONSTANTS,
383
+ }).dropped.filter((name) => !OVERLOAD_COVERED_SKIPS.has(name)).length +
384
+ countDroppedHandleMethods(entry.doc, entry.namespace),
362
385
  optionalAsRequired,
363
386
  };
364
387
  }
@@ -7,7 +7,7 @@
7
7
  "droppedMembers": 0,
8
8
  "optionalAsRequired": 0
9
9
  },
10
- "buffer": {
10
+ "b2d.body": {
11
11
  "droppedElements": 0,
12
12
  "unknownTokens": [],
13
13
  "recordTables": 0,
@@ -15,7 +15,7 @@
15
15
  "droppedMembers": 0,
16
16
  "optionalAsRequired": 0
17
17
  },
18
- "camera": {
18
+ "buffer": {
19
19
  "droppedElements": 0,
20
20
  "unknownTokens": [],
21
21
  "recordTables": 0,
@@ -23,6 +23,14 @@
23
23
  "droppedMembers": 0,
24
24
  "optionalAsRequired": 0
25
25
  },
26
+ "camera": {
27
+ "droppedElements": 0,
28
+ "unknownTokens": [],
29
+ "recordTables": 1,
30
+ "multiReturn": 0,
31
+ "droppedMembers": 0,
32
+ "optionalAsRequired": 7
33
+ },
26
34
  "collectionfactory": {
27
35
  "droppedElements": 0,
28
36
  "unknownTokens": [],
@@ -55,6 +63,14 @@
55
63
  "droppedMembers": 0,
56
64
  "optionalAsRequired": 0
57
65
  },
66
+ "font": {
67
+ "droppedElements": 0,
68
+ "unknownTokens": [],
69
+ "recordTables": 1,
70
+ "multiReturn": 0,
71
+ "droppedMembers": 0,
72
+ "optionalAsRequired": 0
73
+ },
58
74
  "go": {
59
75
  "droppedElements": 0,
60
76
  "unknownTokens": [],
@@ -4,10 +4,33 @@ import {
4
4
  type ApiTarget,
5
5
  generateModuleDeclaration,
6
6
  generateVersionIndex,
7
+ KIND_MODULE_MANIFEST,
8
+ LUA_STDLIB_REFERENCES,
7
9
  type ResolveTargetOptions,
8
10
  resolveTargetModules,
9
11
  } from "./regen";
10
12
 
13
+ export { KIND_MODULE_MANIFEST } from "./regen";
14
+
15
+ export interface RenderMaterializedKindIndexOptions {
16
+ readonly kind: string;
17
+ readonly universalModules: readonly string[];
18
+ readonly restrictedModule: string | null;
19
+ }
20
+
21
+ // Render one per-kind subpath for the materialized surface, mirroring
22
+ // `generateKindIndex` but re-exporting the factory from the installed
23
+ // `@defold-typescript/types/lifecycle` subpath (the materialized surface has no
24
+ // relative `src/lifecycle` to reach). Pure: returns a string, no FS.
25
+ export function renderMaterializedKindIndex(opts: RenderMaterializedKindIndexOptions): string {
26
+ const entry = KIND_MODULE_MANIFEST.find((e) => e.kind === opts.kind);
27
+ if (!entry) throw new Error(`unknown script kind: ${opts.kind}`);
28
+ const universal = [...new Set(["engine-globals", ...opts.universalModules])].sort();
29
+ const lines = universal.map((mod) => `import "../${mod}";`);
30
+ if (opts.restrictedModule) lines.push(`import "../${opts.restrictedModule}";`);
31
+ return `${LUA_STDLIB_REFERENCES}${lines.join("\n")}\n\nexport { ${entry.factory} } from "@defold-typescript/types/lifecycle";\nexport type { ScriptProperties, ScriptProperty } from "@defold-typescript/types/lifecycle";\n`;
32
+ }
33
+
11
34
  export interface MaterializeVersionedSurfaceOptions {
12
35
  readonly destDir: string;
13
36
  readonly resolveOpts?: ResolveTargetOptions;
package/scripts/regen.ts CHANGED
@@ -36,6 +36,10 @@ export interface ApiTarget {
36
36
  readonly coreTypesImport: string;
37
37
  readonly source?: ApiTargetSource;
38
38
  readonly modules: readonly ApiTargetModule[];
39
+ // Docs-only Lua stdlib pages (no generated `.d.ts`): vendored fixtures the
40
+ // docs-site pages under the "Lua standard library" category. Never read by
41
+ // regen/MODULE_MANIFEST; surfaced here so the registry stays type-honest.
42
+ readonly luaStdlib?: readonly { readonly namespace: string; readonly fixture: string }[];
39
43
  }
40
44
 
41
45
  const REGISTRY_PATH = resolve(import.meta.dir, "..", "api-targets.json");
@@ -226,7 +230,7 @@ export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
226
230
  // The Lua standard library rides every per-kind subpath the same as the full
227
231
  // entrypoint. Triple-slash directives must precede the first statement, so they
228
232
  // lead the generated kind index.
229
- const LUA_STDLIB_REFERENCES =
233
+ export const LUA_STDLIB_REFERENCES =
230
234
  '/// <reference types="lua-types/5.1" />\n/// <reference types="lua-types/special/jit-only" />\n';
231
235
 
232
236
  const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { SignatureStore } from "../src/signature-store";
4
+
5
+ // Build-time only: lives under `scripts/` (never reachable from the shipped
6
+ // `src/index.ts` graph) so its `node:fs` import cannot leak into a consumer's
7
+ // typecheck. `src/signature-store.ts` stays pure for exactly that reason.
8
+ const IO_SIGNATURES_PATH = resolve(import.meta.dir, "..", "signatures", "io.json");
9
+
10
+ export function loadSignatures(path: string = IO_SIGNATURES_PATH): SignatureStore {
11
+ let raw: string;
12
+ try {
13
+ raw = readFileSync(path, "utf8");
14
+ } catch {
15
+ return {};
16
+ }
17
+ return JSON.parse(raw) as SignatureStore;
18
+ }
@@ -14,6 +14,11 @@ export interface SyncManifestEntry {
14
14
  readonly namespace: string;
15
15
  readonly zipEntry: string;
16
16
  readonly fixture: string;
17
+ // Extra ref-doc.zip entries whose elements are merged into this namespace at
18
+ // sync time. A namespace whose Lua surface is split across several upstream
19
+ // docs (e.g. `sys`) is reassembled before parse, so the emitter still sees one
20
+ // fixture, one namespace.
21
+ readonly mergeEntries?: readonly string[];
17
22
  }
18
23
 
19
24
  // namespace -> ref-doc.zip entry. Entry paths do not match the namespace (gui ->
@@ -21,12 +26,24 @@ export interface SyncManifestEntry {
21
26
  // so each path is confirmed against the pinned release artifact, not derived.
22
27
  export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
23
28
  entry("b2d", "doc/scripts-box2d-script_box2d.cpp_doc.json"),
29
+ // On-disk name uses underscores to avoid a double-dotted filename; the namespace
30
+ // string stays `b2d.body` so /api/b2d.body routes correctly and the emitter
31
+ // produces `declare global { namespace b2d.body { ... } }`. Use the non-`_defold`
32
+ // variant — the `_defold` variant has duplicate `get_world_center` elements.
33
+ entry(
34
+ "b2d.body",
35
+ "doc/scripts-box2d-script_box2d_body.cpp_doc.json",
36
+ "fixtures/b2d_body_doc.json",
37
+ ),
24
38
  entry("buffer", "doc/scripts-script_buffer.cpp_doc.json"),
25
- entry("camera", "doc/scripts-script_camera.cpp_doc.json"),
39
+ // camera's Lua surface lives in the render-script doc, not the scripts-script
40
+ // one (`doc/scripts-script_camera.cpp_doc.json` is empty upstream in 1.12.4).
41
+ entry("camera", "doc/render-render_script_camera.cpp_doc.json"),
26
42
  entry("collectionfactory", "doc/scripts-script_collection_factory.cpp_doc.json"),
27
43
  entry("collectionproxy", "doc/scripts-script_collectionproxy.cpp_doc.json"),
28
44
  entry("crash", "doc/script_crash.cpp_doc.json"),
29
45
  entry("factory", "doc/scripts-script_factory.cpp_doc.json"),
46
+ entry("font", "doc/scripts-script_font.cpp_doc.json"),
30
47
  entry("go", "doc/gameobject_script.cpp_doc.json"),
31
48
  entry("graphics", "doc/src-script_graphics.cpp_doc.json"),
32
49
  entry("gui", "doc/gui_script.cpp_doc.json"),
@@ -46,9 +63,16 @@ export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
46
63
  entry("socket", "doc/luasocket-luasocket.doc_h_doc.json"),
47
64
  entry("sound", "doc/scripts-script_sound.cpp_doc.json"),
48
65
  entry("sprite", "doc/scripts-script_sprite.cpp_doc.json"),
49
- // The `sys` surface is split across two docs in ref-doc.zip; this is the core
50
- // (src-script_sys) doc — the larger gamesys subset is folded in when wired.
51
- entry("sys", "doc/src-script_sys.cpp_doc.json"),
66
+ // The `sys` Lua surface is split across three docs in ref-doc.zip: the core
67
+ // (src-script_sys), the gamesys subset (`load_buffer`/`load_buffer_async` plus
68
+ // the `REQUEST_STATUS_*` constants), and the engine doc (`set_engine_throttle`/
69
+ // `set_render_enable`). `mergeEntries` folds all three into one fixture at sync
70
+ // time. The `*_ddf.proto` doc is deliberately excluded — it is message-shaped
71
+ // and owned by the builtin-messages catalog.
72
+ entry("sys", "doc/src-script_sys.cpp_doc.json", "fixtures/sys_doc.json", [
73
+ "doc/scripts-script_sys_gamesys.cpp_doc.json",
74
+ "doc/script-script_engine.cpp_doc.json",
75
+ ]),
52
76
  entry("tilemap", "doc/scripts-script_tilemap.cpp_doc.json"),
53
77
  entry("timer", "doc/src-script_timer.cpp_doc.json"),
54
78
  entry("types", "doc/src-script_types.cpp_doc.json"),
@@ -62,8 +86,61 @@ export const SYNC_MANIFEST: readonly SyncManifestEntry[] = [
62
86
  // four extension-only surfaces are wired via EXTENSION_MANIFEST below.
63
87
  export const UNMAPPED: ReadonlyMap<string, string> = new Map();
64
88
 
65
- function entry(namespace: string, zipEntry: string): SyncManifestEntry {
66
- return { namespace, zipEntry, fixture: `fixtures/${namespace}_doc.json` };
89
+ // A Lua script namespace is a lowercase, dot-separated identifier (`sys`,
90
+ // `b2d.body`). The native C/C++ SDK docs (`dmGraphics`, …), the C# bindings
91
+ // (`cs-*`), and the struct-only docs (empty `info.namespace`) fail this shape,
92
+ // so the upstream-coverage guard skips them structurally rather than listing
93
+ // each by hand — that keeps the guard self-maintaining as the SDK grows.
94
+ const LUA_NAMESPACE = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/;
95
+
96
+ // Lowercase-identifier namespaces that pass LUA_NAMESPACE but are intentionally
97
+ // not wired through SYNC_MANIFEST, each with a sourced reason (mirrors the
98
+ // `EMPTY_BY_UPSTREAM` allowlist shape). The upstream-coverage guard treats these
99
+ // as covered. Adding a namespace here is a deliberate, reviewed act.
100
+ export const IGNORED_UPSTREAM: ReadonlyMap<string, string> = new Map([
101
+ ["editor", "editor-scripting API (editor.apidoc), not a runtime game namespace"],
102
+ ["engine", "CLI/engine env doc, not a runtime Lua namespace"],
103
+ [
104
+ "builtins",
105
+ "global Defold builtins (hash/pprint/…) typed as ambient globals, not a `builtins.*` namespace",
106
+ ],
107
+ ]);
108
+
109
+ // Pure-Lua / LuaJIT surfaces Defold documents (https://defold.com/ref/stable/base-lua/,
110
+ // `bit-lua`) whose TypeScript types are owned by the `lua-types` dependency the
111
+ // `lua-stdlib-globals` goal adopted — `lua-types/special/jit-only.d.ts` declares
112
+ // `declare namespace bit { ... }` and `lua-types/core/global.d.ts` carries the
113
+ // base globals. Re-emitting them as generated Defold namespaces would collide
114
+ // (`declare namespace bit` duplicates; base globals are top-level globals, not
115
+ // a `base.*` namespace). LUA_STDLIB_MANIFEST vendors the same ref-doc JSON
116
+ // `SYNC_MANIFEST` carries, but the docs-site is the only consumer — `regen.ts`
117
+ // / `MODULE_MANIFEST` never read it, so no `generated/<ns>.d.ts` is produced.
118
+ export const LUA_STDLIB_MANIFEST: readonly SyncManifestEntry[] = [
119
+ entry("base", "doc/lua_base.doc_h_doc.json"),
120
+ entry("bit", "doc/src-script_bitop.cpp_doc.json"),
121
+ // Core Lua stdlib docs. The 1.12.4 release names these `doc/lua_<ns>.doc_h_doc.json`
122
+ // (confirmed against the pinned ref-doc.zip, not derived — the naming drifted from
123
+ // the 1.9.8 `doc/<ns>_doc.json` form). Typed by the lua-types dependency, so docs-only.
124
+ entry("math", "doc/lua_math.doc_h_doc.json"),
125
+ entry("os", "doc/lua_os.doc_h_doc.json"),
126
+ entry("string", "doc/lua_string.doc_h_doc.json"),
127
+ entry("table", "doc/lua_table.doc_h_doc.json"),
128
+ entry("coroutine", "doc/lua_coroutine.doc_h_doc.json"),
129
+ // Sandboxed-runtime stdlib surfaces. Defold sandboxes the Lua VM, but the pinned
130
+ // 1.12.4 ref-doc.zip documents all three with real function elements; types stay
131
+ // owned by lua-types (core/{debug,io}.d.ts + core/modules.d.ts for `package`).
132
+ entry("debug", "doc/lua_debug.doc_h_doc.json"),
133
+ entry("io", "doc/lua_io.doc_h_doc.json"),
134
+ entry("package", "doc/lua_package.doc_h_doc.json"),
135
+ ];
136
+
137
+ function entry(
138
+ namespace: string,
139
+ zipEntry: string,
140
+ fixture: string = `fixtures/${namespace}_doc.json`,
141
+ mergeEntries?: readonly string[],
142
+ ): SyncManifestEntry {
143
+ return { namespace, zipEntry, fixture, ...(mergeEntries ? { mergeEntries } : {}) };
67
144
  }
68
145
 
69
146
  export interface ExtensionManifestEntry {
@@ -115,7 +192,7 @@ export function parseChecklistNamespaces(visionMarkdown: string): string[] {
115
192
  const seen = new Set<string>();
116
193
  for (const match of segment.matchAll(/`([^`]+)`/g)) {
117
194
  const token = match[1] as string;
118
- if (!/^[a-z][a-z0-9]*$/.test(token) || seen.has(token)) continue;
195
+ if (!/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/.test(token) || seen.has(token)) continue;
119
196
  seen.add(token);
120
197
  namespaces.push(token);
121
198
  }
@@ -168,15 +245,52 @@ export interface ExtractedFixture {
168
245
  contents: string;
169
246
  }
170
247
 
248
+ function isRecord(value: unknown): value is Record<string, unknown> {
249
+ return typeof value === "object" && value !== null && !Array.isArray(value);
250
+ }
251
+
252
+ // Concatenate the `elements` of every doc into the first doc, deduping by
253
+ // (name, type) with first occurrence winning, and keep the first doc's `info`.
254
+ // First-wins guards the `b2d.body` `_defold` duplicate-`get_world_center`
255
+ // hazard. Operates on the raw ref-doc JSON shape (`{ info, elements }`), so the
256
+ // result is still parseable by `parseDefoldApiDoc`.
257
+ export function mergeApiDocs(docs: readonly unknown[]): unknown {
258
+ const first = docs[0];
259
+ const seen = new Set<string>();
260
+ const elements: unknown[] = [];
261
+ for (const doc of docs) {
262
+ if (!isRecord(doc) || !Array.isArray(doc.elements)) continue;
263
+ for (const element of doc.elements) {
264
+ const key =
265
+ isRecord(element) && typeof element.name === "string" && typeof element.type === "string"
266
+ ? `${element.type}${element.name}`
267
+ : null;
268
+ if (key !== null) {
269
+ if (seen.has(key)) continue;
270
+ seen.add(key);
271
+ }
272
+ elements.push(element);
273
+ }
274
+ }
275
+ return isRecord(first) ? { ...first, elements } : { elements };
276
+ }
277
+
171
278
  export function extractFixtures(
172
279
  zip: ZipAccessor,
173
280
  manifest: readonly SyncManifestEntry[] = SYNC_MANIFEST,
174
281
  ): ExtractedFixture[] {
175
282
  return manifest.map((item) => {
176
- if (!zip.has(item.zipEntry)) {
177
- throw new Error(`zip is missing entry ${item.zipEntry} for namespace ${item.namespace}`);
283
+ const sources = [item.zipEntry, ...(item.mergeEntries ?? [])];
284
+ for (const source of sources) {
285
+ if (!zip.has(source)) {
286
+ throw new Error(`zip is missing entry ${source} for namespace ${item.namespace}`);
287
+ }
178
288
  }
179
- return { namespace: item.namespace, fixture: item.fixture, contents: zip.read(item.zipEntry) };
289
+ const contents =
290
+ item.mergeEntries === undefined
291
+ ? zip.read(item.zipEntry)
292
+ : JSON.stringify(mergeApiDocs(sources.map((source) => JSON.parse(zip.read(source)))));
293
+ return { namespace: item.namespace, fixture: item.fixture, contents };
180
294
  });
181
295
  }
182
296
 
@@ -276,11 +390,26 @@ export interface SyncedDoc {
276
390
  doc: unknown;
277
391
  }
278
392
 
393
+ export interface UpstreamNamespace {
394
+ namespace: string;
395
+ functionCount: number;
396
+ zipEntry: string;
397
+ }
398
+
399
+ export interface UnmappedUpstreamNamespace {
400
+ namespace: string;
401
+ zipEntry: string;
402
+ }
403
+
279
404
  export interface CoverageReport {
280
405
  wired: string[];
281
406
  fixtureOnly: string[];
282
407
  missingMapping: string[];
283
408
  unknownTypeTokens: { namespace: string; tokens: string[] }[];
409
+ // Function-bearing Lua namespaces present in the zip but neither mapped nor
410
+ // allowlisted — the next `font`-style miss. The `--check` path fails when this
411
+ // is non-empty.
412
+ unmappedUpstream: UnmappedUpstreamNamespace[];
284
413
  }
285
414
 
286
415
  export interface CoverageInput {
@@ -288,6 +417,38 @@ export interface CoverageInput {
288
417
  moduleManifest: readonly { namespace: string }[];
289
418
  unmapped: ReadonlyMap<string, string>;
290
419
  syncedDocs: readonly SyncedDoc[];
420
+ // Every function-bearing namespace the zip itself exposes (from
421
+ // collectUpstreamNamespaces). Absent in pure-fixture callers that don't audit
422
+ // upstream coverage, in which case unmappedUpstream is empty.
423
+ upstream?: readonly UpstreamNamespace[];
424
+ upstreamMapped?: ReadonlySet<string>;
425
+ ignoredUpstream?: ReadonlyMap<string, string>;
426
+ }
427
+
428
+ // Enumerate every `doc/*.json` zip entry, reading its `info.namespace` and the
429
+ // count of FUNCTION elements. Only function-bearing docs are returned, so
430
+ // message-only / struct-only docs never reach the coverage guard.
431
+ export function collectUpstreamNamespaces(zip: ZipAccessor): UpstreamNamespace[] {
432
+ const out: UpstreamNamespace[] = [];
433
+ for (const zipEntry of zip.entries()) {
434
+ if (!/^doc\/.*\.json$/.test(zipEntry)) continue;
435
+ let doc: unknown;
436
+ try {
437
+ doc = JSON.parse(zip.read(zipEntry));
438
+ } catch {
439
+ continue;
440
+ }
441
+ if (!isRecord(doc)) continue;
442
+ const info = doc.info;
443
+ const namespace = isRecord(info) && typeof info.namespace === "string" ? info.namespace : "";
444
+ const elements = Array.isArray(doc.elements) ? doc.elements : [];
445
+ let functionCount = 0;
446
+ for (const element of elements) {
447
+ if (isRecord(element) && element.type === "FUNCTION") functionCount += 1;
448
+ }
449
+ if (functionCount > 0) out.push({ namespace, functionCount, zipEntry });
450
+ }
451
+ return out;
291
452
  }
292
453
 
293
454
  export function buildCoverageReport(input: CoverageInput): CoverageReport {
@@ -307,7 +468,26 @@ export function buildCoverageReport(input: CoverageInput): CoverageReport {
307
468
  unknownTypeTokens.push({ namespace: synced.namespace, tokens });
308
469
  }
309
470
  }
310
- return { wired, fixtureOnly, missingMapping, unknownTypeTokens };
471
+
472
+ const mapped = input.upstreamMapped ?? new Set<string>();
473
+ const ignored = input.ignoredUpstream ?? new Map<string, string>();
474
+ const flagged = new Map<string, string>();
475
+ for (const item of input.upstream ?? []) {
476
+ if (
477
+ !LUA_NAMESPACE.test(item.namespace) ||
478
+ mapped.has(item.namespace) ||
479
+ ignored.has(item.namespace) ||
480
+ flagged.has(item.namespace)
481
+ ) {
482
+ continue;
483
+ }
484
+ flagged.set(item.namespace, item.zipEntry);
485
+ }
486
+ const unmappedUpstream = [...flagged]
487
+ .map(([namespace, zipEntry]) => ({ namespace, zipEntry }))
488
+ .sort((a, b) => a.namespace.localeCompare(b.namespace));
489
+
490
+ return { wired, fixtureOnly, missingMapping, unknownTypeTokens, unmappedUpstream };
311
491
  }
312
492
 
313
493
  function collectUnknownTokens(module: ApiModule): string[] {
@@ -349,6 +529,15 @@ function printReport(results: FixtureSyncResult[], report: CoverageReport, check
349
529
  console.log(` ${namespace}: ${tokens.join(", ")}`);
350
530
  }
351
531
  }
532
+
533
+ if (report.unmappedUpstream.length > 0) {
534
+ console.log(
535
+ "\nunmapped upstream namespaces (function-bearing, neither mapped nor allowlisted)",
536
+ );
537
+ for (const { namespace, zipEntry } of report.unmappedUpstream) {
538
+ console.log(` ${namespace}: ${zipEntry}`);
539
+ }
540
+ }
352
541
  }
353
542
 
354
543
  if (import.meta.main) {
@@ -359,9 +548,11 @@ if (import.meta.main) {
359
548
 
360
549
  const coreFixtures = extractFixtures(zip);
361
550
  const extensionFixtures = await downloadExtensionFixtures();
551
+ const luaStdlibFixtures = extractFixtures(zip, LUA_STDLIB_MANIFEST);
362
552
  const results = [
363
553
  ...syncExtractedFixtures(coreFixtures, { check }),
364
554
  ...syncExtractedFixtures(extensionFixtures, { check }),
555
+ ...syncExtractedFixtures(luaStdlibFixtures, { check }),
365
556
  ];
366
557
  const syncedDocs = [...coreFixtures, ...extensionFixtures].map((f) => ({
367
558
  namespace: f.namespace,
@@ -372,10 +563,15 @@ if (import.meta.main) {
372
563
  moduleManifest: MODULE_MANIFEST,
373
564
  unmapped: UNMAPPED,
374
565
  syncedDocs,
566
+ upstream: collectUpstreamNamespaces(zip),
567
+ upstreamMapped: new Set(
568
+ [...SYNC_MANIFEST, ...EXTENSION_MANIFEST, ...LUA_STDLIB_MANIFEST].map((e) => e.namespace),
569
+ ),
570
+ ignoredUpstream: IGNORED_UPSTREAM,
375
571
  });
376
572
  printReport(results, report, check);
377
573
 
378
- if (check && results.some((r) => r.status === "drift")) {
574
+ if (check && (results.some((r) => r.status === "drift") || report.unmappedUpstream.length > 0)) {
379
575
  process.exitCode = 1;
380
576
  }
381
577
  }
package/src/core-types.ts CHANGED
@@ -17,7 +17,7 @@ export interface Vector3 {
17
17
  * @remarks
18
18
  * Prefer `v.unm()` over `-v` — TypeScript does not flag unary `-` on object
19
19
  * types and silently produces `number`. See
20
- * `docs/guide/typescript-gotchas.md` for the full story.
20
+ * `packages/docs/guide/typescript-gotchas.md` for the full story.
21
21
  */
22
22
  unm: LuaNegationMethod<Vector3>;
23
23
  }
@@ -35,7 +35,7 @@ export interface Vector4 {
35
35
  * @remarks
36
36
  * Prefer `v.unm()` over `-v` — TypeScript does not flag unary `-` on object
37
37
  * types and silently produces `number`. See
38
- * `docs/guide/typescript-gotchas.md` for the full story.
38
+ * `packages/docs/guide/typescript-gotchas.md` for the full story.
39
39
  */
40
40
  unm: LuaNegationMethod<Vector4>;
41
41
  }
@@ -123,13 +123,20 @@ export const DEFOLD_TYPE_MAP: Readonly<Record<string, string>> = {
123
123
  constant: 'Opaque<"constant">',
124
124
  constant_buffer: 'Opaque<"constant_buffer">',
125
125
  buffer: 'Opaque<"buffer">',
126
- bufferstream: 'Opaque<"bufferstream">',
126
+ bufferstream: 'Opaque<"bufferstream"> & { [index: number]: number }',
127
127
  userdata: 'Opaque<"userdata">',
128
128
  resource: 'Opaque<"resource">',
129
129
  b2World: 'Opaque<"b2World">',
130
130
  b2Body: 'Opaque<"b2Body">',
131
- client: 'Opaque<"client">',
132
- master: 'Opaque<"master">',
133
- unconnected: 'Opaque<"unconnected">',
131
+ b2BodyType:
132
+ '(number & { readonly __brand: "b2d.body.B2_DYNAMIC_BODY" }) | (number & { readonly __brand: "b2d.body.B2_KINEMATIC_BODY" }) | (number & { readonly __brand: "b2d.body.B2_STATIC_BODY" })',
133
+ // socket handle types resolve to the method-bearing `interface <receiver>`
134
+ // emitted inside `namespace socket`, not opaque brands — the documented
135
+ // colon methods (`client:send`, …) make each handle structurally distinct.
136
+ client: "client",
137
+ connected: "connected",
138
+ master: "master",
139
+ server: "server",
140
+ unconnected: "unconnected",
134
141
  any: "unknown",
135
142
  } as const;