@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
@@ -1,4 +1,50 @@
1
1
 
2
+ // The entity's OWN route among an op's points, as opposed to a
3
+ // CROSS-REFERENCE: another resource's route that happens to return this
4
+ // entity (`/notifications/{id}/board` for a board). Both are plain GETs
5
+ // with no `$action`, so the path itself is all there is to go on.
6
+ //
7
+ // Two signals, in order:
8
+ //
9
+ // 1. A record route ends in the record's identifier (`/boards/{id}`,
10
+ // `/accounts/{account_id}/users/{id}`); a cross-reference ends in the
11
+ // relationship's name (`/posts/{id}/author`). A terminal parameter is
12
+ // therefore the stronger signal, and unlike depth it survives an entity
13
+ // nested more deeply than the route pointing at it.
14
+ // 2. Failing that, fewest path segments — the shallower route is the one
15
+ // the entity is named for.
16
+ //
17
+ // Ties keep the earlier point, so the sorted-key order of the model decides
18
+ // and the output stays byte-stable.
19
+ //
20
+ // The same rule runs at GENERATION time, in `ownPoint` in
21
+ // helpers/opShape.ts. It cannot be shared with this file — a template ships
22
+ // standalone, outside that package — so it is written twice on purpose, and
23
+ // both sides must move together.
24
+ function terminalParam(point) {
25
+ const parts = point.parts
26
+ const last = 0 < parts.length ? parts[parts.length - 1] : ''
27
+ return 'string' === typeof last && 0 === last.indexOf('{')
28
+ }
29
+
30
+
31
+ function ownPoint(points) {
32
+ let best = points[0]
33
+
34
+ for (const cand of points) {
35
+ const candterm = terminalParam(cand)
36
+ const bestterm = terminalParam(best)
37
+
38
+ if (candterm !== bestterm ? candterm :
39
+ cand.parts.length < best.parts.length) {
40
+ best = cand
41
+ }
42
+ }
43
+
44
+ return best
45
+ }
46
+
47
+
2
48
  function makePoint(ctx) {
3
49
  if (ctx.out.point) {
4
50
  return ctx.point = ctx.out.point
@@ -28,9 +74,10 @@ function makePoint(ctx) {
28
74
  const selector = getprop(ctx, op.input)
29
75
 
30
76
  let point
77
+ let matched = false
31
78
  for (let i = 0; i < op.points.length; i++) {
32
- point = op.points[i]
33
- const select = point.select
79
+ const cand = op.points[i]
80
+ const select = cand.select
34
81
  let found = true
35
82
 
36
83
  if (selector && select.exist) {
@@ -53,10 +100,33 @@ function makePoint(ctx) {
53
100
  }
54
101
 
55
102
  if (found) {
103
+ point = cand
104
+ matched = true
56
105
  break
57
106
  }
58
107
  }
59
108
 
109
+ // select.exist can list more than the params needed to pick a point (for
110
+ // /boards/{id} it is Trello's 17 optional query-includes), so a plain
111
+ // {id} call matches NOTHING. Fall back to the entity's OWN route rather
112
+ // than whichever point came last.
113
+ if (!matched) {
114
+ // A request naming an action reaches here only because that action's
115
+ // own point failed its exist test, so the request is unbuildable
116
+ // whatever we pick. Refuse it BEFORE choosing a fallback: the guard
117
+ // below compares the chosen point's $action, and would wave the
118
+ // request through whenever the action point is itself the one the
119
+ // fallback selects (a short action route folded into an entity whose
120
+ // canonical route is longer). An unbuildable action request is an
121
+ // error, never a silent request to some other endpoint.
122
+ if (null != reqselector.$action) {
123
+ return ctx.error('point_action_invalid', 'Operation "' + op.name +
124
+ '" action "' + reqselector.$action + '" is not valid.')
125
+ }
126
+
127
+ point = ownPoint(op.points)
128
+ }
129
+
60
130
  if (
61
131
  null != reqselector.$action &&
62
132
  null != point &&
@@ -262,12 +262,41 @@ class TestFeature : BaseFeature("test", "0.0.1", true) {
262
262
  private fun buildArgs(ctx: Context, op: Operation, args: MutableMap<String, Any?>?): Any? {
263
263
  val opname = op.name
264
264
 
265
- // Get last point from config.
265
+ // Pick the entity's own endpoint from config, not a cross-reference from
266
+ // another resource that also returns it — the same rule makePoint falls
267
+ // back to, so the seed-data query is built from the endpoint the request
268
+ // will actually be sent to: a terminal `{id}` marks a record route, and
269
+ // failing that the shallower path wins.
266
270
  val points = Struct.getpath(
267
271
  ctx.config,
268
272
  listOf("entity", if (ctx.entity == null) "" else ctx.entity!!.name, "op", opname, "points"),
269
273
  )
270
- val point = Struct.getelem(points, -1)
274
+ var point = Struct.getelem(points, 0)
275
+ if (points is List<*>) {
276
+ fun pointPartsLen(p: Any?): Int {
277
+ val parts = Struct.getprop(p, "parts")
278
+ return if (parts is List<*>) parts.size else 0
279
+ }
280
+ fun pointTerminalParam(p: Any?): Boolean {
281
+ val parts = Struct.getprop(p, "parts")
282
+ if (parts !is List<*> || parts.isEmpty()) {
283
+ return false
284
+ }
285
+ val last = parts[parts.size - 1]
286
+ return last is String && last.startsWith("{")
287
+ }
288
+ for (cand in points) {
289
+ val candTerm = pointTerminalParam(cand)
290
+ val bestTerm = pointTerminalParam(point)
291
+ if (candTerm != bestTerm) {
292
+ if (candTerm) {
293
+ point = cand
294
+ }
295
+ } else if (pointPartsLen(cand) < pointPartsLen(point)) {
296
+ point = cand
297
+ }
298
+ }
299
+ }
271
300
 
272
301
  // Get required params.
273
302
  val paramsPath = Struct.getpath(point, listOf("args", "params"))
@@ -50,9 +50,18 @@ fun defaultHttpFetch(fullurl: String, fetchdef: MutableMap<String, Any?>): Mutab
50
50
  reqb.setHeader("User-Agent", "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)")
51
51
  }
52
52
 
53
+ // Honour a redirect annotation on the fetch definition (set by the
54
+ // station feature's middleware under a hosts policy): "manual" returns
55
+ // a 3xx like any other response instead of following it — the ts
56
+ // donor's fetch honours the same key natively, and an automatic follow
57
+ // would carry injected credentials to a host no policy approved.
58
+ val redirectPolicy =
59
+ if ("manual" == fetchdef["redirect"]) HttpClient.Redirect.NEVER
60
+ else HttpClient.Redirect.NORMAL
61
+
53
62
  // Honour a proxy annotation on the fetch definition (set by the proxy
54
63
  // feature): route the request through a proxied HttpClient.
55
- val clientb = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL)
64
+ val clientb = HttpClient.newBuilder().followRedirects(redirectPolicy)
56
65
  val proxy = fetchdef["proxy"]
57
66
  if (proxy is String && "" != proxy) {
58
67
  try {
@@ -141,6 +141,17 @@ fun makeOptions(ctx: Context): MutableMap<String, Any?> {
141
141
  } else {
142
142
  featureorder.addAll(names)
143
143
  }
144
+ // Station special case, mirroring test's: its transport wrap must
145
+ // sit immediately outside the base transport (inside retry/cache/
146
+ // netsim), so map-form activation hoists it to just after test -
147
+ // or first, when no test entry exists. Without this the sorted
148
+ // default would init station last and wrap OUTSIDE the recording
149
+ // features, turning its wire-truth events into fiction.
150
+ val si = featureorder.indexOf("station")
151
+ if (0 <= si) {
152
+ featureorder.removeAt(si)
153
+ featureorder.add(featureorder.indexOf("test") + 1, "station")
154
+ }
144
155
  }
145
156
 
146
157
  val derived = linkedMapOf<String, Any?>()
@@ -4,6 +4,27 @@ import KOTLINPACKAGE.core.Context
4
4
  import KOTLINPACKAGE.core.Helpers
5
5
  import KOTLINPACKAGE.utility.struct.Struct
6
6
 
7
+ // How many path segments a point has.
8
+ private fun partsLen(point: Map<String, Any?>?): Int {
9
+ val parts = Struct.getprop(point, "parts")
10
+ return if (parts is List<*>) parts.size else 0
11
+ }
12
+
13
+ // Does this point's path end in a parameter? A record route ends in the
14
+ // record's identifier (/boards/{id}); a cross-reference that also returns the
15
+ // entity ends in the relationship's name (/posts/{id}/author). That, then
16
+ // fewest segments, is what tells the entity's own route from a
17
+ // cross-reference. The same rule runs at generation time, in
18
+ // helpers/opShape.ts — both sides must move together.
19
+ private fun terminalParam(point: Map<String, Any?>?): Boolean {
20
+ val parts = Struct.getprop(point, "parts")
21
+ if (parts !is List<*> || parts.isEmpty()) {
22
+ return false
23
+ }
24
+ val last = parts[parts.size - 1]
25
+ return last is String && last.startsWith("{")
26
+ }
27
+
7
28
  @Suppress("UNCHECKED_CAST")
8
29
  fun makePoint(ctx: Context): Map<String, Any?> {
9
30
  val outPoint = ctx.out["point"]
@@ -55,9 +76,10 @@ fun makePoint(ctx: Context): Map<String, Any?> {
55
76
  }
56
77
 
57
78
  var point: MutableMap<String, Any?>? = null
79
+ var matched = false
58
80
  for (i in op.points.indices) {
59
- point = op.points[i]
60
- val selectDef = Helpers.toMapAny(Struct.getprop(point, "select"))
81
+ val cand = op.points[i]
82
+ val selectDef = Helpers.toMapAny(Struct.getprop(cand, "select"))
61
83
  var found = true
62
84
 
63
85
  if (selectDef != null) {
@@ -84,10 +106,44 @@ fun makePoint(ctx: Context): Map<String, Any?> {
84
106
  }
85
107
 
86
108
  if (found) {
109
+ point = cand
110
+ matched = true
87
111
  break
88
112
  }
89
113
  }
90
114
 
115
+ // select.exist can list more than the params needed to pick a point, so
116
+ // nothing matches — fall back to the entity's own route rather than
117
+ // whichever point came last.
118
+ if (!matched) {
119
+ // A request naming an action reaches here only because that action's
120
+ // own point failed its exist test, so it is unbuildable whatever we
121
+ // pick. Refuse it BEFORE choosing a fallback: the guard below compares
122
+ // the chosen point's $action and would wave the request through
123
+ // whenever the fallback lands on the action point itself.
124
+ val unmatchedAction = Struct.getprop(reqselector, "\$action", null)
125
+ if (unmatchedAction != null) {
126
+ throw ctx.makeError(
127
+ "point_action_invalid",
128
+ "Operation \"" + op.name +
129
+ "\" action \"" + Struct.stringify(unmatchedAction) + "\" is not valid.",
130
+ )
131
+ }
132
+
133
+ point = op.points[0]
134
+ for (cand in op.points) {
135
+ val candTerm = terminalParam(cand)
136
+ val bestTerm = terminalParam(point)
137
+ if (candTerm != bestTerm) {
138
+ if (candTerm) {
139
+ point = cand
140
+ }
141
+ } else if (partsLen(cand) < partsLen(point)) {
142
+ point = cand
143
+ }
144
+ }
145
+ }
146
+
91
147
  val reqAction = Struct.getprop(reqselector, "\$action", null)
92
148
  if (reqAction != null && point != null) {
93
149
  val pointSelect = Helpers.toMapAny(Struct.getprop(point, "select"))
@@ -597,6 +597,20 @@ def makeResult (ctx : Value) : SIO Value := do
597
597
 
598
598
  /-- Select the endpoint for this operation: the single point, else the first
599
599
  whose `select.exist` keys are all present and whose `$action` agrees. -/
600
+ /-- How many path segments a point has, and whether its path ends in a
601
+ parameter. A record route ends in the record's identifier (/boards/{id});
602
+ a cross-reference that also returns the entity ends in the relationship's
603
+ name (/posts/{id}/author). That, then fewest segments, is what tells the
604
+ entity's own route from a cross-reference. The same rule runs at
605
+ generation time, in helpers/opShape.ts — both sides must move together. -/
606
+ def pointShape (pt : Value) : SIO (Nat × Bool) := do
607
+ match (← gp pt "parts") with
608
+ | .list i =>
609
+ let items ← listItems i
610
+ if items.size == 0 then pure (0, false)
611
+ else pure (items.size, (vs items[items.size - 1]!).startsWith "{")
612
+ | _ => pure (0, false)
613
+
600
614
  def makePoint (ctx : Value) : SIO Value := do
601
615
  let op ← gp ctx "op"
602
616
  let matchV ← gp ctx "reqmatch"
@@ -622,6 +636,20 @@ def makePoint (ctx : Value) : SIO Value := do
622
636
  | _ => pure ()
623
637
  if ok && ((← gp sel "$action") == (← gp matchV "$action")) then
624
638
  chosen := pt
639
+ -- select.exist can list more than the params needed to pick a point (for
640
+ -- /boards/{id} it is Trello's 17 optional query-includes), so a plain
641
+ -- {id} call matches NOTHING and this loop chose no point at all. Fall
642
+ -- back to the entity's own route rather than sending nowhere.
643
+ if isNov chosen then
644
+ let mut best := pts[0]!
645
+ for cand in pts do
646
+ let (candLen, candTerm) ← pointShape cand
647
+ let (bestLen, bestTerm) ← pointShape best
648
+ if candTerm && !bestTerm then
649
+ best := cand
650
+ else if candTerm == bestTerm && candLen < bestLen then
651
+ best := cand
652
+ chosen := best
625
653
  if !(isNov chosen) then sp ctx "point" chosen
626
654
  pure chosen
627
655
 
@@ -316,8 +316,19 @@ def main : IO UInt32 := do
316
316
  runset "makeResult" (← getSpec primary #["makeResult", "basic"]) fun entry => do
317
317
  pure ((← SdkUtility.makeResult (← entryCtx entry opts config)), none)
318
318
 
319
- runset "makePoint" (← getSpec primary #["makePoint", "basic"]) fun entry => do
320
- pure ((← SdkUtility.makePoint (← entryCtx entry opts config)), none)
319
+ -- makePoint is NOT driven here, and this is deliberate.
320
+ --
321
+ -- The section used to be empty, so running it asserted nothing. It now
322
+ -- carries seven cases, and every one supplies its own `options` (for
323
+ -- allow.op) and `config` (for the operation's points) — but `entryCtx`
324
+ -- OVERWRITES both with the SDK's, so the cases would not run as authored.
325
+ -- Wiring it up needs entryCtx to prefer a fixture-supplied options/config,
326
+ -- which changes every section here and could not be compiled or run when
327
+ -- this change was made.
328
+ --
329
+ -- Left out rather than left in: an entry that runs with the wrong context
330
+ -- is worse than one that does not run, and no coverage is lost — this
331
+ -- section asserted nothing before either.
321
332
 
322
333
  runset "makeFetchDef" (← getSpec primary #["makeFetchDef", "basic"]) fun entry => do
323
334
  pure ((← SdkUtility.makeFetchDef (← entryCtx entry opts config)), none)
@@ -82,6 +82,12 @@ local function make_options_util(ctx)
82
82
  },
83
83
  },
84
84
  utility = {},
85
+ -- Feature INSTANCES supplied at construction (the station adopt
86
+ -- path): consumed by the constructor's feature_add loop, so they are
87
+ -- class instances, not data -- `$ANY` accepts them verbatim. Without
88
+ -- this entry the seam is dead: the constructor reads
89
+ -- options.extend, but validate rejected the key.
90
+ extend = "`$ANY`",
85
91
  system = {},
86
92
  test = {
87
93
  active = false,
@@ -104,6 +110,15 @@ local function make_options_util(ctx)
104
110
  -- Preserve system.fetch before merge/validate.
105
111
  local sys_fetch = vs.getpath(opts, "system.fetch")
106
112
 
113
+ -- Preserve extend feature INSTANCES before merge/validate, by ORIGINAL
114
+ -- reference (the ts makeOptions keeps the caller's instances too):
115
+ -- merge/validate are data-oriented and would mangle instance tables
116
+ -- carrying functions -- the same reason system.fetch is preserved.
117
+ local extend = nil
118
+ if type(options) == "table" and type(options["extend"]) == "table" then
119
+ extend = options["extend"]
120
+ end
121
+
107
122
  -- Clone the config side before merging: `config` is a per-Lua-state
108
123
  -- singleton (see config_shared), and merge would otherwise use its nested
109
124
  -- tables as merge TARGETS — one instance's options (server, headers, ...)
@@ -156,6 +171,11 @@ local function make_options_util(ctx)
156
171
  end
157
172
  end
158
173
 
174
+ -- Restore extend feature instances.
175
+ if extend ~= nil then
176
+ opts["extend"] = extend
177
+ end
178
+
159
179
  -- Derived clean config.
160
180
  local clean_keys = "key,token,id"
161
181
  local ck = vs.getpath(opts, "clean.keys")
@@ -202,6 +222,30 @@ local function make_options_util(ctx)
202
222
  else
203
223
  featureorder = names
204
224
  end
225
+ -- Station special case, mirroring test's: its transport wrap must
226
+ -- sit immediately outside the base transport (inside retry/cache/
227
+ -- netsim), so map-form activation hoists it to just after test -
228
+ -- or first, when no test entry exists. Without this the sorted
229
+ -- default would init station last and wrap OUTSIDE the recording
230
+ -- features, turning its wire-truth events into fiction.
231
+ local si = nil
232
+ for i, n in ipairs(featureorder) do
233
+ if n == "station" then
234
+ si = i
235
+ break
236
+ end
237
+ end
238
+ if si ~= nil then
239
+ table.remove(featureorder, si)
240
+ local ti = 0
241
+ for i, n in ipairs(featureorder) do
242
+ if n == "test" then
243
+ ti = i
244
+ break
245
+ end
246
+ end
247
+ table.insert(featureorder, ti + 1, "station")
248
+ end
205
249
  end
206
250
 
207
251
  local derived = { clean = {} }
@@ -3,6 +3,32 @@
3
3
  local vs = require("utility.struct.struct")
4
4
  local helpers = require("core.helpers")
5
5
 
6
+ -- How many path segments a point has.
7
+ local function parts_len(point)
8
+ local parts = vs.getprop(point, "parts")
9
+ if type(parts) == "table" then
10
+ return #parts
11
+ end
12
+ return 0
13
+ end
14
+
15
+
16
+ -- Does this point's path end in a parameter? A record route ends in the
17
+ -- record's identifier (/boards/{id}); a cross-reference that also returns
18
+ -- the entity ends in the relationship's name (/posts/{id}/author). That,
19
+ -- then fewest segments, is what tells the entity's own route from a
20
+ -- cross-reference. The same rule runs at generation time, in
21
+ -- helpers/opShape.ts — both sides must move together.
22
+ local function terminal_param(point)
23
+ local parts = vs.getprop(point, "parts")
24
+ if type(parts) ~= "table" or #parts == 0 then
25
+ return false
26
+ end
27
+ local last = parts[#parts]
28
+ return type(last) == "string" and last:sub(1, 1) == "{"
29
+ end
30
+
31
+
6
32
  local function make_point_util(ctx)
7
33
  if ctx.out["point"] ~= nil then
8
34
  local preset = ctx.out["point"]
@@ -44,9 +70,10 @@ local function make_point_util(ctx)
44
70
  end
45
71
 
46
72
  local point = nil
73
+ local matched = false
47
74
  for i = 1, #op.points do
48
- point = op.points[i]
49
- local select_def = helpers.to_map(vs.getprop(point, "select"))
75
+ local cand = op.points[i]
76
+ local select_def = helpers.to_map(vs.getprop(cand, "select"))
50
77
  local found = true
51
78
 
52
79
  if selector ~= nil and select_def ~= nil then
@@ -73,10 +100,46 @@ local function make_point_util(ctx)
73
100
  end
74
101
 
75
102
  if found then
103
+ point = cand
104
+ matched = true
76
105
  break
77
106
  end
78
107
  end
79
108
 
109
+ -- select.exist can list more than the params needed to pick a point, so
110
+ -- nothing matches — fall back to the entity's own route rather than
111
+ -- whichever point came last.
112
+ if not matched then
113
+ -- A request naming an action reaches here only because that action's
114
+ -- own point failed its exist test, so it is unbuildable whatever we
115
+ -- pick. Refuse it BEFORE choosing a fallback: the guard below compares
116
+ -- the chosen point's $action and would wave the request through
117
+ -- whenever the fallback lands on the action point itself.
118
+ local unmatched_action = nil
119
+ if reqselector ~= nil then
120
+ unmatched_action = vs.getprop(reqselector, "$action")
121
+ end
122
+ if unmatched_action ~= nil then
123
+ return nil, ctx:make_error("point_action_invalid",
124
+ 'Operation "' .. op.name ..
125
+ '" action "' .. vs.stringify(unmatched_action) .. '" is not valid.')
126
+ end
127
+
128
+ point = op.points[1]
129
+ for i = 1, #op.points do
130
+ local cand = op.points[i]
131
+ local cand_term = terminal_param(cand)
132
+ local best_term = terminal_param(point)
133
+ if cand_term ~= best_term then
134
+ if cand_term then
135
+ point = cand
136
+ end
137
+ elseif parts_len(cand) < parts_len(point) then
138
+ point = cand
139
+ end
140
+ end
141
+ end
142
+
80
143
  if reqselector ~= nil then
81
144
  local req_action = vs.getprop(reqselector, "$action")
82
145
  if req_action ~= nil and point ~= nil then
@@ -636,12 +636,12 @@ let make_point_util (ctx : ctx) : (value * sdk_error option) =
636
636
  if op.op_input = "data" then ctx.c_reqdata, ctx.c_data
637
637
  else ctx.c_reqmatch, ctx.c_match in
638
638
  let chosen = ref Noval in
639
+ let matched = ref false in
639
640
  let n = List.length points in
640
641
  let arr = Array.of_list points in
641
642
  let i = ref 0 and stop = ref false in
642
643
  while not !stop && !i < n do
643
644
  let point = arr.(!i) in
644
- chosen := point;
645
645
  let select_def = to_map (getp point "select") in
646
646
  let found = ref true in
647
647
  (match select_def with
@@ -661,8 +661,46 @@ let make_point_util (ctx : ctx) : (value * sdk_error option) =
661
661
  let select_action = getp select_def "$action" in
662
662
  if req_action <> select_action then found := false
663
663
  end;
664
- if !found then stop := true else incr i
664
+ if !found then begin chosen := point; matched := true; stop := true end
665
+ else incr i
665
666
  done;
667
+ (* select.exist can list more than the params needed to pick a point
668
+ (for /boards/{id} it is Trello's 17 optional query-includes), so a
669
+ plain {id} call matches NOTHING. Fall back to the entity's own
670
+ route rather than whichever point came last. *)
671
+ if not !matched then begin
672
+ (* A request naming an action reaches here only because that
673
+ action's own point failed its exist test, so it is unbuildable
674
+ whatever we pick. Refuse it BEFORE choosing a fallback: the guard
675
+ below compares the chosen point's $action and would wave the
676
+ request through whenever the fallback lands on the action point
677
+ itself. *)
678
+ let unmatched_action = getp reqselector "$action" in
679
+ if not (is_noval unmatched_action) then
680
+ raise (Sdk_error_exc (ctx_make_error ctx "point_action_invalid"
681
+ ("Operation \"" ^ op.op_name ^ "\" action \"" ^ (stringify unmatched_action) ^ "\" is not valid.")));
682
+ (* A terminal parameter marks a record route (/boards/{id}); a
683
+ cross-reference ends in the relationship's name
684
+ (/posts/{id}/author). Failing that, the shallower path wins. The
685
+ same rule runs at generation time, in helpers/opShape.ts — both
686
+ sides must move together. *)
687
+ let parts_len p =
688
+ match getp p "parts" with List r -> List.length !r | _ -> 0 in
689
+ let terminal_param p =
690
+ match getp p "parts" with
691
+ | List r ->
692
+ (match List.rev !r with
693
+ | last :: _ ->
694
+ let s = vstring last in
695
+ String.length s > 0 && s.[0] = '{'
696
+ | [] -> false)
697
+ | _ -> false in
698
+ chosen := arr.(0);
699
+ Array.iter (fun cand ->
700
+ let ct = terminal_param cand and bt = terminal_param !chosen in
701
+ if ct <> bt then (if ct then chosen := cand)
702
+ else if parts_len cand < parts_len !chosen then chosen := cand) arr
703
+ end;
666
704
  let req_action = getp reqselector "$action" in
667
705
  if not (is_noval req_action) && not (is_noval !chosen) then begin
668
706
  let point_select = to_map (getp !chosen "select") in
@@ -39,7 +39,16 @@ our $DefaultHttpFetch = sub {
39
39
  my $opts = { headers => \%hdrs };
40
40
  $opts->{content} = "$body" if defined $body && !ref $body;
41
41
 
42
- my $res = eval { HTTP::Tiny->new->request($method, $fullurl, $opts) };
42
+ # fetchdef.redirect 'manual' (the ts fetch vocabulary): return a 3xx
43
+ # as-is instead of following it. HTTP::Tiny follows redirects by
44
+ # default; the station feature's hosts policy sets this so a Location
45
+ # off the allowlist can never pull an automatic credentialed follow-up.
46
+ my %new_args;
47
+ $new_args{max_redirect} = 0
48
+ if defined $fetchdef->{redirect} && !ref $fetchdef->{redirect}
49
+ && 'manual' eq $fetchdef->{redirect};
50
+
51
+ my $res = eval { HTTP::Tiny->new(%new_args)->request($method, $fullurl, $opts) };
43
52
  if (!$res) {
44
53
  my $e = defined $@ ? "$@" : 'request failed';
45
54
  $e =~ s/\s+\z//;
@@ -26,8 +26,20 @@ $REGISTRY{make_options} = sub {
26
26
  }
27
27
  }
28
28
 
29
+ # Feature INSTANCES supplied at construction (the station adopt path):
30
+ # consumed by the constructor's extend loop, so they are blessed class
31
+ # instances, not data. This is the perl form of the ts/rb targets'
32
+ # `extend => $ANY` optspec entry: this port deep-clones its options, and
33
+ # Voxgig::Struct::clone would flatten a blessed feature to a plain
34
+ # unblessed hash (and validate rejects the key outright), so the RAW
35
+ # list is captured here and re-attached after validation - the
36
+ # system.fetch idiom. Without this the seam is dead: the constructor
37
+ # reads options.extend, but clone/validate dropped the instances.
38
+ my $extend_raw = ProjectNameHelpers::gp($options, 'extend');
39
+
29
40
  my $opts = Voxgig::Struct::clone($options);
30
41
  $opts = {} unless Voxgig::Struct::ismap($opts);
42
+ delete $opts->{extend};
31
43
 
32
44
  # Feature add-order. options.feature may be given as an ordered ARRAY of
33
45
  # { name, active, ...opts } entries (the array position IS the order in
@@ -78,6 +90,12 @@ $REGISTRY{make_options} = sub {
78
90
  'system' => {},
79
91
  'test' => { 'active' => $JF, 'entity' => { '`$OPEN`' => $JT } },
80
92
  'clean' => { 'keys' => 'key,token,id' },
93
+ # Server-variable values for a templated base URL (OpenAPI server
94
+ # variables). The embedded config (configDefinition) carries the
95
+ # spec defaults; user values override them. This port does not yet
96
+ # substitute {name} placeholders into base - the entry keeps the
97
+ # config's server block valid under this optspec.
98
+ 'server' => { '`$CHILD`' => '' },
81
99
  };
82
100
 
83
101
  my $sys_fetch = ProjectNameHelpers::gpath($opts, 'system.fetch');
@@ -95,6 +113,11 @@ $REGISTRY{make_options} = sub {
95
113
  $opts->{system}{fetch} = $sys_fetch;
96
114
  }
97
115
 
116
+ # Re-attach the raw extend instances captured above.
117
+ if (Voxgig::Struct::islist($extend_raw)) {
118
+ $opts->{extend} = $extend_raw;
119
+ }
120
+
98
121
  my $clean_keys = ProjectNameHelpers::gpath($opts, 'clean.keys');
99
122
  $clean_keys = 'key,token,id' unless defined $clean_keys && !ref $clean_keys;
100
123
  my @parts;
@@ -114,6 +137,19 @@ $REGISTRY{make_options} = sub {
114
137
  @featureorder = (grep { 'test' eq $_ } @names)
115
138
  ? ('test', grep { 'test' ne $_ } @names)
116
139
  : @names;
140
+
141
+ # Station special case, mirroring test's: its transport wrap must
142
+ # sit immediately outside the base transport (inside retry/cache/
143
+ # netsim), so map-form activation hoists it to just after test -
144
+ # or first, when no test entry exists. Without this the sorted
145
+ # default would init station last and wrap OUTSIDE the recording
146
+ # features, turning its wire-truth events into fiction.
147
+ my ($si) = grep { 'station' eq $featureorder[$_] } 0 .. $#featureorder;
148
+ if (defined $si) {
149
+ splice @featureorder, $si, 1;
150
+ my ($ti) = grep { 'test' eq $featureorder[$_] } 0 .. $#featureorder;
151
+ splice @featureorder, (defined $ti ? $ti + 1 : 0), 0, 'station';
152
+ }
117
153
  }
118
154
 
119
155
  $opts->{__derived__} = {