@voxgig/sdkgen 4.4.1 → 4.5.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 (97) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/FeatureDocs.d.ts +16 -0
  3. package/dist/cmp/FeatureDocs.js +72 -0
  4. package/dist/cmp/FeatureDocs.js.map +1 -0
  5. package/dist/cmp/Readme.js +2 -0
  6. package/dist/cmp/Readme.js.map +1 -1
  7. package/dist/cmp/ReadmeFeatures.d.ts +2 -0
  8. package/dist/cmp/ReadmeFeatures.js +95 -0
  9. package/dist/cmp/ReadmeFeatures.js.map +1 -0
  10. package/dist/cmp/ReadmeRef.js +4 -0
  11. package/dist/cmp/ReadmeRef.js.map +1 -1
  12. package/dist/cmp/ReadmeRefFeatures.d.ts +2 -0
  13. package/dist/cmp/ReadmeRefFeatures.js +130 -0
  14. package/dist/cmp/ReadmeRefFeatures.js.map +1 -0
  15. package/dist/cmp/ReadmeTop.js +46 -0
  16. package/dist/cmp/ReadmeTop.js.map +1 -1
  17. package/dist/helpers/opShape.js +5 -1
  18. package/dist/helpers/opShape.js.map +1 -1
  19. package/dist/sdkgen.d.ts +3 -2
  20. package/dist/sdkgen.js +30 -10
  21. package/dist/sdkgen.js.map +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/dist/utility.d.ts +2 -1
  24. package/dist/utility.js +17 -1
  25. package/dist/utility.js.map +1 -1
  26. package/package.json +1 -1
  27. package/project/.sdk/src/cmp/c/ReadmeRef_c.ts +5 -0
  28. package/project/.sdk/src/cmp/clojure/ReadmeRef_clojure.ts +5 -0
  29. package/project/.sdk/src/cmp/cpp/ReadmeRef_cpp.ts +5 -0
  30. package/project/.sdk/src/cmp/csharp/ReadmeRef_csharp.ts +5 -0
  31. package/project/.sdk/src/cmp/dart/ReadmeRef_dart.ts +5 -0
  32. package/project/.sdk/src/cmp/elixir/ReadmeRef_elixir.ts +5 -0
  33. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -0
  34. package/project/.sdk/src/cmp/go/TestDirect_go.ts +123 -2
  35. package/project/.sdk/src/cmp/java/ReadmeRef_java.ts +5 -0
  36. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -0
  37. package/project/.sdk/src/cmp/js/TestDirect_js.ts +62 -0
  38. package/project/.sdk/src/cmp/kotlin/ReadmeRef_kotlin.ts +5 -0
  39. package/project/.sdk/src/cmp/lean/ReadmeRef_lean.ts +27 -3
  40. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -0
  41. package/project/.sdk/src/cmp/ocaml/ReadmeRef_ocaml.ts +5 -0
  42. package/project/.sdk/src/cmp/perl/ReadmeRef_perl.ts +5 -0
  43. package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +5 -0
  44. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +5 -0
  45. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -0
  46. package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +5 -0
  47. package/project/.sdk/src/cmp/scala/ReadmeRef_scala.ts +5 -0
  48. package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +19 -1
  49. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +27 -2
  50. package/project/.sdk/src/cmp/swift/ReadmeRef_swift.ts +5 -0
  51. package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
  52. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +9 -4
  53. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +8 -3
  54. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +3 -2
  55. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +7 -2
  56. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +3 -2
  57. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +5 -2
  58. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +5 -2
  59. package/project/.sdk/src/cmp/zig/ReadmeRef_zig.ts +5 -0
  60. package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
  61. package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
  62. package/project/.sdk/tm/go/feature/test_feature.go +17 -6
  63. package/project/.sdk/tm/go/test/feature_corpus_test.go +10 -3
  64. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +5 -4
  65. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
  66. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
  67. package/project/.sdk/tm/js/test/feature/Corpus.test.js +13 -3
  68. package/project/.sdk/tm/js/test/feature.test.js +19 -9
  69. package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
  70. package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
  71. package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
  72. package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
  73. package/project/.sdk/tm/ocaml/sdk_features.ml +5 -0
  74. package/project/.sdk/tm/perl/t/feature_corpus.t +5 -4
  75. package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
  76. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +7 -4
  77. package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
  78. package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
  79. package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
  80. package/project/.sdk/tm/py/test/test_feature.py +40 -0
  81. package/project/.sdk/tm/py/test/test_feature_corpus.py +14 -3
  82. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -4
  83. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
  84. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
  85. package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
  86. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +13 -3
  87. package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
  88. package/project/sdkgen-package.json +1 -1
  89. package/src/cmp/FeatureDocs.ts +100 -0
  90. package/src/cmp/Readme.ts +2 -0
  91. package/src/cmp/ReadmeFeatures.ts +113 -0
  92. package/src/cmp/ReadmeRef.ts +4 -0
  93. package/src/cmp/ReadmeRefFeatures.ts +149 -0
  94. package/src/cmp/ReadmeTop.ts +48 -0
  95. package/src/helpers/opShape.ts +5 -1
  96. package/src/sdkgen.ts +28 -7
  97. package/src/utility.ts +21 -1
@@ -0,0 +1,100 @@
1
+ import { each } from 'jostraca'
2
+
3
+ import { KIT, getModelPath } from '../types'
4
+
5
+
6
+ // Shared feature facts, derived from the model, for the three doc surfaces
7
+ // that describe features: the root README summary, the per-target README
8
+ // section, and REFERENCE.md.
9
+ //
10
+ // ONE SOURCE. The feature set, its defaults and its ordering constraints all
11
+ // come from `main.kit.feature`, so a feature added to the model documents
12
+ // itself everywhere rather than in whichever file someone remembered.
13
+
14
+ type FeatureDoc = {
15
+ name: string
16
+ Name: string
17
+ title: string
18
+ transport: string
19
+ wraps: boolean
20
+ options: Array<{ name: string, value: string }>
21
+ }
22
+
23
+
24
+ // `transport` says how a feature attaches, and that is the whole of the
25
+ // ordering story:
26
+ // wrap wraps the transport chain — activation order IS nesting order
27
+ // base installs the base transport others wrap (test)
28
+ // none pipeline hooks only; order does not affect it
29
+ function isWrapping(feat: any): boolean {
30
+ return 'wrap' === feat.transport
31
+ }
32
+
33
+
34
+ function renderValue(v: any): string {
35
+ if (null == v) { return '' }
36
+ if (Array.isArray(v)) { return '[' + v.map((x) => renderValue(x)).join(', ') + ']' }
37
+ if ('object' === typeof v) {
38
+ const keys = Object.keys(v)
39
+ return 0 === keys.length ? '{}' :
40
+ '{' + keys.map((k) => k + ': ' + renderValue(v[k])).join(', ') + '}'
41
+ }
42
+ if ('string' === typeof v) { return `'${v}'` }
43
+ return String(v)
44
+ }
45
+
46
+
47
+ // Every feature the model declares active, in a stable order, with its
48
+ // options and their defaults.
49
+ function featureDocs(model: any): FeatureDoc[] {
50
+ const feature = getModelPath(model, `main.${KIT}.feature`)
51
+
52
+ return each(feature)
53
+ .filter((f: any) => false !== f.active && 'base' !== f.name)
54
+ .map((f: any) => {
55
+ const opts = (f.config && f.config.options) || {}
56
+ const options = Object.keys(opts).sort().map((k) => ({
57
+ name: k,
58
+ value: renderValue(opts[k]),
59
+ }))
60
+ return {
61
+ name: f.name,
62
+ Name: f.Name || f.name,
63
+ title: f.title || '',
64
+ transport: f.transport || 'none',
65
+ wraps: isWrapping(f),
66
+ options,
67
+ }
68
+ })
69
+ .sort((a: FeatureDoc, b: FeatureDoc) => a.name.localeCompare(b.name))
70
+ }
71
+
72
+
73
+
74
+
75
+ // Targets that compose transport features in a FIXED catalog order rather
76
+ // than the order the caller activates them in.
77
+ //
78
+ // Every other target derives `__derived__.featureorder` from the options and
79
+ // adds features in that order, so an ordered activation list is what fixes
80
+ // nesting. lean has no featureorder at all — SdkFeatures.featureNames is a
81
+ // fixed array — and its resolveFeatureOpts accepts only a map, silently
82
+ // replacing a list with an empty one. Telling a lean reader to activate
83
+ // features as an ordered list would therefore disable every feature they
84
+ // asked for.
85
+ //
86
+ // Verify with: grep -rl featureorder tm/<target>
87
+ const FIXED_ORDER_TARGETS = ['lean']
88
+
89
+ function honoursActivationOrder(target: any): boolean {
90
+ return !FIXED_ORDER_TARGETS.includes(target?.name)
91
+ }
92
+
93
+
94
+ export {
95
+ featureDocs,
96
+ renderValue,
97
+ honoursActivationOrder,
98
+ }
99
+
100
+ export type { FeatureDoc }
package/src/cmp/Readme.ts CHANGED
@@ -13,6 +13,7 @@ import { ReadmeEntity } from './ReadmeEntity'
13
13
  import { ReadmeUnions } from './ReadmeUnions'
14
14
  import { ReadmeHowto } from './ReadmeHowto'
15
15
  import { ReadmeExplanation } from './ReadmeExplanation'
16
+ import { ReadmeFeatures } from './ReadmeFeatures'
16
17
  import { ReadmeRef } from './ReadmeRef'
17
18
 
18
19
 
@@ -31,6 +32,7 @@ const Readme = cmp(function Readme(props: any) {
31
32
  ReadmeModel({ target })
32
33
  ReadmeOptions({ target })
33
34
  ReadmeEntity({ target })
35
+ ReadmeFeatures({ target })
34
36
  ReadmeUnions({ target })
35
37
  ReadmeExplanation({ target })
36
38
 
@@ -0,0 +1,113 @@
1
+ import { cmp, Content } from 'jostraca'
2
+
3
+ import { requirePath } from '../utility'
4
+ import { featureDocs, honoursActivationOrder } from './FeatureDocs'
5
+ import type { FeatureDoc } from './FeatureDocs'
6
+
7
+
8
+ // The `## Features` section of a target README: one subsection per feature
9
+ // the SDK actually ships.
10
+ //
11
+ // WHY IT IS SHARED AND NOT PER-LANGUAGE. Every target activates features the
12
+ // same way — a `feature` entry in the client options — and the options and
13
+ // their defaults come from the model, so the substance is identical in all
14
+ // twenty-five languages. Only the literal syntax of the options map differs,
15
+ // and that is already shown once, in the Options section above. A per-target
16
+ // `ReadmeFeatures_<lang>.ts` may override this to add idiomatic snippets;
17
+ // none is required for the section to be correct.
18
+ const ReadmeFeatures = cmp(function ReadmeFeatures(props: any) {
19
+ const { target, ctx$ } = props
20
+ const { model } = ctx$
21
+
22
+ const override =
23
+ requirePath(ctx$, `./cmp/${target.name}/ReadmeFeatures_${target.name}`, { ignore: true })
24
+
25
+ if (override) {
26
+ override['ReadmeFeatures']({ target })
27
+ return
28
+ }
29
+
30
+ const features: FeatureDoc[] = featureDocs(model)
31
+
32
+ if (0 === features.length) {
33
+ return
34
+ }
35
+
36
+ Content(`## Features
37
+
38
+ This SDK ships ${features.length} optional features. Each is **inactive until you
39
+ switch it on**, so an SDK you have not configured behaves exactly as if none of
40
+ them existed — no retries, no cache, no logging, no measurable overhead.
41
+
42
+ Activate a feature by name in the client options, alongside the options shown
43
+ above:
44
+
45
+ | Feature | What it does |
46
+ |---|---|
47
+ `)
48
+
49
+ for (const f of features) {
50
+ Content(`| [\`${f.name}\`](#${f.name}) | ${f.title} |
51
+ `)
52
+ }
53
+
54
+ Content(`
55
+ `)
56
+
57
+ const wrapping = features.filter((f) => f.wraps).map((f) => '`' + f.name + '`')
58
+ if (1 < wrapping.length) {
59
+ if (honoursActivationOrder(target)) {
60
+ Content(`> **Order matters for ${wrapping.join(', ')}.** These wrap the
61
+ > transport, so each one wraps whatever is already installed: the order you
62
+ > activate them in IS the nesting order. Activating them as an ordered list
63
+ > rather than a map is what fixes that order.
64
+
65
+ `)
66
+ }
67
+ else {
68
+ Content(`> **${wrapping.join(', ')} wrap the transport**, so each one wraps
69
+ > whatever is already installed. This SDK composes them in a fixed catalog
70
+ > order, not the order you activate them in, and takes \`feature\` as a map.
71
+
72
+ `)
73
+ }
74
+ }
75
+
76
+ for (const f of features) {
77
+ Content(`### ${f.name}
78
+
79
+ ${f.title}.
80
+
81
+ `)
82
+
83
+ if (0 < f.options.length) {
84
+ Content(`| Option | Default |
85
+ |---|---|
86
+ `)
87
+ for (const o of f.options) {
88
+ Content(`| \`${o.name}\` | \`${o.value}\` |
89
+ `)
90
+ }
91
+ Content(`
92
+ `)
93
+ }
94
+
95
+ Content(`Set \`feature.${f.name}.active\` to enable it${
96
+ 0 < f.options.length ? ', then override any of the options above' : ''}.
97
+
98
+ `)
99
+
100
+ if (f.wraps) {
101
+ Content(`\`${f.name}\` wraps the transport, so its position among the other
102
+ transport features decides what it sees. A feature activated later wraps one
103
+ activated earlier.
104
+
105
+ `)
106
+ }
107
+ }
108
+ })
109
+
110
+
111
+ export {
112
+ ReadmeFeatures
113
+ }
@@ -15,6 +15,10 @@ const ReadmeRef = cmp(function ReadmeRef(props: any) {
15
15
  requirePath(ctx$, `./cmp/${target.name}/ReadmeRef_${target.name}`, { ignore: true })
16
16
 
17
17
  if (ReadmeRef_sdk) {
18
+ // The per-language component owns the REFERENCE.md File, so anything
19
+ // appended out here lands outside it and silently vanishes. The shared
20
+ // feature reference is therefore called from INSIDE each
21
+ // ReadmeRef_<lang>.ts, at the end of its own features section.
18
22
  ReadmeRef_sdk['ReadmeRef']({ target })
19
23
  }
20
24
  })
@@ -0,0 +1,149 @@
1
+ import { cmp, Content } from 'jostraca'
2
+
3
+ import { featureDocs, honoursActivationOrder } from './FeatureDocs'
4
+ import type { FeatureDoc } from './FeatureDocs'
5
+
6
+
7
+ // The feature reference, appended to every REFERENCE.md.
8
+ //
9
+ // REFERENCE.md is otherwise generated per-language, because the constructor
10
+ // signature, op spelling and code fence differ. Feature CONFIGURATION does
11
+ // not: the option names and defaults are model facts, identical in every
12
+ // target. So this section is written once and appended to all of them, rather
13
+ // than copied into twenty-five ReadmeRef_<lang>.ts files where it would drift.
14
+ //
15
+ // Covers what a reference has to cover and a README does not: every option
16
+ // with its default, what the feature does at runtime, and the considerations
17
+ // — ordering, interaction, and cost — that decide whether to switch it on.
18
+ const ReadmeRefFeatures = cmp(function ReadmeRefFeatures(props: any) {
19
+ const { target, ctx$ } = props
20
+ const { model } = ctx$
21
+
22
+ const features: FeatureDoc[] = featureDocs(model)
23
+
24
+ if (0 === features.length) {
25
+ return
26
+ }
27
+
28
+ // Emitted INSIDE the per-language `## Features` section, after its summary
29
+ // table and activation snippet, so these are subsections of it rather than
30
+ // a competing heading.
31
+ Content(`
32
+ ### Configuring features
33
+
34
+ Each feature is inactive until switched on, and an SDK with no feature
35
+ configured does no feature work at all. Every option below keeps its default
36
+ unless you name it.
37
+
38
+ ${honoursActivationOrder(target) ?
39
+ 'The array form of \\`feature\\` is significant: several features wrap the\ntransport, and the order you list them in is the order they nest.' :
40
+ 'This SDK takes \\`feature\\` as a map and composes the transport-wrapping\nfeatures in a fixed catalog order, so activation order does not change\nnesting here.'}
41
+
42
+ `)
43
+
44
+ const wrapping = features.filter((f) => f.wraps)
45
+ const hooked = features.filter((f) => !f.wraps)
46
+
47
+ if (0 < wrapping.length) {
48
+ Content(`#### Ordering
49
+
50
+ ${wrapping.map((f) => '`' + f.name + '`').join(', ')} wrap the transport. Each
51
+ wraps whatever is already installed${honoursActivationOrder(target) ?
52
+ ', so **activation order is nesting order**:\na feature activated later sits OUTSIDE one activated earlier, and sees the call\nfirst.' :
53
+ '. This SDK fixes that order in its own\ncatalog rather than taking it from the caller.'}
54
+
55
+ ${(features.some((f) => 'cost' === f.name) && features.some((f) => 'cache' === f.name)) ?
56
+ `That decides behaviour, not just sequence. \\\`cost\\\` activated before \\\`cache\\\`
57
+ sits inside it, so a response served from the cache never reaches \\\`cost\\\` and is
58
+ correctly charged nothing; reverse them and every cache hit is billed for money
59
+ that was never spent.
60
+ ` : `That decides behaviour, not just sequence: a feature that short-circuits the
61
+ call, such as a cache serving a hit, stops every feature nested inside it from
62
+ ever seeing that call.
63
+ `}
64
+ ${hooked.map((f) => '`' + f.name + '`').join(', ')} attach to pipeline hooks
65
+ rather than the transport, so their order does not affect what they observe.
66
+
67
+ `)
68
+ }
69
+
70
+ for (const f of features) {
71
+ Content(`#### \`${f.name}\`
72
+
73
+ ${f.title}.
74
+
75
+ `)
76
+
77
+ Content(`**Configuration**
78
+
79
+ `)
80
+
81
+ // THE TABLE IS THE MODEL'S OPTIONS, AND THE MODEL IS INCOMPLETE. Several
82
+ // features accept runtime-only options the model never declares — cost
83
+ // reads `actor` and `sink`, audit reads `sink` — so a table derived from
84
+ // `config.options` cannot list them. Deriving from a second hand-written
85
+ // list would reintroduce exactly the drift this module exists to avoid,
86
+ // so the honest fix is upstream: declare them in the feature model. Until
87
+ // then, say so rather than imply the table is exhaustive.
88
+
89
+
90
+ if (0 < f.options.length) {
91
+ Content(`| Option | Default |
92
+ |---|---|
93
+ `)
94
+ for (const o of f.options) {
95
+ Content(`| \`${o.name}\` | \`${o.value}\` |
96
+ `)
97
+ }
98
+ Content(`
99
+ `)
100
+ }
101
+ else {
102
+ Content(`\`active\` only — this feature takes no further options.
103
+
104
+ `)
105
+ }
106
+
107
+ Content(`Options above are those the model carries a default for. A feature may
108
+ also accept callback options — a \`sink\` to receive each record, for
109
+ instance — which have no default and are covered in the full feature
110
+ reference.
111
+
112
+ **Usage**
113
+
114
+ Set \`feature.${f.name}.active\` to true in the client options${
115
+ 0 < f.options.length ?
116
+ ', and override any option above in the same entry' : ''}. Every option keeps
117
+ its default unless you name it.
118
+
119
+ **Considerations**
120
+
121
+ `)
122
+
123
+ if (f.wraps) {
124
+ Content(`- Wraps the transport: its place in the activation order decides what it
125
+ sees. See [Ordering](#ordering) above.
126
+ `)
127
+ }
128
+ else {
129
+ Content(`- Attaches to pipeline hooks, not the transport, so activation order does
130
+ not change what it observes.
131
+ `)
132
+ }
133
+
134
+ if ('base' === f.transport) {
135
+ Content(`- Installs the BASE transport that the wrapping features wrap, so it must be
136
+ activated before them.
137
+ `)
138
+ }
139
+
140
+ Content(`- Inactive by default: leaving it out costs nothing at runtime.
141
+
142
+ `)
143
+ }
144
+ })
145
+
146
+
147
+ export {
148
+ ReadmeRefFeatures
149
+ }
@@ -7,6 +7,7 @@ import {
7
7
  } from '../types'
8
8
 
9
9
  import { requirePath } from '../utility'
10
+ import { featureDocs } from './FeatureDocs'
10
11
 
11
12
  import {
12
13
  entityPrimaryOp, entityIdField, opRequestShape, entityPath, entityActions,
@@ -206,6 +207,20 @@ Learn more about Voxgig SDKs at [voxgig.com/sdk](${VOXGIG_SDK}).
206
207
  : surfaces[0]
207
208
  Content(`> ${surfaceList} — all generated from one OpenAPI spec by [@voxgig/sdkgen](${SDKGEN_REPO}).
208
209
 
210
+ `)
211
+ }
212
+
213
+ // FEATURES BELONG BESIDE THE TARGETS. What an SDK can do is as much a
214
+ // reason to choose it as which language it is in, and a reader who has to
215
+ // scroll past every language section to discover that retries, caching and
216
+ // tracing are built in has already decided it is a thin HTTP wrapper.
217
+ // Named here, in one line, with the detail left to each target's README.
218
+ const features = featureDocs(model)
219
+ if (0 < features.length) {
220
+ Content(`> **Features:** ${features.map((f: any) => '`' + f.name + '`').join(', ')} — opt-in,
221
+ > inactive until switched on, and configured per client. See the Features
222
+ > section of any SDK README below for what each one does.
223
+
209
224
  `)
210
225
  }
211
226
 
@@ -561,6 +576,39 @@ forking the SDK.
561
576
  Content(`
562
577
  Pass custom features via the \`extend\` option at construction time.
563
578
 
579
+ `)
580
+
581
+ // 11c. Customization — the generator's whole story for "the output is
582
+ // not quite right": model-driven declarations, in-repo templates and
583
+ // components, merge-aware regeneration, and packages for custom targets
584
+ // and features. Every SDK repo carries its own generator, so this
585
+ // belongs in every README.
586
+ Content(`## Customizing this SDK
587
+
588
+ This repository contains its own generator (\`.sdk/\`), so the SDK is
589
+ customizable without forking any upstream tool:
590
+
591
+ - **The model** (\`.sdk/model/\`) declares everything this project owns:
592
+ package names, versions, active features, per-target settings. It is
593
+ written in [aontu](https://github.com/aontu-lang/aontu), a JSON-based
594
+ specification language designed for building ontologies: easy to edit
595
+ by hand, and files unify rather than override, so small declarations
596
+ compose into one model. Regeneration re-reads it every time.
597
+ - **Templates** (\`.sdk/tm/\`) and **components** (\`.sdk/src/cmp/\`) are
598
+ the two layers of generation, copied into this repo: templates are the
599
+ literal per-language source, components generate the API-shaped parts.
600
+ - **Regeneration merges.** By default, newly generated content is
601
+ three-way merged into existing files, so generator updates and local
602
+ edits usually converge without manual conflict handling. A project can
603
+ opt for plain overwrite instead.
604
+ - **Custom features and entire custom targets** arrive through sdkgen
605
+ packages (\`voxgig-sdkgen package add\`), on the same rails as the
606
+ bundled languages, and \`voxgig-sdkgen doctor\` reports any drift from
607
+ what a resync would write.
608
+
609
+ How-to: [customize and propagate templates](https://github.com/voxgig/sdkgen/blob/main/docs/how-to/customize-and-propagate-templates.md).
610
+ The full story: [voxgig.com/sdk/custom](https://voxgig.com/sdk/custom).
611
+
564
612
  `)
565
613
 
566
614
  // 12. Per-language docs links
@@ -269,7 +269,11 @@ function opParams(op: any): any[] {
269
269
  const out: any[] = []
270
270
 
271
271
  points.forEach((pt: any, pointIndex: number) => {
272
- const params = pt && pt.args && pt.args.params ? each(pt.args.params) : []
272
+ // Path AND query: a path-param-only read misses e.g. GET /result?trace_id=,
273
+ // which has no path param at all but still addresses one record.
274
+ const pathParams = pt && pt.args && pt.args.params ? each(pt.args.params) : []
275
+ const queryParams = pt && pt.args && pt.args.query ? each(pt.args.query) : []
276
+ const params = [...pathParams, ...queryParams]
273
277
  const requiredHere: Record<string, boolean> = {}
274
278
  params.forEach((p: any) => {
275
279
  if (p && null != p.name) {
package/src/sdkgen.ts CHANGED
@@ -17,7 +17,7 @@ import type {
17
17
  ActionResult,
18
18
  } from './types'
19
19
 
20
- import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix,
20
+ import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix, isHttpBasicAuth,
21
21
  CONFIG_DATA_THRESHOLD, CONFIG_REPR_VALUES, isConfigData, configRepr,
22
22
  configReprSetting, configDefinition, clean, rawStringLiteral } from './utility'
23
23
 
@@ -49,6 +49,7 @@ import { ReadmeEntity } from './cmp/ReadmeEntity'
49
49
  import { ReadmeHowto } from './cmp/ReadmeHowto'
50
50
  import { ReadmeExplanation } from './cmp/ReadmeExplanation'
51
51
  import { ReadmeRef } from './cmp/ReadmeRef'
52
+ import { ReadmeRefFeatures } from './cmp/ReadmeRefFeatures'
52
53
  import { FeatureHook } from './cmp/FeatureHook'
53
54
  import { registerComponent } from './cmp/Registered'
54
55
  import type { RegisterOptions } from './cmp/Registered'
@@ -392,10 +393,20 @@ function SdkGen(opts: SdkGenOptions) {
392
393
  // A skipped external target is still removed from the in-tree model
393
394
  // above: neither `active: false` nor a missing `output.create: false`
394
395
  // destination may relocate it into `<sdk-repo>/<target>/`.
395
- if (null != ext.skip) {
396
+ // The snapshot is deliberately not re-taken to ADMIT an item: a
397
+ // destination that appeared since would be written without ever having
398
+ // been ownership-validated. Re-taking it to SKIP one is the opposite
399
+ // direction and cannot open that hole. Without this, an optional
400
+ // destination deleted or moved between the snapshot and this pass is
401
+ // recreated by jostraca's ensureDir — exactly what `output.create:
402
+ // false` promises not to do.
403
+ const gone = false === ext.target.output?.create && !fs.existsSync(ext.folder)
404
+
405
+ if (null != ext.skip || gone) {
396
406
  log.info({
397
407
  point: 'generate-external-skip', target: ext.name, folder: ext.folder,
398
- note: ext.skip
408
+ note: ext.skip ?? (ext.name + ' output folder disappeared after ' +
409
+ 'preflight and output.create=false, not generated')
399
410
  })
400
411
  continue
401
412
  }
@@ -794,10 +805,18 @@ function checkExternalFolders(external: ExternalPlan[], root: string, fs: any) {
794
805
  const claimed: Record<string, string> = {}
795
806
 
796
807
  for (const ext of external) {
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
808
+ // An INACTIVE item writes nothing and claims nothing: it is out of the
809
+ // model for this run.
810
+ //
811
+ // An item skipped only because its optional destination is absent is a
812
+ // different case. Path containment and the duplicate claim are model
813
+ // invariants — they do not depend on folder contents — so skipping them
814
+ // here let two items claim one absent path: the ordinary one generated
815
+ // there, and the NEXT identical run then saw the folder, detected the
816
+ // duplicate, and refused everything. The failure appeared one run after
817
+ // the mistake. Validate the path; the `existsSync` guard below still
818
+ // skips every check that reads the folder.
819
+ if (!ext.active) continue
801
820
 
802
821
  const label = ext.kind.charAt(0).toUpperCase() + ext.kind.slice(1)
803
822
 
@@ -1067,6 +1086,7 @@ export {
1067
1086
  ReadmeHowto,
1068
1087
  ReadmeExplanation,
1069
1088
  ReadmeRef,
1089
+ ReadmeRefFeatures,
1070
1090
  FeatureHook,
1071
1091
  registerComponent,
1072
1092
 
@@ -1076,6 +1096,7 @@ export {
1076
1096
  requirePath,
1077
1097
  isAuthActive,
1078
1098
  resolveAuthPrefix,
1099
+ isHttpBasicAuth,
1079
1100
  CONFIG_DATA_THRESHOLD,
1080
1101
  CONFIG_REPR_VALUES,
1081
1102
  isConfigData,
package/src/utility.ts CHANGED
@@ -62,6 +62,24 @@ function resolveAuthPrefix(model: any): string {
62
62
  }
63
63
 
64
64
 
65
+ // True when the spec's security scheme is genuine HTTP Basic Auth (two
66
+ // credentials, base64-joined) rather than a single bearer-style token with
67
+ // a prefix. Priority order mirrors resolveAuthPrefix:
68
+ // 1. main.kit.config.auth.basic (per-SDK user override)
69
+ // 2. main.kit.info.security, spec-derived: type 'http' + a 'basic' prefix
70
+ // (apidef sets prefix from the OpenAPI `scheme: basic` value, title-cased)
71
+ function isHttpBasicAuth(model: any): boolean {
72
+ const auth = getModelPath(model, `main.${KIT}.config.auth`,
73
+ { only_active: false, required: false })
74
+ if (null != auth && null != auth.basic) return Boolean(auth.basic)
75
+
76
+ const security = getModelPath(model, `main.${KIT}.info.security`,
77
+ { only_active: false, required: false })
78
+ return null != security && 'http' === security.type &&
79
+ 'basic' === String(security.prefix || '').toLowerCase()
80
+ }
81
+
82
+
65
83
  function requirePath(ctx$: any, path: string, flags?: { ignore?: boolean }): any {
66
84
  const fullpath = resolvePath(ctx$, path)
67
85
  const ignore = null == flags?.ignore ? false : flags.ignore
@@ -98,6 +116,7 @@ export {
98
116
  requirePath,
99
117
  isAuthActive,
100
118
  resolveAuthPrefix,
119
+ isHttpBasicAuth,
101
120
  SdkGenError,
102
121
  CONFIG_DATA_THRESHOLD,
103
122
  CONFIG_REPR_VALUES,
@@ -298,6 +317,7 @@ function configDefinition(model: any, targetname?: string): { def: any, json: st
298
317
 
299
318
  const authActive = isAuthActive(model)
300
319
  const authPrefix = resolveAuthPrefix(model)
320
+ const authBasic = isHttpBasicAuth(model)
301
321
 
302
322
  let baseUrl = ''
303
323
  try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
@@ -340,7 +360,7 @@ function configDefinition(model: any, targetname?: string): { def: any, json: st
340
360
  options.server = svars.reduce((a: any, v: any) => (a[v.name] = v.dflt, a), {})
341
361
  }
342
362
  if (authActive) {
343
- options.auth = { prefix: authPrefix }
363
+ options.auth = authBasic ? { prefix: authPrefix, basic: true } : { prefix: authPrefix }
344
364
  }
345
365
  options.headers = headers
346
366
  options.entity = entityStubs