@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
@@ -86,46 +86,82 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
86
86
  const elements: Record<string, unknown>[] = [];
87
87
  const emittedNames = new Set<string>();
88
88
  const referencedTypeNodes: ts.TypeNode[] = [];
89
+ // Type nodes reached from file-scope declarations, kept apart from the module
90
+ // block's so an ambient global can never pull a module-block shape onto `/api`
91
+ // that the module surface alone did not reach.
92
+ const globalTypeNodes: ts.TypeNode[] = [];
93
+ // Each alias's declaration paired with the bare element it already pushed, so
94
+ // a reachable object-literal alias is filled in by merging into that element
95
+ // rather than by appending a second one — element order stays as emitted.
96
+ const aliasElements: { node: ts.TypeAliasDeclaration; element: Record<string, unknown> }[] = [];
89
97
 
90
98
  // Members nested in an `export namespace` (`bridge.bridge`) keep their
91
99
  // namespace path so same-named members across namespaces (e.g. `is_supported`)
92
100
  // stay distinct instead of colliding in `emittedNames`.
93
- const collect = (nodes: readonly ts.Statement[], prefix: string): void => {
101
+ //
102
+ // `global` switches the walk to the ambient lane: the file's own statements
103
+ // rather than the module block's. There the gate is the `declare` modifier
104
+ // (there is no `export =` to suppress bare declarations, so `emitBare` does not
105
+ // apply), each element carries the `global` marker, and a type alias is not
106
+ // pushed eagerly — file scope carries far more incidental type machinery than a
107
+ // module block, so a shape publishes only once reachability proves a published
108
+ // signature names it.
109
+ const collect = (nodes: readonly ts.Statement[], prefix: string, global: boolean): void => {
94
110
  const qualify = (name: string): string => (prefix ? `${prefix}.${name}` : name);
111
+ const mark = global ? { global: true } : {};
112
+ const typeNodes = global ? globalTypeNodes : referencedTypeNodes;
113
+ // Inside an ambient namespace every member is ambient by containment, so the
114
+ // nested lane emits regardless of an `export` keyword.
115
+ const gate = (stmt: ts.HasModifiers): boolean =>
116
+ global ? prefix !== "" || isAmbient(stmt) || isExported(stmt) : isExported(stmt) || emitBare;
95
117
  for (const stmt of nodes) {
96
- if (ts.isFunctionDeclaration(stmt) && stmt.name && (isExported(stmt) || emitBare)) {
97
- collectFunctionReferenceTypes(stmt, referencedTypeNodes);
118
+ if (ts.isFunctionDeclaration(stmt) && stmt.name && gate(stmt)) {
119
+ collectFunctionReferenceTypes(stmt, typeNodes);
98
120
  const name = qualify(stmt.name.text);
99
- elements.push(functionElement(stmt, name, sf));
121
+ elements.push({ ...functionElement(stmt, name, sf), ...mark });
100
122
  emittedNames.add(name);
101
- } else if (ts.isVariableStatement(stmt) && (isExported(stmt) || emitBare)) {
123
+ } else if (ts.isVariableStatement(stmt) && gate(stmt)) {
124
+ // The `VariableStatement` carries the JSDoc, not the declaration, so both the
125
+ // summary and the deprecation tag are read once from the statement and shared
126
+ // by every declarator under it.
127
+ const summary = jsDocSummary(stmt);
102
128
  for (const decl of stmt.declarationList.declarations) {
103
- if (decl.type) referencedTypeNodes.push(decl.type);
129
+ if (decl.type) typeNodes.push(decl.type);
104
130
  const fields = objectFields(decl.type, sf);
105
131
  const name = qualify(decl.name.getText(sf));
106
132
  elements.push({
107
133
  type: "VARIABLE",
108
134
  name,
135
+ // Unconditional, matching `functionElement`: an absent key and an empty one
136
+ // would be two shapes the docs-site had to tell apart for no gain.
137
+ brief: briefOf(summary),
138
+ description: summary,
109
139
  types: decl.type ? [typeText(decl.type, sf)] : [],
110
140
  ...(fields ? { fields } : {}),
141
+ ...deprecatedKey(stmt),
142
+ ...mark,
111
143
  });
112
144
  emittedNames.add(name);
113
145
  }
114
146
  } else if (ts.isTypeAliasDeclaration(stmt)) {
147
+ if (global) continue;
115
148
  const name = qualify(stmt.name.text);
116
- elements.push({ type: "TYPEDEF", name });
149
+ const element: Record<string, unknown> = { type: "TYPEDEF", name };
150
+ elements.push(element);
117
151
  emittedNames.add(name);
152
+ aliasElements.push({ node: stmt, element });
118
153
  } else if (
119
154
  ts.isModuleDeclaration(stmt) &&
155
+ !ts.isStringLiteral(stmt.name) &&
120
156
  stmt.body &&
121
157
  ts.isModuleBlock(stmt.body) &&
122
- (isExported(stmt) || emitBare)
158
+ gate(stmt)
123
159
  ) {
124
- collect(stmt.body.statements, qualify(stmt.name.getText(sf)));
160
+ collect(stmt.body.statements, qualify(stmt.name.getText(sf)), global);
125
161
  }
126
162
  }
127
163
  };
128
- collect(statements, "");
164
+ collect(statements, "", false);
129
165
 
130
166
  const moduleValueInterfaces = exportedValueInterfaces(moduleBlock);
131
167
  for (const iface of moduleValueInterfaces) {
@@ -145,6 +181,7 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
145
181
  name,
146
182
  types: member.type ? [typeText(member.type, sf)] : [],
147
183
  ...(fields ? { fields } : {}),
184
+ ...deprecatedKey(member),
148
185
  });
149
186
  emittedNames.add(name);
150
187
  }
@@ -152,16 +189,59 @@ export function extractApiDoc(source: string, moduleName: string): unknown {
152
189
  }
153
190
 
154
191
  const moduleValueInterfaceNames = new Set(moduleValueInterfaces.map((iface) => iface.name.text));
155
- for (const iface of referencedInterfaces(
156
- moduleBlock,
157
- referencedTypeNodes,
158
- moduleValueInterfaceNames,
159
- )) {
160
- if (emittedNames.has(iface.name.text)) continue;
192
+ const declared = moduleBlock ? moduleTypeDeclarations(moduleBlock) : undefined;
193
+ const reachable = referencedTypeNames(declared, referencedTypeNodes);
194
+
195
+ for (const { node, element } of aliasElements) {
196
+ const name = node.name.text;
197
+ // Reachability is resolved against the module block, so identity keeps a
198
+ // same-named alias nested in a namespace from borrowing the outer one's.
199
+ if (declared?.aliases.get(name) !== node) continue;
200
+ if (!reachable.has(name) || !ts.isTypeLiteralNode(node.type)) continue;
201
+ const { functions, properties } = shapeMembers(node.type.members, sf);
202
+ if (functions.length > 0) element.functions = functions;
203
+ if (properties.length > 0) element.properties = properties;
204
+ }
205
+
206
+ for (const name of reachable) {
207
+ if (moduleValueInterfaceNames.has(name) || emittedNames.has(name)) continue;
208
+ const iface = declared?.interfaces.get(name);
209
+ if (!iface) continue;
161
210
  const typedef = typedefElement(iface, sf);
162
211
  if (!typedef) continue;
163
212
  elements.push(typedef);
164
- emittedNames.add(iface.name.text);
213
+ emittedNames.add(name);
214
+ }
215
+
216
+ // The ambient lane runs last so every module-block element keeps the index it
217
+ // has today. Its shapes resolve against file-scope declarations only, by node
218
+ // identity, so a file-scope shape never borrows a same-named module-block one.
219
+ collect(sf.statements, "", true);
220
+ const globalDeclared = typeDeclarationsIn(sf.statements);
221
+ for (const name of referencedTypeNames(globalDeclared, globalTypeNodes)) {
222
+ if (emittedNames.has(name)) continue;
223
+ const alias = globalDeclared.aliases.get(name);
224
+ if (alias) {
225
+ if (!ts.isTypeLiteralNode(alias.type)) continue;
226
+ const { functions, properties } = shapeMembers(alias.type.members, sf);
227
+ if (functions.length === 0 && properties.length === 0) continue;
228
+ elements.push({
229
+ type: "TYPEDEF",
230
+ name,
231
+ ...(functions.length > 0 ? { functions } : {}),
232
+ ...(properties.length > 0 ? { properties } : {}),
233
+ ...deprecatedKey(alias),
234
+ global: true,
235
+ });
236
+ emittedNames.add(name);
237
+ continue;
238
+ }
239
+ const iface = globalDeclared.interfaces.get(name);
240
+ if (!iface) continue;
241
+ const typedef = typedefElement(iface, sf);
242
+ if (!typedef) continue;
243
+ elements.push({ ...typedef, global: true });
244
+ emittedNames.add(name);
165
245
  }
166
246
 
167
247
  return {
@@ -212,6 +292,7 @@ function functionElement(
212
292
  parameters,
213
293
  returnvalues,
214
294
  ...(example === "" ? {} : { examples: example }),
295
+ ...deprecatedKey(decl),
215
296
  };
216
297
  }
217
298
 
@@ -225,13 +306,22 @@ function collectFunctionReferenceTypes(
225
306
  if (decl.type) out.push(decl.type);
226
307
  }
227
308
 
228
- function typedefElement(
229
- iface: ts.InterfaceDeclaration,
230
- sf: ts.SourceFile,
231
- ): Record<string, unknown> | undefined {
309
+ interface ShapeMembers {
310
+ functions: Record<string, unknown>[];
311
+ properties: Record<string, unknown>[];
312
+ }
313
+
314
+ /**
315
+ * The one member reader shared by both shape carriers — an `interface`
316
+ * declaration and a `type X = { … }` alias — so the two lanes cannot drift.
317
+ * `is_optional` is written only on an optional member, matching how
318
+ * `deprecatedKey` encodes absence and keeping a regen diff proportional to what
319
+ * actually changed.
320
+ */
321
+ function shapeMembers(members: readonly ts.TypeElement[], sf: ts.SourceFile): ShapeMembers {
232
322
  const functions: Record<string, unknown>[] = [];
233
323
  const properties: Record<string, unknown>[] = [];
234
- for (const member of iface.members) {
324
+ for (const member of members) {
235
325
  if (!member.name) continue;
236
326
  const name = memberName(member.name, sf);
237
327
  if (ts.isMethodSignature(member)) {
@@ -244,50 +334,80 @@ function typedefElement(
244
334
  brief: briefOf(summary),
245
335
  description: summary,
246
336
  types: member.type ? [typeText(member.type, sf)] : [],
337
+ ...(member.questionToken ? { is_optional: "True" } : {}),
247
338
  ...(fields ? { fields } : {}),
339
+ ...deprecatedKey(member),
248
340
  });
249
341
  }
250
342
  }
343
+ return { functions, properties };
344
+ }
345
+
346
+ function typedefElement(
347
+ iface: ts.InterfaceDeclaration,
348
+ sf: ts.SourceFile,
349
+ ): Record<string, unknown> | undefined {
350
+ const { functions, properties } = shapeMembers(iface.members, sf);
251
351
  if (functions.length === 0 && properties.length === 0) return undefined;
252
352
  return {
253
353
  type: "TYPEDEF",
254
354
  name: iface.name.text,
255
355
  ...(functions.length > 0 ? { functions } : {}),
256
356
  ...(properties.length > 0 ? { properties } : {}),
357
+ ...deprecatedKey(iface),
257
358
  };
258
359
  }
259
360
 
260
- function referencedInterfaces(
261
- moduleBlock: ts.ModuleBlock | undefined,
262
- typeNodes: ts.TypeNode[],
263
- excludedNames: ReadonlySet<string>,
264
- ): ts.InterfaceDeclaration[] {
265
- if (!moduleBlock) return [];
361
+ interface TypeDeclarations {
362
+ aliases: Map<string, ts.TypeAliasDeclaration>;
363
+ interfaces: Map<string, ts.InterfaceDeclaration>;
364
+ }
365
+
366
+ function typeDeclarationsIn(statements: readonly ts.Statement[]): TypeDeclarations {
266
367
  const aliases = new Map<string, ts.TypeAliasDeclaration>();
267
368
  const interfaces = new Map<string, ts.InterfaceDeclaration>();
268
- for (const stmt of moduleBlock.statements) {
369
+ for (const stmt of statements) {
269
370
  if (ts.isTypeAliasDeclaration(stmt)) aliases.set(stmt.name.text, stmt);
270
371
  if (ts.isInterfaceDeclaration(stmt)) interfaces.set(stmt.name.text, stmt);
271
372
  }
373
+ return { aliases, interfaces };
374
+ }
375
+
376
+ function moduleTypeDeclarations(moduleBlock: ts.ModuleBlock): TypeDeclarations {
377
+ return typeDeclarationsIn(moduleBlock.statements);
378
+ }
379
+
380
+ /**
381
+ * Every alias and interface name declared in `declared` that is reachable from
382
+ * an emitted member's type, in first-reached order. Both shape lanes gate on
383
+ * this single traversal — a reachable interface becomes its own TYPEDEF element,
384
+ * a reachable alias over an object literal has that element's members filled in
385
+ * — so a shape no published member names stays off `/api` either way. The
386
+ * module-block and file-scope lanes each pass their own declarations, so a name
387
+ * declared in both resolves to the lane that named it.
388
+ */
389
+ function referencedTypeNames(
390
+ declared: TypeDeclarations | undefined,
391
+ typeNodes: ts.TypeNode[],
392
+ ): Set<string> {
393
+ const names = new Set<string>();
394
+ if (!declared) return names;
395
+ const { aliases, interfaces } = declared;
272
396
 
273
- const found = new Map<string, ts.InterfaceDeclaration>();
274
- const seenAliases = new Set<string>();
275
397
  const visit = (node: ts.Node): void => {
276
398
  if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
277
399
  const name = node.typeName.text;
278
- const iface = interfaces.get(name);
279
- if (iface && !excludedNames.has(name)) found.set(name, iface);
280
400
  const alias = aliases.get(name);
281
- if (alias && !seenAliases.has(name)) {
282
- seenAliases.add(name);
283
- visit(alias.type);
401
+ if ((alias || interfaces.has(name)) && !names.has(name)) {
402
+ names.add(name);
403
+ if (alias) visit(alias.type);
284
404
  }
285
405
  }
286
406
  ts.forEachChild(node, visit);
287
407
  };
288
408
 
289
409
  for (const node of typeNodes) visit(node);
290
- return [...found.values()];
410
+ return names;
291
411
  }
292
412
 
293
413
  function exportedValueInterfaces(
@@ -350,9 +470,17 @@ function memberName(name: ts.PropertyName, sf: ts.SourceFile): string {
350
470
  return name.getText(sf);
351
471
  }
352
472
 
473
+ // string-named only: a file-scope `declare namespace` is a `ModuleDeclaration`
474
+ // too, so an identifier-named block appearing before the real `declare
475
+ // module '<name>'` would otherwise be mistaken for the module surface.
353
476
  function findModuleBlock(sf: ts.SourceFile): ts.ModuleBlock | undefined {
354
477
  for (const stmt of sf.statements) {
355
- if (ts.isModuleDeclaration(stmt) && stmt.body && ts.isModuleBlock(stmt.body)) {
478
+ if (
479
+ ts.isModuleDeclaration(stmt) &&
480
+ ts.isStringLiteral(stmt.name) &&
481
+ stmt.body &&
482
+ ts.isModuleBlock(stmt.body)
483
+ ) {
356
484
  return stmt.body;
357
485
  }
358
486
  }
@@ -363,6 +491,10 @@ function isExported(node: ts.HasModifiers): boolean {
363
491
  return (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0;
364
492
  }
365
493
 
494
+ function isAmbient(node: ts.HasModifiers): boolean {
495
+ return (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0;
496
+ }
497
+
366
498
  /** The closest non-empty JSDoc summary text attached to a node, tags stripped. */
367
499
  function jsDocSummary(node: ts.Node): string {
368
500
  const comments = ts
@@ -412,6 +544,25 @@ function exampleText(decl: ts.FunctionDeclaration | ts.MethodSignature): string
412
544
  return "";
413
545
  }
414
546
 
547
+ // `undefined` when the node carries no `@deprecated`, the trimmed tag text when
548
+ // it does — `""` for a bare tag. The caller spreads the key only on a defined
549
+ // result, so absence of the key is the sole encoding of "not deprecated" and a
550
+ // bare tag stays distinguishable from an untagged symbol.
551
+ function deprecatedText(node: ts.Node): string | undefined {
552
+ for (const tag of ts.getJSDocTags(node)) {
553
+ if (tag.tagName.text === "deprecated") {
554
+ return (ts.getTextOfJSDocComment(tag.comment) ?? "").trim();
555
+ }
556
+ }
557
+ return undefined;
558
+ }
559
+
560
+ /** The `{ deprecated }` key to spread onto an element, empty when untagged. */
561
+ function deprecatedKey(node: ts.Node): Record<string, string> {
562
+ const text = deprecatedText(node);
563
+ return text === undefined ? {} : { deprecated: text };
564
+ }
565
+
415
566
  /** Trim a JSDoc `@param`/`@returns` comment and drop a leading `-` delimiter. */
416
567
  function cleanDoc(comment: string | undefined): string {
417
568
  return (comment ?? "").trim().replace(/^-\s*/, "");
@@ -0,0 +1,178 @@
1
+ /**
2
+ * A one-sided coverage ratchet over the committed `fidelity/*.json` reports.
3
+ *
4
+ * Each emitting lane already round-trips its committed report against a freshly
5
+ * built one, which proves the report matches what the pipeline builds *today* —
6
+ * but a regeneration that lowers coverage rewrites the report too, so the drop
7
+ * lands silently. `fidelity-floor.json` pins each report's coverage from the
8
+ * outside: `regen` never touches it, so the only way a floor moves is the
9
+ * explicit, monotone `--raise` below — which also makes it the one hand-editable
10
+ * input here, so it is parsed under a validating contract (`parseFloors`).
11
+ *
12
+ * Reports are keyed by package-root-relative POSIX path, not by namespace:
13
+ * `fidelity/openapi/nakama.nakama.json` reports `namespace: "nakama"`, and two
14
+ * lanes emitting the same namespace would collide on a namespace key.
15
+ *
16
+ * The walk is universal over `fidelity/` with one carve-out: `fidelity/authored/`
17
+ * holds *surface* parity, not *type-token* coverage. Those reports have no
18
+ * `totalTypeTokens` to be a fraction of, and their coverage means something else —
19
+ * upstream members declared at the right arity, from `authored-parity.ts`. They get
20
+ * their own ratchet in `authored-parity-floor.json`, so folding them in here would
21
+ * only mean two incompatible denominators under one floor manifest. That manifest
22
+ * also ratchets *two* axes per key (`callableCoverage` and `fieldCoverage`, never
23
+ * averaged), which a flat one-ratio-per-key manifest cannot express — a second,
24
+ * independent reason the two do not merge. Its entries are parsed by
25
+ * `parseAuthoredFloors`, not by `parseFloors` below.
26
+ */
27
+
28
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
29
+ import { join, resolve } from "node:path";
30
+
31
+ export const FIDELITY_DIR = "fidelity";
32
+ /** Directory name under `fidelity/` the token-coverage walk skips — see the
33
+ * module note; ratcheted by `authored-parity-floor.json` instead. */
34
+ export const AUTHORED_PARITY_DIRNAME = "authored";
35
+ export const FLOOR_MANIFEST_FILE = "fidelity-floor.json";
36
+ export const FLOOR_RAISE_COMMAND = "bun run --cwd packages/library-types fidelity:floor";
37
+
38
+ export interface FidelityFloorReport {
39
+ coverage: number;
40
+ totalMembers: number;
41
+ totalTypeTokens: number;
42
+ }
43
+
44
+ function numberField(raw: Record<string, unknown>, field: string, path: string): number {
45
+ const value = raw[field];
46
+ if (typeof value !== "number" || !Number.isFinite(value)) {
47
+ throw new Error(`${path}: expected a finite numeric "${field}", got ${JSON.stringify(value)}`);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function walkJsonFiles(dir: string, prefix: string, out: string[]): void {
53
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
54
+ const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
55
+ if (entry.isDirectory()) {
56
+ if (prefix === "" && entry.name === AUTHORED_PARITY_DIRNAME) continue;
57
+ walkJsonFiles(join(dir, entry.name), rel, out);
58
+ } else if (entry.name.endsWith(".json")) out.push(rel);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Every committed fidelity report under `fidelity/`, at any depth, keyed by its
64
+ * package-root-relative POSIX path and sorted by that key. Reads only — the walk
65
+ * is the enumeration the gate trusts, so a lane that emits into a nested
66
+ * directory (`fidelity/openapi/`) is picked up without registration.
67
+ */
68
+ export function collectFidelityReports(root: string): Record<string, FidelityFloorReport> {
69
+ const dir = join(root, FIDELITY_DIR);
70
+ if (!existsSync(dir)) return {};
71
+ const relatives: string[] = [];
72
+ walkJsonFiles(dir, "", relatives);
73
+ const reports: Record<string, FidelityFloorReport> = {};
74
+ for (const rel of relatives.sort()) {
75
+ const key = `${FIDELITY_DIR}/${rel}`;
76
+ const raw = JSON.parse(readFileSync(join(dir, rel), "utf8")) as Record<string, unknown>;
77
+ reports[key] = {
78
+ coverage: numberField(raw, "coverage", key),
79
+ totalMembers: numberField(raw, "totalMembers", key),
80
+ totalTypeTokens: numberField(raw, "totalTypeTokens", key),
81
+ };
82
+ }
83
+ return reports;
84
+ }
85
+
86
+ // `JSON.stringify` renders NaN and Infinity as `null` and returns `undefined` for
87
+ // `undefined`, both of which would misreport the value the manifest actually holds.
88
+ function describeFloor(value: unknown): string {
89
+ if (typeof value === "number") return String(value);
90
+ return JSON.stringify(value) ?? String(value);
91
+ }
92
+
93
+ /**
94
+ * The floor manifest's parse-time contract: a plain object of finite numbers in
95
+ * `[0, 1]`. Every consuming assertion compares coercively, so an unvalidated
96
+ * `null` or `false` floor would pass `floor !== undefined` and fail every
97
+ * `coverage < floor` comparison — switching that report's ratchet off silently.
98
+ * Key order is the writer's contract and is preserved here, not sorted.
99
+ */
100
+ export function parseFloors(raw: unknown, path: string): Record<string, number> {
101
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
102
+ throw new Error(`${path}: expected a JSON object of floors, got ${describeFloor(raw)}`);
103
+ }
104
+ const floors: Record<string, number> = {};
105
+ for (const [key, value] of Object.entries(raw)) {
106
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
107
+ throw new Error(
108
+ `${path}: floor "${key}" must be a finite number in [0, 1], got ${describeFloor(value)}`,
109
+ );
110
+ }
111
+ floors[key] = value;
112
+ }
113
+ return floors;
114
+ }
115
+
116
+ /** The committed floor manifest, or an empty manifest when it does not exist yet. */
117
+ export function readFloors(root: string): Record<string, number> {
118
+ const path = join(root, FLOOR_MANIFEST_FILE);
119
+ if (!existsSync(path)) return {};
120
+ return parseFloors(JSON.parse(readFileSync(path, "utf8")), FLOOR_MANIFEST_FILE);
121
+ }
122
+
123
+ /**
124
+ * The next floor manifest: each report's floor rises to its current coverage and
125
+ * never falls, a report with no floor gains one, and a floor whose report is gone
126
+ * is dropped. Monotone by construction — no code path here writes a smaller
127
+ * number, so a regression can only ever be reported by the gate, never absorbed.
128
+ */
129
+ export function raiseFloors(
130
+ floors: Record<string, number>,
131
+ reports: Record<string, FidelityFloorReport>,
132
+ ): Record<string, number> {
133
+ const next: Record<string, number> = {};
134
+ for (const key of Object.keys(reports).sort()) {
135
+ const existing = floors[key];
136
+ const { coverage } = reports[key] as FidelityFloorReport;
137
+ next[key] = existing === undefined ? coverage : Math.max(existing, coverage);
138
+ }
139
+ return next;
140
+ }
141
+
142
+ // `fidelity-floor.json` sits at the package root, so unlike the reports under
143
+ // `fidelity/` it is not biome-excluded and must match Biome's formatting.
144
+ function biomeFormatJson(raw: string): string {
145
+ const out = Bun.spawnSync(
146
+ ["bunx", "biome", "format", `--stdin-file-path=${FLOOR_MANIFEST_FILE}`],
147
+ {
148
+ stdin: Buffer.from(raw),
149
+ },
150
+ );
151
+ if (out.exitCode !== 0) {
152
+ throw new Error(`biome format failed: ${out.stderr.toString()}`);
153
+ }
154
+ return out.stdout.toString();
155
+ }
156
+
157
+ if (import.meta.main) {
158
+ const root = resolve(import.meta.dir, "..");
159
+ const reports = collectFidelityReports(root);
160
+ const current = readFloors(root);
161
+ const next = raiseFloors(current, reports);
162
+
163
+ if (process.argv.includes("--raise")) {
164
+ for (const key of Object.keys(next)) {
165
+ const before = current[key];
166
+ if (before === undefined) console.log(`${key}: (new) -> ${next[key]}`);
167
+ else if (before !== next[key]) console.log(`${key}: ${before} -> ${next[key]}`);
168
+ }
169
+ for (const key of Object.keys(current)) {
170
+ if (next[key] === undefined) console.log(`${key}: dropped (no such report)`);
171
+ }
172
+ const path = join(root, FLOOR_MANIFEST_FILE);
173
+ writeFileSync(path, biomeFormatJson(JSON.stringify(next)));
174
+ console.log(`wrote ${path}`);
175
+ } else {
176
+ console.log(JSON.stringify(next, null, 2));
177
+ }
178
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Carrying upstream's own LuaDoc into the authored lane's api-doc, for members the
3
+ * fork documents nowhere.
4
+ *
5
+ * `parse-lua-surface.ts` already reads each upstream member's `---` block and
6
+ * `authored-parity.ts` already counts how many of them the fork drops, so the text
7
+ * exists on one side of the pipeline and was thrown away before the docs-site saw
8
+ * it. This module is the merge, and it runs at api-doc lowering rather than in the
9
+ * vendored `.d.ts`: writing upstream prose into `fixtures/authored/*.d.ts` would
10
+ * still pass the forked-vs-generated identity diff while destroying what that diff
11
+ * exists to prove — that the emitted surface *is* the vendored fork — and imported
12
+ * prose would be indistinguishable from fork prose forever after.
13
+ *
14
+ * Three rules carry the whole merge:
15
+ *
16
+ * - **Fork prose always wins.** An element the fork gave either a `brief` or a
17
+ * `description` is returned untouched and gains no `docSource`, so authoring the
18
+ * fork's own doc-comment is how a member opts out. There is no exclusion list:
19
+ * the only thing one would add is "show no prose at all for a member upstream
20
+ * documented", which is worse than writing the correct brief.
21
+ * - **Tags never come with the summary.** Every block in this corpus is a summary
22
+ * followed by `@param`/`@return` lines, and a tag's continuation lines carry no
23
+ * marker of their own, so truncating at the *first* `@` line is what keeps
24
+ * LuaDoc-derived types and parameter names out of the api-doc.
25
+ * - **A brief never restates its own symbol.** A leading line that is exactly the
26
+ * element's name is upstream's heading, not prose, and is dropped before the brief is
27
+ * taken. That is the importer's knowledge, not `summarizeLuaDoc`'s, which stays a pure
28
+ * block-to-prose function.
29
+ */
30
+
31
+ import type { LuaMember } from "./parse-lua-surface";
32
+
33
+ /** One `api-doc/<namespace>.json` element, open at the edges: this module reads the
34
+ * four keys the merge turns on and passes every other key through in place. */
35
+ export interface ApiDocElement {
36
+ type: string;
37
+ name: string;
38
+ global?: boolean;
39
+ brief?: string;
40
+ description?: string;
41
+ docSource?: string;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ /**
46
+ * The prose half of a LuaDoc block: its lines up to the first whose trimmed text
47
+ * starts with `@`, rejoined and trimmed. Empty for a block that is only tags.
48
+ *
49
+ * Interior newlines and blank lines survive. Markdown collapses a soft break at
50
+ * render time, so no unwrapping is done and the imported description stays
51
+ * upstream's own text.
52
+ */
53
+ export function summarizeLuaDoc(doc: string): string {
54
+ const summary: string[] = [];
55
+ for (const line of doc.split("\n")) {
56
+ if (line.trim().startsWith("@")) break;
57
+ summary.push(line);
58
+ }
59
+ return summary.join("\n").trim();
60
+ }
61
+
62
+ /** The summary without a leading line that is only the element's own name. nakama writes
63
+ * `--- <member_name>` above the real summary, and `briefOf` takes the first line, so
64
+ * leaving it in ships a brief that restates the symbol it sits under. The comparison is
65
+ * exact: a first line that merely mentions the name is prose and survives. */
66
+ function withoutNameLine(summary: string, name: string): string {
67
+ const [first, ...rest] = summary.split("\n");
68
+ return (first as string).trim() === name ? rest.join("\n").trim() : summary;
69
+ }
70
+
71
+ /** The element with upstream's summary in place, `docSource` inserted immediately
72
+ * after `description` by copying the element's own entries in order rather than
73
+ * assigning onto it — the key order is what the committed api-doc golden diffs on. */
74
+ function withImportedDoc(element: ApiDocElement, summary: string): ApiDocElement {
75
+ const brief = summary.split("\n")[0]?.trim() ?? "";
76
+ const imported: ApiDocElement = { type: element.type, name: element.name };
77
+ let placedBrief = false;
78
+ let placedDescription = false;
79
+ for (const [key, value] of Object.entries(element)) {
80
+ if (key === "type" || key === "name") continue;
81
+ if (key === "brief") {
82
+ imported.brief = brief;
83
+ placedBrief = true;
84
+ } else if (key === "description") {
85
+ imported.description = summary;
86
+ imported.docSource = "upstream";
87
+ placedDescription = true;
88
+ } else imported[key] = value;
89
+ }
90
+ if (!placedDescription) {
91
+ imported.description = summary;
92
+ imported.docSource = "upstream";
93
+ }
94
+ if (!placedBrief) imported.brief = brief;
95
+ return imported;
96
+ }
97
+
98
+ /**
99
+ * The api-doc element list with upstream prose merged in, as a new list.
100
+ *
101
+ * An element is imported when it is a `FUNCTION` or a `VARIABLE` — the two kinds
102
+ * `authored-parity.ts` compares, a `TYPEDEF` being a type rather than a runtime
103
+ * member — is not `global: true`, has an empty `brief` *and* an empty
104
+ * `description`, matches an upstream member by exact name, and that member's
105
+ * summary survives tag stripping. Every other element is returned unchanged.
106
+ *
107
+ * The name match is exact on purpose: a namespace-qualified api-doc name
108
+ * (`bridge.bridge`) never matches a bare upstream name, which is the same non-match
109
+ * the parity pass makes.
110
+ */
111
+ export function importUpstreamDocs(
112
+ elements: readonly ApiDocElement[],
113
+ members: ReadonlyMap<string, LuaMember>,
114
+ ): ApiDocElement[] {
115
+ return elements.map((element) => {
116
+ if (element.type !== "FUNCTION" && element.type !== "VARIABLE") return element;
117
+ if (element.global === true) return element;
118
+ if ((element.brief ?? "") !== "" || (element.description ?? "") !== "") return element;
119
+ const member = members.get(element.name);
120
+ if (member === undefined) return element;
121
+ const summary = withoutNameLine(summarizeLuaDoc(member.doc), element.name);
122
+ if (summary === "") return element;
123
+ return withImportedDoc(element, summary);
124
+ });
125
+ }