@voxgig/sdkgen 4.4.1 → 4.5.1

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 (104) 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/core.clj +6 -2
  61. package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
  62. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +5 -0
  63. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +4 -0
  64. package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
  65. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +4 -1
  66. package/project/.sdk/tm/go/feature/test_feature.go +17 -6
  67. package/project/.sdk/tm/go/test/feature_corpus_test.go +10 -3
  68. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +5 -4
  69. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
  70. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
  71. package/project/.sdk/tm/js/test/feature/Corpus.test.js +13 -3
  72. package/project/.sdk/tm/js/test/feature.test.js +19 -9
  73. package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
  74. package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
  75. package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
  76. package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
  77. package/project/.sdk/tm/ocaml/sdk_features.ml +5 -0
  78. package/project/.sdk/tm/ocaml/sdk_runtime.ml +4 -1
  79. package/project/.sdk/tm/perl/t/feature_corpus.t +5 -4
  80. package/project/.sdk/tm/perl/utility/make_options.pm +4 -1
  81. package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
  82. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +7 -4
  83. package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
  84. package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
  85. package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
  86. package/project/.sdk/tm/py/test/test_feature.py +40 -0
  87. package/project/.sdk/tm/py/test/test_feature_corpus.py +14 -3
  88. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -4
  89. package/project/.sdk/tm/rb/utility/make_options.rb +4 -1
  90. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
  91. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
  92. package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
  93. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +13 -3
  94. package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
  95. package/project/sdkgen-package.json +1 -1
  96. package/src/cmp/FeatureDocs.ts +100 -0
  97. package/src/cmp/Readme.ts +2 -0
  98. package/src/cmp/ReadmeFeatures.ts +113 -0
  99. package/src/cmp/ReadmeRef.ts +4 -0
  100. package/src/cmp/ReadmeRefFeatures.ts +149 -0
  101. package/src/cmp/ReadmeTop.ts +48 -0
  102. package/src/helpers/opShape.ts +5 -1
  103. package/src/sdkgen.ts +28 -7
  104. package/src/utility.ts +21 -1
@@ -25,9 +25,19 @@ import { join } from 'node:path'
25
25
  import { SDK, TEST_JSON_FILE } from '../utility/index'
26
26
 
27
27
 
28
- // Features with a corpus section. A name here with no section is a skip, not
29
- // a failure: an SDK generated without the feature has nothing to run.
30
- const FEATURES = ['cost']
28
+ // Features with a corpus section read from the corpus itself, so a
29
+ // project-authored section (a custom feature added under
30
+ // .sdk/test/feature/) runs without editing this file. Read eagerly: the
31
+ // node test runner collects the per-feature tests at describe time,
32
+ // before any `before` hook fires. An SDK generated without a listed
33
+ // feature still skips, not fails.
34
+ const FEATURES = Object.keys((() => {
35
+ try {
36
+ return JSON.parse(readFileSync(
37
+ join(__dirname, '..', TEST_JSON_FILE), 'utf8')).feature || {}
38
+ }
39
+ catch (e) { return {} }
40
+ })()).sort()
31
41
 
32
42
 
33
43
  // One operation this SDK can actually perform.
@@ -21,6 +21,16 @@ function recordingServer(reply?: (n: number, fetchdef: any) => any) {
21
21
  }
22
22
 
23
23
 
24
+ // A subtest that drives a second feature (netsim as the simulated network)
25
+ // can only run when this SDK was generated with it: the harness skips
26
+ // absent features, which would leave the scenario unsimulated and the
27
+ // assertions meaningless. Same convention as the other targets.
28
+ function skipWithout(name: string): string | boolean {
29
+ return hasFeature(name) ? false
30
+ : ('this SDK was generated without the ' + name + ' feature')
31
+ }
32
+
33
+
24
34
  describe('feature', () => {
25
35
 
26
36
  test('at least the test feature is present', () => {
@@ -95,7 +105,7 @@ describe('feature', () => {
95
105
  // --- retry ----------------------------------------------------------------
96
106
  if (hasFeature('retry')) describe('retry', () => {
97
107
 
98
- test('retries transient failures then succeeds', async () => {
108
+ test('retries transient failures then succeeds', { skip: skipWithout('netsim') }, async () => {
99
109
  const clock = makeClock()
100
110
  const h = makeClient({ features: [
101
111
  { name: 'netsim', options: { failTimes: 2, failStatus: 503 } },
@@ -105,7 +115,7 @@ describe('feature', () => {
105
115
  strictEqual(h.client._retry.attempts, 2)
106
116
  })
107
117
 
108
- test('gives up after the budget', async () => {
118
+ test('gives up after the budget', { skip: skipWithout('netsim') }, async () => {
109
119
  const clock = makeClock()
110
120
  const h = makeClient({ features: [
111
121
  { name: 'netsim', options: { failTimes: 9, failStatus: 500 } },
@@ -131,7 +141,7 @@ describe('feature', () => {
131
141
  strictEqual(n, 3)
132
142
  })
133
143
 
134
- test('honours a server Retry-After', async () => {
144
+ test('honours a server Retry-After', { skip: skipWithout('netsim') }, async () => {
135
145
  const clock = makeClock()
136
146
  const h = makeClient({ features: [
137
147
  { name: 'netsim', options: { rateLimitTimes: 1, retryAfter: 2 } },
@@ -141,7 +151,7 @@ describe('feature', () => {
141
151
  strictEqual(clock.time, 2000)
142
152
  })
143
153
 
144
- test('default jitter path still succeeds', async () => {
154
+ test('default jitter path still succeeds', { skip: skipWithout('netsim') }, async () => {
145
155
  const h = makeClient({ features: [
146
156
  { name: 'netsim', options: { failTimes: 1 } },
147
157
  { name: 'retry', options: { retries: 2, minDelay: 0 } },
@@ -154,7 +164,7 @@ describe('feature', () => {
154
164
  // --- timeout --------------------------------------------------------------
155
165
  if (hasFeature('timeout')) describe('timeout', () => {
156
166
 
157
- test('a slow request times out', async () => {
167
+ test('a slow request times out', { skip: skipWithout('netsim') }, async () => {
158
168
  const h = makeClient({ features: [
159
169
  { name: 'netsim', options: { latency: 80 } },
160
170
  { name: 'timeout', options: { ms: 10 } },
@@ -319,7 +329,7 @@ describe('feature', () => {
319
329
  // --- metrics --------------------------------------------------------------
320
330
  if (hasFeature('metrics')) describe('metrics', () => {
321
331
 
322
- test('counts ok and err per op', async () => {
332
+ test('counts ok and err per op', { skip: skipWithout('netsim') }, async () => {
323
333
  const h = makeClient({ features: [
324
334
  { name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
325
335
  { name: 'metrics', options: {} },
@@ -352,7 +362,7 @@ describe('feature', () => {
352
362
  ok(/^00-.+-.+-01$/.test(sent['traceparent']))
353
363
  })
354
364
 
355
- test('records a failed span on error', async () => {
365
+ test('records a failed span on error', { skip: skipWithout('netsim') }, async () => {
356
366
  const h = makeClient({ features: [
357
367
  { name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
358
368
  { name: 'telemetry', options: {} },
@@ -377,7 +387,7 @@ describe('feature', () => {
377
387
  strictEqual(seen[0].headers.authorization, '<redacted>')
378
388
  })
379
389
 
380
- test('captures failures', async () => {
390
+ test('captures failures', { skip: skipWithout('netsim') }, async () => {
381
391
  const h = makeClient({ features: [
382
392
  { name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
383
393
  { name: 'debug', options: {} },
@@ -391,7 +401,7 @@ describe('feature', () => {
391
401
  // --- audit ----------------------------------------------------------------
392
402
  if (hasFeature('audit')) describe('audit', () => {
393
403
 
394
- test('one record per op with sink + actor', async () => {
404
+ test('one record per op with sink + actor', { skip: skipWithout('netsim') }, async () => {
395
405
  const sink: any[] = []
396
406
  const h = makeClient({ features: [
397
407
  { name: 'netsim', options: { failTimes: 1, failStatus: 500 } },
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.4.1",
6
+ "version": "4.5.1",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",
@@ -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,