@voxgig/create-sdkgen 0.20.1 → 0.20.2

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.
package/README.md CHANGED
@@ -6,6 +6,8 @@ Ruby, Lua, plus a CLI and MCP server).
6
6
 
7
7
  ## Quickstart
8
8
 
9
+ From a spec to a tested SDK, in four steps:
10
+
9
11
  ```sh
10
12
  # 1. Scaffold a project from your OpenAPI 3 spec
11
13
  create-sdkgen my-api -d ./openapi.yaml -o ./my-api-sdk
@@ -32,20 +34,39 @@ generated output and is overwritten on each regenerate.
32
34
  | --- | --- |
33
35
  | `<name>` | SDK name (kebab-case), the package base name |
34
36
  | `-d, --def <spec>` | OpenAPI 3 spec file (`.yaml`/`.json`) |
35
- | `-o, --folder <dir>` | output directory |
37
+ | `-o, --folder <dir>` | output directory (default `<name>-sdk`) |
36
38
  | `-t, --target <langs>` | targets to add during scaffold (e.g. `ts,py,go`) |
37
39
  | `-f, --feature <feats>` | features to add (e.g. `test`) |
38
40
  | `--no-install` | skip `npm install` |
39
41
  | `-h`, `-v` | help, version |
40
42
 
41
- ## Building an SDK with an AI agent?
43
+ ## After the scaffold
44
+
45
+ The loop from here is:
46
+
47
+ ```
48
+ edit .sdk/model/entity/*.aontu → (cd .sdk && npm run generate) → re-run the target tests
49
+ ```
50
+
51
+ Commit before regenerating, since generation overwrites the target
52
+ directories. Every generated target carries a `readme_examples` test that
53
+ extracts each code block from that language's `README.md` and `REFERENCE.md`,
54
+ compiles it, and runs the runnable ones in offline test mode, so a documented
55
+ example that does not work fails that target's build. When the tests are
56
+ green, publish each language package the way its ecosystem expects (npm, PyPI,
57
+ Packagist, RubyGems, LuaRocks, or a Go module tag).
42
58
 
43
- Point your agent at **[`AGENTS.md`](./AGENTS.md)** — the full end-to-end guide
44
- (spec → scaffold → generate → test → publish), what to edit vs. what is
45
- generated, the doc-example test guarantee, and the real-world shape gotchas.
59
+ The step-by-step tutorial, the model reference, and the generator's CLI flags
60
+ are in the [sdkgen documentation](https://github.com/voxgig/sdkgen/tree/main/docs).
46
61
 
47
62
  ## The toolchain
48
63
 
49
64
  - **create-sdkgen** (this package) — scaffolds the project.
50
65
  - **[@voxgig/apidef](https://github.com/voxgig/apidef)** — parses your OpenAPI spec into the model.
51
66
  - **[@voxgig/sdkgen](https://github.com/voxgig/sdkgen)** — generates the SDKs from the model.
67
+
68
+ ## Contributing
69
+
70
+ How to validate a change, and why a pull request from a fork gets no CI, are
71
+ in [`CONTRIBUTING.md`](./CONTRIBUTING.md). The documentation follows
72
+ [the style guide](./STYLE-GUIDE.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxgig/create-sdkgen",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
4
4
  "main": "dist/create-sdkgen.js",
5
5
  "type": "commonjs",
6
6
  "types": "dist/create-sdkgen.d.ts",
@@ -26,6 +26,7 @@
26
26
  "test-some": "node --enable-source-maps --test-name-pattern=\"$npm_config_pattern\" --test dist-test/**/*.test.js",
27
27
  "watch": "tsc --build src test -w",
28
28
  "build": "tsc --build src test",
29
+ "scan-prose": "python3 tools/check_prose.py",
29
30
  "test-model": "voxgig-model project/.sdk/test/test.aon",
30
31
  "test-model-watch": "voxgig-model -w project/.sdk/test/test.aon",
31
32
  "clean": "rm -rf dist dist-test node_modules yarn.lock package-lock.json",
@@ -84,7 +84,7 @@ jobs:
84
84
  - if: hashFiles('go-mcp/go.mod') != ''
85
85
  uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
86
86
  with:
87
- go-version: '1.24'
87
+ go-version: '1.25'
88
88
  - if: hashFiles('go-mcp/go.mod') != ''
89
89
  run: |
90
90
  cd go-mcp
@@ -369,7 +369,10 @@ jobs:
369
369
  with:
370
370
  ocaml-compiler: '5.2'
371
371
  - if: hashFiles('ocaml/Makefile') != ''
372
- run: make -C ocaml test
372
+ # setup-ocaml installs the compiler into an opam switch; without
373
+ # `opam exec` the switch is not on PATH and make dies with
374
+ # `ocamlc: No such file or directory` (exit 127).
375
+ run: opam exec -- make -C ocaml test
373
376
 
374
377
  haskell:
375
378
  runs-on: ubuntu-latest
@@ -0,0 +1,172 @@
1
+
2
+ # STORED-NULL SEMANTICS.
3
+ #
4
+ # One question, asked of every reader: when a key is PRESENT and holds a JSON
5
+ # null, is that "no value" (the same as an absent key) or is it a value?
6
+ #
7
+ # Nothing in the corpus asked it before. `minor.getprop` and `minor.haskey`
8
+ # have ~50 cases each and not one stores a null: every entry is an absent key
9
+ # or a non-null value. So the ports diverged silently, and they HAVE:
10
+ #
11
+ # port getprop({x:null},'x','ALT') haskey({x:null},'x')
12
+ # ──── ─────────────────────────── ────────────────────
13
+ # go 'ALT' false
14
+ # py 'ALT' false
15
+ # perl 'ALT' false
16
+ # rb null true
17
+ # php null true
18
+ # js null true
19
+ #
20
+ # That is not academic. It is the direct cause of the `auth: null` bug family:
21
+ # `auth: null` is the documented way to suppress auth outright, and where a
22
+ # stored null reads as "no value" the optspec `auth` default fires, the
23
+ # suppression becomes "use the default auth", and the credential the caller
24
+ # withheld goes out on the wire.
25
+ #
26
+ # 0.3.2 answers "no value" — the top three rows. This section pins that.
27
+ #
28
+ #
29
+ # THIS SECTION IS OPT-IN, AND DELIBERATELY SO.
30
+ #
31
+ # Half the tree still vendors a struct that answers the other way. Putting
32
+ # these cases in `minor` would red those targets today and gate the whole
33
+ # corpus on finishing the struct migration everywhere first. Instead a target
34
+ # wires this section up as it migrates:
35
+ #
36
+ # runsetflags(nullsemSpec["getprop"], {"null": False}, getprop_wrapper)
37
+ #
38
+ # so the section becomes that target's null-semantics gate, and a target that
39
+ # has not migrated simply does not reference it yet.
40
+ #
41
+ #
42
+ # `null: false` IS NOT OPTIONAL HERE.
43
+ #
44
+ # The runner's default fixJSON rewrites every JSON null - on BOTH the `in` and
45
+ # `out` sides - to the string '__NULL__' before the subject ever sees it. Run
46
+ # without the flag, every case below would pass the STRING '__NULL__' as the
47
+ # stored value, which is not a null and is not the question. The section would
48
+ # then pass on every port while asserting nothing about null at all - the
49
+ # exact "green while checking nothing" failure the corpus guards exist to
50
+ # prevent. `minor.getelem` and `minor.clone` already run with the flag for the
51
+ # same reason.
52
+ #
53
+ # With the flag, an entry with NO `out` expects a null result, and `out:` with
54
+ # a value expects that value.
55
+ #
56
+ #
57
+ # VERIFIED AGAINST THE REFERENCE, NOT GUESSED.
58
+ #
59
+ # All 33 cases below were run against upstream @voxgig/struct 0.3.2
60
+ # (typescript/src/StructUtility.ts, the canonical port) and all 33 pass. They
61
+ # are what the reference DOES, not what it was assumed to do.
62
+ #
63
+ # That mattered on the first run: `getprop([null], 0, 'ALT')` was assumed to
64
+ # be a formality and turned out to be a live defect in the py port's vendored
65
+ # copy. Canonical getprop tests `isnode(val)` - map AND list - and then
66
+ # applies the null rule once to whatever it read. The py copy branches on
67
+ # ismap/islist and its list branch does `return val[key]`, an early return
68
+ # that skips the null check entirely, so it hands back the stored null where
69
+ # every other reader would substitute the alt. Its map path is correct, which
70
+ # is why nothing noticed.
71
+ #
72
+ # So py cannot opt in to this section until its vendored struct is resynced.
73
+ # That is the section working as intended: it says "not yet" in a test result
74
+ # instead of in a comment.
75
+
76
+
77
+ # Does the ALT fire for a stored null, as it does for an absent key?
78
+ getprop: {
79
+ set: [
80
+ # THE CASE. Absent and stored-null must answer alike.
81
+ { in: { val: {x:null}, key: x, alt: 'ALT' }, out: 'ALT' }
82
+ { in: { val: {}, key: x, alt: 'ALT' }, out: 'ALT' }
83
+
84
+ # No alt: a stored null reads back as null either way, so this pins that
85
+ # the rule does not turn a null into something else.
86
+ { in: { val: {x:null}, key: x } }
87
+
88
+ # A stored null must not leak past its own key.
89
+ { in: { val: {x:null, y:1}, key: y, alt: 'ALT' }, out: 1 }
90
+ { in: { val: {x:1, y:null}, key: x, alt: 'ALT' }, out: 1 }
91
+
92
+ # false, 0 and '' are VALUES, not absence. A port that collapses "nullish"
93
+ # to "falsy" passes every case above and fails these.
94
+ { in: { val: {x:false}, key: x, alt: 'ALT' }, out: false }
95
+ { in: { val: {x:0}, key: x, alt: 'ALT' }, out: 0 }
96
+ { in: { val: {x:''}, key: x, alt: 'ALT' }, out: '' }
97
+
98
+ # Lists too: index 0 holding a null is the list-shaped form of the same
99
+ # question, and a port can easily fix one and not the other.
100
+ { in: { val: [null], key: 0, alt: 'ALT' }, out: 'ALT' }
101
+ { in: { val: [null, 1], key: 1, alt: 'ALT' }, out: 1 }
102
+ ]
103
+ }
104
+
105
+
106
+ # getelem is the list-index reader. Same rule, different entry point - and it
107
+ # is a separate function in every port, so it needs its own cases.
108
+ getelem: {
109
+ set: [
110
+ { in: { val: [null], key: 0, alt: 'ALT' }, out: 'ALT' }
111
+ { in: { val: [10, null, 30], key: 1, alt: 'ALT' }, out: 'ALT' }
112
+ { in: { val: [10, null, 30], key: 2, alt: 'ALT' }, out: 30 }
113
+
114
+ # Negative indices are getelem's whole reason for existing; a stored null
115
+ # at a negative index is the case a port is most likely to miss.
116
+ { in: { val: [10, null], key: -1, alt: 'ALT' }, out: 'ALT' }
117
+ { in: { val: [null, 20], key: -1, alt: 'ALT' }, out: 20 }
118
+
119
+ { in: { val: [false], key: 0, alt: 'ALT' }, out: false }
120
+ ]
121
+ }
122
+
123
+
124
+ # A path that lands on a stored null, and - the case that actually bites - a
125
+ # path that must TRAVERSE one. Traversal through a null is not the same
126
+ # question as reading one, and a port can get the leaf right and still walk
127
+ # into a null parent.
128
+ getpath: {
129
+ set: [
130
+ { in: { path: 'a', store: {a:null} } }
131
+ { in: { path: 'a.b', store: {a:null} } }
132
+ { in: { path: 'a.b.c', store: {a:{b:null}} } }
133
+ { in: { path: 'a.b', store: {a:{b:null}} } }
134
+ { in: { path: 'a.b', store: {a:{b:1}} }, out: 1 }
135
+ { in: { path: 'a.b', store: {a:{b:false}} }, out: false }
136
+ ]
137
+ }
138
+
139
+
140
+ # Presence, asked directly. This is the reader the auth-null fix needs in
141
+ # every port, and the one whose answer splits the tree.
142
+ haskey: {
143
+ set: [
144
+ { in: { src: {x:null}, key: x }, out: false }
145
+ { in: { src: {}, key: x }, out: false }
146
+ { in: { src: {x:1}, key: x }, out: true }
147
+
148
+ # Again: falsy is not absent.
149
+ { in: { src: {x:false}, key: x }, out: true }
150
+ { in: { src: {x:0}, key: x }, out: true }
151
+ { in: { src: {x:''}, key: x }, out: true }
152
+
153
+ { in: { src: [null], key: 0 }, out: false }
154
+ { in: { src: [1], key: 0 }, out: true }
155
+ ]
156
+ }
157
+
158
+
159
+ # The other direction, and the reason `auth: null` is expressible at all:
160
+ # presence is STRUCTURAL. keysof lists a null-valued key, because the key IS
161
+ # there - it is only the READERS above that treat its value as no value.
162
+ #
163
+ # A port that made haskey false by DELETING the key would pass every case
164
+ # above and fail here, and it would also make the suppression unwritable:
165
+ # there would be nothing left for makeOptions to detect.
166
+ keysof: {
167
+ set: [
168
+ { in: {a:null}, out: [a] }
169
+ { in: {a:null, b:1}, out: [a, b] }
170
+ { in: {a:1, b:null}, out: [a, b] }
171
+ ]
172
+ }
@@ -12,6 +12,7 @@ struct: transform: @"transform.aon"
12
12
  struct: walk: @"walk.aon"
13
13
  struct: validate: @"validate.aon"
14
14
  struct: select: @"select.aon"
15
+ struct: nullsem: @"nullsem.aon"
15
16
 
16
17
  primary: check: {
17
18
  DEF: {
@@ -1543,7 +1543,11 @@
1543
1543
  "DEF": {
1544
1544
  "setup": {
1545
1545
  "a": {
1546
- "apikey": "APIKEY01"
1546
+ "apikey": "APIKEY01",
1547
+ "auth": {
1548
+ "basic": false,
1549
+ "prefix": ""
1550
+ }
1547
1551
  }
1548
1552
  }
1549
1553
  },
@@ -10276,6 +10280,351 @@
10276
10280
  ]
10277
10281
  }
10278
10282
  },
10283
+ "nullsem": {
10284
+ "getelem": {
10285
+ "set": [
10286
+ {
10287
+ "in": {
10288
+ "alt": "ALT",
10289
+ "key": 0,
10290
+ "val": [
10291
+ null
10292
+ ]
10293
+ },
10294
+ "out": "ALT"
10295
+ },
10296
+ {
10297
+ "in": {
10298
+ "alt": "ALT",
10299
+ "key": 1,
10300
+ "val": [
10301
+ 10,
10302
+ null,
10303
+ 30
10304
+ ]
10305
+ },
10306
+ "out": "ALT"
10307
+ },
10308
+ {
10309
+ "in": {
10310
+ "alt": "ALT",
10311
+ "key": 2,
10312
+ "val": [
10313
+ 10,
10314
+ null,
10315
+ 30
10316
+ ]
10317
+ },
10318
+ "out": 30
10319
+ },
10320
+ {
10321
+ "in": {
10322
+ "alt": "ALT",
10323
+ "key": -1,
10324
+ "val": [
10325
+ 10,
10326
+ null
10327
+ ]
10328
+ },
10329
+ "out": "ALT"
10330
+ },
10331
+ {
10332
+ "in": {
10333
+ "alt": "ALT",
10334
+ "key": -1,
10335
+ "val": [
10336
+ null,
10337
+ 20
10338
+ ]
10339
+ },
10340
+ "out": 20
10341
+ },
10342
+ {
10343
+ "in": {
10344
+ "alt": "ALT",
10345
+ "key": 0,
10346
+ "val": [
10347
+ false
10348
+ ]
10349
+ },
10350
+ "out": false
10351
+ }
10352
+ ]
10353
+ },
10354
+ "getpath": {
10355
+ "set": [
10356
+ {
10357
+ "in": {
10358
+ "path": "a",
10359
+ "store": {
10360
+ "a": null
10361
+ }
10362
+ }
10363
+ },
10364
+ {
10365
+ "in": {
10366
+ "path": "a.b",
10367
+ "store": {
10368
+ "a": null
10369
+ }
10370
+ }
10371
+ },
10372
+ {
10373
+ "in": {
10374
+ "path": "a.b.c",
10375
+ "store": {
10376
+ "a": {
10377
+ "b": null
10378
+ }
10379
+ }
10380
+ }
10381
+ },
10382
+ {
10383
+ "in": {
10384
+ "path": "a.b",
10385
+ "store": {
10386
+ "a": {
10387
+ "b": null
10388
+ }
10389
+ }
10390
+ }
10391
+ },
10392
+ {
10393
+ "in": {
10394
+ "path": "a.b",
10395
+ "store": {
10396
+ "a": {
10397
+ "b": 1
10398
+ }
10399
+ }
10400
+ },
10401
+ "out": 1
10402
+ },
10403
+ {
10404
+ "in": {
10405
+ "path": "a.b",
10406
+ "store": {
10407
+ "a": {
10408
+ "b": false
10409
+ }
10410
+ }
10411
+ },
10412
+ "out": false
10413
+ }
10414
+ ]
10415
+ },
10416
+ "getprop": {
10417
+ "set": [
10418
+ {
10419
+ "in": {
10420
+ "alt": "ALT",
10421
+ "key": "x",
10422
+ "val": {
10423
+ "x": null
10424
+ }
10425
+ },
10426
+ "out": "ALT"
10427
+ },
10428
+ {
10429
+ "in": {
10430
+ "alt": "ALT",
10431
+ "key": "x",
10432
+ "val": {}
10433
+ },
10434
+ "out": "ALT"
10435
+ },
10436
+ {
10437
+ "in": {
10438
+ "key": "x",
10439
+ "val": {
10440
+ "x": null
10441
+ }
10442
+ }
10443
+ },
10444
+ {
10445
+ "in": {
10446
+ "alt": "ALT",
10447
+ "key": "y",
10448
+ "val": {
10449
+ "x": null,
10450
+ "y": 1
10451
+ }
10452
+ },
10453
+ "out": 1
10454
+ },
10455
+ {
10456
+ "in": {
10457
+ "alt": "ALT",
10458
+ "key": "x",
10459
+ "val": {
10460
+ "x": 1,
10461
+ "y": null
10462
+ }
10463
+ },
10464
+ "out": 1
10465
+ },
10466
+ {
10467
+ "in": {
10468
+ "alt": "ALT",
10469
+ "key": "x",
10470
+ "val": {
10471
+ "x": false
10472
+ }
10473
+ },
10474
+ "out": false
10475
+ },
10476
+ {
10477
+ "in": {
10478
+ "alt": "ALT",
10479
+ "key": "x",
10480
+ "val": {
10481
+ "x": 0
10482
+ }
10483
+ },
10484
+ "out": 0
10485
+ },
10486
+ {
10487
+ "in": {
10488
+ "alt": "ALT",
10489
+ "key": "x",
10490
+ "val": {
10491
+ "x": ""
10492
+ }
10493
+ },
10494
+ "out": ""
10495
+ },
10496
+ {
10497
+ "in": {
10498
+ "alt": "ALT",
10499
+ "key": 0,
10500
+ "val": [
10501
+ null
10502
+ ]
10503
+ },
10504
+ "out": "ALT"
10505
+ },
10506
+ {
10507
+ "in": {
10508
+ "alt": "ALT",
10509
+ "key": 1,
10510
+ "val": [
10511
+ null,
10512
+ 1
10513
+ ]
10514
+ },
10515
+ "out": 1
10516
+ }
10517
+ ]
10518
+ },
10519
+ "haskey": {
10520
+ "set": [
10521
+ {
10522
+ "in": {
10523
+ "key": "x",
10524
+ "src": {
10525
+ "x": null
10526
+ }
10527
+ },
10528
+ "out": false
10529
+ },
10530
+ {
10531
+ "in": {
10532
+ "key": "x",
10533
+ "src": {}
10534
+ },
10535
+ "out": false
10536
+ },
10537
+ {
10538
+ "in": {
10539
+ "key": "x",
10540
+ "src": {
10541
+ "x": 1
10542
+ }
10543
+ },
10544
+ "out": true
10545
+ },
10546
+ {
10547
+ "in": {
10548
+ "key": "x",
10549
+ "src": {
10550
+ "x": false
10551
+ }
10552
+ },
10553
+ "out": true
10554
+ },
10555
+ {
10556
+ "in": {
10557
+ "key": "x",
10558
+ "src": {
10559
+ "x": 0
10560
+ }
10561
+ },
10562
+ "out": true
10563
+ },
10564
+ {
10565
+ "in": {
10566
+ "key": "x",
10567
+ "src": {
10568
+ "x": ""
10569
+ }
10570
+ },
10571
+ "out": true
10572
+ },
10573
+ {
10574
+ "in": {
10575
+ "key": 0,
10576
+ "src": [
10577
+ null
10578
+ ]
10579
+ },
10580
+ "out": false
10581
+ },
10582
+ {
10583
+ "in": {
10584
+ "key": 0,
10585
+ "src": [
10586
+ 1
10587
+ ]
10588
+ },
10589
+ "out": true
10590
+ }
10591
+ ]
10592
+ },
10593
+ "keysof": {
10594
+ "set": [
10595
+ {
10596
+ "in": {
10597
+ "a": null
10598
+ },
10599
+ "out": [
10600
+ "a"
10601
+ ]
10602
+ },
10603
+ {
10604
+ "in": {
10605
+ "a": null,
10606
+ "b": 1
10607
+ },
10608
+ "out": [
10609
+ "a",
10610
+ "b"
10611
+ ]
10612
+ },
10613
+ {
10614
+ "in": {
10615
+ "a": 1,
10616
+ "b": null
10617
+ },
10618
+ "out": [
10619
+ "a",
10620
+ "b"
10621
+ ]
10622
+ }
10623
+ ]
10624
+ },
10625
+ "name": "nullsem",
10626
+ "set": []
10627
+ },
10279
10628
  "select": {
10280
10629
  "alts": {
10281
10630
  "data": {