@defold-typescript/library-types 0.22.0 → 0.24.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 (145) hide show
  1. package/api-doc/boom.json +3870 -0
  2. package/api-doc/{bridge.bridge.json → bridge.json} +754 -844
  3. package/api-doc/bzAnim.json +338 -0
  4. package/api-doc/decore.json +23 -23
  5. package/api-doc/defcon.json +150 -0
  6. package/api-doc/{defmath.defmath.json → defmath.json} +6 -46
  7. package/api-doc/defsave.json +518 -0
  8. package/api-doc/deftest.json +741 -0
  9. package/api-doc/{dicebag.dicebag.json → dicebag.json} +19 -1
  10. package/api-doc/druid.json +249 -1846
  11. package/api-doc/event.json +1074 -0
  12. package/api-doc/gooey.json +2050 -0
  13. package/api-doc/immutable.json +65 -0
  14. package/api-doc/in.accelerometer.json +75 -1
  15. package/api-doc/in.button.json +17 -10
  16. package/api-doc/in.cursor.json +26 -0
  17. package/api-doc/in.gesture.json +401 -6
  18. package/api-doc/in.keyboard.json +2 -0
  19. package/api-doc/in.mapper.json +12 -8
  20. package/api-doc/in.onscreen.json +61 -5
  21. package/api-doc/in.state.json +61 -13
  22. package/api-doc/in.textbox.json +23 -12
  23. package/api-doc/in.triggers.json +336 -0
  24. package/api-doc/lang.json +528 -0
  25. package/api-doc/{event.event.json → log.json} +96 -101
  26. package/api-doc/metrics.fps.json +61 -2
  27. package/api-doc/metrics.mem.json +61 -2
  28. package/api-doc/monarch.monarch.json +249 -7
  29. package/api-doc/monarch.transitions.easings.json +19 -26
  30. package/api-doc/monarch.transitions.gui.json +782 -79
  31. package/api-doc/nakama.engine.defold.json +31 -18
  32. package/api-doc/{nakama.nakama.json → nakama.json} +4752 -2554
  33. package/api-doc/nakama.util.log.json +32 -4
  34. package/api-doc/narrator.json +647 -0
  35. package/api-doc/openapi/nakama.nakama.json +7680 -0
  36. package/api-doc/orthographic.camera.json +96 -98
  37. package/api-doc/orthographic.json +602 -0
  38. package/api-doc/panthera.json +1183 -0
  39. package/api-doc/{persist.persist.json → persist.json} +26 -1
  40. package/api-doc/{platypus.platypus.json → platypus.json} +68 -14
  41. package/api-doc/proto.json +995 -0
  42. package/api-doc/{rendy.rendy.json → rendy.json} +46 -99
  43. package/api-doc/richtext.color.json +77 -0
  44. package/api-doc/richtext.richtext.json +375 -5
  45. package/api-doc/saver.saver.json +714 -263
  46. package/api-doc/saver.storage.json +283 -52
  47. package/api-doc/squid.json +846 -0
  48. package/api-doc/{starly.starly.json → starly.json} +1 -1
  49. package/api-doc/tweener.json +277 -0
  50. package/api-doc/{yagames.yagames.json → yagames.json} +784 -189
  51. package/api-doc/{zzfx.api.json → zzfx.json} +9 -9
  52. package/authored-targets.json +403 -0
  53. package/generated/{boom.boom.d.ts → boom.d.ts} +14 -0
  54. package/generated/bridge.d.ts +468 -0
  55. package/generated/{bzAnim.bzLibrary.d.ts → bzAnim.d.ts} +6 -0
  56. package/generated/decore.d.ts +37 -37
  57. package/generated/defcon.d.ts +43 -0
  58. package/generated/{defmath.defmath.d.ts → defmath.d.ts} +3 -2
  59. package/generated/defsave.d.ts +144 -0
  60. package/generated/{dicebag.dicebag.d.ts → dicebag.d.ts} +25 -0
  61. package/generated/druid.d.ts +198 -502
  62. package/generated/event.d.ts +318 -0
  63. package/generated/{gooey.gooey.d.ts → gooey.d.ts} +61 -6
  64. package/generated/immutable.d.ts +13 -0
  65. package/generated/in.onscreen.d.ts +5 -0
  66. package/generated/in.textbox.d.ts +4 -0
  67. package/generated/lang.d.ts +101 -0
  68. package/generated/log.d.ts +36 -0
  69. package/generated/metrics.fps.d.ts +19 -1
  70. package/generated/metrics.mem.d.ts +19 -1
  71. package/generated/monarch.monarch.d.ts +44 -1
  72. package/generated/monarch.transitions.easings.d.ts +0 -1
  73. package/generated/monarch.transitions.gui.d.ts +96 -12
  74. package/generated/nakama.d.ts +1158 -0
  75. package/generated/nakama.engine.defold.d.ts +3 -1
  76. package/generated/nakama.util.log.d.ts +6 -0
  77. package/generated/narrator.d.ts +121 -0
  78. package/generated/openapi/nakama.nakama.d.ts +884 -0
  79. package/generated/orthographic.camera.d.ts +24 -38
  80. package/generated/orthographic.d.ts +155 -0
  81. package/generated/panthera.d.ts +170 -0
  82. package/generated/{persist.persist.d.ts → persist.d.ts} +3 -0
  83. package/generated/{platypus.platypus.d.ts → platypus.d.ts} +9 -0
  84. package/generated/proto.d.ts +146 -0
  85. package/generated/{rendy.rendy.d.ts → rendy.d.ts} +33 -80
  86. package/generated/richtext.color.d.ts +15 -0
  87. package/generated/saver.saver.d.ts +289 -42
  88. package/generated/saver.storage.d.ts +79 -14
  89. package/generated/squid.d.ts +127 -0
  90. package/generated/tweener.d.ts +42 -0
  91. package/generated/{yagames.yagames.d.ts → yagames.d.ts} +151 -29
  92. package/generated/{zzfx.api.d.ts → zzfx.d.ts} +7 -0
  93. package/library-classification.json +0 -227
  94. package/library-targets.json +1 -284
  95. package/luals-targets.json +168 -1
  96. package/openapi-targets.json +17 -0
  97. package/package.json +9 -88
  98. package/script-api-targets.json +15 -0
  99. package/scripts/__snapshots__/parse-luals.test.ts.snap +504 -85
  100. package/scripts/apply-luals-overrides.ts +95 -0
  101. package/scripts/authored-parity.ts +718 -0
  102. package/scripts/emit-library-dts.ts +177 -19
  103. package/scripts/extract-api-doc.ts +189 -38
  104. package/scripts/fidelity-floor.ts +178 -0
  105. package/scripts/import-upstream-docs.ts +125 -0
  106. package/scripts/lower-api-doc.ts +69 -19
  107. package/scripts/luals-fidelity.ts +22 -10
  108. package/scripts/map-luals-types.ts +86 -22
  109. package/scripts/parse-lua-surface.ts +187 -0
  110. package/scripts/parse-luals.ts +473 -22
  111. package/scripts/parse-markdown-api.ts +247 -0
  112. package/scripts/parse-openapi-api.ts +313 -0
  113. package/scripts/sync-authored-types.ts +249 -0
  114. package/scripts/sync-library-types.ts +99 -3
  115. package/scripts/sync-luals-types.ts +29 -2
  116. package/scripts/sync-markdown-types.ts +761 -0
  117. package/scripts/sync-openapi-types.ts +308 -0
  118. package/scripts/sync-script-api-types.ts +367 -0
  119. package/api-doc/boom.boom.json +0 -26
  120. package/api-doc/bzAnim.bzLibrary.json +0 -156
  121. package/api-doc/defcon.console.json +0 -109
  122. package/api-doc/defsave.defsave.json +0 -175
  123. package/api-doc/deftest.deftest.json +0 -71
  124. package/api-doc/gooey.gooey.json +0 -963
  125. package/api-doc/immutable.immutable.json +0 -63
  126. package/api-doc/lang.lang.json +0 -411
  127. package/api-doc/log.log.json +0 -50
  128. package/api-doc/narrator.narrator.json +0 -150
  129. package/api-doc/proto.proto.json +0 -355
  130. package/api-doc/squid.squid.json +0 -660
  131. package/api-doc/tweener.tweener.json +0 -419
  132. package/generated/bridge.bridge.d.ts +0 -533
  133. package/generated/defcon.console.d.ts +0 -24
  134. package/generated/defsave.defsave.d.ts +0 -31
  135. package/generated/event.event.d.ts +0 -54
  136. package/generated/immutable.immutable.d.ts +0 -13
  137. package/generated/lang.lang.d.ts +0 -33
  138. package/generated/log.log.d.ts +0 -40
  139. package/generated/nakama.nakama.d.ts +0 -594
  140. package/generated/narrator.narrator.d.ts +0 -66
  141. package/generated/proto.proto.d.ts +0 -36
  142. package/generated/squid.squid.d.ts +0 -106
  143. package/generated/tweener.tweener.d.ts +0 -151
  144. /package/generated/{deftest.deftest.d.ts → deftest.d.ts} +0 -0
  145. /package/generated/{starly.starly.d.ts → starly.d.ts} +0 -0
@@ -0,0 +1,249 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { extractApiDoc } from "./extract-api-doc";
4
+ import { type ApiDocElement, importUpstreamDocs } from "./import-upstream-docs";
5
+ import { type LuaMember, parseLuaSurface } from "./parse-lua-surface";
6
+
7
+ /**
8
+ * The authored/forked front-end: a fourth `library-types` corpus mode beside the
9
+ * LuaLS (`sync-luals-types.ts`), `.script_api` (`sync-script-api-types.ts`), and
10
+ * markdown (`sync-markdown-types.ts`) lanes. The other three ingest a *non-*`.d.ts`
11
+ * primary source and generate a declaration; this one is for a library whose
12
+ * upstream has no usable structured source, so its `.d.ts` is hand-authored or
13
+ * forked from the retired ts-defold `generated/` golden and vendored under
14
+ * `fixtures/authored/<moduleId>.d.ts`.
15
+ *
16
+ * A vendored authored `.d.ts` is already target-form — a `declare module
17
+ * '<moduleId>'` ambient — so the lane is light: it emits the vendored source
18
+ * verbatim as the `generated/<namespace>.d.ts` golden and runs
19
+ * `extractApiDoc` for the `api-doc/<namespace>.json` the docs-site consumes. The
20
+ * api-doc alone carries one thing the fork does not: where a target vendors its
21
+ * upstream `.lua` and the fork documents a member nowhere, upstream's own LuaDoc
22
+ * summary is lowered in, marked `docSource: "upstream"` (see
23
+ * `import-upstream-docs.ts`). The *emitted* surface is still the vendored authored
24
+ * source untouched, so the go/no-go gate is a
25
+ * forked-vs-generated identity diff (see the golden loop) — emission fidelity.
26
+ * There is no *type* coverage comparison because upstream declares no types; the
27
+ * *surface* is another matter, and a target that vendors its upstream `.lua` under
28
+ * `upstreamLua` is measured on member names and arity by `authored-parity.ts`.
29
+ * Beyond that, a corrected fork's accuracy rests on its manual audit against
30
+ * upstream plus the per-library shape assertions in the sibling test.
31
+ */
32
+ /**
33
+ * Why a target's surface cannot be measured against upstream. A closed set: the
34
+ * alternative to `upstreamLua` has to be a *chosen* category, because free prose
35
+ * would let "nobody looked yet" and "looked, and it is genuinely unmeasurable"
36
+ * read the same in the config.
37
+ *
38
+ * - `unresolved-path` — the `moduleId`-derived path does not resolve at the
39
+ * pinned `ref` (including a repository that no longer exists there).
40
+ * - `no-module-file` — the upstream resolves but declares no single module file
41
+ * this target could be measured against.
42
+ * - `unparseable-shape` — the module file exists but closes in a way
43
+ * `parseLuaSurface` refuses, such as a metatable that could delegate members
44
+ * no column-0 scan would find.
45
+ */
46
+ export const PARITY_VERDICT_REASONS = [
47
+ "no-module-file",
48
+ "unresolved-path",
49
+ "unparseable-shape",
50
+ ] as const;
51
+
52
+ export type ParityVerdictReason = (typeof PARITY_VERDICT_REASONS)[number];
53
+
54
+ export interface ParityVerdict {
55
+ reason: ParityVerdictReason;
56
+ note: string;
57
+ }
58
+
59
+ export interface AuthoredTarget {
60
+ repo: string;
61
+ ref: string;
62
+ // Package-relative path of the vendored authored/forked `.d.ts`
63
+ // (`fixtures/authored/<moduleId>.d.ts`).
64
+ authored: string;
65
+ moduleId: string;
66
+ namespace: string;
67
+ generated: string;
68
+ apiDoc: string;
69
+ // Defaults to `fidelity/<namespace>.json` when omitted. A fork carries no
70
+ // fidelity artifact — there is no primary source to measure against, and the
71
+ // emit is lossless by construction — but the field mirrors the sibling lanes
72
+ // so a future hand-authored target can record one.
73
+ fidelity: string;
74
+ // SPDX-style license id, surfaced by the docs-site provenance block. Optional
75
+ // in the config; defaults to "".
76
+ license: string;
77
+ // Package-relative paths of the vendored upstream `.lua` sources this target's
78
+ // surface is measured against (`authored-parity.ts`). Optional and defaulted to
79
+ // `[]`: a target with no vendored upstream is simply unmeasured, which is why it
80
+ // must not join `REQUIRED_FIELDS`.
81
+ upstreamLua: string[];
82
+ // Why this target vendors no upstream `.lua`. Mutually exclusive with a
83
+ // non-empty `upstreamLua`, and one of the two is mandatory: neither field is
84
+ // required on its own, but the *disjunction* is, so a target cannot land
85
+ // unexamined and read as measured-and-clean. Absent on a measured target.
86
+ parityVerdict?: ParityVerdict;
87
+ }
88
+
89
+ export interface AuthoredTargets {
90
+ targets: AuthoredTarget[];
91
+ }
92
+
93
+ const REQUIRED_FIELDS = [
94
+ "repo",
95
+ "ref",
96
+ "authored",
97
+ "moduleId",
98
+ "namespace",
99
+ "generated",
100
+ "apiDoc",
101
+ ] as const;
102
+
103
+ /** Validate the parity declaration: exactly one of a non-empty `upstreamLua` and
104
+ * a well-formed `parityVerdict`. Enforced here rather than through
105
+ * `REQUIRED_FIELDS` because neither field is required on its own — the bijection
106
+ * between them is the gate. */
107
+ function validateParityDeclaration(
108
+ label: string,
109
+ upstreamLua: string[],
110
+ verdict: ParityVerdict | undefined,
111
+ ): void {
112
+ if (upstreamLua.length > 0 && verdict !== undefined) {
113
+ throw new Error(
114
+ `authored-targets.json: entry ${label} declares both upstreamLua and parityVerdict — a measured target carries no verdict.`,
115
+ );
116
+ }
117
+ if (upstreamLua.length === 0 && verdict === undefined) {
118
+ throw new Error(
119
+ `authored-targets.json: entry ${label} declares neither upstreamLua nor parityVerdict — vendor its upstream .lua, or record a parityVerdict saying why it cannot be measured.`,
120
+ );
121
+ }
122
+ if (verdict === undefined) return;
123
+ if (!(PARITY_VERDICT_REASONS as readonly string[]).includes(verdict.reason)) {
124
+ throw new Error(
125
+ `authored-targets.json: entry ${label} has parityVerdict reason "${verdict.reason}", which is not one of ${PARITY_VERDICT_REASONS.join(", ")}.`,
126
+ );
127
+ }
128
+ if (typeof verdict.note !== "string" || verdict.note.trim() === "") {
129
+ throw new Error(
130
+ `authored-targets.json: entry ${label} has a parityVerdict with an empty note — say what was looked at and why it could not be measured.`,
131
+ );
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Read `authored-targets.json`, validate every required field per entry, and fill
137
+ * optional defaults (`fidelity` → `fidelity/<namespace>.json`, `license` → "",
138
+ * `upstreamLua` → []).
139
+ * Throws on the first missing field naming both the field and the offending entry
140
+ * (its `moduleId`, or its index when `moduleId` itself is absent) — the loud-fail
141
+ * discipline `readMarkdownTargets`/`readScriptApiTargets`/`readLualsTargets` use.
142
+ * Also throws when an entry's parity declaration is missing, doubled, or
143
+ * malformed (see `validateParityDeclaration`).
144
+ */
145
+ export function readAuthoredTargets(packageRoot: string): AuthoredTarget[] {
146
+ const parsed = JSON.parse(readFileSync(join(packageRoot, "authored-targets.json"), "utf8")) as {
147
+ targets: Partial<AuthoredTarget>[];
148
+ };
149
+ return parsed.targets.map((entry, index) => {
150
+ const label = typeof entry.moduleId === "string" ? entry.moduleId : `index ${index}`;
151
+ for (const field of REQUIRED_FIELDS) {
152
+ if (entry[field] === undefined) {
153
+ throw new Error(
154
+ `authored-targets.json: entry ${label} is missing required field "${field}".`,
155
+ );
156
+ }
157
+ }
158
+ const upstreamLua = entry.upstreamLua ?? [];
159
+ validateParityDeclaration(label, upstreamLua, entry.parityVerdict);
160
+ return {
161
+ repo: entry.repo as string,
162
+ ref: entry.ref as string,
163
+ authored: entry.authored as string,
164
+ moduleId: entry.moduleId as string,
165
+ namespace: entry.namespace as string,
166
+ generated: entry.generated as string,
167
+ apiDoc: entry.apiDoc as string,
168
+ fidelity: entry.fidelity ?? `fidelity/${entry.namespace as string}.json`,
169
+ license: entry.license ?? "",
170
+ upstreamLua,
171
+ ...(entry.parityVerdict === undefined ? {} : { parityVerdict: entry.parityVerdict }),
172
+ };
173
+ });
174
+ }
175
+
176
+ function readAuthoredSource(packageRoot: string, target: AuthoredTarget): string {
177
+ return readFileSync(join(packageRoot, target.authored), "utf8");
178
+ }
179
+
180
+ /**
181
+ * The `generated/<namespace>.d.ts` golden, under whatever stem the entry pins —
182
+ * a bare segment or the dotted `moduleId`. The vendored authored
183
+ * `.d.ts` is already a `declare module '<moduleId>'` ambient, so the emit is the
184
+ * source verbatim — the vendored surface passes through unchanged, which is what
185
+ * makes the emission lossless. It says nothing about how that vendored surface
186
+ * compares to upstream.
187
+ */
188
+ export function emitAuthoredDeclaration(packageRoot: string, target: AuthoredTarget): string {
189
+ return readAuthoredSource(packageRoot, target);
190
+ }
191
+
192
+ /** The pinned upstream members by name, the way `authored-parity.ts` builds them:
193
+ * a later definition of a name wins, matching Lua. Empty for a target that vendors
194
+ * no upstream `.lua`, which is then never parsed. */
195
+ function upstreamMembers(packageRoot: string, target: AuthoredTarget): Map<string, LuaMember> {
196
+ const members = new Map<string, LuaMember>();
197
+ for (const relative of target.upstreamLua) {
198
+ for (const member of parseLuaSurface(readFileSync(join(packageRoot, relative), "utf8")).members)
199
+ members.set(member.name, member);
200
+ }
201
+ return members;
202
+ }
203
+
204
+ /**
205
+ * `extractApiDoc` the vendored `.d.ts` under the pinned publish namespace, plus
206
+ * upstream's own LuaDoc for the members the fork documents nowhere, pretty-printed
207
+ * as the `api-doc/<namespace>.json` golden.
208
+ *
209
+ * The api-doc is therefore the fork *plus* imported upstream prose, and it is the
210
+ * only artifact that is: the emitted `generated/<namespace>.d.ts` stays the vendored
211
+ * source verbatim, so the identity diff still proves the emit is lossless. An
212
+ * imported element carries `docSource: "upstream"` through to the docs-site, which
213
+ * marks it rather than presenting it as first-party. See `import-upstream-docs.ts`
214
+ * for the merge rules.
215
+ */
216
+ export function lowerAuthoredApiDoc(packageRoot: string, target: AuthoredTarget): string {
217
+ // `extractApiDoc` returns `unknown`; the merge needs the element list, and
218
+ // overwriting the existing `elements` key by spread keeps it in place, so the
219
+ // golden's key order is unchanged.
220
+ const doc = extractApiDoc(readAuthoredSource(packageRoot, target), target.namespace) as Record<
221
+ string,
222
+ unknown
223
+ > & { elements: ApiDocElement[] };
224
+ const elements = importUpstreamDocs(doc.elements, upstreamMembers(packageRoot, target));
225
+ return `${JSON.stringify({ ...doc, elements }, null, 2)}\n`;
226
+ }
227
+
228
+ if (import.meta.main) {
229
+ const root = join(import.meta.dir, "..");
230
+ const argv = process.argv.slice(2);
231
+ if (argv.includes("--emit")) {
232
+ for (const target of readAuthoredTargets(root)) {
233
+ const contents = emitAuthoredDeclaration(root, target);
234
+ const dest = join(root, target.generated);
235
+ mkdirSync(dirname(dest), { recursive: true });
236
+ writeFileSync(dest, contents);
237
+ console.log(`emitted ${target.moduleId} -> ${target.generated}`);
238
+ }
239
+ }
240
+ if (argv.includes("--api-doc")) {
241
+ for (const target of readAuthoredTargets(root)) {
242
+ const json = lowerAuthoredApiDoc(root, target);
243
+ const dest = join(root, target.apiDoc);
244
+ mkdirSync(dirname(dest), { recursive: true });
245
+ writeFileSync(dest, json);
246
+ console.log(`lowered ${target.moduleId} -> ${target.apiDoc}`);
247
+ }
248
+ }
249
+ }
@@ -2,6 +2,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import ts from "typescript";
4
4
  import { extractApiDoc } from "./extract-api-doc";
5
+ import { readAuthoredTargets } from "./sync-authored-types";
6
+ import { readLualsTargets } from "./sync-luals-types";
7
+ import { readMarkdownTargets } from "./sync-markdown-types";
8
+ import { readOpenApiTargets } from "./sync-openapi-types";
9
+ import { readScriptApiTargets } from "./sync-script-api-types";
5
10
 
6
11
  /**
7
12
  * ts-defold/library core-type references -> the @defold-typescript/types surface.
@@ -330,6 +335,65 @@ export function classifyLibraryDirs(
330
335
  .sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
331
336
  }
332
337
 
338
+ /**
339
+ * The union every maintained-here lane contributes: authored `moduleId`s, and
340
+ * the `namespace`s of the LuaLS, script_api, OpenAPI and markdown front-ends.
341
+ * Namespaces are matched in both forms because the lanes use both — druid-style
342
+ * libraries register a bare `tweener`, while `defold-saver` registers the dotted
343
+ * `saver.saver` / `saver.storage`.
344
+ */
345
+ export interface MaintainedHereRegistry {
346
+ moduleIds: ReadonlySet<string>;
347
+ namespaces: ReadonlySet<string>;
348
+ }
349
+
350
+ /**
351
+ * The subset of `modules` this repo maintains itself rather than sourcing from
352
+ * ts-defold/library. A live `library-targets.json` row always wins, so a module
353
+ * registered on a maintained-here lane *and* still vendored stays
354
+ * ts-defold-sourced and keeps its dir classified `already-vendored`. The
355
+ * registry is empty today, so no committed module takes that branch; it is what
356
+ * a returning row would take.
357
+ */
358
+ export function maintainedHereModules(
359
+ modules: readonly string[],
360
+ registry: MaintainedHereRegistry,
361
+ liveModules: ReadonlySet<string>,
362
+ ): Set<string> {
363
+ return new Set(
364
+ modules.filter(
365
+ (m) =>
366
+ !liveModules.has(m) &&
367
+ (registry.moduleIds.has(m) ||
368
+ registry.namespaces.has(m) ||
369
+ registry.namespaces.has(m.split(".")[0] as string)),
370
+ ),
371
+ );
372
+ }
373
+
374
+ /**
375
+ * The upstream dirs this repo has severed onto a maintained-here lane. Severance
376
+ * deletes a library's `library-targets.json` row, but its dir still exists
377
+ * upstream — so a regen would re-add it under whatever its module shape implies
378
+ * unless it is filtered out first. The registries give an exact derivation with
379
+ * no hand-maintained list: a dir is severed iff it has at least one module and
380
+ * every module is maintained here. A dir mixing maintained-here and live modules
381
+ * is still upstream-sourced and stays classified — `nakama-defold` was the last
382
+ * such dir, and severed whole once its live row went.
383
+ */
384
+ export function severedDirsFromModules(
385
+ modulesByDir: Map<string, string[]>,
386
+ registry: MaintainedHereRegistry,
387
+ liveModules: ReadonlySet<string>,
388
+ ): Set<string> {
389
+ const severed = new Set<string>();
390
+ for (const [dir, modules] of modulesByDir) {
391
+ const maintained = maintainedHereModules(modules, registry, liveModules);
392
+ if (modules.length > 0 && maintained.size === modules.length) severed.add(dir);
393
+ }
394
+ return severed;
395
+ }
396
+
333
397
  interface GithubTreeResponse {
334
398
  tree?: { path: string }[];
335
399
  }
@@ -376,8 +440,28 @@ export type ListTree = (source: LibrarySource) => Promise<string[]>;
376
440
  const defaultListTree: ListTree = (source) => githubTreePaths(repoSlug(source.repo), source.commit);
377
441
 
378
442
  /**
379
- * Enumerate every ts-defold/library dir at the pin, classify each by its
380
- * module-name shape, and write `library-classification.json`. The `listTree`
443
+ * Read every maintained-here registry off the package root and union them.
444
+ * Each reader `readFileSync`s unconditionally and throws on a missing required
445
+ * field — deliberately no absent-file fallback, so a lane added without its
446
+ * fixture fails loudly rather than silently un-severing that lane's dirs.
447
+ */
448
+ export function readMaintainedHereRegistry(packageRoot: string): MaintainedHereRegistry {
449
+ return {
450
+ moduleIds: new Set(readAuthoredTargets(packageRoot).map((t) => t.moduleId)),
451
+ namespaces: new Set([
452
+ ...readLualsTargets(packageRoot).map((t) => t.namespace),
453
+ ...readScriptApiTargets(packageRoot).map((t) => t.namespace),
454
+ ...readOpenApiTargets(packageRoot).map((t) => t.namespace),
455
+ ...readMarkdownTargets(packageRoot).map((t) => t.namespace),
456
+ ]),
457
+ };
458
+ }
459
+
460
+ /**
461
+ * Enumerate every ts-defold/library dir at the pin, drop the dirs severed onto
462
+ * a maintained-here lane, classify each survivor by its module-name shape, and write
463
+ * `library-classification.json`. Filtering happens here, not in
464
+ * `classifyLibraryDirs`, so that pass stays a pure map-and-sort. The `listTree`
381
465
  * seam keeps the pass offline-testable; only the CLI wires the real call, and it
382
466
  * stays out of CI (mirrors `--check`). The manifest pins the same `source` as
383
467
  * `library-targets.json`.
@@ -392,9 +476,21 @@ export async function writeClassification(
392
476
  targets.map((t) => t.path.split("/")[1]).filter((d): d is string => d !== undefined),
393
477
  );
394
478
  const coveredByGoalDirs = new Set(["defold-lldebugger", "defold-xmath"]);
479
+ const liveModules = new Set(targets.map((t) => t.module));
480
+
481
+ const registry = readMaintainedHereRegistry(packageRoot);
395
482
 
396
483
  const modulesByDir = libraryModulesFromTree(await listTree(source));
397
- const dirs = [...modulesByDir].map(([dir, modules]) => ({ dir, modules }));
484
+ const severedDirs = severedDirsFromModules(modulesByDir, registry, liveModules);
485
+ const dirs = [...modulesByDir]
486
+ // Vendored wins: a stale maintained-here entry can never erase a live ts-defold row.
487
+ .filter(([dir]) => !severedDirs.has(dir) || vendoredDirs.has(dir))
488
+ // A survivor records only its ts-defold-sourced remainder as evidence, so its
489
+ // written shape agrees with its own `modules` array.
490
+ .map(([dir, modules]) => {
491
+ const maintained = maintainedHereModules(modules, registry, liveModules);
492
+ return { dir, modules: modules.filter((m) => !maintained.has(m)) };
493
+ });
398
494
  const entries = classifyLibraryDirs(dirs, { vendoredDirs, coveredByGoalDirs });
399
495
  writeFileSync(
400
496
  join(packageRoot, "library-classification.json"),
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { emitLibraryDeclarations } from "./emit-library-dts";
3
+ import { type AnnotationOverrides, applyAnnotationOverrides } from "./apply-luals-overrides";
4
+ import { type ExternalTypeRef, emitLibraryDeclarations } from "./emit-library-dts";
4
5
  import { lowerLibraryModel } from "./lower-api-doc";
5
6
  import { buildFidelityReport, type FidelityReport } from "./luals-fidelity";
6
7
  import { type LibraryModel, mergeLibraryModels, parseLualsSource } from "./parse-luals";
@@ -17,11 +18,24 @@ export interface LualsTarget {
17
18
  sourceGlobs: string[];
18
19
  moduleId: string;
19
20
  namespace: string;
21
+ // Token -> TypeScript type *text*, not an identifier: the mapper emits the value
22
+ // verbatim. The value may name an ambient global that needs no import — a core
23
+ // rename (`Vector3`, `Opaque<"node">`) or a type the Lua stdlib already declares
24
+ // (`debug.FunctionInfo`) — which is what distinguishes it from `externalTypes`.
20
25
  typeRenames: Record<string, string>;
21
26
  ignore: string[];
22
27
  // SPDX-style license id, surfaced by the docs-site provenance block. Optional
23
28
  // in the config; the docs-site defaults an absent value to "".
24
29
  license?: string;
30
+ // Post-merge corrections applied in `buildTargetModel` for the cases where an
31
+ // upstream annotation the fixtures freeze diverges from the runtime. Optional and
32
+ // loud-failing on an absent key — see `apply-luals-overrides.ts`.
33
+ annotationOverrides?: AnnotationOverrides;
34
+ // Type tokens this library references but another target owns, keyed by the token:
35
+ // the emitter writes a real cross-module `import` for each. A project consuming this
36
+ // library must declare the named upstream dependency too — `resolve` materializes no
37
+ // inter-library graph. See `emit-library-dts.ts`.
38
+ externalTypes?: Record<string, ExternalTypeRef>;
25
39
  }
26
40
 
27
41
  export interface LualsTargets {
@@ -57,6 +71,10 @@ export function readLualsTargets(packageRoot: string): LualsTarget[] {
57
71
  typeRenames: entry.typeRenames ?? {},
58
72
  ignore: entry.ignore ?? [],
59
73
  ...(entry.license !== undefined ? { license: entry.license } : {}),
74
+ ...(entry.annotationOverrides !== undefined
75
+ ? { annotationOverrides: entry.annotationOverrides }
76
+ : {}),
77
+ ...(entry.externalTypes !== undefined ? { externalTypes: entry.externalTypes } : {}),
60
78
  };
61
79
  });
62
80
  }
@@ -188,7 +206,13 @@ export function buildTargetModel(
188
206
  const parsed = new Map<string, LibraryModel>();
189
207
  for (const rel of files) parsed.set(rel, parseLualsSource(readFileSync(join(root, rel), "utf8")));
190
208
  const merged = mergeLibraryModels([...parsed.values()]);
191
- return { ...merged, moduleFunctions: parsed.get(ownFile)?.moduleFunctions ?? [] };
209
+ const model: LibraryModel = {
210
+ ...merged,
211
+ moduleFunctions: parsed.get(ownFile)?.moduleFunctions ?? [],
212
+ };
213
+ return target.annotationOverrides
214
+ ? applyAnnotationOverrides(model, target.annotationOverrides)
215
+ : model;
192
216
  }
193
217
 
194
218
  export function buildTargetFidelity(packageRoot: string, target: LualsTarget): FidelityReport {
@@ -196,6 +220,7 @@ export function buildTargetFidelity(packageRoot: string, target: LualsTarget): F
196
220
  target.namespace,
197
221
  buildTargetModel(packageRoot, target),
198
222
  target.typeRenames,
223
+ target.externalTypes,
199
224
  );
200
225
  }
201
226
 
@@ -279,6 +304,7 @@ if (import.meta.main) {
279
304
  const declarations = emitLibraryDeclarations(model, {
280
305
  moduleId: target.moduleId,
281
306
  typeRenames: target.typeRenames,
307
+ externalTypes: target.externalTypes,
282
308
  });
283
309
  const dest = join(root, "generated", `${target.namespace}.d.ts`);
284
310
  mkdirSync(dirname(dest), { recursive: true });
@@ -293,6 +319,7 @@ if (import.meta.main) {
293
319
  const lowered = lowerLibraryModel(model, {
294
320
  namespace: target.namespace,
295
321
  typeRenames: target.typeRenames,
322
+ externalTypes: target.externalTypes,
296
323
  });
297
324
  const dest = join(root, "api-doc", `${target.namespace}.json`);
298
325
  mkdirSync(dirname(dest), { recursive: true });