@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
@@ -17,6 +17,27 @@ package ProjectNameUtilities;
17
17
 
18
18
  our %REGISTRY;
19
19
 
20
+ # How many path segments a point has.
21
+ sub _parts_len {
22
+ my ($point) = @_;
23
+ my $parts = ProjectNameHelpers::gp($point, 'parts');
24
+ return Voxgig::Struct::islist($parts) ? scalar(@$parts) : 0;
25
+ }
26
+
27
+ # Does this point's path end in a parameter? A record route ends in the
28
+ # record's identifier (/boards/{id}); a cross-reference that also returns the
29
+ # entity ends in the relationship's name (/posts/{id}/author). That, then
30
+ # fewest segments, is what tells the entity's own route from a
31
+ # cross-reference. The same rule runs at generation time, in
32
+ # helpers/opShape.ts — both sides must move together.
33
+ sub _terminal_param {
34
+ my ($point) = @_;
35
+ my $parts = ProjectNameHelpers::gp($point, 'parts');
36
+ return 0 unless Voxgig::Struct::islist($parts) && 0 < scalar(@$parts);
37
+ my $last = $parts->[-1];
38
+ return (defined $last && !ref $last && $last =~ /^\{/) ? 1 : 0;
39
+ }
40
+
20
41
  $REGISTRY{make_point} = sub {
21
42
  my ($ctx) = @_;
22
43
 
@@ -54,8 +75,8 @@ $REGISTRY{make_point} = sub {
54
75
  my $selector = ('data' eq $op->{input}) ? $ctx->{data} : $ctx->{match};
55
76
 
56
77
  my $point;
78
+ my $matched = 0;
57
79
  for my $p (@{ $op->{points} }) {
58
- $point = $p;
59
80
  my $select_def = ProjectNameHelpers::to_map(ProjectNameHelpers::gp($p, 'select'));
60
81
  my $found = 1;
61
82
 
@@ -79,7 +100,39 @@ $REGISTRY{make_point} = sub {
79
100
  $found = 0 unless ProjectNameHelpers::eqv($req_action, $select_action);
80
101
  }
81
102
 
82
- last if $found;
103
+ if ($found) {
104
+ $point = $p;
105
+ $matched = 1;
106
+ last;
107
+ }
108
+ }
109
+
110
+ # select.exist can list more than the params needed to pick a point, so
111
+ # nothing matches — fall back to the entity's own route rather than
112
+ # whichever point came last.
113
+ unless ($matched) {
114
+ # A request naming an action reaches here only because that action's
115
+ # own point failed its exist test, so it is unbuildable whatever we
116
+ # pick. Refuse it BEFORE choosing a fallback: the guard below compares
117
+ # the chosen point's $action and would wave the request through
118
+ # whenever the fallback lands on the action point itself.
119
+ my $unmatched_action = $reqselector
120
+ ? ProjectNameHelpers::gp($reqselector, '$action') : undef;
121
+ if (defined $unmatched_action) {
122
+ return (undef, $ctx->make_error('point_action_invalid',
123
+ 'Operation "' . $op->{name} . '" action "' .
124
+ Voxgig::Struct::stringify($unmatched_action) . '" is not valid.'));
125
+ }
126
+
127
+ $point = $op->{points}[0];
128
+ for my $p (@{ $op->{points} }) {
129
+ if (_terminal_param($p) != _terminal_param($point)) {
130
+ $point = $p if _terminal_param($p);
131
+ }
132
+ elsif (_parts_len($p) < _parts_len($point)) {
133
+ $point = $p;
134
+ }
135
+ }
83
136
  }
84
137
 
85
138
  if ($reqselector) {
@@ -369,11 +369,40 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
369
369
  // any missing piece falls back to "no required params".
370
370
  $reqd_names = [];
371
371
  if (is_string($opname) && is_string($entityName) && isset($ctx->config)) {
372
+ // Pick the entity's own endpoint, not a cross-reference from
373
+ // another resource that also returns it — the same rule
374
+ // MakePoint falls back to, so the seed-data query is built from
375
+ // the endpoint the request will actually be sent to: a terminal
376
+ // `{id}` marks a record route, and failing that the shallower
377
+ // path wins.
372
378
  $points = \Voxgig\Struct\Struct::getpath(
373
379
  ['entity', $entityName, 'op', $opname, 'points'],
374
380
  $ctx->config
375
381
  );
376
- $point = \Voxgig\Struct\Struct::getelem($points, -1);
382
+ $point = \Voxgig\Struct\Struct::getelem($points, 0);
383
+ if (is_array($points)) {
384
+ $point_parts_len = function ($p) {
385
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
386
+ return is_array($parts) ? count($parts) : 0;
387
+ };
388
+ $point_terminal_param = function ($p) {
389
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
390
+ if (!is_array($parts) || 0 === count($parts)) {
391
+ return false;
392
+ }
393
+ $last = $parts[count($parts) - 1];
394
+ return is_string($last) && 0 === strpos($last, '{');
395
+ };
396
+ foreach ($points as $cand) {
397
+ if ($point_terminal_param($cand) !== $point_terminal_param($point)) {
398
+ if ($point_terminal_param($cand)) {
399
+ $point = $cand;
400
+ }
401
+ } elseif ($point_parts_len($cand) < $point_parts_len($point)) {
402
+ $point = $cand;
403
+ }
404
+ }
405
+ }
377
406
  $params = is_array($point) ? ($point['args']['params'] ?? null) : null;
378
407
  if (is_array($params)) {
379
408
  foreach ($params as $p) {
@@ -154,6 +154,26 @@ class PipelineTest extends TestCase
154
154
  ]));
155
155
  }
156
156
 
157
+ // Station special case, mirroring test's: its transport wrap must sit
158
+ // immediately outside the base transport, so map-form activation hoists
159
+ // it to just after test - or first, when no test entry exists.
160
+ public function test_feature_order_map_hoists_station_after_test(): void
161
+ {
162
+ $this->assertSame('test,station,metrics', self::resolve_order([
163
+ 'metrics' => ['active' => true],
164
+ 'station' => ['active' => true],
165
+ 'test' => ['active' => true],
166
+ ]));
167
+ }
168
+
169
+ public function test_feature_order_map_hoists_station_first_without_test(): void
170
+ {
171
+ $this->assertSame('station,metrics', self::resolve_order([
172
+ 'metrics' => ['active' => true],
173
+ 'station' => ['active' => true],
174
+ ]));
175
+ }
176
+
157
177
  public function test_feature_order_no_test_deterministic(): void
158
178
  {
159
179
  $this->assertSame('cache,retry', self::resolve_order([
@@ -35,7 +35,8 @@ class ProjectNameFetcher
35
35
  // Prefer cURL when available — its header capture is reliable across
36
36
  // PHP versions, while file_get_contents + custom stream wrappers
37
37
  // don't propagate $http_response_header for user-defined wrappers
38
- // in PHP 8.3+.
38
+ // in PHP 8.3+. (cURL is configured with FOLLOWLOCATION off, so the
39
+ // manual-redirect annotation below holds there by construction.)
39
40
  if (function_exists('curl_init')) {
40
41
  return self::curlFetch($fullurl, $method_str, $body_str, $header_lines);
41
42
  }
@@ -47,6 +48,16 @@ class ProjectNameFetcher
47
48
  ],
48
49
  ];
49
50
 
51
+ // Honour a redirect annotation on the fetch definition (set by the
52
+ // station feature's middleware under a hosts policy): "manual"
53
+ // surfaces a 3xx as an ordinary response instead of auto-following
54
+ // it — the http stream wrapper otherwise replays the request,
55
+ // headers included, against whatever host Location names, carrying
56
+ // injected credentials to a host no policy approved.
57
+ if (($fetchdef['redirect'] ?? null) === 'manual') {
58
+ $opts['http']['follow_location'] = 0;
59
+ }
60
+
50
61
  if (is_string($body_str)) {
51
62
  $opts['http']['content'] = $body_str;
52
63
  }
@@ -37,6 +37,34 @@ class ProjectNameMakeOptions
37
37
  // Preserve system.fetch before merge/validate.
38
38
  $sys_fetch = \Voxgig\Struct\Struct::getpath($options, 'system.fetch');
39
39
 
40
+ // Feature INSTANCES supplied at construction (the station adopt path)
41
+ // are consumed by the constructor's feature-add loop straight from the
42
+ // RAW construction options - extend is consumed exactly once, at
43
+ // construction. Strip it here so it never enters the cloned option
44
+ // map: Struct::clone flattens arbitrary objects, and options_map()
45
+ // re-clones $this->options on every request.
46
+ if (isset($options['extend'])) {
47
+ unset($options['extend']);
48
+ }
49
+
50
+ // The station feature's binding handle and caller opts are live
51
+ // values (connect/adopt/st->options() plant them): hold them aside so
52
+ // the clone below cannot flatten them, and reattach after validate.
53
+ $station_handle = null;
54
+ $station_caller = null;
55
+ $station_has_caller = false;
56
+ if (is_array($options['feature']['station'] ?? null)) {
57
+ if (is_object($options['feature']['station']['station'] ?? null)) {
58
+ $station_handle = $options['feature']['station']['station'];
59
+ unset($options['feature']['station']['station']);
60
+ }
61
+ if (array_key_exists('calleropts', $options['feature']['station'])) {
62
+ $station_caller = $options['feature']['station']['calleropts'];
63
+ $station_has_caller = true;
64
+ unset($options['feature']['station']['calleropts']);
65
+ }
66
+ }
67
+
40
68
  $opts = \Voxgig\Struct\Struct::clone($options);
41
69
  $opts = self::to_array_deep($opts);
42
70
  if (!is_array($opts)) {
@@ -106,6 +134,24 @@ class ProjectNameMakeOptions
106
134
  $opts = [];
107
135
  }
108
136
 
137
+ // Reattach the station binding handle held aside above (the feature
138
+ // optspec is `$OPEN`, so the extra keys are legal; they were only
139
+ // kept out of the clone/validate round-trip).
140
+ if (null !== $station_handle || $station_has_caller) {
141
+ if (!is_array($opts['feature'] ?? null)) {
142
+ $opts['feature'] = [];
143
+ }
144
+ if (!is_array($opts['feature']['station'] ?? null)) {
145
+ $opts['feature']['station'] = [];
146
+ }
147
+ if (null !== $station_handle) {
148
+ $opts['feature']['station']['station'] = $station_handle;
149
+ }
150
+ if ($station_has_caller) {
151
+ $opts['feature']['station']['calleropts'] = $station_caller;
152
+ }
153
+ }
154
+
109
155
  // Resolve a templated base URL (e.g. https://{tenant_id}.hanko.io).
110
156
  // Every placeholder must resolve to a non-empty value: from
111
157
  // $options['server'] (user), else the config default. A placeholder
@@ -181,6 +227,18 @@ class ProjectNameMakeOptions
181
227
  } else {
182
228
  $featureorder = $names;
183
229
  }
230
+ // Station special case, mirroring test's: its transport wrap must
231
+ // sit immediately outside the base transport (inside retry/cache/
232
+ // netsim), so map-form activation hoists it to just after test -
233
+ // or first, when no test entry exists. Without this the sorted
234
+ // default would init station last and wrap OUTSIDE the recording
235
+ // features, turning its wire-truth events into fiction.
236
+ $si = array_search('station', $featureorder, true);
237
+ if (false !== $si) {
238
+ array_splice($featureorder, $si, 1);
239
+ $ti = array_search('test', $featureorder, true);
240
+ array_splice($featureorder, false === $ti ? 0 : $ti + 1, 0, ['station']);
241
+ }
184
242
  }
185
243
 
186
244
  $derived = ['clean' => $keyre === '' ? [] : ['keyre' => $keyre]];
@@ -42,8 +42,8 @@ class ProjectNameMakePoint
42
42
  $selector = $op->input === 'data' ? $ctx->data : $ctx->match;
43
43
 
44
44
  $point = null;
45
+ $matched = false;
45
46
  foreach ($op->points as $p) {
46
- $point = $p;
47
47
  $select_def = ProjectNameHelpers::to_map(\Voxgig\Struct\Struct::getprop($p, 'select'));
48
48
  $found = true;
49
49
 
@@ -70,10 +70,58 @@ class ProjectNameMakePoint
70
70
  }
71
71
 
72
72
  if ($found) {
73
+ $point = $p;
74
+ $matched = true;
73
75
  break;
74
76
  }
75
77
  }
76
78
 
79
+ // select.exist can list more than the params needed to pick a
80
+ // point, so nothing matches — fall back to the entity's own
81
+ // route rather than the last point.
82
+ if (!$matched) {
83
+ // A request naming an action reaches here only because that
84
+ // action's own point failed its exist test, so it is
85
+ // unbuildable whatever we pick. Refuse it BEFORE choosing a
86
+ // fallback: the guard below compares the chosen point's
87
+ // $action and would wave the request through whenever the
88
+ // fallback lands on the action point itself.
89
+ $unmatched_action = $reqselector
90
+ ? \Voxgig\Struct\Struct::getprop($reqselector, '$action') : null;
91
+ if (null !== $unmatched_action) {
92
+ return [null, $ctx->make_error('point_action_invalid',
93
+ "Operation \"{$op->name}\" action \"" .
94
+ \Voxgig\Struct\Struct::stringify($unmatched_action) .
95
+ "\" is not valid.")];
96
+ }
97
+
98
+ // A terminal parameter marks a record route (/boards/{id});
99
+ // a cross-reference ends in the relationship's name
100
+ // (/posts/{id}/author). Failing that, the shallower wins.
101
+ $parts_len = function ($p) {
102
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
103
+ return is_array($parts) ? count($parts) : 0;
104
+ };
105
+ $terminal_param = function ($p) {
106
+ $parts = \Voxgig\Struct\Struct::getprop($p, 'parts');
107
+ if (!is_array($parts) || 0 === count($parts)) {
108
+ return false;
109
+ }
110
+ $last = $parts[count($parts) - 1];
111
+ return is_string($last) && 0 === strpos($last, '{');
112
+ };
113
+ $point = $op->points[0];
114
+ foreach ($op->points as $p) {
115
+ if ($terminal_param($p) !== $terminal_param($point)) {
116
+ if ($terminal_param($p)) {
117
+ $point = $p;
118
+ }
119
+ } elseif ($parts_len($p) < $parts_len($point)) {
120
+ $point = $p;
121
+ }
122
+ }
123
+ }
124
+
77
125
  if ($reqselector) {
78
126
  $req_action = \Voxgig\Struct\Struct::getprop($reqselector, '$action');
79
127
  if ($req_action && $point) {
@@ -35,8 +35,20 @@ def _default_http_fetch(fullurl, fetchdef):
35
35
  if not has_ua:
36
36
  req.add_header("User-Agent", _DEFAULT_USER_AGENT)
37
37
 
38
+ # Manual redirects: fetchdef["redirect"] == "manual" surfaces a 3xx as
39
+ # an ordinary response instead of auto-following it — urllib would
40
+ # otherwise replay the request, headers included, against whatever host
41
+ # Location names. Set by the station feature's transport middleware
42
+ # under an egress hosts policy (mirrors the ts fetch option).
43
+ opener = None
44
+ if fetchdef.get("redirect") == "manual":
45
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
46
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
47
+ return None
48
+ opener = urllib.request.build_opener(_NoRedirect)
49
+
38
50
  try:
39
- resp = urllib.request.urlopen(req)
51
+ resp = opener.open(req) if opener is not None else urllib.request.urlopen(req)
40
52
  body = resp.read().decode("utf-8")
41
53
  resp_headers = {}
42
54
  for k, v in resp.getheaders():
@@ -15,6 +15,15 @@ def make_options_util(ctx):
15
15
  for key, val in custom_utils.items():
16
16
  utility.custom[key] = val
17
17
 
18
+ # Feature INSTANCES supplied at construction (the station adopt path)
19
+ # are consumed by the constructor's feature-add loop straight from the
20
+ # RAW construction options - extend is consumed exactly once, at
21
+ # construction. They are stripped here so they never enter the cloned
22
+ # option map: vs.clone flattens arbitrary objects, and options_map()
23
+ # re-clones self.options on every request.
24
+ if isinstance(options, dict) and options.get("extend") is not None:
25
+ options = {k: v for k, v in options.items() if k != "extend"}
26
+
18
27
  opts = vs.clone(options)
19
28
  if not isinstance(opts, dict):
20
29
  opts = {}
@@ -74,6 +83,10 @@ def make_options_util(ctx):
74
83
  },
75
84
  },
76
85
  "utility": {},
86
+ # Extension feature instances (see above) - stripped before the
87
+ # clone, but the key stays legal so a passed-through map cannot
88
+ # fail validation.
89
+ "extend": "`$ANY`",
77
90
  "system": {},
78
91
  "test": {
79
92
  "active": False,
@@ -171,6 +184,16 @@ def make_options_util(ctx):
171
184
  featureorder = ["test"] + [n for n in names if n != "test"]
172
185
  else:
173
186
  featureorder = names
187
+ # Station special case, mirroring test's: its transport wrap must
188
+ # sit immediately outside the base transport (inside retry/cache/
189
+ # netsim), so map-form activation hoists it to just after test -
190
+ # or first, when no test entry exists. Without this the sorted
191
+ # default would init station last and wrap OUTSIDE the recording
192
+ # features, turning its wire-truth events into fiction.
193
+ if "station" in featureorder:
194
+ featureorder = [n for n in featureorder if n != "station"]
195
+ at = featureorder.index("test") + 1 if "test" in featureorder else 0
196
+ featureorder.insert(at, "station")
174
197
 
175
198
  derived = {"clean": {}}
176
199
  if keyre != "":
@@ -5,6 +5,38 @@ from projectname_sdk.utility.voxgig_struct import voxgig_struct as vs
5
5
  from projectname_sdk.core.helpers import to_map
6
6
 
7
7
 
8
+ def _parts_len(point):
9
+ parts = vs.getprop(point, "parts")
10
+ return len(parts) if isinstance(parts, list) else 0
11
+
12
+
13
+ def _terminal_param(point):
14
+ # A record route ends in the record's identifier (/boards/{id}); a
15
+ # cross-reference that also returns the entity ends in the
16
+ # relationship's name (/posts/{id}/author).
17
+ parts = vs.getprop(point, "parts")
18
+ if not isinstance(parts, list) or 0 == len(parts):
19
+ return False
20
+ return str(parts[-1]).startswith("{")
21
+
22
+
23
+ def _own_point(points):
24
+ # The entity's OWN route: a terminal parameter first, then the fewest
25
+ # path segments. Ties keep the earlier point, so the model's sorted-key
26
+ # order decides. The same rule runs at generation time, in
27
+ # helpers/opShape.ts — both sides must move together.
28
+ best = points[0]
29
+ for cand in points:
30
+ cand_term = _terminal_param(cand)
31
+ best_term = _terminal_param(best)
32
+ if cand_term != best_term:
33
+ if cand_term:
34
+ best = cand
35
+ elif _parts_len(cand) < _parts_len(best):
36
+ best = cand
37
+ return best
38
+
39
+
8
40
  def make_point_util(ctx):
9
41
  pre = ctx.out.get("point")
10
42
  if pre is not None:
@@ -40,9 +72,10 @@ def make_point_util(ctx):
40
72
  selector = ctx.match
41
73
 
42
74
  point = None
75
+ matched = False
43
76
  for i in range(len(op.points)):
44
- point = op.points[i]
45
- select_def = to_map(vs.getprop(point, "select"))
77
+ cand = op.points[i]
78
+ select_def = to_map(vs.getprop(cand, "select"))
46
79
  found = True
47
80
 
48
81
  if selector is not None and select_def is not None:
@@ -63,8 +96,29 @@ def make_point_util(ctx):
63
96
  found = False
64
97
 
65
98
  if found:
99
+ point = cand
100
+ matched = True
66
101
  break
67
102
 
103
+ # select.exist can list more than the params needed to pick a point,
104
+ # so nothing matches — fall back to the entity's own route rather
105
+ # than whichever point came last.
106
+ if not matched:
107
+ # A request naming an action reaches here only because that
108
+ # action's own point failed its exist test, so it is unbuildable
109
+ # whatever we pick. Refuse it BEFORE choosing a fallback: the
110
+ # guard below compares the chosen point's $action and would wave
111
+ # the request through whenever the fallback lands on the action
112
+ # point itself.
113
+ req_action = vs.getprop(reqselector, "$action") \
114
+ if reqselector is not None else None
115
+ if req_action is not None:
116
+ return None, ctx.make_error("point_action_invalid",
117
+ 'Operation "' + op.name + '" action "' +
118
+ vs.stringify(req_action) + '" is not valid.')
119
+
120
+ point = _own_point(op.points)
121
+
68
122
  if reqselector is not None:
69
123
  req_action = vs.getprop(reqselector, "$action")
70
124
  if req_action is not None and point is not None:
@@ -49,6 +49,12 @@ module ProjectNameUtilities
49
49
  "entity" => { "`$CHILD`" => { "`$OPEN`" => true, "active" => false, "alias" => {} } },
50
50
  "feature" => { "`$CHILD`" => { "`$OPEN`" => true, "active" => false } },
51
51
  "utility" => {},
52
+ # Feature INSTANCES supplied at construction (the station adopt
53
+ # path): consumed by the constructor's feature_add loop, so they are
54
+ # class instances, not data — `$ANY` accepts them verbatim. Without
55
+ # this entry the seam is dead: the constructor reads
56
+ # options["extend"], but validate rejected the key.
57
+ "extend" => "`$ANY`",
52
58
  "system" => {},
53
59
  "test" => { "active" => false, "entity" => { "`$OPEN`" => true } },
54
60
  "clean" => { "keys" => "key,token,id" },
@@ -122,6 +128,18 @@ module ProjectNameUtilities
122
128
  else
123
129
  featureorder = names
124
130
  end
131
+ # Station special case, mirroring test's: its transport wrap must
132
+ # sit immediately outside the base transport (inside retry/cache/
133
+ # netsim), so map-form activation hoists it to just after test -
134
+ # or first, when no test entry exists. Without this the sorted
135
+ # default would init station last and wrap OUTSIDE the recording
136
+ # features, turning its wire-truth events into fiction.
137
+ si = featureorder.index("station")
138
+ unless si.nil?
139
+ featureorder.delete_at(si)
140
+ ti = featureorder.index("test")
141
+ featureorder.insert(ti.nil? ? 0 : ti + 1, "station")
142
+ end
125
143
  end
126
144
 
127
145
  derived = { "clean" => keyre.empty? ? {} : { "keyre" => keyre } }
@@ -35,8 +35,8 @@ module ProjectNameUtilities
35
35
  selector = op.input == "data" ? ctx.data : ctx.match
36
36
 
37
37
  point = nil
38
+ matched = false
38
39
  op.points.each do |p|
39
- point = p
40
40
  select_def = ProjectNameHelpers.to_map(VoxgigStruct.getprop(p, "select"))
41
41
  found = true
42
42
 
@@ -60,7 +60,47 @@ module ProjectNameUtilities
60
60
  found = false if req_action != select_action
61
61
  end
62
62
 
63
- break if found
63
+ if found
64
+ point = p
65
+ matched = true
66
+ break
67
+ end
68
+ end
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
+ unless 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
77
+ # compares the chosen point's $action and would wave the request
78
+ # through whenever the fallback lands on the action point itself.
79
+ req_action = reqselector ? VoxgigStruct.getprop(reqselector, "$action") : nil
80
+ if req_action
81
+ return nil, ctx.make_error("point_action_invalid",
82
+ "Operation \"#{op.name}\" action \"#{VoxgigStruct.stringify(req_action)}\" is not valid.")
83
+ end
84
+
85
+ # A terminal parameter marks a record route (/boards/{id}); a
86
+ # cross-reference ends in the relationship's name
87
+ # (/posts/{id}/author). Failing that, the shallower path wins.
88
+ parts_len = ->(p) {
89
+ parts = VoxgigStruct.getprop(p, "parts")
90
+ parts.is_a?(Array) ? parts.length : 0
91
+ }
92
+ terminal_param = ->(p) {
93
+ parts = VoxgigStruct.getprop(p, "parts")
94
+ parts.is_a?(Array) && parts.length > 0 && parts[-1].to_s.start_with?("{")
95
+ }
96
+ point = op.points[0]
97
+ op.points.each do |p|
98
+ if terminal_param.call(p) != terminal_param.call(point)
99
+ point = p if terminal_param.call(p)
100
+ elsif parts_len.call(p) < parts_len.call(point)
101
+ point = p
102
+ end
103
+ end
64
104
  end
65
105
 
66
106
  if reqselector
@@ -295,7 +295,7 @@ pub fn match_deep(check: &Value, base: &Value) -> Option<String> {
295
295
  // (.sdk/test/primary/<name>.aontu carries a PENDING header). Everything else
296
296
  // MUST contribute cases.
297
297
  pub const PENDING_SECTIONS: &[&str] = &[
298
- "fetcher", "makeFetchDef", "makePoint", "makeResult", "featureAdd",
298
+ "fetcher", "makeFetchDef", "makeResult", "featureAdd",
299
299
  "featureHook", "featureInit",
300
300
  ];
301
301
 
@@ -1826,6 +1826,33 @@ fn feature_proxy_inactive_does_not_wrap() {
1826
1826
  );
1827
1827
  }
1828
1828
 
1829
+ // --- station --------------------------------------------------------------------
1830
+
1831
+ // The station adapter (installed by @voxgig/sdkgen-station) with NO open
1832
+ // station must be an inert no-op that emits nothing and fails nothing
1833
+ // (station design 3.1). Constructed through the generated make_feature
1834
+ // factory - never a compile-time module reference - so this file stays
1835
+ // compilable both with and without the station feature installed: absent,
1836
+ // the factory answers with the inert BaseFeature and fh_present skips the
1837
+ // assertions. Bound behaviour (registration, injection, wire events) is
1838
+ // exercised by the station library's own suite and the consumer-side
1839
+ // station validation, not here.
1840
+ #[test]
1841
+ fn feature_station_inert_without_open_station() {
1842
+ if !fh_present(&["station"]) {
1843
+ return;
1844
+ }
1845
+ let f = RUSTCRATE::make_feature("station");
1846
+ assert_eq!(
1847
+ "station",
1848
+ f.borrow().name(),
1849
+ "generated factory has a station arm"
1850
+ );
1851
+ let h = fh_make(None, vec![(f, Value::Noval)]);
1852
+ let res = h.op(opspec("load"));
1853
+ assert!(res.ok, "expected inert station to pass the op through");
1854
+ }
1855
+
1829
1856
  // --- composition ----------------------------------------------------------------
1830
1857
 
1831
1858
  #[test]
@@ -19,6 +19,14 @@ fn default_http_fetch(fullurl: &str, fetchdef: &Value) -> Result<Value, ProjectN
19
19
  builder = builder.proxy(p);
20
20
  }
21
21
  }
22
+ // A `redirect: "manual"` annotation (set by the station feature when a
23
+ // hosts policy is active - the same in-band channel as `proxy` above)
24
+ // disables automatic redirect following, so a 3xx rides back as a normal
25
+ // response: a Location pointing off an egress allowlist must never pull
26
+ // an automatic credentialed follow-up request.
27
+ if get_str(fetchdef, "redirect").as_deref() == Some("manual") {
28
+ builder = builder.redirects(0);
29
+ }
22
30
  let agent = builder.build();
23
31
 
24
32
  let mut req = agent.request(&method, fullurl);
@@ -228,6 +228,21 @@ pub fn make_options_util(ctx: &Rc<Context>) -> Value {
228
228
  } else {
229
229
  feature_order = names;
230
230
  }
231
+ // Station special case, mirroring test's: its transport wrap must
232
+ // sit immediately outside the base transport (inside retry/cache/
233
+ // netsim), so map-form activation hoists it to just after test -
234
+ // or first, when no test entry exists. Without this the sorted
235
+ // default would init station last and wrap OUTSIDE the recording
236
+ // features, turning its wire-truth events into fiction.
237
+ if let Some(si) = feature_order.iter().position(|n| n == "station") {
238
+ feature_order.remove(si);
239
+ let at = feature_order
240
+ .iter()
241
+ .position(|n| n == "test")
242
+ .map(|ti| ti + 1)
243
+ .unwrap_or(0);
244
+ feature_order.insert(at, "station".to_string());
245
+ }
231
246
  }
232
247
  }
233
248
  let order_list =