@voxgig/sdkgen 3.5.0 → 3.6.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 (171) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/ExternalTarget.js +1 -1
  3. package/dist/cmp/ExternalTarget.js.map +1 -1
  4. package/dist/helpers/naming.d.ts +4 -1
  5. package/dist/helpers/naming.js +119 -5
  6. package/dist/helpers/naming.js.map +1 -1
  7. package/dist/helpers/opShape.js +72 -0
  8. package/dist/helpers/opShape.js.map +1 -1
  9. package/dist/helpers/packageMeta.d.ts +2 -1
  10. package/dist/helpers/packageMeta.js +57 -20
  11. package/dist/helpers/packageMeta.js.map +1 -1
  12. package/dist/sdkgen.d.ts +3 -3
  13. package/dist/sdkgen.js +61 -18
  14. package/dist/sdkgen.js.map +1 -1
  15. package/dist/testkit.d.ts +39 -0
  16. package/dist/testkit.js +663 -0
  17. package/dist/testkit.js.map +1 -0
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +32 -8
  20. package/project/.sdk/src/cmp/c/Test_c.ts +5 -7
  21. package/project/.sdk/src/cmp/clojure/Test_clojure.ts +3 -7
  22. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +4 -7
  23. package/project/.sdk/src/cmp/csharp/Package_csharp.ts +1 -1
  24. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -7
  25. package/project/.sdk/src/cmp/dart/Test_dart.ts +3 -4
  26. package/project/.sdk/src/cmp/elixir/Package_elixir.ts +1 -1
  27. package/project/.sdk/src/cmp/elixir/Test_elixir.ts +5 -6
  28. package/project/.sdk/src/cmp/go/EntityTypes_go.ts +1 -1
  29. package/project/.sdk/src/cmp/go/Entity_go.ts +1 -1
  30. package/project/.sdk/src/cmp/go/Gitignore_go.ts +6 -0
  31. package/project/.sdk/src/cmp/go/Main_go.ts +2 -2
  32. package/project/.sdk/src/cmp/go/Package_go.ts +1 -1
  33. package/project/.sdk/src/cmp/go/ReadmeExamplesTest_go.ts +59 -6
  34. package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +1 -1
  35. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +1 -1
  36. package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +1 -1
  37. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +1 -1
  38. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +1 -1
  39. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +1 -1
  40. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +1 -1
  41. package/project/.sdk/src/cmp/go/Test_go.ts +6 -8
  42. package/project/.sdk/src/cmp/go-cli/Main_go-cli.ts +6 -2
  43. package/project/.sdk/src/cmp/go-mcp/Main_go-mcp.ts +6 -2
  44. package/project/.sdk/src/cmp/java/Test_java.ts +4 -7
  45. package/project/.sdk/src/cmp/js/Package_js.ts +1 -1
  46. package/project/.sdk/src/cmp/js/Test_js.ts +4 -7
  47. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +4 -7
  48. package/project/.sdk/src/cmp/lean/Test_lean.ts +3 -3
  49. package/project/.sdk/src/cmp/lua/Package_lua.ts +2 -2
  50. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -7
  51. package/project/.sdk/src/cmp/ocaml/Test_ocaml.ts +3 -7
  52. package/project/.sdk/src/cmp/perl/Package_perl.ts +1 -1
  53. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -7
  54. package/project/.sdk/src/cmp/php/EntityTypes_php.ts +13 -2
  55. package/project/.sdk/src/cmp/php/Package_php.ts +2 -2
  56. package/project/.sdk/src/cmp/php/Test_php.ts +5 -7
  57. package/project/.sdk/src/cmp/py/Package_py.ts +2 -2
  58. package/project/.sdk/src/cmp/py/Test_py.ts +5 -7
  59. package/project/.sdk/src/cmp/py-data/Main_py-data.ts +14 -19
  60. package/project/.sdk/src/cmp/rb/Package_rb.ts +3 -3
  61. package/project/.sdk/src/cmp/rb/ReadmeInstall_rb.ts +1 -1
  62. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -7
  63. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -7
  64. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -2
  65. package/project/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +161 -0
  66. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +20 -11
  67. package/project/.sdk/src/cmp/swift/Test_swift.ts +5 -7
  68. package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
  69. package/project/.sdk/src/cmp/ts/Test_ts.ts +8 -7
  70. package/project/.sdk/src/cmp/zig/Test_zig.ts +3 -7
  71. package/project/.sdk/tm/c/utility/make_point.c +64 -3
  72. package/project/.sdk/tm/clojure/src/sdk/core.clj +49 -14
  73. package/project/.sdk/tm/cpp/utility/pipeline.hpp +55 -3
  74. package/project/.sdk/tm/csharp/feature/TestFeature.cs +36 -2
  75. package/project/.sdk/tm/csharp/utility/MakePoint.cs +65 -2
  76. package/project/.sdk/tm/dart/lib/feature/test/TestFeature.dart +34 -4
  77. package/project/.sdk/tm/dart/lib/utility/MakePointUtility.dart +48 -2
  78. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +70 -10
  79. package/project/.sdk/tm/go/feature/test_feature.go +34 -2
  80. package/project/.sdk/tm/go/utility/make_point.go +66 -2
  81. package/project/.sdk/tm/java/feature/TestFeature.java +42 -2
  82. package/project/.sdk/tm/java/utility/MakePoint.java +61 -2
  83. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +22 -3
  84. package/project/.sdk/tm/js/src/utility/MakePointUtility.js +72 -2
  85. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +31 -2
  86. package/project/.sdk/tm/kotlin/utility/MakePoint.kt +58 -2
  87. package/project/.sdk/tm/lean/src/SdkUtility.lean +28 -0
  88. package/project/.sdk/tm/lua/utility/make_point.lua +65 -2
  89. package/project/.sdk/tm/ocaml/sdk_runtime.ml +40 -2
  90. package/project/.sdk/tm/perl/utility/make_point.pm +55 -2
  91. package/project/.sdk/tm/php/feature/TestFeature.php +30 -1
  92. package/project/.sdk/tm/php/utility/MakePoint.php +49 -1
  93. package/project/.sdk/tm/py/pkg/utility/make_point.py +56 -2
  94. package/project/.sdk/tm/rb/utility/make_point.rb +42 -2
  95. package/project/.sdk/tm/rust/utility/make_point.rs +69 -2
  96. package/project/.sdk/tm/scala/feature/TestFeature.scala +36 -1
  97. package/project/.sdk/tm/scala/utility/Make.scala +57 -3
  98. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +29 -1
  99. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Make.swift +51 -2
  100. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +22 -3
  101. package/project/.sdk/tm/ts/src/utility/MakePointUtility.ts +72 -2
  102. package/project/.sdk/tm/zig/core/utility.zig +66 -3
  103. package/project/sdkgen-package.json +1 -2
  104. package/src/cmp/ExternalTarget.ts +1 -1
  105. package/src/helpers/naming.ts +129 -5
  106. package/src/helpers/opShape.ts +85 -0
  107. package/src/helpers/packageMeta.ts +59 -20
  108. package/src/sdkgen.ts +68 -20
  109. package/src/testkit.ts +863 -0
  110. package/testkit.d.ts +3 -0
  111. package/testkit.js +12 -0
  112. package/project/.sdk/model/target/haskell.aontu +0 -72
  113. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +0 -98
  114. package/project/.sdk/src/cmp/haskell/Entity_haskell.ts +0 -13
  115. package/project/.sdk/src/cmp/haskell/Gitignore_haskell.ts +0 -27
  116. package/project/.sdk/src/cmp/haskell/Main_haskell.ts +0 -123
  117. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +0 -60
  118. package/project/.sdk/src/cmp/haskell/ReadmeEntity_haskell.ts +0 -186
  119. package/project/.sdk/src/cmp/haskell/ReadmeExamplesTest_haskell.ts +0 -135
  120. package/project/.sdk/src/cmp/haskell/ReadmeExplanation_haskell.ts +0 -48
  121. package/project/.sdk/src/cmp/haskell/ReadmeHowto_haskell.ts +0 -168
  122. package/project/.sdk/src/cmp/haskell/ReadmeInstall_haskell.ts +0 -51
  123. package/project/.sdk/src/cmp/haskell/ReadmeIntro_haskell.ts +0 -67
  124. package/project/.sdk/src/cmp/haskell/ReadmeModel_haskell.ts +0 -162
  125. package/project/.sdk/src/cmp/haskell/ReadmeOptions_haskell.ts +0 -74
  126. package/project/.sdk/src/cmp/haskell/ReadmeQuick_haskell.ts +0 -241
  127. package/project/.sdk/src/cmp/haskell/ReadmeRef_haskell.ts +0 -408
  128. package/project/.sdk/src/cmp/haskell/ReadmeTopHowto_haskell.ts +0 -31
  129. package/project/.sdk/src/cmp/haskell/ReadmeTopQuick_haskell.ts +0 -104
  130. package/project/.sdk/src/cmp/haskell/ReadmeTopTest_haskell.ts +0 -80
  131. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +0 -300
  132. package/project/.sdk/src/cmp/haskell/tsconfig.json +0 -15
  133. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +0 -166
  134. package/project/.sdk/tm/haskell/LICENSE +0 -21
  135. package/project/.sdk/tm/haskell/Makefile +0 -22
  136. package/project/.sdk/tm/haskell/VERSION +0 -1
  137. package/project/.sdk/tm/haskell/src/SdkFeatures.hs +0 -1440
  138. package/project/.sdk/tm/haskell/src/SdkHelpers.hs +0 -337
  139. package/project/.sdk/tm/haskell/src/SdkJson.hs +0 -111
  140. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +0 -1219
  141. package/project/.sdk/tm/haskell/src/SdkTypes.hs +0 -195
  142. package/project/.sdk/tm/haskell/src/VoxgigStruct.hs +0 -2299
  143. package/project/.sdk/tm/haskell/src/Vregex.hs +0 -237
  144. package/project/.sdk/tm/haskell/src/feature/README.md +0 -5
  145. package/project/.sdk/tm/haskell/src/feature/audit/.gitkeep +0 -0
  146. package/project/.sdk/tm/haskell/src/feature/base/.gitkeep +0 -0
  147. package/project/.sdk/tm/haskell/src/feature/cache/.gitkeep +0 -0
  148. package/project/.sdk/tm/haskell/src/feature/clienttrack/.gitkeep +0 -0
  149. package/project/.sdk/tm/haskell/src/feature/debug/.gitkeep +0 -0
  150. package/project/.sdk/tm/haskell/src/feature/idempotency/.gitkeep +0 -0
  151. package/project/.sdk/tm/haskell/src/feature/log/.gitkeep +0 -0
  152. package/project/.sdk/tm/haskell/src/feature/metrics/.gitkeep +0 -0
  153. package/project/.sdk/tm/haskell/src/feature/netsim/.gitkeep +0 -0
  154. package/project/.sdk/tm/haskell/src/feature/paging/.gitkeep +0 -0
  155. package/project/.sdk/tm/haskell/src/feature/proxy/.gitkeep +0 -0
  156. package/project/.sdk/tm/haskell/src/feature/ratelimit/.gitkeep +0 -0
  157. package/project/.sdk/tm/haskell/src/feature/rbac/.gitkeep +0 -0
  158. package/project/.sdk/tm/haskell/src/feature/retry/.gitkeep +0 -0
  159. package/project/.sdk/tm/haskell/src/feature/streaming/.gitkeep +0 -0
  160. package/project/.sdk/tm/haskell/src/feature/telemetry/.gitkeep +0 -0
  161. package/project/.sdk/tm/haskell/src/feature/test/.gitkeep +0 -0
  162. package/project/.sdk/tm/haskell/src/feature/timeout/.gitkeep +0 -0
  163. package/project/.sdk/tm/haskell/test/Harness.hs +0 -281
  164. package/project/.sdk/tm/haskell/test/Runner.hs +0 -47
  165. package/project/.sdk/tm/haskell/test/StructCorpus.hs +0 -449
  166. package/project/.sdk/tm/haskell/test/TCustomUtility.hs +0 -38
  167. package/project/.sdk/tm/haskell/test/TFeature.hs +0 -794
  168. package/project/.sdk/tm/haskell/test/TNetsim.hs +0 -50
  169. package/project/.sdk/tm/haskell/test/TPipeline.hs +0 -334
  170. package/project/.sdk/tm/haskell/test/TPrimaryUtility.hs +0 -386
  171. package/project/.sdk/tm/haskell/test/Testutil.hs +0 -59
@@ -112,17 +112,67 @@ function isRbCoreConstant(Name: string): boolean {
112
112
  }
113
113
 
114
114
 
115
+ // RUBY SDK-OWNED CONSTANTS — the OTHER half of "already taken".
116
+ //
117
+ // `RB_CORE_CONSTANTS` guards names the LANGUAGE owns. This guards names the
118
+ // GENERATED SCAFFOLDING claims for itself, which is a distinct hazard and the
119
+ // one that stayed open: an entity named `Runner` produced `class Runner` in
120
+ // `<Sdk>_types.rb`, and the generated harness then does
121
+ //
122
+ // Runner = ProjectNameTestRunner # tm/rb/test/runner.rb
123
+ //
124
+ // so inside the test process the entity TYPE silently *is* the test runner.
125
+ // Ruby warns (`already initialized constant Runner`) and carries on, which is
126
+ // why it survived: nothing in the suite exercises the type, so `rb` passed.
127
+ // Reported on gitlab-sdk, which has a `Runner` entity (issue #64).
128
+ //
129
+ // EVERYTHING SHARES ONE NAMESPACE HERE, unlike swift. Swift's Tests module is
130
+ // separate from the SDK module, so `SWIFT_SDK_TYPES` can exclude test-only
131
+ // declarations. Ruby has no such split — the types file and the test files are
132
+ // all required into one process when the suite runs, which is exactly the
133
+ // reported symptom — so test-declared names belong in this set.
134
+ //
135
+ // PREFIXED DECLARATIONS ARE DELIBERATELY ABSENT. `ProjectNameUtility` and
136
+ // friends substitute to `<Sdk>Utility`, and a generated entity type is the
137
+ // bare entity name, so those cannot collide. Only the UNPREFIXED declarations
138
+ // are reachable.
139
+ //
140
+ // `rb-sdk-constants.test.ts` re-derives this list from the templates AND the
141
+ // components and fails on drift — the same discipline as
142
+ // `swift-sdk-types.test.ts`, and for the same reason: the first cut of that
143
+ // list was collected by hand and missed three names, one of them declared by
144
+ // a component rather than a template.
145
+ const RB_SDK_CONSTANTS = new Set<string>([
146
+ // module-level aliases the test harness defines for convenience
147
+ 'Helpers', 'Runner', 'Vs',
148
+ // vendored struct library and its test scaffolding
149
+ 'StructRunner', 'StructTestClient', 'StructUtilityTest', 'VoxgigStruct',
150
+ 'STRUCT_TEST_JSON_FILE',
151
+ // the generated/templated test classes
152
+ 'ExistsTest', 'FeatureTest', 'NetsimTest', 'PipelineTest',
153
+ 'PrimaryUtilityTest', 'ReadmeExamplesTest', 'TestHookFeature',
154
+ 'TestInitFeature',
155
+ ])
156
+
157
+
158
+ // Does `Name` collide with a constant the generated Ruby SDK already declares?
159
+ function isRbSdkConstant(Name: string): boolean {
160
+ return RB_SDK_CONSTANTS.has(Name)
161
+ }
162
+
163
+
115
164
  // A top-level-safe Ruby constant name for a generated type: the name
116
- // unchanged, unless Ruby core already owns it, in which case `Type` is
117
- // appended (`File` -> `FileType`). Applied ONLY to the bare entity data type
118
- // the per-op type names always carry a suffix (`FileCreateData`,
119
- // `FileLoadMatch`), which no core constant matches.
165
+ // unchanged, unless it is ALREADY TAKEN — by Ruby core, or by the SDK's own
166
+ // scaffolding — in which case `Type` is appended (`File` -> `FileType`,
167
+ // `Runner` -> `RunnerType`). Applied ONLY to the bare entity data type: the
168
+ // per-op type names always carry a suffix (`FileCreateData`,
169
+ // `FileLoadMatch`), which nothing in either set matches.
120
170
  //
121
171
  // The entity ACCESSOR keeps its original name (`client.File(...)`): methods
122
172
  // and constants live in separate namespaces in Ruby, so the accessor never
123
173
  // collided and the public surface is unchanged.
124
174
  function rbSafeTypeName(Name: string): string {
125
- return isRbCoreConstant(Name) ? Name + 'Type' : Name
175
+ return isRbCoreConstant(Name) || isRbSdkConstant(Name) ? Name + 'Type' : Name
126
176
  }
127
177
 
128
178
 
@@ -195,6 +245,77 @@ function swiftSafeTypeName(Name: string): string {
195
245
  }
196
246
 
197
247
 
248
+ // PHP RESERVED WORDS THAT CANNOT BE A CLASS NAME — a fourth hazard, and the
249
+ // only one of the four where the check must be CASE-INSENSITIVE.
250
+ //
251
+ // PHP's grammar refuses a reserved word where a class name is expected, so a
252
+ // spec with a `Namespace` entity emitted a file that does not parse at all:
253
+ //
254
+ // /** Namespace entity data model. */
255
+ // class Namespace
256
+ // {
257
+ // }
258
+ // -> PHP Parse error: syntax error, unexpected token "namespace",
259
+ // expecting identifier
260
+ //
261
+ // Found on gitlab-sdk (issue #64), and LATENT in the worst way: `types/` is on
262
+ // the composer classmap but nothing references the file, so PHP never loads it
263
+ // and the php lane passed for as long as the target has existed. It would
264
+ // fatal the moment anything required it, and the documented type is unusable
265
+ // either way.
266
+ //
267
+ // CASE-INSENSITIVITY IS THE WHOLE TRICK. Class and function names in PHP are
268
+ // case-insensitive, so `Namespace`, `NAMESPACE` and `namespace` are one
269
+ // identifier — and the generated name is PascalCase while the reserved word is
270
+ // lowercase. A case-sensitive `Set.has('Namespace')` against a lowercase list
271
+ // therefore matches NOTHING and reopens the bug silently. Entries are stored
272
+ // folded, and the lookup folds too.
273
+ //
274
+ // Taken from the PHP manual's reserved-words tables wholesale rather than
275
+ // grown one collision at a time: the list is fixed by the language, and
276
+ // picking from it by hand is how the second `Namespace` gets shipped.
277
+ const PHP_RESERVED_TYPES = new Set<string>([
278
+ // keywords
279
+ 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch',
280
+ 'class', 'clone', 'const', 'continue', 'declare', 'default', 'do', 'echo',
281
+ 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif',
282
+ 'endswitch', 'endwhile', 'enum', 'eval', 'exit', 'extends', 'final',
283
+ 'finally', 'fn', 'for', 'foreach', 'function', 'global', 'goto', 'if',
284
+ 'implements', 'include', 'include_once', 'instanceof', 'insteadof',
285
+ 'interface', 'isset', 'list', 'match', 'namespace', 'new', 'or', 'print',
286
+ 'private', 'protected', 'public', 'readonly', 'require', 'require_once',
287
+ 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use',
288
+ 'var', 'while', 'xor', 'yield',
289
+ // compile-time constants
290
+ '__class__', '__dir__', '__file__', '__function__', '__line__',
291
+ '__method__', '__namespace__', '__trait__',
292
+ // reserved as type names / cannot be declared as a class
293
+ 'bool', 'false', 'float', 'int', 'iterable', 'mixed', 'never', 'null',
294
+ 'numeric', 'object', 'parent', 'resource', 'self', 'string', 'true', 'void',
295
+ ])
296
+
297
+
298
+ // Does `Name` collide with a word PHP reserves, ignoring case as PHP does?
299
+ function isPhpReservedType(Name: string): boolean {
300
+ return PHP_RESERVED_TYPES.has(String(Name).toLowerCase())
301
+ }
302
+
303
+
304
+ // A declarable PHP class name for a generated type: unchanged, unless PHP
305
+ // reserves the word, in which case `Type` is appended (`Namespace` ->
306
+ // `NamespaceType`). Mirrors rbSafeTypeName and swiftSafeTypeName deliberately
307
+ // — same suffix, same "only rename on an actual collision" rule, so every SDK
308
+ // that does not collide is byte-identical to before.
309
+ //
310
+ // Applied ONLY to the bare entity data class. Per-op type names already carry
311
+ // their own suffix (`NamespaceLoadData`, `NamespaceCreateData`), which no
312
+ // reserved word matches, and the entity ACCESSOR is a method rather than a
313
+ // class — PHP resolves those separately — so the public surface is unchanged.
314
+ function phpSafeTypeName(Name: string): string {
315
+ return isPhpReservedType(Name) ? Name + 'Type' : Name
316
+ }
317
+
318
+
198
319
  // Is `name` a reserved word (an illegal identifier) in the target language?
199
320
  function isReservedName(name: string, lang: string): boolean {
200
321
  const set = RESERVED[lang]
@@ -328,9 +449,12 @@ export {
328
449
  phpEntityAccessor,
329
450
  entityCacheField,
330
451
  isRbCoreConstant,
452
+ isRbSdkConstant,
331
453
  rbSafeTypeName,
332
454
  isSwiftSdkType,
333
455
  swiftSafeTypeName,
456
+ isPhpReservedType,
457
+ phpSafeTypeName,
334
458
  jsProp,
335
459
  jsOptProp,
336
460
  jsKey,
@@ -189,6 +189,72 @@ function entityPath(entity: any): string {
189
189
  }
190
190
 
191
191
 
192
+ // The entity's OWN endpoint among an op's points.
193
+ //
194
+ // The other notion of canonical above — "not a `$action` route" — separates a
195
+ // folded-in custom action from the real op. This one separates the entity's
196
+ // own route from a CROSS-REFERENCE: another resource's route that happens to
197
+ // return this entity (`/notifications/{id}/board` for a board). Both are
198
+ // plain GETs with no `$action`, so the path itself is all there is to go on.
199
+ //
200
+ // Two signals, in order:
201
+ //
202
+ // 1. A record route ends in the record's identifier (`/boards/{id}`,
203
+ // `/accounts/{account_id}/users/{id}`); a cross-reference ends in the
204
+ // relationship's name (`/posts/{id}/author`). A terminal parameter is
205
+ // the stronger signal, and unlike depth alone it survives an entity
206
+ // nested more deeply than the route that points at it — depth by itself
207
+ // picked `/posts/{id}/author` over `/accounts/{account_id}/users/{id}`
208
+ // and then generated that op's required params from the wrong route.
209
+ // 2. Failing that, fewest path segments — the shallower route is the one
210
+ // the entity is named for, which is what settles a `list` op where
211
+ // neither route ends in an id (`/boards` over `/members/{id}/boards`).
212
+ //
213
+ // Ties keep the earlier point, so the sorted-key order of the model decides
214
+ // and the output stays byte-stable.
215
+ //
216
+ // The same rule runs at RUNTIME, in each language's makePoint template, when
217
+ // no point's `select.exist` matches. It cannot be shared with them — a
218
+ // template ships standalone, outside this package — so it is written twice on
219
+ // purpose, and both sides must move together.
220
+ function terminalParam(point: any): boolean {
221
+ const parts: any[] = (point && point.parts) || []
222
+ const last = 0 < parts.length ? parts[parts.length - 1] : ''
223
+ return 'string' === typeof last && last.startsWith('{')
224
+ }
225
+
226
+
227
+ function ownPoint(points: any[]): any {
228
+ let best = points[0]
229
+
230
+ for (const pt of points) {
231
+ if (null == pt || null == pt.parts || null == best || null == best.parts) {
232
+ continue
233
+ }
234
+
235
+ const ptterm = terminalParam(pt)
236
+ const bestterm = terminalParam(best)
237
+
238
+ if (ptterm !== bestterm ? ptterm : pt.parts.length < best.parts.length) {
239
+ best = pt
240
+ }
241
+ }
242
+
243
+ return best
244
+ }
245
+
246
+
247
+ // Do all these points describe the same route? Then they are alternative
248
+ // selectors on one endpoint (the same path, chosen by different query
249
+ // params), not cross-references to different resources.
250
+ function samePath(points: any[]): boolean {
251
+ const first = ((points[0] && points[0].parts) || []).join('/')
252
+
253
+ return points.every((pt: any) =>
254
+ first === ((pt && pt.parts) || []).join('/'))
255
+ }
256
+
257
+
192
258
  function opParams(op: any): any[] {
193
259
  let points: any[] = op && op.points ? each(op.points) : []
194
260
 
@@ -227,6 +293,25 @@ function opParams(op: any): any[] {
227
293
  p.reqd = true === requiredOnAll[p.name]
228
294
  })
229
295
 
296
+ // Unrelated cross-references share no param, so the intersection came out
297
+ // empty and NOTHING is required — which generates a `load({})` that drops
298
+ // the id. Fall back to the entity's own point and take its params whole.
299
+ //
300
+ // Two guards, and both are load-bearing:
301
+ //
302
+ // - only when the intersection produced nothing, so a genuine set of
303
+ // alternative routes with a shared param still merges (a `page_id` on
304
+ // every route stays required, per-route siblings stay optional);
305
+ // - only when the points describe DIFFERENT routes. Points on one path
306
+ // that differ solely by optional query params (`/users?email=` and
307
+ // `/users?name=`) legitimately require nothing, and are not
308
+ // cross-references — collapsing them to one point would drop the
309
+ // other's fields from the generated request type, so a caller could
310
+ // not express them at all.
311
+ if (1 < points.length && !out.some((p: any) => p.reqd) && !samePath(points)) {
312
+ return opParams({ points: [ownPoint(points)] })
313
+ }
314
+
230
315
  return out
231
316
  }
232
317
 
@@ -40,6 +40,27 @@ function langLabel(target: string): string {
40
40
  return LANG_LABEL[target] || target
41
41
  }
42
42
 
43
+
44
+ // THE NAME WHOSE LANGUAGE RULES APPLY.
45
+ //
46
+ // A target name does two unrelated jobs in this file, and conflating them is
47
+ // the defect this exists to end. `go~go2` installs a SECOND Go SDK: its
48
+ // CONFIG lives under `main.kit.target.go2` (its own module path, its own
49
+ // registry state), but it is still Go — the same go.mod shape, the same
50
+ // `go get` install line, the same "Go" label.
51
+ //
52
+ // So: look config up by the target's OWN name, and select behaviour by this.
53
+ // `origname` is stamped at add time precisely for aliased installs and is
54
+ // empty when a target was installed under its own name, which makes the
55
+ // unaliased case identical to what it was.
56
+ //
57
+ // Ecosystem keys ('npm', 'gem', 'composer') pass through untouched: there is
58
+ // no target node under those names, so they answer for themselves.
59
+ function originName(model: any, target: string): string {
60
+ const orig = model?.main?.[KIT]?.target?.[target]?.origname
61
+ return (null != orig && '' !== orig) ? String(orig) : target
62
+ }
63
+
43
64
  // Git host, org/repo path, and the canonical repo URLs.
44
65
  //
45
66
  // `<origin>/<slug>-sdk` is a DERIVATION, not a law. A project whose repo is
@@ -185,7 +206,10 @@ function goPackageIdent(model: any, target: string): string {
185
206
  // The legacy boolean `registry.active: true` is honoured as a back-compat
186
207
  // alias for state === 'active'.
187
208
  function registryState(model: any, target: string): 'tag' | 'pending' | 'active' | 'inactive' {
188
- if ('go' === target || 'go-cli' === target || 'go-mcp' === target) return 'tag'
209
+ // Tag-only is a property of the Go toolchain, not of one target name, so
210
+ // an aliased `go~go2` must be tag-only too.
211
+ const eco = originName(model, target)
212
+ if ('go' === eco || 'go-cli' === eco || 'go-mcp' === eco) return 'tag'
189
213
  const reg = model?.main?.[KIT]?.target?.[target]?.publish?.registry
190
214
  if (null == reg || '' === (reg.name || '')) return 'tag'
191
215
  if (true === reg.active) return 'active' // legacy alias
@@ -215,13 +239,16 @@ function registryName(model: any, target: string): string {
215
239
  // pointer carrying the releases URL.
216
240
  function vendorCommand(model: any, target: string): string {
217
241
  const { releasesUrl } = repoInfo(model)
218
- switch (target) {
242
+ switch (originName(model, target)) {
243
+ // `target`, not the literal — the case says WHICH command, the argument
244
+ // says whose module. Writing the literal here reintroduces the same bug
245
+ // the switch above exists to fix, one level down, which is exactly what
246
+ // the first cut of this change did.
219
247
  case 'go':
220
- return `go get ${packageName(model, 'go')}@latest`
221
248
  case 'go-mcp':
222
- return `go get ${packageName(model, 'go-mcp')}@latest`
249
+ return `go get ${packageName(model, target)}@latest`
223
250
  case 'go-cli':
224
- return `go install ${packageName(model, 'go-cli')}/cmd/${model.name}@latest`
251
+ return `go install ${packageName(model, target)}/cmd/${model.name}@latest`
225
252
  default: {
226
253
  const reg = registryName(model, target)
227
254
  return `not yet on ${reg || 'the registry'} — install from the git tag: ${releasesUrl}`
@@ -266,7 +293,12 @@ function packageName(model: any, eco: string): string {
266
293
  return String(declared)
267
294
  }
268
295
 
269
- switch (eco) {
296
+ // The lookup above is keyed by the target's OWN name, so an alias reads its
297
+ // own declared package. The switch below is about FORM — npm scoping,
298
+ // slash-separated composer names — which belongs to the language, so it
299
+ // follows the origin. Passing `ts2` here without this would miss every case
300
+ // and fall to `default`, silently publishing under a non-npm name.
301
+ switch (originName(model, eco)) {
270
302
  case 'npm':
271
303
  case 'ts':
272
304
  return npmScoped
@@ -282,12 +314,13 @@ function packageName(model: any, eco: string): string {
282
314
  case 'composer':
283
315
  case 'php':
284
316
  return `${origin}/${base}`
317
+ // `eco`, not the literal: the case is chosen by the LANGUAGE but the
318
+ // module belongs to the target that asked. `packageName(model, 'go2')`
319
+ // returning go's module was the same conflation one level down.
285
320
  case 'go':
286
- return goModule(model, 'go')
287
321
  case 'go-cli':
288
- return goModule(model, 'go-cli')
289
322
  case 'go-mcp':
290
- return goModule(model, 'go-mcp')
323
+ return goModule(model, eco)
291
324
  // The notebook/analyst package layered on `py`. Distinct PyPI name so it
292
325
  // can version and publish independently of the SDK it wraps.
293
326
  case 'py-data':
@@ -306,25 +339,29 @@ function installCommand(model: any, target: string): string {
306
339
  if (!isPublished(model, target)) {
307
340
  return vendorCommand(model, target)
308
341
  }
309
- switch (target) {
342
+ // WHICH package manager is the language's business, so the switch follows
343
+ // the origin; WHICH package name is this target's own, so every arm passes
344
+ // `target` rather than a hardcoded ecosystem key. Before, an aliased
345
+ // `ts~ts2` matched no case and returned '' — a README with an empty install
346
+ // line — and even had it matched, `packageName(model, 'npm')` would have
347
+ // printed the ORIGIN's package.
348
+ switch (originName(model, target)) {
310
349
  case 'ts':
311
- return `npm install ${packageName(model, 'npm')}`
312
350
  case 'js':
313
- return `npm install ${packageName(model, 'js')}`
351
+ return `npm install ${packageName(model, target)}`
314
352
  case 'py':
315
- return `pip install ${packageName(model, 'pypi')}`
316
353
  case 'py-data':
317
- return `pip install ${packageName(model, 'py-data')}`
354
+ return `pip install ${packageName(model, target)}`
318
355
  case 'php':
319
- return `composer require ${packageName(model, 'composer')}`
356
+ return `composer require ${packageName(model, target)}`
320
357
  case 'rb':
321
- return `gem install ${packageName(model, 'gem')}`
358
+ return `gem install ${packageName(model, target)}`
322
359
  case 'lua':
323
- return `luarocks install ${packageName(model, 'luarocks')}`
360
+ return `luarocks install ${packageName(model, target)}`
324
361
  case 'go':
325
- return `go get ${packageName(model, 'go')}`
362
+ return `go get ${packageName(model, target)}`
326
363
  case 'go-cli':
327
- return `go install ${packageName(model, 'go-cli')}/cmd/${model.name}@latest`
364
+ return `go install ${packageName(model, target)}/cmd/${model.name}@latest`
328
365
  default:
329
366
  return ''
330
367
  }
@@ -333,7 +370,8 @@ function installCommand(model: any, target: string): string {
333
370
  // The standard one-line package description (with the generic non-affiliation
334
371
  // statement inline) used in every manifest.
335
372
  function pkgDescription(model: any, target: string): string {
336
- return `Unofficial generated ${langLabel(target)} SDK for the ${apiName(model)} public API.` +
373
+ return `Unofficial generated ${langLabel(originName(model, target))} SDK` +
374
+ ` for the ${apiName(model)} public API.` +
337
375
  ` Not affiliated with or endorsed by the upstream API provider.`
338
376
  }
339
377
 
@@ -437,6 +475,7 @@ export {
437
475
  GENERATOR_URL,
438
476
  LANG_LABEL,
439
477
  langLabel,
478
+ originName,
440
479
  repoInfo,
441
480
  apiName,
442
481
  packageName,
package/src/sdkgen.ts CHANGED
@@ -10,7 +10,6 @@ import { Aontu } from 'aontu'
10
10
 
11
11
  import {
12
12
  showChanges,
13
- getdlog,
14
13
  } from '@voxgig/util'
15
14
 
16
15
  import type {
@@ -60,7 +59,7 @@ import { collectDeps } from './helpers/collectDeps'
60
59
  import type { DepEntry } from './helpers/collectDeps'
61
60
  import { canonToType, canonToDtype, canonKey } from './helpers/canonType'
62
61
  import { OP_SUFFIX, opTypeName, opParams, opActions, entityActions, entityPath, opRequestShape, entityIdField, entityDataIdField, entityOps, entityPrimaryOp, pickExampleEntity, entityClassName, entityTypeCollisions, warnEntityTypeCollisions, deriveEntityNames, entityCollection } from './helpers/opShape'
63
- import { isReservedName, safeVarName, exampleVarName, phpEntityAccessor, entityCacheField, isRbCoreConstant, rbSafeTypeName, isSwiftSdkType, swiftSafeTypeName, jsProp, jsOptProp, jsKey } from './helpers/naming'
62
+ import { isReservedName, safeVarName, exampleVarName, phpEntityAccessor, entityCacheField, isRbCoreConstant, isRbSdkConstant, rbSafeTypeName, isSwiftSdkType, swiftSafeTypeName, isPhpReservedType, phpSafeTypeName, jsProp, jsOptProp, jsKey } from './helpers/naming'
64
63
  import { serverVariables, hasServerVariables } from './helpers/serverVars'
65
64
  import { primaryOpCall, idLiteral, matchArg, dataArg, litFor } from './helpers/opExample'
66
65
  import type { ExampleLang } from './helpers/opExample'
@@ -108,6 +107,7 @@ import {
108
107
  repoInfo,
109
108
  apiName,
110
109
  langLabel,
110
+ originName,
111
111
  PUBLISHER,
112
112
  PUBLISHER_URL,
113
113
  SECURITY_EMAIL,
@@ -176,7 +176,38 @@ type SdkGenOptions = {
176
176
  const { Jostraca } = JostracaModule
177
177
 
178
178
 
179
- const dlog = getdlog('sdkgen', __filename)
179
+ // A BROKEN PROJECT MODEL IS THE USER'S FILE, NOT A CRASH.
180
+ //
181
+ // One constructor for both routes out of `resolveModel` — the throw aontu
182
+ // actually takes, and the `errs` array it documents but never fills — because
183
+ // two spellings of the same error is how this repo keeps reintroducing the
184
+ // same defect.
185
+ //
186
+ // aontu's own diagnostic is the valuable part and is passed through
187
+ // UNTOUCHED: it carries the source excerpt, a caret under the offending
188
+ // token, and an explanation. What it lacks is any statement of WHOSE file
189
+ // this is, so the path leads. Wrapping it in an SdkGenError is what makes the
190
+ // CLI print it as a message instead of dumping the error object and a stack
191
+ // trace through sdkgen's internals — `handleError` reserves clean output for
192
+ // that name.
193
+ function modelError(path: string, cause: any, rooterrs: any[]): any {
194
+ const detail = String(cause?.msg ?? cause?.message ?? cause ?? '').trim()
195
+
196
+ const err: any = new SdkGenError(
197
+ 'Model Error: ' + path + '\n' + detail)
198
+
199
+ err.cause$ = [cause]
200
+ err.rooterrs$ = rooterrs
201
+
202
+ // A syntax error is the user's typo, and there is nothing for them to do
203
+ // with a stack. `why` lives on the structured entries rather than the
204
+ // thrown wrapper, so it is read from the first of them.
205
+ if ('syntax' === (cause?.why ?? rooterrs?.[0]?.why)) {
206
+ err.uxmsg$ = true
207
+ }
208
+
209
+ return err
210
+ }
180
211
 
181
212
 
182
213
  function SdkGen(opts: SdkGenOptions) {
@@ -393,13 +424,6 @@ function SdkGen(opts: SdkGenOptions) {
393
424
  showChanges(jopts.log, 'generate-result', eres, Path.dirname(process.cwd()))
394
425
  }
395
426
 
396
- const dlogs = dlog.log()
397
- if (0 < dlogs.length) {
398
- for (let dlogentry of dlogs) {
399
- log.debug({ point: 'generate-warning', dlogentry, note: String(dlogentry) })
400
- }
401
- }
402
-
403
427
  log.info({ point: 'generate-end' })
404
428
 
405
429
  return { ok: true, name: 'sdkgen' }
@@ -482,19 +506,39 @@ function SdkGen(opts: SdkGenOptions) {
482
506
  const aopts = { path, errs }
483
507
  const src = fs.readFileSync(path, 'utf8')
484
508
 
485
- const model = aontu.generate(src, aopts)
486
-
487
- if (0 < errs.length) {
488
- const serr = errs[0]
489
- const err: any = new SdkGenError('Model Error: ' + serr.msg)
490
- err.cause$ = [serr]
509
+ // AONTU THROWS; IT DOES NOT FILL `errs`.
510
+ //
511
+ // The `errs` array is handed to `generate` and stays empty on every
512
+ // failure syntax, unresolved path and unify conflict alike all raise an
513
+ // AontuError instead (verified against 0.52 by running each). So the
514
+ // `0 < errs.length` branch this used to have could never fire, and every
515
+ // broken project model reached the CLI as a bare AontuError: `handleError`
516
+ // prints the raw object for anything that is not an SdkGenError, so the
517
+ // user got aontu's diagnostic followed by a stack trace into sdkgen's
518
+ // `dist/` and a dump of the error's own fields. It read as an sdkgen
519
+ // crash rather than a problem in their own file.
520
+ //
521
+ // The array is still passed, and still checked below, because that is
522
+ // aontu's documented option and a later version may start using it. Both
523
+ // routes build the error the same way, so they cannot drift.
524
+ let model: any
491
525
 
492
- if ('syntax' === serr.why) {
493
- err.uxmsg$ = true
526
+ try {
527
+ model = aontu.generate(src, aopts)
528
+ }
529
+ catch (aerr: any) {
530
+ if (true !== aerr?.aontu) {
531
+ throw aerr
494
532
  }
495
533
 
496
- err.rooterrs$ = errs
497
- throw err
534
+ // The structured errors are reachable after all — the thrown error
535
+ // carries the accessor the empty array was standing in for.
536
+ throw modelError(path, aerr,
537
+ 'function' === typeof aerr.errs ? aerr.errs() : [])
538
+ }
539
+
540
+ if (0 < errs.length) {
541
+ throw modelError(path, errs[0], errs)
498
542
  }
499
543
 
500
544
  model.const = { name: model.name }
@@ -1039,9 +1083,12 @@ export {
1039
1083
  phpEntityAccessor,
1040
1084
  entityCacheField,
1041
1085
  isRbCoreConstant,
1086
+ isRbSdkConstant,
1042
1087
  rbSafeTypeName,
1043
1088
  isSwiftSdkType,
1044
1089
  swiftSafeTypeName,
1090
+ isPhpReservedType,
1091
+ phpSafeTypeName,
1045
1092
  serverVariables,
1046
1093
  hasServerVariables,
1047
1094
  liveStrict,
@@ -1090,6 +1137,7 @@ export {
1090
1137
  repoInfo,
1091
1138
  apiName,
1092
1139
  langLabel,
1140
+ originName,
1093
1141
  PUBLISHER,
1094
1142
  PUBLISHER_URL,
1095
1143
  SECURITY_EMAIL,