@voxgig/sdkgen 4.2.3 → 4.2.5

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 (81) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideTop.js +15 -7
  3. package/dist/cmp/AgentGuideTop.js.map +1 -1
  4. package/dist/cmp/ReadmeStation.js +88 -12
  5. package/dist/cmp/ReadmeStation.js.map +1 -1
  6. package/dist/helpers/station.d.ts +3 -0
  7. package/dist/helpers/station.js +67 -0
  8. package/dist/helpers/station.js.map +1 -0
  9. package/dist/sdkgen.d.ts +2 -1
  10. package/dist/sdkgen.js +41 -15
  11. package/dist/sdkgen.js.map +1 -1
  12. package/dist/tsconfig.tsbuildinfo +1 -1
  13. package/dist/types.d.ts +2 -0
  14. package/dist/utility.js +15 -1
  15. package/dist/utility.js.map +1 -1
  16. package/model/sdkgen.aon +20 -0
  17. package/package.json +1 -1
  18. package/project/.sdk/model/feature/audit.aon +1 -0
  19. package/project/.sdk/model/feature/cache.aon +1 -0
  20. package/project/.sdk/model/feature/clienttrack.aon +1 -0
  21. package/project/.sdk/model/feature/debug.aon +1 -0
  22. package/project/.sdk/model/feature/idempotency.aon +1 -0
  23. package/project/.sdk/model/feature/log.aon +1 -0
  24. package/project/.sdk/model/feature/metrics.aon +1 -0
  25. package/project/.sdk/model/feature/netsim.aon +1 -0
  26. package/project/.sdk/model/feature/paging.aon +1 -0
  27. package/project/.sdk/model/feature/proxy.aon +1 -0
  28. package/project/.sdk/model/feature/ratelimit.aon +1 -0
  29. package/project/.sdk/model/feature/rbac.aon +1 -0
  30. package/project/.sdk/model/feature/retry.aon +1 -0
  31. package/project/.sdk/model/feature/streaming.aon +1 -0
  32. package/project/.sdk/model/feature/telemetry.aon +1 -0
  33. package/project/.sdk/model/feature/test.aon +1 -0
  34. package/project/.sdk/model/feature/timeout.aon +1 -0
  35. package/project/.sdk/src/cmp/c/Config_c.ts +13 -10
  36. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +4 -1
  37. package/project/.sdk/src/cmp/dart/Config_dart.ts +4 -1
  38. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +4 -1
  39. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +21 -2
  40. package/project/.sdk/src/cmp/go/Config_go.ts +4 -1
  41. package/project/.sdk/src/cmp/java/Config_java.ts +11 -8
  42. package/project/.sdk/src/cmp/js/Config_js.ts +4 -1
  43. package/project/.sdk/src/cmp/js/Main_js.ts +20 -1
  44. package/project/.sdk/src/cmp/js/fragment/MainStation.fragment.js +36 -0
  45. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +11 -7
  46. package/project/.sdk/src/cmp/lua/Config_lua.ts +4 -1
  47. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +3 -1
  48. package/project/.sdk/src/cmp/php/Config_php.ts +4 -1
  49. package/project/.sdk/src/cmp/py/Config_py.ts +4 -1
  50. package/project/.sdk/src/cmp/rb/Config_rb.ts +4 -1
  51. package/project/.sdk/src/cmp/rust/ReadmeEntity_rust.ts +2 -2
  52. package/project/.sdk/src/cmp/rust/ReadmeHowto_rust.ts +2 -2
  53. package/project/.sdk/src/cmp/rust/ReadmeIntro_rust.ts +2 -2
  54. package/project/.sdk/src/cmp/rust/ReadmeModel_rust.ts +2 -2
  55. package/project/.sdk/src/cmp/rust/ReadmeQuick_rust.ts +3 -3
  56. package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +3 -3
  57. package/project/.sdk/src/cmp/rust/ReadmeTopQuick_rust.ts +2 -2
  58. package/project/.sdk/src/cmp/rust/ReadmeTopTest_rust.ts +2 -2
  59. package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +7 -7
  60. package/project/.sdk/src/cmp/swift/EntityTypes_swift.ts +2 -2
  61. package/project/.sdk/src/cmp/swift/Entity_swift.ts +3 -1
  62. package/project/.sdk/src/cmp/swift/Main_swift.ts +37 -2
  63. package/project/.sdk/src/cmp/swift/Package_swift.ts +9 -6
  64. package/project/.sdk/src/cmp/swift/ReadmeExamplesTest_swift.ts +1 -1
  65. package/project/.sdk/src/cmp/swift/ReadmeExplanation_swift.ts +5 -3
  66. package/project/.sdk/src/cmp/swift/Test_swift.ts +3 -1
  67. package/project/.sdk/src/cmp/swift/utility_swift.ts +16 -0
  68. package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
  69. package/project/.sdk/src/cmp/ts/Main_ts.ts +20 -1
  70. package/project/.sdk/src/cmp/ts/fragment/MainStation.fragment.ts +36 -0
  71. package/project/.sdk/src/cmp/zig/Config_zig.ts +4 -2
  72. package/project/.sdk/tm/elixir/lib/utility/struct/voxgig_struct.ex +66 -12
  73. package/project/.sdk/tm/zig/Makefile +20 -0
  74. package/project/.sdk/tm/zig/feature/test.zig +1 -1
  75. package/project/sdkgen-package.json +1 -1
  76. package/src/cmp/AgentGuideTop.ts +15 -7
  77. package/src/cmp/ReadmeStation.ts +93 -13
  78. package/src/helpers/station.ts +80 -0
  79. package/src/sdkgen.ts +52 -15
  80. package/src/types.ts +6 -0
  81. package/src/utility.ts +15 -1
@@ -83,19 +83,27 @@ package (other languages). Each target's guide documents its features.
83
83
  `)
84
84
  }
85
85
 
86
- // Station paragraph (station design §9.4), only when the model
87
- // carries the feature: an agent working on this repo should know
88
- // the runtime story without leaving AGENTS.md.
86
+ // Station paragraph (station design §9.4; declarative design §11
87
+ // item 4), only when the model carries the feature: an agent working
88
+ // on this repo should know the runtime story without leaving
89
+ // AGENTS.md — and, first, that the application's integrations are
90
+ // DECLARED in station.json, so that file is where to look.
89
91
  if (features.some((f: any) => 'station' === f.name)) {
90
92
  Content(`**Station**: this SDK is a
91
93
  [voxgig/station](https://github.com/voxgig/station) plugin (the
92
- \`station\` feature, off by default). Bound to an open \`Station\`,
93
- the credential is resolved by sekreto under the plugin's secret name
94
+ \`station\` feature, off by default). An application's outbound
95
+ integrations are **declared in \`station.json\`** at its repo root
96
+ to learn what the application talks to, read that file: every SDK
97
+ instance (\`sdk\`), per-api default (\`api\`), feature setting, and
98
+ egress policy is declared there, and never a credential value.
99
+ \`station.sdk('<name>')\` builds a declared instance on first ask;
100
+ \`station.instances()\` lists them. Bound to an open \`Station\`,
101
+ the credential is resolved by sekreto under the instance's secret name
94
102
  and injected at the transport seam — \`options()\` and
95
103
  \`prepare()\` output hold only a placeholder, so both are safe to
96
104
  inspect and log. \`station.tap(...)\`/\`station.events()\` show live
97
- traffic; \`station.plugins()\` lists descriptors. See the "Use with
98
- Station" README section and \`src/feature/station/\` (or the
105
+ traffic; \`station.plugins()\` lists live descriptors. See the "Use
106
+ with Station" README section and \`src/feature/station/\` (or the
99
107
  target's feature container) for the generated adapter.
100
108
 
101
109
  `)
@@ -6,20 +6,39 @@ import {
6
6
  getModelPath
7
7
  } from '../types'
8
8
 
9
- import { envName } from '../helpers/packageMeta'
9
+ import { envName, packageName } from '../helpers/packageMeta'
10
10
 
11
11
 
12
12
  // The "Use with Station" README section (station design §9.4): rendered
13
13
  // ONLY when the project's model carries the station feature (installed
14
14
  // via `package add @voxgig/sdkgen-station`) — a project without it sees
15
- // nothing. Documents the binding forms and the secret name; store
16
- // configuration is sekreto's documentation, deliberately not restated
17
- // here (one canonical source).
15
+ // nothing. Leads with the DECLARATIVE flow a `station.json` block and
16
+ // `station.sdk()` (declarative design §11 item 3) — keeps the imperative
17
+ // `connect()` form as the retrofit path, and documents the
18
+ // instance-derived secret/env-var name (declarative design §3.4, §5.1:
19
+ // the same envtoken grammar as sdkgen's envName, applied to the INSTANCE
20
+ // name, so the untagged instance keeps the env var this README already
21
+ // documents to the byte). Store configuration is sekreto's
22
+ // documentation, deliberately not restated here (one canonical source);
23
+ // the error codes live in sdkgen's one catalog page, linked rather than
24
+ // restated for the same reason.
18
25
 
19
26
  // Targets where station.connect(SDK) is the idiomatic binding; everything
20
27
  // else uses inverted binding through the SDK's own constructor.
21
28
  const CONNECT_TARGETS = ['ts', 'js', 'py', 'rb', 'php', 'lua', 'perl']
22
29
 
30
+ // Targets whose module system has an init hook that actually runs
31
+ // (station design §6.2 path 1): there, linking the generated package
32
+ // fills the process-global factory table and `station.sdk()` needs no
33
+ // application code. Everywhere else the README must say
34
+ // `Station.provide` (path 2) plainly rather than imply an import is
35
+ // enough — a Java import is a compile-time alias that runs nothing.
36
+ const SELF_REGISTER_TARGETS =
37
+ ['ts', 'js', 'go', 'py', 'rb', 'php', 'lua', 'perl', 'elixir', 'clojure']
38
+
39
+ const ERROR_CATALOG_URL =
40
+ 'https://github.com/voxgig/sdkgen/blob/main/docs/reference/station-errors.md'
41
+
23
42
  const ReadmeStation = cmp(function ReadmeStation(props: any) {
24
43
  const { target } = props
25
44
  const { model } = props.ctx$
@@ -30,9 +49,15 @@ const ReadmeStation = cmp(function ReadmeStation(props: any) {
30
49
  .find((f: any) => 'station' === f?.name)
31
50
  if (null == station) { return }
32
51
 
52
+ // The descriptor slug is the model's hyphenated name (carried as
53
+ // main.slug in the embedded config); an untagged instance ref IS the
54
+ // slug, so this is also the default instance name.
55
+ const slug = model.name
33
56
  const env = envName(model)
34
- const secretname = env.toLowerCase() + '.apikey'
57
+ const secretbase = env.toLowerCase()
58
+ const secretname = secretbase + '.apikey'
35
59
  const connect = CONNECT_TARGETS.includes(target.name)
60
+ const selfreg = SELF_REGISTER_TARGETS.includes(target.name)
36
61
 
37
62
  Content(`
38
63
  ## Use with Station
@@ -42,24 +67,79 @@ plugin: bind it to a local \`Station\` and outbound configuration,
42
67
  credentials, and observability move to one place. The feature is
43
68
  present but **off by default** — nothing changes until you bind.
44
69
 
70
+ ### Declarative: \`station.json\` + \`station.sdk()\`
71
+
72
+ Declare an instance in \`station.json\` at the repo root (committable —
73
+ names and stores, never values):
74
+
75
+ \`\`\`json
76
+ { "station": 1,
77
+ "profiles": { "default": {
78
+ "sdk": { "${slug}": {${selfreg ?
79
+ `\n "package": "${packageName(model, target.name)}"` : ''} } } } } }
80
+ \`\`\`
81
+
82
+ Then get the client where you need it:
83
+
84
+ 1. \`station = Station.open()\` — reads and validates \`station.json\`;
85
+ constructs nothing.
86
+ 2. \`client = station.sdk('${slug}')\` — built on first ask and cached,
87
+ so the same name returns the same client.
88
+
89
+ ${selfreg
90
+ ? `Loading this package is the whole bootstrap: it registers its own
91
+ factory (constructor plus embedded config) with the station library at
92
+ module init, so \`station.sdk()\` needs no SDK import in application
93
+ code. That is what the \`package\` key above is for — it names the module
94
+ for station to load, since nothing else in this example would execute
95
+ it. An application that imports this SDK for its types anyway can drop
96
+ the key: that import is itself the bootstrap.`
97
+ : `In this language an import runs no code, so register the factory
98
+ once at startup — \`Station.provide('${slug}', ...)\`, one line — and
99
+ every other line of configuration stays in \`station.json\`.`}
100
+
101
+ A second instance of the same API is one more key — \`"${slug}$test"\`
102
+ beside \`"${slug}"\` — and \`station.sdk('${slug}$test')\` returns it.
103
+ SDK features are configured in the same file too: fleet-wide, per api,
104
+ or per instance. \`station.check()\` resolves and constructs every
105
+ active instance without sending a request — run it in CI.
106
+
107
+ ### The secret name derives from the instance
108
+
109
+ The credential comes from [sekreto](https://github.com/voxgig/sekreto)
110
+ under a name derived from the **instance** name — the instance token
111
+ lowercased, plus \`.apikey\`. The untagged instance \`${slug}\` derives
112
+ \`${secretname}\` — by default the \`${env}_APIKEY\` environment
113
+ variable this README already documents, unchanged. A tagged instance
114
+ derives its own: \`${slug}$test\` → \`${secretbase}_test.apikey\` →
115
+ \`${env}_TEST_APIKEY\` — each instance is a separate credentialed use
116
+ of the API, and its env var is derivable from the name you chose. To
117
+ pin a name instead, set \`secret\` on the instance block, or at the
118
+ api level for several instances sharing one key. Point a profile at a
119
+ vault later and application code does not change; sekreto's own
120
+ documentation covers the stores. The key stays out of \`options()\` and
121
+ \`prepare()\` output; \`station.tap(...)\` shows live traffic.
122
+
123
+ ### Imperative: ${connect ? '`connect()`' : 'inverted binding'} — the retrofit path
124
+
45
125
  ${connect
46
126
  ? `Bind by passing the SDK class to the station:
47
127
 
48
128
  1. \`station = Station.open()\` — profile, env, and proxy all defaulted.
49
129
  2. \`client = station.connect(${model.const.Name}SDK)\` — replaces direct
50
- construction.`
130
+ construction; \`connect(${model.const.Name}SDK, { as: 'test' })\` binds
131
+ a second, tagged instance.`
51
132
  : `Bind through the constructor this SDK already has (inverted
52
133
  binding): open a station, then construct with station-built options —
53
134
  \`station.options()\` merges the handle, the activation entry, and the
54
135
  correct feature order into the plain options the constructor accepts.`}
55
136
 
56
- The credential comes from [sekreto](https://github.com/voxgig/sekreto)
57
- under the name \`${secretname}\` by default the \`${env}_APIKEY\`
58
- environment variable this README already documents, unchanged. Point a
59
- profile in \`station.json\` at a vault later and application code does
60
- not change; sekreto's own documentation covers the stores. The key
61
- stays out of \`options()\` and \`prepare()\` output; \`station.tap(...)\`
62
- shows live traffic.
137
+ With no \`station.json\` at all this runs solo with everything
138
+ defaulted the two-line form is how an existing application starts.
139
+
140
+ Station failures surface through this SDK's own error path
141
+ (\`err.code\`, \`station_*\`); the codes are catalogued in
142
+ [station error codes](${ERROR_CATALOG_URL}).
63
143
  `)
64
144
  })
65
145
 
@@ -0,0 +1,80 @@
1
+ // The station SELF-REGISTRATION seam (station design station.md §9 /
2
+ // station-declarative-config.md §6.2 path 1, §11 item 2): when the station
3
+ // feature is installed and active, the generated MAIN module registers the
4
+ // SDK's `{construct, config}` factory pair with the station library at
5
+ // module init, so `station.sdk('<name>')` needs no imports in application
6
+ // code. The registration key is the descriptor slug — `config.main.slug`,
7
+ // the same field station's `normalizeDescriptor` reads — so the generated
8
+ // code passes the embedded config's own value rather than re-deriving it.
9
+ //
10
+ // This helper answers the ONE question every Main_<lang> asks before
11
+ // emitting that registration: is the station feature ACTIVE in this model,
12
+ // and what is the station library package for this target? Both come from
13
+ // the model — the active-filtered feature map (exactly the view the Main
14
+ // components already emit from) and the station feature's own
15
+ // `deps.<target>` block, which is the same entry `collectDeps` flows into
16
+ // the generated manifest. ONE RULE, ONE PLACE: reading the package name
17
+ // here, never hardcoding it per language, means the manifest dependency
18
+ // and the emitted require can not disagree.
19
+ //
20
+ // Returns undefined when the station feature is absent, inactive, or
21
+ // declares no active station library dep for this target (the vendored
22
+ // targets carry the library inside their tm overlay instead of as a dep —
23
+ // station.md §9.2): no dep, no package to require, no registration to emit.
24
+
25
+ import { each } from 'jostraca'
26
+
27
+ import {
28
+ KIT,
29
+ getModelPath,
30
+ } from '@voxgig/apidef'
31
+
32
+ import { SdkGenError } from '../utility'
33
+
34
+ import type { SdkModel, ModelFeature } from '../types'
35
+
36
+
37
+ function stationLibrary(model: SdkModel, targetName: string): string | undefined {
38
+ // Active-filtered on purpose: an inactive feature ships no source, no
39
+ // embedded config entry and no manifest dep, so it must emit no
40
+ // registration either.
41
+ const feature = getModelPath(model, `main.${KIT}.feature`,
42
+ { required: false }) || {}
43
+
44
+ const station: ModelFeature | undefined = feature.station
45
+ if (null == station) { return undefined }
46
+
47
+ const deps = station.deps?.[targetName]
48
+ if (null == deps) { return undefined }
49
+
50
+ // Feature deps count only when explicitly active — collectDeps semantics,
51
+ // so the require target is exactly the set the manifest carries.
52
+ const names = each(deps)
53
+ .filter((dep: any) => true === dep?.active)
54
+ .map((dep: any) => dep?.key$)
55
+ .filter((name: any) => null != name && '' !== name)
56
+
57
+ if (0 === names.length) { return undefined }
58
+
59
+ // EXACTLY ONE, or say so. Picking the first of several — each() sorts,
60
+ // so it would be the alphabetically first — means the generated main
61
+ // can `require` an unrelated helper package, find no `provide`, and
62
+ // leave the factory table silently empty. The station feature model
63
+ // declares one library per target; a second active dep is a model
64
+ // question only its author can answer.
65
+ if (1 < names.length) {
66
+ throw new SdkGenError(
67
+ 'station: feature `station` declares ' + names.length + ' active ' +
68
+ 'dependencies for target `' + targetName + '` (' +
69
+ names.map(String).sort().join(', ') + '), so the station library to ' +
70
+ 'register with is ambiguous. Declare exactly one active dep per ' +
71
+ 'target in the feature model, or mark which one is the library.')
72
+ }
73
+
74
+ return String(names[0])
75
+ }
76
+
77
+
78
+ export {
79
+ stationLibrary,
80
+ }
package/src/sdkgen.ts CHANGED
@@ -73,6 +73,7 @@ import {
73
73
  srcFeatureExcludes,
74
74
  } from './helpers/featureSource'
75
75
  import type { FeatureSource } from './helpers/featureSource'
76
+ import { stationLibrary } from './helpers/station'
76
77
  import {
77
78
  definitionPath,
78
79
  definitionFolder,
@@ -299,7 +300,13 @@ function SdkGen(opts: SdkGenOptions) {
299
300
  // so resolve it ONCE: every destination is compared against it, and a
300
301
  // comparison between a relative and an absolute path is meaningless.
301
302
  const root = Path.resolve(folder)
302
- const external = externalItems(model, root, ['target', 'docs'])
303
+ // Snapshot the decision before preflight. In particular, do not check a
304
+ // missing optional destination once for safety and AGAIN before writing:
305
+ // if it appeared between those checks, the pass could write into content
306
+ // that was never ownership-validated.
307
+ const external: ExternalPlan[] =
308
+ externalItems(model, root, ['target', 'docs'])
309
+ .map((ext) => ({ ...ext, skip: externalSkipReason(ext, fs) }))
303
310
 
304
311
  // Before ANY file is written, in-tree included: a destination that turns
305
312
  // out to be wrong must abort the whole generation, not leave half of it
@@ -382,17 +389,13 @@ function SdkGen(opts: SdkGenOptions) {
382
389
  }
383
390
 
384
391
  for (const ext of external) {
385
- // `active: false` is the project's only lever to stop the generator
386
- // writing into a repo it does not own, so it has to be honoured HERE —
387
- // the consumer Root iterates targets raw and does not check it. The
388
- // target is still removed from the in-tree model above (withoutExternal
389
- // takes every `output: path` target, active or not), so switching one
390
- // off generates it nowhere rather than relocating it into
391
- // `<sdk-repo>/<target>/`.
392
- if (!ext.active) {
392
+ // A skipped external target is still removed from the in-tree model
393
+ // above: neither `active: false` nor a missing `output.create: false`
394
+ // destination may relocate it into `<sdk-repo>/<target>/`.
395
+ if (null != ext.skip) {
393
396
  log.info({
394
397
  point: 'generate-external-skip', target: ext.name, folder: ext.folder,
395
- note: ext.name + ' inactive, not generated'
398
+ note: ext.skip
396
399
  })
397
400
  continue
398
401
  }
@@ -673,6 +676,13 @@ type ExternalSpec = {
673
676
  }
674
677
 
675
678
 
679
+ type ExternalPlan = ExternalSpec & {
680
+ // Snapshotted before destination validation and consumed unchanged by the
681
+ // generation loop, so a filesystem race cannot bypass the safety guard.
682
+ skip: string | null
683
+ }
684
+
685
+
676
686
  // Targets declaring `output: path` — generated into their own repo rather
677
687
  // than into `<sdk-repo>/<target>/`.
678
688
  //
@@ -715,6 +725,32 @@ function externalItems(
715
725
  }
716
726
 
717
727
 
728
+ // Why this external item should not get a generation pass RIGHT NOW.
729
+ //
730
+ // `active: false` disables the target itself. `output.create: false` does
731
+ // something deliberately narrower: the target stays active in the model but
732
+ // an absent destination is treated as an optional checkout rather than a
733
+ // folder sdkgen should fabricate. If that repo is checked out later, the same
734
+ // unchanged model generates it normally.
735
+ //
736
+ // Snapshotted into ExternalPlan because both the pre-write destination guard
737
+ // and the actual pass must make the identical decision. If the guard skipped
738
+ // an item that the pass did not, generation could write outside the project
739
+ // without any of the ownership checks below.
740
+ function externalSkipReason(ext: ExternalSpec, fs: any): string | null {
741
+ if (!ext.active) {
742
+ return ext.name + ' inactive, not generated'
743
+ }
744
+
745
+ if (false === ext.target.output?.create && !fs.existsSync(ext.folder)) {
746
+ return ext.name + ' output folder does not exist and ' +
747
+ 'output.create=false, not generated'
748
+ }
749
+
750
+ return null
751
+ }
752
+
753
+
718
754
  // The `.jostraca` bookkeeping tree (meta log + a duplicate of the last
719
755
  // generated output) that jostraca leaves at an output root. It is the only
720
756
  // on-disk evidence that this toolchain has generated into a directory
@@ -754,14 +790,14 @@ const EXTERNAL_MARKER = '.jostraca'
754
790
  // generating somewhere other than it believes must be told. A destination
755
791
  // that legitimately holds other content first (a repo seeded with a README
756
792
  // and LICENCE) says so once in the model, with `output: adopt: true`.
757
- function checkExternalFolders(external: ExternalSpec[], root: string, fs: any) {
793
+ function checkExternalFolders(external: ExternalPlan[], root: string, fs: any) {
758
794
  const claimed: Record<string, string> = {}
759
795
 
760
796
  for (const ext of external) {
761
- // An inactive target writes nothing, so its destination is not a hazard
762
- // and switching a target off must not require keeping its now-unused
763
- // path valid.
764
- if (!ext.active) continue
797
+ // A skipped item writes nothing, so its destination is not a hazard. The
798
+ // snapshotted decision gates the actual pass too; keeping it identical is
799
+ // what makes the pre-write check complete.
800
+ if (null != ext.skip) continue
765
801
 
766
802
  const label = ext.kind.charAt(0).toUpperCase() + ext.kind.slice(1)
767
803
 
@@ -1107,6 +1143,7 @@ export {
1107
1143
  featureExcludes,
1108
1144
  fullsetExcludes,
1109
1145
  srcFeatureExcludes,
1146
+ stationLibrary,
1110
1147
 
1111
1148
  definitionPath,
1112
1149
  definitionFolder,
package/src/types.ts CHANGED
@@ -47,6 +47,11 @@ type ModelFeature = NameCases & {
47
47
  active?: boolean
48
48
  title?: string
49
49
  version?: string
50
+ // Transport role (station design §8.4): 'base' replaces the transport
51
+ // slot (only `test`), 'wrap' wraps it, 'none' is hook-only. Schema
52
+ // default is 'none'; configDefinition carries it into the embedded
53
+ // config beside the feature's `config.options`.
54
+ transport?: string
50
55
  hook?: Record<string, ModelHook>
51
56
  deps?: Record<string, Record<string, ModelDep>>
52
57
  [extra: string]: any
@@ -71,6 +76,7 @@ type ModelTarget = NameCases & {
71
76
  output?: {
72
77
  path?: string
73
78
  repo?: string
79
+ create?: boolean
74
80
  adopt?: boolean
75
81
  sdkrel?: string
76
82
  }
package/src/utility.ts CHANGED
@@ -318,7 +318,21 @@ function configDefinition(model: any, targetname?: string): { def: any, json: st
318
318
 
319
319
  const featureDefs: any = {}
320
320
  each(feature, (f: any) => {
321
- featureDefs[f.name] = f.config || {}
321
+ // The feature's declared config (its `options` key set with typed
322
+ // defaults) PLUS its transport role (station design §8.4): 'base'
323
+ // replaces the transport slot, 'wrap' wraps it, 'none' is hook-only.
324
+ // Station's descriptor (normalizeDescriptor in voxgig/station) reads
325
+ // `transport` beside `options` to validate the resolved feature order.
326
+ // The role is DECLARED in the feature model, never inferred - an empty
327
+ // `hook: {}` is wrong for station, which both wraps and dispatches
328
+ // hooks. Additive: a model unified without the schema's `transport`
329
+ // default simply omits the key, which station tolerates by degrading
330
+ // its role checks to nothing.
331
+ const fdef: any = { ...(f.config || {}) }
332
+ if (null != f.transport && '' !== f.transport) {
333
+ fdef.transport = String(f.transport)
334
+ }
335
+ featureDefs[f.name] = fdef
322
336
  })
323
337
 
324
338
  const options: any = { base: baseUrl }