@voxgig/sdkgen 3.5.1 → 3.7.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 (255) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideTop.js +17 -0
  3. package/dist/cmp/AgentGuideTop.js.map +1 -1
  4. package/dist/cmp/ExternalTarget.js +1 -1
  5. package/dist/cmp/ExternalTarget.js.map +1 -1
  6. package/dist/cmp/Readme.js +2 -0
  7. package/dist/cmp/Readme.js.map +1 -1
  8. package/dist/cmp/ReadmeStation.d.ts +2 -0
  9. package/dist/cmp/ReadmeStation.js +57 -0
  10. package/dist/cmp/ReadmeStation.js.map +1 -0
  11. package/dist/helpers/naming.d.ts +6 -1
  12. package/dist/helpers/naming.js +156 -5
  13. package/dist/helpers/naming.js.map +1 -1
  14. package/dist/helpers/opShape.js +72 -0
  15. package/dist/helpers/opShape.js.map +1 -1
  16. package/dist/helpers/packageMeta.d.ts +2 -1
  17. package/dist/helpers/packageMeta.js +57 -20
  18. package/dist/helpers/packageMeta.js.map +1 -1
  19. package/dist/sdkgen.d.ts +3 -3
  20. package/dist/sdkgen.js +63 -18
  21. package/dist/sdkgen.js.map +1 -1
  22. package/dist/testkit.d.ts +39 -0
  23. package/dist/testkit.js +663 -0
  24. package/dist/testkit.js.map +1 -0
  25. package/dist/tsconfig.tsbuildinfo +1 -1
  26. package/dist/utility.d.ts +1 -1
  27. package/dist/utility.js +16 -2
  28. package/dist/utility.js.map +1 -1
  29. package/package.json +32 -8
  30. package/project/.sdk/src/cmp/c/Config_c.ts +34 -13
  31. package/project/.sdk/src/cmp/c/Test_c.ts +5 -7
  32. package/project/.sdk/src/cmp/clojure/Test_clojure.ts +3 -7
  33. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +9 -46
  34. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +4 -7
  35. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +7 -1
  36. package/project/.sdk/src/cmp/csharp/Package_csharp.ts +1 -1
  37. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -7
  38. package/project/.sdk/src/cmp/dart/Config_dart.ts +14 -2
  39. package/project/.sdk/src/cmp/dart/Test_dart.ts +3 -4
  40. package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +5 -0
  41. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -0
  42. package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +13 -4
  43. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +12 -2
  44. package/project/.sdk/src/cmp/elixir/Package_elixir.ts +42 -3
  45. package/project/.sdk/src/cmp/elixir/Test_elixir.ts +5 -6
  46. package/project/.sdk/src/cmp/go/Config_go.ts +8 -1
  47. package/project/.sdk/src/cmp/go/EntityTypes_go.ts +1 -1
  48. package/project/.sdk/src/cmp/go/Entity_go.ts +1 -1
  49. package/project/.sdk/src/cmp/go/Gitignore_go.ts +6 -0
  50. package/project/.sdk/src/cmp/go/Main_go.ts +2 -2
  51. package/project/.sdk/src/cmp/go/Package_go.ts +1 -1
  52. package/project/.sdk/src/cmp/go/ReadmeExamplesTest_go.ts +59 -6
  53. package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +1 -1
  54. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +1 -1
  55. package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +1 -1
  56. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +1 -1
  57. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +1 -1
  58. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +1 -1
  59. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +1 -1
  60. package/project/.sdk/src/cmp/go/Test_go.ts +6 -8
  61. package/project/.sdk/src/cmp/go-cli/Main_go-cli.ts +6 -2
  62. package/project/.sdk/src/cmp/go-mcp/Main_go-mcp.ts +6 -2
  63. package/project/.sdk/src/cmp/java/Config_java.ts +8 -1
  64. package/project/.sdk/src/cmp/java/Test_java.ts +4 -7
  65. package/project/.sdk/src/cmp/js/Config_js.ts +9 -1
  66. package/project/.sdk/src/cmp/js/Package_js.ts +1 -1
  67. package/project/.sdk/src/cmp/js/Test_js.ts +4 -7
  68. package/project/.sdk/src/cmp/js/fragment/Config.data.fragment.js +7 -0
  69. package/project/.sdk/src/cmp/js/fragment/Config.fragment.js +8 -0
  70. package/project/.sdk/src/cmp/js/fragment/Main.fragment.js +33 -5
  71. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +8 -1
  72. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +4 -7
  73. package/project/.sdk/src/cmp/lean/Test_lean.ts +3 -3
  74. package/project/.sdk/src/cmp/lua/Config_lua.ts +11 -2
  75. package/project/.sdk/src/cmp/lua/Package_lua.ts +26 -3
  76. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -7
  77. package/project/.sdk/src/cmp/ocaml/Test_ocaml.ts +3 -7
  78. package/project/.sdk/src/cmp/perl/Config_perl.ts +8 -60
  79. package/project/.sdk/src/cmp/perl/Package_perl.ts +1 -1
  80. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -7
  81. package/project/.sdk/src/cmp/php/Config_php.ts +11 -2
  82. package/project/.sdk/src/cmp/php/EntityTypes_php.ts +13 -2
  83. package/project/.sdk/src/cmp/php/Main_php.ts +25 -0
  84. package/project/.sdk/src/cmp/php/Package_php.ts +2 -2
  85. package/project/.sdk/src/cmp/php/Test_php.ts +5 -7
  86. package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +23 -6
  87. package/project/.sdk/src/cmp/py/Config_py.ts +11 -2
  88. package/project/.sdk/src/cmp/py/Main_py.ts +17 -7
  89. package/project/.sdk/src/cmp/py/Package_py.ts +2 -2
  90. package/project/.sdk/src/cmp/py/Test_py.ts +5 -7
  91. package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +23 -6
  92. package/project/.sdk/src/cmp/py-data/Main_py-data.ts +14 -19
  93. package/project/.sdk/src/cmp/rb/Config_rb.ts +7 -1
  94. package/project/.sdk/src/cmp/rb/Package_rb.ts +3 -3
  95. package/project/.sdk/src/cmp/rb/ReadmeInstall_rb.ts +1 -1
  96. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -7
  97. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -2
  98. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -7
  99. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -2
  100. package/project/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +161 -0
  101. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +20 -11
  102. package/project/.sdk/src/cmp/swift/Config_swift.ts +8 -54
  103. package/project/.sdk/src/cmp/swift/Package_swift.ts +52 -10
  104. package/project/.sdk/src/cmp/swift/Test_swift.ts +5 -7
  105. package/project/.sdk/src/cmp/ts/Config_ts.ts +9 -1
  106. package/project/.sdk/src/cmp/ts/EntityTypes_ts.ts +5 -2
  107. package/project/.sdk/src/cmp/ts/Entity_ts.ts +8 -2
  108. package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
  109. package/project/.sdk/src/cmp/ts/Test_ts.ts +8 -7
  110. package/project/.sdk/src/cmp/ts/fragment/Config.data.fragment.ts +7 -0
  111. package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +8 -0
  112. package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.ts +1 -1
  113. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +12 -4
  114. package/project/.sdk/src/cmp/zig/Test_zig.ts +3 -7
  115. package/project/.sdk/tm/c/Makefile +6 -1
  116. package/project/.sdk/tm/c/utility/make_options.c +12 -1
  117. package/project/.sdk/tm/c/utility/make_point.c +64 -3
  118. package/project/.sdk/tm/clojure/src/sdk/core.clj +49 -14
  119. package/project/.sdk/tm/cpp/utility/pipeline.hpp +64 -4
  120. package/project/.sdk/tm/csharp/feature/TestFeature.cs +36 -2
  121. package/project/.sdk/tm/csharp/utility/Fetcher.cs +23 -5
  122. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +13 -0
  123. package/project/.sdk/tm/csharp/utility/MakePoint.cs +65 -2
  124. package/project/.sdk/tm/dart/lib/feature/test/TestFeature.dart +34 -4
  125. package/project/.sdk/tm/dart/lib/utility/FetcherUtility.dart +9 -0
  126. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +17 -0
  127. package/project/.sdk/tm/dart/lib/utility/MakePointUtility.dart +48 -2
  128. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +109 -13
  129. package/project/.sdk/tm/elixir/test/feature_test.exs +19 -0
  130. package/project/.sdk/tm/elixir/test/primary_utility_test.exs +72 -0
  131. package/project/.sdk/tm/go/feature/test_feature.go +34 -2
  132. package/project/.sdk/tm/go/test/primary_utility_test.go +42 -20
  133. package/project/.sdk/tm/go/test/runner_test.go +1 -1
  134. package/project/.sdk/tm/go/utility/fetcher.go +14 -0
  135. package/project/.sdk/tm/go/utility/make_options.go +31 -4
  136. package/project/.sdk/tm/go/utility/make_point.go +66 -2
  137. package/project/.sdk/tm/java/feature/TestFeature.java +42 -2
  138. package/project/.sdk/tm/java/utility/Fetcher.java +11 -1
  139. package/project/.sdk/tm/java/utility/MakeOptions.java +11 -0
  140. package/project/.sdk/tm/java/utility/MakePoint.java +61 -2
  141. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +22 -3
  142. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +20 -2
  143. package/project/.sdk/tm/js/src/utility/MakePointUtility.js +72 -2
  144. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +31 -2
  145. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +10 -1
  146. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +11 -0
  147. package/project/.sdk/tm/kotlin/utility/MakePoint.kt +58 -2
  148. package/project/.sdk/tm/lean/src/SdkUtility.lean +28 -0
  149. package/project/.sdk/tm/lean/test/TPrimaryUtility.lean +13 -2
  150. package/project/.sdk/tm/lua/utility/make_options.lua +44 -0
  151. package/project/.sdk/tm/lua/utility/make_point.lua +65 -2
  152. package/project/.sdk/tm/ocaml/sdk_runtime.ml +40 -2
  153. package/project/.sdk/tm/perl/utility/fetcher.pm +10 -1
  154. package/project/.sdk/tm/perl/utility/make_options.pm +36 -0
  155. package/project/.sdk/tm/perl/utility/make_point.pm +55 -2
  156. package/project/.sdk/tm/php/feature/TestFeature.php +30 -1
  157. package/project/.sdk/tm/php/test/PipelineTest.php +20 -0
  158. package/project/.sdk/tm/php/utility/Fetcher.php +12 -1
  159. package/project/.sdk/tm/php/utility/MakeOptions.php +58 -0
  160. package/project/.sdk/tm/php/utility/MakePoint.php +49 -1
  161. package/project/.sdk/tm/py/pkg/utility/fetcher.py +13 -1
  162. package/project/.sdk/tm/py/pkg/utility/make_options.py +23 -0
  163. package/project/.sdk/tm/py/pkg/utility/make_point.py +56 -2
  164. package/project/.sdk/tm/rb/utility/make_options.rb +18 -0
  165. package/project/.sdk/tm/rb/utility/make_point.rb +42 -2
  166. package/project/.sdk/tm/rust/tests/common/mod.rs +1 -1
  167. package/project/.sdk/tm/rust/tests/feature_test.rs +27 -0
  168. package/project/.sdk/tm/rust/utility/fetcher.rs +8 -0
  169. package/project/.sdk/tm/rust/utility/make_options.rs +15 -0
  170. package/project/.sdk/tm/rust/utility/make_point.rs +69 -2
  171. package/project/.sdk/tm/scala/feature/TestFeature.scala +36 -1
  172. package/project/.sdk/tm/scala/utility/Make.scala +57 -3
  173. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +29 -1
  174. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +24 -1
  175. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Make.swift +51 -2
  176. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +12 -0
  177. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +22 -3
  178. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +20 -2
  179. package/project/.sdk/tm/ts/src/utility/MakePointUtility.ts +72 -2
  180. package/project/.sdk/tm/ts/test/utility/Corpus.test.ts +85 -0
  181. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +10 -17
  182. package/project/.sdk/tm/zig/core/utility.zig +66 -3
  183. package/project/sdkgen-package.json +1 -2
  184. package/src/cmp/AgentGuideTop.ts +18 -0
  185. package/src/cmp/ExternalTarget.ts +1 -1
  186. package/src/cmp/Readme.ts +2 -0
  187. package/src/cmp/ReadmeStation.ts +69 -0
  188. package/src/helpers/naming.ts +172 -5
  189. package/src/helpers/opShape.ts +85 -0
  190. package/src/helpers/packageMeta.ts +59 -20
  191. package/src/sdkgen.ts +70 -20
  192. package/src/testkit.ts +863 -0
  193. package/src/utility.ts +17 -2
  194. package/testkit.d.ts +3 -0
  195. package/testkit.js +12 -0
  196. package/project/.sdk/model/target/haskell.aontu +0 -72
  197. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +0 -98
  198. package/project/.sdk/src/cmp/haskell/Entity_haskell.ts +0 -13
  199. package/project/.sdk/src/cmp/haskell/Gitignore_haskell.ts +0 -27
  200. package/project/.sdk/src/cmp/haskell/Main_haskell.ts +0 -123
  201. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +0 -60
  202. package/project/.sdk/src/cmp/haskell/ReadmeEntity_haskell.ts +0 -186
  203. package/project/.sdk/src/cmp/haskell/ReadmeExamplesTest_haskell.ts +0 -135
  204. package/project/.sdk/src/cmp/haskell/ReadmeExplanation_haskell.ts +0 -48
  205. package/project/.sdk/src/cmp/haskell/ReadmeHowto_haskell.ts +0 -168
  206. package/project/.sdk/src/cmp/haskell/ReadmeInstall_haskell.ts +0 -51
  207. package/project/.sdk/src/cmp/haskell/ReadmeIntro_haskell.ts +0 -67
  208. package/project/.sdk/src/cmp/haskell/ReadmeModel_haskell.ts +0 -162
  209. package/project/.sdk/src/cmp/haskell/ReadmeOptions_haskell.ts +0 -74
  210. package/project/.sdk/src/cmp/haskell/ReadmeQuick_haskell.ts +0 -241
  211. package/project/.sdk/src/cmp/haskell/ReadmeRef_haskell.ts +0 -408
  212. package/project/.sdk/src/cmp/haskell/ReadmeTopHowto_haskell.ts +0 -31
  213. package/project/.sdk/src/cmp/haskell/ReadmeTopQuick_haskell.ts +0 -104
  214. package/project/.sdk/src/cmp/haskell/ReadmeTopTest_haskell.ts +0 -80
  215. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +0 -300
  216. package/project/.sdk/src/cmp/haskell/tsconfig.json +0 -15
  217. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +0 -166
  218. package/project/.sdk/tm/haskell/LICENSE +0 -21
  219. package/project/.sdk/tm/haskell/Makefile +0 -22
  220. package/project/.sdk/tm/haskell/VERSION +0 -1
  221. package/project/.sdk/tm/haskell/src/SdkFeatures.hs +0 -1440
  222. package/project/.sdk/tm/haskell/src/SdkHelpers.hs +0 -337
  223. package/project/.sdk/tm/haskell/src/SdkJson.hs +0 -111
  224. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +0 -1219
  225. package/project/.sdk/tm/haskell/src/SdkTypes.hs +0 -195
  226. package/project/.sdk/tm/haskell/src/VoxgigStruct.hs +0 -2299
  227. package/project/.sdk/tm/haskell/src/Vregex.hs +0 -237
  228. package/project/.sdk/tm/haskell/src/feature/README.md +0 -5
  229. package/project/.sdk/tm/haskell/src/feature/audit/.gitkeep +0 -0
  230. package/project/.sdk/tm/haskell/src/feature/base/.gitkeep +0 -0
  231. package/project/.sdk/tm/haskell/src/feature/cache/.gitkeep +0 -0
  232. package/project/.sdk/tm/haskell/src/feature/clienttrack/.gitkeep +0 -0
  233. package/project/.sdk/tm/haskell/src/feature/debug/.gitkeep +0 -0
  234. package/project/.sdk/tm/haskell/src/feature/idempotency/.gitkeep +0 -0
  235. package/project/.sdk/tm/haskell/src/feature/log/.gitkeep +0 -0
  236. package/project/.sdk/tm/haskell/src/feature/metrics/.gitkeep +0 -0
  237. package/project/.sdk/tm/haskell/src/feature/netsim/.gitkeep +0 -0
  238. package/project/.sdk/tm/haskell/src/feature/paging/.gitkeep +0 -0
  239. package/project/.sdk/tm/haskell/src/feature/proxy/.gitkeep +0 -0
  240. package/project/.sdk/tm/haskell/src/feature/ratelimit/.gitkeep +0 -0
  241. package/project/.sdk/tm/haskell/src/feature/rbac/.gitkeep +0 -0
  242. package/project/.sdk/tm/haskell/src/feature/retry/.gitkeep +0 -0
  243. package/project/.sdk/tm/haskell/src/feature/streaming/.gitkeep +0 -0
  244. package/project/.sdk/tm/haskell/src/feature/telemetry/.gitkeep +0 -0
  245. package/project/.sdk/tm/haskell/src/feature/test/.gitkeep +0 -0
  246. package/project/.sdk/tm/haskell/src/feature/timeout/.gitkeep +0 -0
  247. package/project/.sdk/tm/haskell/test/Harness.hs +0 -281
  248. package/project/.sdk/tm/haskell/test/Runner.hs +0 -47
  249. package/project/.sdk/tm/haskell/test/StructCorpus.hs +0 -449
  250. package/project/.sdk/tm/haskell/test/TCustomUtility.hs +0 -38
  251. package/project/.sdk/tm/haskell/test/TFeature.hs +0 -794
  252. package/project/.sdk/tm/haskell/test/TNetsim.hs +0 -50
  253. package/project/.sdk/tm/haskell/test/TPipeline.hs +0 -334
  254. package/project/.sdk/tm/haskell/test/TPrimaryUtility.hs +0 -386
  255. package/project/.sdk/tm/haskell/test/Testutil.hs +0 -59
@@ -330,9 +330,10 @@ inline Value makePoint(CtxPtr ctx) {
330
330
  }
331
331
 
332
332
  Value point;
333
+ bool matched = false;
333
334
  for (size_t i = 0; i < op->points.size(); i++) {
334
- point = op->points[i];
335
- Value selectDef = Helpers::toMapAny(getp(point, "select"));
335
+ Value cand = op->points[i];
336
+ Value selectDef = Helpers::toMapAny(getp(cand, "select"));
336
337
  bool found = true;
337
338
 
338
339
  if (selector.is_map() && selectDef.is_map()) {
@@ -356,7 +357,58 @@ inline Value makePoint(CtxPtr ctx) {
356
357
  if (reqAction != selectAction) found = false;
357
358
  }
358
359
 
359
- if (found) break;
360
+ if (found) {
361
+ point = cand;
362
+ matched = true;
363
+ break;
364
+ }
365
+ }
366
+
367
+ // select.exist can list more than the params needed to pick a point (for
368
+ // /boards/{id} it is Trello's 17 optional query-includes), so a plain
369
+ // {id} call matches NOTHING. Fall back to the entity's own route rather
370
+ // than whichever point came last.
371
+ if (!matched) {
372
+ // A request naming an action reaches here only because that action's
373
+ // own point failed its exist test, so it is unbuildable whatever we
374
+ // pick. Refuse it BEFORE choosing a fallback: the guard below compares
375
+ // the chosen point's $action and would wave the request through
376
+ // whenever the fallback lands on the action point itself.
377
+ Value unmatchedAction = getp(reqselector, "$action", Value(nullptr));
378
+ if (!unmatchedAction.is_null()) {
379
+ throw ctx->makeError("point_action_invalid",
380
+ "Operation \"" + op->name + "\" action \"" +
381
+ Struct::stringify(unmatchedAction) + "\" is not valid.");
382
+ }
383
+
384
+ // A terminal parameter marks a record route (/boards/{id}); a
385
+ // cross-reference ends in the relationship's name (/posts/{id}/author).
386
+ // Failing that, the shallower path wins. The same rule runs at
387
+ // generation time, in helpers/opShape.ts — both sides must move
388
+ // together.
389
+ auto partsLen = [](const Value& p) -> size_t {
390
+ Value parts = getp(p, "parts");
391
+ return parts.is_list() ? parts.as_list()->size() : 0;
392
+ };
393
+ auto terminalParam = [](const Value& p) -> bool {
394
+ Value parts = getp(p, "parts");
395
+ if (!parts.is_list() || parts.as_list()->empty()) return false;
396
+ const Value& last = parts.as_list()->back();
397
+ return last.is_string() && 0 == last.as_string().rfind("{", 0);
398
+ };
399
+
400
+ point = op->points[0];
401
+ for (size_t i = 0; i < op->points.size(); i++) {
402
+ const Value& cand = op->points[i];
403
+ bool candTerm = terminalParam(cand);
404
+ bool bestTerm = terminalParam(point);
405
+ if (candTerm != bestTerm) {
406
+ if (candTerm) point = cand;
407
+ }
408
+ else if (partsLen(cand) < partsLen(point)) {
409
+ point = cand;
410
+ }
411
+ }
360
412
  }
361
413
 
362
414
  if (reqselector.is_map()) {
@@ -1159,8 +1211,16 @@ inline Value makeOptions(CtxPtr ctx) {
1159
1211
  std::sort(names.begin(), names.end());
1160
1212
  bool hasTest = std::find(names.begin(), names.end(), "test") != names.end();
1161
1213
  if (hasTest) featureOrder.push_back("test");
1214
+ // Station special case, mirroring test's: its transport wrap must
1215
+ // sit immediately outside the base transport (inside retry/cache/
1216
+ // netsim), so map-form activation hoists it to just after test -
1217
+ // or first, when no test entry exists. Without this the sorted
1218
+ // default would init station last and wrap OUTSIDE the recording
1219
+ // features, turning its wire-truth events into fiction.
1220
+ bool hasStation = std::find(names.begin(), names.end(), "station") != names.end();
1221
+ if (hasStation) featureOrder.push_back("station");
1162
1222
  for (const auto& n : names) {
1163
- if (n != "test") featureOrder.push_back(n);
1223
+ if (n != "test" && n != "station") featureOrder.push_back(n);
1164
1224
  }
1165
1225
  }
1166
1226
  }
@@ -313,10 +313,44 @@ public class TestFeature : BaseFeature
313
313
  {
314
314
  var opname = op.Name;
315
315
 
316
- // Get last point from config.
316
+ // Pick the entity's own endpoint from config, not a cross-reference
317
+ // from another resource that also returns it — the same rule
318
+ // MakePoint falls back to, so the seed-data query is built from the
319
+ // endpoint the request will actually be sent to: a terminal `{id}`
320
+ // marks a record route, and failing that the shallower path wins.
317
321
  var points = StructUtils.GetPath(ctx.Config,
318
322
  StructUtils.Jt("entity", ctx.Entity!.GetName(), "op", opname, "points"));
319
- var point = StructUtils.GetElem(points, -1);
323
+ var point = StructUtils.GetElem(points, 0);
324
+ if (points is List<object?> pointList)
325
+ {
326
+ static int PointPartsLen(object? p) =>
327
+ StructUtils.GetProp(p, "parts") is List<object?> parts ? parts.Count : 0;
328
+ static bool PointTerminalParam(object? p)
329
+ {
330
+ if (StructUtils.GetProp(p, "parts") is not List<object?> parts || parts.Count == 0)
331
+ {
332
+ return false;
333
+ }
334
+ return parts[parts.Count - 1] is string last && last.StartsWith("{");
335
+ }
336
+
337
+ foreach (var candidate in pointList)
338
+ {
339
+ var candTerm = PointTerminalParam(candidate);
340
+ var bestTerm = PointTerminalParam(point);
341
+ if (candTerm != bestTerm)
342
+ {
343
+ if (candTerm)
344
+ {
345
+ point = candidate;
346
+ }
347
+ }
348
+ else if (PointPartsLen(candidate) < PointPartsLen(point))
349
+ {
350
+ point = candidate;
351
+ }
352
+ }
353
+ }
320
354
 
321
355
  // Get required params.
322
356
  var paramsPath = StructUtils.GetPath(point, StructUtils.Jt("args", "params"));
@@ -13,30 +13,48 @@ public static partial class SdkUtility
13
13
  {
14
14
  private static readonly HttpClient DefaultHttpClient = new();
15
15
 
16
- // Proxy-routed clients, cached per proxy URL (see the proxy feature's
17
- // fetchdef annotation).
16
+ // Non-following twin of DefaultHttpClient. The station feature's
17
+ // middleware sets `redirect: manual` on the fetch definition under a
18
+ // hosts egress policy: a 3xx must come back as a response like any
19
+ // other, because an automatic follow would carry injected credentials
20
+ // to a Location no policy approved (the ts donor's fetch honours the
21
+ // same key natively; redirect policy is per-handler in .NET, hence a
22
+ // second client).
23
+ private static readonly HttpClient ManualRedirectHttpClient = new(
24
+ new HttpClientHandler
25
+ {
26
+ AllowAutoRedirect = false,
27
+ });
28
+
29
+ // Proxy-routed clients, cached per proxy URL and redirect policy (see
30
+ // the proxy feature's fetchdef annotation).
18
31
  private static readonly Dictionary<string, HttpClient> ProxyClients = new();
19
32
  private static readonly object ProxyClientsLock = new();
20
33
 
21
34
  private static HttpClient ClientFor(Dictionary<string, object?> fetchdef)
22
35
  {
36
+ var manual = fetchdef.TryGetValue("redirect", out var rraw) &&
37
+ rraw is string redirect && redirect == "manual";
38
+
23
39
  if (fetchdef.TryGetValue("proxy", out var raw) && raw is string proxy && proxy != "")
24
40
  {
25
41
  lock (ProxyClientsLock)
26
42
  {
27
- if (!ProxyClients.TryGetValue(proxy, out var client))
43
+ var key = (manual ? "manual|" : "auto|") + proxy;
44
+ if (!ProxyClients.TryGetValue(key, out var client))
28
45
  {
29
46
  client = new HttpClient(new HttpClientHandler
30
47
  {
31
48
  Proxy = new System.Net.WebProxy(proxy),
32
49
  UseProxy = true,
50
+ AllowAutoRedirect = !manual,
33
51
  });
34
- ProxyClients[proxy] = client;
52
+ ProxyClients[key] = client;
35
53
  }
36
54
  return client;
37
55
  }
38
56
  }
39
- return DefaultHttpClient;
57
+ return manual ? ManualRedirectHttpClient : DefaultHttpClient;
40
58
  }
41
59
 
42
60
  internal static Dictionary<string, object?> DefaultHttpFetch(
@@ -248,6 +248,19 @@ public static partial class SdkUtility
248
248
  featureorder.Add(n);
249
249
  }
250
250
  }
251
+
252
+ // Station special case, mirroring test's: its transport wrap must
253
+ // sit immediately outside the base transport (inside retry/cache/
254
+ // netsim), so map-form activation hoists it to just after test -
255
+ // or first, when no test entry exists. Without this the sorted
256
+ // default would init station last and wrap OUTSIDE the recording
257
+ // features, turning its wire-truth events into fiction.
258
+ var si = featureorder.IndexOf("station");
259
+ if (si >= 0)
260
+ {
261
+ featureorder.RemoveAt(si);
262
+ featureorder.Insert(featureorder.IndexOf("test") + 1, "station");
263
+ }
251
264
  }
252
265
 
253
266
  var derived = new Dictionary<string, object?>
@@ -6,6 +6,27 @@ namespace ProjectNameSdk.Util;
6
6
 
7
7
  public static partial class SdkUtility
8
8
  {
9
+ // How many path segments a point has.
10
+ private static int PartsLen(Dictionary<string, object?>? point)
11
+ {
12
+ return StructUtils.GetProp(point, "parts") is List<object?> parts ? parts.Count : 0;
13
+ }
14
+
15
+ // Does this point's path end in a parameter? A record route ends in the
16
+ // record's identifier (/boards/{id}); a cross-reference that also returns
17
+ // the entity ends in the relationship's name (/posts/{id}/author). That,
18
+ // then fewest segments, is what tells the entity's own route from a
19
+ // cross-reference. The same rule runs at generation time, in
20
+ // helpers/opShape.ts — both sides must move together.
21
+ private static bool TerminalParam(Dictionary<string, object?>? point)
22
+ {
23
+ if (StructUtils.GetProp(point, "parts") is not List<object?> parts || parts.Count == 0)
24
+ {
25
+ return false;
26
+ }
27
+ return parts[parts.Count - 1] is string last && last.StartsWith("{");
28
+ }
29
+
9
30
  internal static Dictionary<string, object?>? MakePointUtil(Context ctx)
10
31
  {
11
32
  if (ctx.Out.TryGetValue("point", out var outPoint) && outPoint != null)
@@ -62,10 +83,10 @@ public static partial class SdkUtility
62
83
  }
63
84
 
64
85
  Dictionary<string, object?>? point = null;
86
+ var matched = false;
65
87
  foreach (var candidate in op.Points)
66
88
  {
67
- point = candidate;
68
- var selectDef = Helpers.ToMapAny(StructUtils.GetProp(point, "select"));
89
+ var selectDef = Helpers.ToMapAny(StructUtils.GetProp(candidate, "select"));
69
90
  var found = true;
70
91
 
71
92
  if (selector != null && selectDef != null)
@@ -98,10 +119,52 @@ public static partial class SdkUtility
98
119
 
99
120
  if (found)
100
121
  {
122
+ point = candidate;
123
+ matched = true;
101
124
  break;
102
125
  }
103
126
  }
104
127
 
128
+ // select.exist can list more than the params needed to pick a
129
+ // point, so nothing matches — fall back to the entity's own
130
+ // route rather than the last point.
131
+ if (!matched)
132
+ {
133
+ // A request naming an action reaches here only because that
134
+ // action's own point failed its exist test, so it is
135
+ // unbuildable whatever we pick. Refuse it BEFORE choosing a
136
+ // fallback: the guard below compares the chosen point's
137
+ // $action and would wave the request through whenever the
138
+ // fallback lands on the action point itself.
139
+ var unmatchedAction = reqselector != null
140
+ ? StructUtils.GetProp(reqselector, "$action") : null;
141
+ if (unmatchedAction != null)
142
+ {
143
+ throw ctx.MakeError("point_action_invalid",
144
+ "Operation \"" + op.Name +
145
+ "\" action \"" + StructUtils.Stringify(unmatchedAction) +
146
+ "\" is not valid.");
147
+ }
148
+
149
+ point = op.Points[0];
150
+ foreach (var candidate in op.Points)
151
+ {
152
+ var candTerm = TerminalParam(candidate);
153
+ var bestTerm = TerminalParam(point);
154
+ if (candTerm != bestTerm)
155
+ {
156
+ if (candTerm)
157
+ {
158
+ point = candidate;
159
+ }
160
+ }
161
+ else if (PartsLen(candidate) < PartsLen(point))
162
+ {
163
+ point = candidate;
164
+ }
165
+ }
166
+ }
167
+
105
168
  if (reqselector != null)
106
169
  {
107
170
  var reqAction = StructUtils.GetProp(reqselector, "$action");
@@ -214,10 +214,40 @@ class TestFeature extends BaseFeature {
214
214
  final param = ctx.utility.param;
215
215
 
216
216
  final opname = op.name;
217
- final point = vs.getelem(
218
- vs.getpath(ctx.config,
219
- ['entity', _entname(ctx.entity), 'op', opname, 'points']),
220
- -1);
217
+
218
+ // Pick the entity's own endpoint from config, not a cross-reference from
219
+ // another resource that also returns it — the same rule makePoint falls
220
+ // back to, so the seed-data query is built from the endpoint the request
221
+ // will actually be sent to: a terminal `{id}` marks a record route, and
222
+ // failing that the shallower path wins.
223
+ final points = vs.getpath(
224
+ ctx.config, ['entity', _entname(ctx.entity), 'op', opname, 'points']);
225
+ var point = vs.getelem(points, 0);
226
+ if (points is List) {
227
+ int pointPartsLen(dynamic p) {
228
+ final parts = vs.getprop(p, 'parts');
229
+ return parts is List ? parts.length : 0;
230
+ }
231
+
232
+ bool pointTerminalParam(dynamic p) {
233
+ final parts = vs.getprop(p, 'parts');
234
+ if (parts is! List || parts.isEmpty) {
235
+ return false;
236
+ }
237
+ final last = parts[parts.length - 1];
238
+ return last is String && last.startsWith('{');
239
+ }
240
+
241
+ for (final cand in points) {
242
+ if (pointTerminalParam(cand) != pointTerminalParam(point)) {
243
+ if (pointTerminalParam(cand)) {
244
+ point = cand;
245
+ }
246
+ } else if (pointPartsLen(cand) < pointPartsLen(point)) {
247
+ point = cand;
248
+ }
249
+ }
250
+ }
221
251
 
222
252
  final reqd = vs.transform(
223
253
  vs.select(vs.getpath(point, ['args', 'params']), {'reqd': true}),
@@ -46,6 +46,15 @@ Future<dynamic> httpFetch(dynamic fullurl, dynamic fetchdef) async {
46
46
 
47
47
  final req = await client.openUrl(method, uri);
48
48
 
49
+ // Manual redirects: fetchdef['redirect'] == 'manual' surfaces a 3xx as
50
+ // an ordinary response instead of auto-following it — HttpClient would
51
+ // otherwise replay the request, headers included, against whatever host
52
+ // Location names. Set by the station feature's transport middleware
53
+ // under an egress hosts policy (mirrors the ts fetch option).
54
+ if ('manual' == vs.getprop(fetchdef, 'redirect')) {
55
+ req.followRedirects = false;
56
+ }
57
+
49
58
  final headers = vs.getprop(fetchdef, 'headers');
50
59
  if (headers is Map) {
51
60
  headers.forEach((k, v) {
@@ -47,6 +47,12 @@ dynamic makeOptions(dynamic ctx) {
47
47
  },
48
48
  },
49
49
  'utility': {},
50
+ // Feature INSTANCES supplied at construction (the station adopt
51
+ // path): consumed by the constructor's featureAdd loop, so they are
52
+ // class instances, not data - `$ANY` accepts them verbatim. Without
53
+ // this entry the seam is dead: the constructor reads
54
+ // options.extend, but validate rejected the key.
55
+ 'extend': '`\$ANY`',
50
56
  'system': {},
51
57
  'test': {
52
58
  'active': false,
@@ -177,6 +183,17 @@ dynamic makeOptions(dynamic ctx) {
177
183
  } else {
178
184
  featureorder.addAll(names);
179
185
  }
186
+ // Station special case, mirroring test's: its transport wrap must
187
+ // sit immediately outside the base transport (inside retry/cache/
188
+ // netsim), so map-form activation hoists it to just after test -
189
+ // or first, when no test entry exists. Without this the sorted
190
+ // default would init station last and wrap OUTSIDE the recording
191
+ // features, turning its wire-truth events into fiction.
192
+ if (featureorder.contains('station')) {
193
+ featureorder.remove('station');
194
+ final ti = featureorder.indexOf('test');
195
+ featureorder.insert(-1 == ti ? 0 : ti + 1, 'station');
196
+ }
180
197
  }
181
198
 
182
199
  opts['__derived__'] = {
@@ -34,9 +34,10 @@ dynamic makePoint(dynamic ctx) {
34
34
  final selector = 'data' == op.input ? ctx.data : ctx.match;
35
35
 
36
36
  dynamic point;
37
+ var matched = false;
37
38
  for (var i = 0; i < op.points.length; i++) {
38
- point = op.points[i];
39
- final select = point.select;
39
+ final cand = op.points[i];
40
+ final select = cand.select;
40
41
  var found = true;
41
42
 
42
43
  final exist = vs.getprop(select, 'exist');
@@ -60,10 +61,55 @@ dynamic makePoint(dynamic ctx) {
60
61
  }
61
62
 
62
63
  if (found) {
64
+ point = cand;
65
+ matched = true;
63
66
  break;
64
67
  }
65
68
  }
66
69
 
70
+ // select.exist can list more than the params needed to pick a point, so
71
+ // nothing matches — fall back to the entity's own route rather than
72
+ // whichever point came last.
73
+ if (!matched) {
74
+ // A request naming an action reaches here only because that action's
75
+ // own point failed its exist test, so it is unbuildable whatever we
76
+ // pick. Refuse it BEFORE choosing a fallback: the guard below compares
77
+ // the chosen point's $action and would wave the request through
78
+ // whenever the fallback lands on the action point itself.
79
+ if (null != vs.getprop(reqselector, r'$action')) {
80
+ return ctx.error(
81
+ 'point_action_invalid',
82
+ 'Operation "' +
83
+ op.name.toString() +
84
+ '" action "' +
85
+ vs.getprop(reqselector, r'$action').toString() +
86
+ '" is not valid.');
87
+ }
88
+
89
+ // A terminal parameter marks a record route (/boards/{id}); a
90
+ // cross-reference ends in the relationship's name
91
+ // (/posts/{id}/author). Failing that, the shallower path wins.
92
+ bool terminalParam(dynamic p) {
93
+ final parts = p.parts;
94
+ if (parts.isEmpty) {
95
+ return false;
96
+ }
97
+ final last = parts[parts.length - 1];
98
+ return last is String && last.startsWith('{');
99
+ }
100
+
101
+ point = op.points[0];
102
+ for (final cand in op.points) {
103
+ if (terminalParam(cand) != terminalParam(point)) {
104
+ if (terminalParam(cand)) {
105
+ point = cand;
106
+ }
107
+ } else if (cand.parts.length < point.parts.length) {
108
+ point = cand;
109
+ }
110
+ }
111
+ }
112
+
67
113
  if (null != vs.getprop(reqselector, r'$action') &&
68
114
  null != point &&
69
115
  vs.getprop(reqselector, r'$action') !=
@@ -261,6 +261,12 @@ defmodule ProjectName.Utility do
261
261
  "entity" => %{"`$CHILD`" => %{"`$OPEN`" => true, "active" => false, "alias" => %{}}},
262
262
  "feature" => %{"`$CHILD`" => %{"`$OPEN`" => true, "active" => false}},
263
263
  "utility" => %{},
264
+ # Feature INSTANCES supplied at construction (the station adopt
265
+ # path): consumed by the constructor's extend loop, so they are
266
+ # struct feature nodes, not data - `$ANY` accepts them verbatim.
267
+ # Without this entry the seam is dead: the constructor reads
268
+ # options.extend, but validate rejected the key.
269
+ "extend" => "`$ANY`",
264
270
  "system" => %{},
265
271
  "test" => %{"active" => false, "entity" => %{"`$OPEN`" => true}},
266
272
  "clean" => %{"keys" => "key,token,id"},
@@ -359,8 +365,29 @@ defmodule ProjectName.Utility do
359
365
  fmap = if S.ismap(fmap), do: fmap, else: S.jm([])
360
366
  names = S.keysof(fmap)
361
367
 
362
- if Enum.member?(names, "test") do
363
- ["test" | Enum.reject(names, &(&1 == "test"))]
368
+ names =
369
+ if Enum.member?(names, "test") do
370
+ ["test" | Enum.reject(names, &(&1 == "test"))]
371
+ else
372
+ names
373
+ end
374
+
375
+ # Station special case, mirroring test's: its transport wrap must
376
+ # sit immediately outside the base transport (inside retry/cache/
377
+ # netsim), so map-form activation hoists it to just after test -
378
+ # or first, when no test entry exists. Without this the sorted
379
+ # default would init station last and wrap OUTSIDE the recording
380
+ # features, turning its wire-truth events into fiction.
381
+ if Enum.member?(names, "station") do
382
+ rest = Enum.reject(names, &(&1 == "station"))
383
+
384
+ at =
385
+ case Enum.find_index(rest, &(&1 == "test")) do
386
+ nil -> 0
387
+ ti -> ti + 1
388
+ end
389
+
390
+ List.insert_at(rest, at, "station")
364
391
  else
365
392
  names
366
393
  end
@@ -465,21 +492,81 @@ defmodule ProjectName.Utility do
465
492
 
466
493
  if found, do: {:halt, point}, else: {:cont, point}
467
494
  end)
468
- |> elem(1)
469
495
 
470
- err =
471
- if reqselector != nil do
472
- req_action = S.getprop(reqselector, "$action")
496
+ matched? = match?({:halt, _}, point)
473
497
 
474
- if req_action != nil and point != nil do
475
- point_select = H.to_map(S.getprop(point, "select"))
476
- point_action = S.getprop(point_select, "$action")
498
+ point =
499
+ if matched? do
500
+ elem(point, 1)
501
+ else
502
+ # select.exist can list more than the params needed to pick a
503
+ # point, so nothing matched. Fall back to the entity's own
504
+ # route: a terminal parameter marks a record route
505
+ # (/boards/{id}) where a cross-reference ends in the
506
+ # relationship's name (/posts/{id}/author), and failing that
507
+ # the shallower path wins. The same rule runs at generation
508
+ # time, in helpers/opShape.ts — both sides must move together.
509
+ parts_len = fn p ->
510
+ parts = S.getprop(p, "parts")
511
+ if S.islist(parts), do: S.size(parts), else: 0
512
+ end
513
+
514
+ terminal_param? = fn p ->
515
+ parts = S.getprop(p, "parts")
477
516
 
478
- if req_action != point_action do
479
- Context.make_error(ctx, "point_action_invalid",
480
- "Operation \"" <> opname <> "\" action \"" <> S.stringify(req_action) <> "\" is not valid.")
517
+ if S.islist(parts) and S.size(parts) > 0 do
518
+ last = S.getelem(parts, S.size(parts) - 1)
519
+ is_binary(last) and String.starts_with?(last, "{")
520
+ else
521
+ false
481
522
  end
482
523
  end
524
+
525
+ Enum.reduce(0..(npoints - 1), S.getelem(points, 0), fn i, best ->
526
+ cand = S.getelem(points, i)
527
+ ct = terminal_param?.(cand)
528
+ bt = terminal_param?.(best)
529
+
530
+ cond do
531
+ ct != bt -> if ct, do: cand, else: best
532
+ parts_len.(cand) < parts_len.(best) -> cand
533
+ true -> best
534
+ end
535
+ end)
536
+ end
537
+
538
+ unmatched_action =
539
+ if not matched? and reqselector != nil,
540
+ do: S.getprop(reqselector, "$action"),
541
+ else: nil
542
+
543
+ err =
544
+ cond do
545
+ # A request naming an action reaches the fallback only because
546
+ # that action's own point failed its exist test, so it is
547
+ # unbuildable whatever we pick. Refuse it BEFORE the guard
548
+ # below, which compares the chosen point's $action and would
549
+ # wave the request through whenever the fallback lands on the
550
+ # action point itself.
551
+ unmatched_action != nil ->
552
+ Context.make_error(ctx, "point_action_invalid",
553
+ "Operation \"" <> opname <> "\" action \"" <> S.stringify(unmatched_action) <> "\" is not valid.")
554
+
555
+ reqselector != nil ->
556
+ req_action = S.getprop(reqselector, "$action")
557
+
558
+ if req_action != nil and point != nil do
559
+ point_select = H.to_map(S.getprop(point, "select"))
560
+ point_action = S.getprop(point_select, "$action")
561
+
562
+ if req_action != point_action do
563
+ Context.make_error(ctx, "point_action_invalid",
564
+ "Operation \"" <> opname <> "\" action \"" <> S.stringify(req_action) <> "\" is not valid.")
565
+ end
566
+ end
567
+
568
+ true ->
569
+ nil
483
570
  end
484
571
 
485
572
  if err != nil do
@@ -1458,7 +1545,16 @@ defmodule ProjectName.Utility do
1458
1545
  {url, hlist}
1459
1546
  end
1460
1547
 
1461
- case :httpc.request(method, request, [], body_format: :binary) do
1548
+ # A `redirect: "manual"` annotation (set by the station feature when a
1549
+ # hosts egress policy is active - the same in-band channel go and rust
1550
+ # use) disables :httpc's automatic redirect following, so a 3xx rides
1551
+ # back as a normal response: a Location pointing off an egress
1552
+ # allowlist must never pull an automatic credentialed follow-up
1553
+ # request.
1554
+ http_opts =
1555
+ if S.getprop(fetchdef, "redirect") == "manual", do: [autoredirect: false], else: []
1556
+
1557
+ case :httpc.request(method, request, http_opts, body_format: :binary) do
1462
1558
  {:ok, {{_v, status, _reason}, resp_headers, resp_body}} ->
1463
1559
  rh =
1464
1560
  Enum.reduce(resp_headers, S.jm([]), fn {k, v}, acc ->
@@ -50,6 +50,25 @@ defmodule ProjectName.FeatureTest do
50
50
  assert has?("test")
51
51
  end
52
52
 
53
+ # === station ===
54
+
55
+ # The station adapter (installed by @voxgig/sdkgen-station) with NO open
56
+ # station must be an inert no-op that emits nothing and fails nothing
57
+ # (station design 3.1). Driven through the generated factory - never a
58
+ # compile-time module reference - so this file stays green both with and
59
+ # without the feature installed (absent, the harness simply adds no
60
+ # feature). Bound behaviour (registration, injection, wire events) is
61
+ # exercised by the station library's own suite and the consumer-side
62
+ # station validation, not here.
63
+ test "station adapter with no open station is an inert no-op" do
64
+ h = harness([fspec("station")])
65
+ res = FH.op(h, %{})
66
+ assert res.ok == true
67
+
68
+ # Inert: the transport is neither wrapped nor marked.
69
+ assert S.getprop(h.utility, "__station_wrap__") == nil
70
+ end
71
+
53
72
  # === netsim ===
54
73
 
55
74
  test "netsim fixed latency then delegate" do