@voxgig/sdkgen 3.5.1 → 3.6.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 (171) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/ExternalTarget.js +1 -1
  3. package/dist/cmp/ExternalTarget.js.map +1 -1
  4. package/dist/helpers/naming.d.ts +4 -1
  5. package/dist/helpers/naming.js +119 -5
  6. package/dist/helpers/naming.js.map +1 -1
  7. package/dist/helpers/opShape.js +72 -0
  8. package/dist/helpers/opShape.js.map +1 -1
  9. package/dist/helpers/packageMeta.d.ts +2 -1
  10. package/dist/helpers/packageMeta.js +57 -20
  11. package/dist/helpers/packageMeta.js.map +1 -1
  12. package/dist/sdkgen.d.ts +3 -3
  13. package/dist/sdkgen.js +61 -18
  14. package/dist/sdkgen.js.map +1 -1
  15. package/dist/testkit.d.ts +39 -0
  16. package/dist/testkit.js +663 -0
  17. package/dist/testkit.js.map +1 -0
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +32 -8
  20. package/project/.sdk/src/cmp/c/Test_c.ts +5 -7
  21. package/project/.sdk/src/cmp/clojure/Test_clojure.ts +3 -7
  22. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +4 -7
  23. package/project/.sdk/src/cmp/csharp/Package_csharp.ts +1 -1
  24. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -7
  25. package/project/.sdk/src/cmp/dart/Test_dart.ts +3 -4
  26. package/project/.sdk/src/cmp/elixir/Package_elixir.ts +1 -1
  27. package/project/.sdk/src/cmp/elixir/Test_elixir.ts +5 -6
  28. package/project/.sdk/src/cmp/go/EntityTypes_go.ts +1 -1
  29. package/project/.sdk/src/cmp/go/Entity_go.ts +1 -1
  30. package/project/.sdk/src/cmp/go/Gitignore_go.ts +6 -0
  31. package/project/.sdk/src/cmp/go/Main_go.ts +2 -2
  32. package/project/.sdk/src/cmp/go/Package_go.ts +1 -1
  33. package/project/.sdk/src/cmp/go/ReadmeExamplesTest_go.ts +59 -6
  34. package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +1 -1
  35. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +1 -1
  36. package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +1 -1
  37. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +1 -1
  38. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +1 -1
  39. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +1 -1
  40. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +1 -1
  41. package/project/.sdk/src/cmp/go/Test_go.ts +6 -8
  42. package/project/.sdk/src/cmp/go-cli/Main_go-cli.ts +6 -2
  43. package/project/.sdk/src/cmp/go-mcp/Main_go-mcp.ts +6 -2
  44. package/project/.sdk/src/cmp/java/Test_java.ts +4 -7
  45. package/project/.sdk/src/cmp/js/Package_js.ts +1 -1
  46. package/project/.sdk/src/cmp/js/Test_js.ts +4 -7
  47. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +4 -7
  48. package/project/.sdk/src/cmp/lean/Test_lean.ts +3 -3
  49. package/project/.sdk/src/cmp/lua/Package_lua.ts +2 -2
  50. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -7
  51. package/project/.sdk/src/cmp/ocaml/Test_ocaml.ts +3 -7
  52. package/project/.sdk/src/cmp/perl/Package_perl.ts +1 -1
  53. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -7
  54. package/project/.sdk/src/cmp/php/EntityTypes_php.ts +13 -2
  55. package/project/.sdk/src/cmp/php/Package_php.ts +2 -2
  56. package/project/.sdk/src/cmp/php/Test_php.ts +5 -7
  57. package/project/.sdk/src/cmp/py/Package_py.ts +2 -2
  58. package/project/.sdk/src/cmp/py/Test_py.ts +5 -7
  59. package/project/.sdk/src/cmp/py-data/Main_py-data.ts +14 -19
  60. package/project/.sdk/src/cmp/rb/Package_rb.ts +3 -3
  61. package/project/.sdk/src/cmp/rb/ReadmeInstall_rb.ts +1 -1
  62. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -7
  63. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -7
  64. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -2
  65. package/project/.sdk/src/cmp/seneca-provider/Gitignore_seneca-provider.ts +161 -0
  66. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +20 -11
  67. package/project/.sdk/src/cmp/swift/Test_swift.ts +5 -7
  68. package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
  69. package/project/.sdk/src/cmp/ts/Test_ts.ts +8 -7
  70. package/project/.sdk/src/cmp/zig/Test_zig.ts +3 -7
  71. package/project/.sdk/tm/c/utility/make_point.c +64 -3
  72. package/project/.sdk/tm/clojure/src/sdk/core.clj +49 -14
  73. package/project/.sdk/tm/cpp/utility/pipeline.hpp +55 -3
  74. package/project/.sdk/tm/csharp/feature/TestFeature.cs +36 -2
  75. package/project/.sdk/tm/csharp/utility/MakePoint.cs +65 -2
  76. package/project/.sdk/tm/dart/lib/feature/test/TestFeature.dart +34 -4
  77. package/project/.sdk/tm/dart/lib/utility/MakePointUtility.dart +48 -2
  78. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +70 -10
  79. package/project/.sdk/tm/go/feature/test_feature.go +34 -2
  80. package/project/.sdk/tm/go/utility/make_point.go +66 -2
  81. package/project/.sdk/tm/java/feature/TestFeature.java +42 -2
  82. package/project/.sdk/tm/java/utility/MakePoint.java +61 -2
  83. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +22 -3
  84. package/project/.sdk/tm/js/src/utility/MakePointUtility.js +72 -2
  85. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +31 -2
  86. package/project/.sdk/tm/kotlin/utility/MakePoint.kt +58 -2
  87. package/project/.sdk/tm/lean/src/SdkUtility.lean +28 -0
  88. package/project/.sdk/tm/lua/utility/make_point.lua +65 -2
  89. package/project/.sdk/tm/ocaml/sdk_runtime.ml +40 -2
  90. package/project/.sdk/tm/perl/utility/make_point.pm +55 -2
  91. package/project/.sdk/tm/php/feature/TestFeature.php +30 -1
  92. package/project/.sdk/tm/php/utility/MakePoint.php +49 -1
  93. package/project/.sdk/tm/py/pkg/utility/make_point.py +56 -2
  94. package/project/.sdk/tm/rb/utility/make_point.rb +42 -2
  95. package/project/.sdk/tm/rust/utility/make_point.rs +69 -2
  96. package/project/.sdk/tm/scala/feature/TestFeature.scala +36 -1
  97. package/project/.sdk/tm/scala/utility/Make.scala +57 -3
  98. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +29 -1
  99. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Make.swift +51 -2
  100. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +22 -3
  101. package/project/.sdk/tm/ts/src/utility/MakePointUtility.ts +72 -2
  102. package/project/.sdk/tm/zig/core/utility.zig +66 -3
  103. package/project/sdkgen-package.json +1 -2
  104. package/src/cmp/ExternalTarget.ts +1 -1
  105. package/src/helpers/naming.ts +129 -5
  106. package/src/helpers/opShape.ts +85 -0
  107. package/src/helpers/packageMeta.ts +59 -20
  108. package/src/sdkgen.ts +68 -20
  109. package/src/testkit.ts +863 -0
  110. package/testkit.d.ts +3 -0
  111. package/testkit.js +12 -0
  112. package/project/.sdk/model/target/haskell.aontu +0 -72
  113. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +0 -98
  114. package/project/.sdk/src/cmp/haskell/Entity_haskell.ts +0 -13
  115. package/project/.sdk/src/cmp/haskell/Gitignore_haskell.ts +0 -27
  116. package/project/.sdk/src/cmp/haskell/Main_haskell.ts +0 -123
  117. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +0 -60
  118. package/project/.sdk/src/cmp/haskell/ReadmeEntity_haskell.ts +0 -186
  119. package/project/.sdk/src/cmp/haskell/ReadmeExamplesTest_haskell.ts +0 -135
  120. package/project/.sdk/src/cmp/haskell/ReadmeExplanation_haskell.ts +0 -48
  121. package/project/.sdk/src/cmp/haskell/ReadmeHowto_haskell.ts +0 -168
  122. package/project/.sdk/src/cmp/haskell/ReadmeInstall_haskell.ts +0 -51
  123. package/project/.sdk/src/cmp/haskell/ReadmeIntro_haskell.ts +0 -67
  124. package/project/.sdk/src/cmp/haskell/ReadmeModel_haskell.ts +0 -162
  125. package/project/.sdk/src/cmp/haskell/ReadmeOptions_haskell.ts +0 -74
  126. package/project/.sdk/src/cmp/haskell/ReadmeQuick_haskell.ts +0 -241
  127. package/project/.sdk/src/cmp/haskell/ReadmeRef_haskell.ts +0 -408
  128. package/project/.sdk/src/cmp/haskell/ReadmeTopHowto_haskell.ts +0 -31
  129. package/project/.sdk/src/cmp/haskell/ReadmeTopQuick_haskell.ts +0 -104
  130. package/project/.sdk/src/cmp/haskell/ReadmeTopTest_haskell.ts +0 -80
  131. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +0 -300
  132. package/project/.sdk/src/cmp/haskell/tsconfig.json +0 -15
  133. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +0 -166
  134. package/project/.sdk/tm/haskell/LICENSE +0 -21
  135. package/project/.sdk/tm/haskell/Makefile +0 -22
  136. package/project/.sdk/tm/haskell/VERSION +0 -1
  137. package/project/.sdk/tm/haskell/src/SdkFeatures.hs +0 -1440
  138. package/project/.sdk/tm/haskell/src/SdkHelpers.hs +0 -337
  139. package/project/.sdk/tm/haskell/src/SdkJson.hs +0 -111
  140. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +0 -1219
  141. package/project/.sdk/tm/haskell/src/SdkTypes.hs +0 -195
  142. package/project/.sdk/tm/haskell/src/VoxgigStruct.hs +0 -2299
  143. package/project/.sdk/tm/haskell/src/Vregex.hs +0 -237
  144. package/project/.sdk/tm/haskell/src/feature/README.md +0 -5
  145. package/project/.sdk/tm/haskell/src/feature/audit/.gitkeep +0 -0
  146. package/project/.sdk/tm/haskell/src/feature/base/.gitkeep +0 -0
  147. package/project/.sdk/tm/haskell/src/feature/cache/.gitkeep +0 -0
  148. package/project/.sdk/tm/haskell/src/feature/clienttrack/.gitkeep +0 -0
  149. package/project/.sdk/tm/haskell/src/feature/debug/.gitkeep +0 -0
  150. package/project/.sdk/tm/haskell/src/feature/idempotency/.gitkeep +0 -0
  151. package/project/.sdk/tm/haskell/src/feature/log/.gitkeep +0 -0
  152. package/project/.sdk/tm/haskell/src/feature/metrics/.gitkeep +0 -0
  153. package/project/.sdk/tm/haskell/src/feature/netsim/.gitkeep +0 -0
  154. package/project/.sdk/tm/haskell/src/feature/paging/.gitkeep +0 -0
  155. package/project/.sdk/tm/haskell/src/feature/proxy/.gitkeep +0 -0
  156. package/project/.sdk/tm/haskell/src/feature/ratelimit/.gitkeep +0 -0
  157. package/project/.sdk/tm/haskell/src/feature/rbac/.gitkeep +0 -0
  158. package/project/.sdk/tm/haskell/src/feature/retry/.gitkeep +0 -0
  159. package/project/.sdk/tm/haskell/src/feature/streaming/.gitkeep +0 -0
  160. package/project/.sdk/tm/haskell/src/feature/telemetry/.gitkeep +0 -0
  161. package/project/.sdk/tm/haskell/src/feature/test/.gitkeep +0 -0
  162. package/project/.sdk/tm/haskell/src/feature/timeout/.gitkeep +0 -0
  163. package/project/.sdk/tm/haskell/test/Harness.hs +0 -281
  164. package/project/.sdk/tm/haskell/test/Runner.hs +0 -47
  165. package/project/.sdk/tm/haskell/test/StructCorpus.hs +0 -449
  166. package/project/.sdk/tm/haskell/test/TCustomUtility.hs +0 -38
  167. package/project/.sdk/tm/haskell/test/TFeature.hs +0 -794
  168. package/project/.sdk/tm/haskell/test/TNetsim.hs +0 -50
  169. package/project/.sdk/tm/haskell/test/TPipeline.hs +0 -334
  170. package/project/.sdk/tm/haskell/test/TPrimaryUtility.hs +0 -386
  171. package/project/.sdk/tm/haskell/test/Testutil.hs +0 -59
@@ -0,0 +1,663 @@
1
+ "use strict";
2
+ /* Copyright (c) 2024-2026 Voxgig Ltd, MIT License */
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SDKGEN_ROOT = exports.PLACEHOLDERS = void 0;
8
+ exports.volumeKey = volumeKey;
9
+ exports.stageConsumer = stageConsumer;
10
+ exports.generateInto = generateInto;
11
+ exports.manifestParity = manifestParity;
12
+ // THE TEST KIT: `@voxgig/sdkgen/testkit`.
13
+ //
14
+ // See §14 of docs/design/sdkgen-packages.md.
15
+ //
16
+ // WHY THIS EXISTS
17
+ //
18
+ // sdkgen's own suites are CLOSED. `parity.test.ts` and `featuremodel.test.ts`
19
+ // derive their sets from `ts/project/.sdk` listings; `generate.test.ts` runs
20
+ // the per-language components out of a staged copy of that same tree. All of
21
+ // it is excellent coverage that an external package gets exactly none of —
22
+ // and an external package is where the coverage is needed most, because its
23
+ // content reaches a consumer through the same add pipeline with none of the
24
+ // same review.
25
+ //
26
+ // So the machinery is parameterised rather than reimplemented: this module is
27
+ // the staging in `build/scaffold-stage.js` and the generation harness in
28
+ // `ts/test/generateharness.ts`, with the paths taken as arguments instead of
29
+ // hardcoded to the bundled scaffold.
30
+ //
31
+ // WHAT A PACKAGE AUTHOR DOES WITH IT
32
+ //
33
+ // const consumer = stageConsumer()
34
+ // await consumer.addPackage(__dirname + '/..') // the package under test
35
+ // consumer.compile() // as a consumer's build does
36
+ // const { files, leaks } = await generateInto(consumer, { model })
37
+ //
38
+ // That runs the REAL add pipeline and the REAL generation, so provenance,
39
+ // index handling, the feature fan-out and the trim catalogue are all exercised
40
+ // against the package as published rather than as described.
41
+ //
42
+ // NO RUNTIME DEPENDENCIES. This package has none and the test kit does not
43
+ // introduce any: `compile()` reaches for a transpiler at call time and says
44
+ // which ones it looked for if it finds none.
45
+ const node_fs_1 = __importDefault(require("node:fs"));
46
+ const node_os_1 = __importDefault(require("node:os"));
47
+ const node_path_1 = __importDefault(require("node:path"));
48
+ const jostraca_1 = require("jostraca");
49
+ const sdkgen_1 = require("./sdkgen");
50
+ const package_1 = require("./action/package");
51
+ const dispatch_1 = require("./action/dispatch");
52
+ const types_1 = require("./types");
53
+ // This package's own root — `<...>/node_modules/@voxgig/sdkgen` for a
54
+ // consumer, or the checkout when sdkgen tests itself. Computed from this
55
+ // module's location rather than by `require.resolve`, which would go through
56
+ // the `exports` map and answer with `dist/sdkgen.js` instead of the root.
57
+ const SDKGEN_ROOT = node_path_1.default.resolve(__dirname, '..');
58
+ exports.SDKGEN_ROOT = SDKGEN_ROOT;
59
+ // The placeholder tokens template substitution is supposed to replace. One
60
+ // surviving into generated output means a replace map did not reach a file —
61
+ // the failure mode `generate.test.ts` scans the bundled targets for, made
62
+ // available to packages that ship template trees of their own.
63
+ //
64
+ // KEPT IN STEP WITH `generate.test.ts` DELIBERATELY. `PROJECTENV` and
65
+ // `PROJECTVERSION` are added by `ensureStdrep` / `templateReplacements`, not
66
+ // by the name map, and a kit that scanned only for the name tokens would let a
67
+ // package ship an unsubstituted version string and still report `leaks: []`.
68
+ const PLACEHOLDERS = [
69
+ 'ProjectName', 'PROJECTNAME', 'PROJECTENV', 'PROJECTVERSION', 'GOMODULE',
70
+ ];
71
+ exports.PLACEHOLDERS = PLACEHOLDERS;
72
+ // A MODEL PATH between the delimiters — identifiers and dots — not any `$$`
73
+ // pair. A surviving `$$model.path$$` means a Fragment or Copy whose model
74
+ // interpolation never ran, which the token list above cannot see.
75
+ //
76
+ // Constrained on purpose, and this is the same pattern `generate.test.ts`
77
+ // settled on: in a Makefile `$$` is how you write a literal `$`, so a loose
78
+ // pattern reported py-data's `$${GITHUB_TOKEN:-$$(gh auth token)}` — a correct
79
+ // recipe — as a leak.
80
+ const PLACEHOLDER_REF = /\$\$[A-Za-z_][A-Za-z0-9_.]*\$\$/;
81
+ const noop = () => { };
82
+ function makeLog(lines) {
83
+ const push = (level) => (entry) => {
84
+ if (lines)
85
+ lines.push({ level, ...entry });
86
+ };
87
+ const log = {
88
+ lines,
89
+ info: push('info'), debug: push('debug'), warn: push('warn'),
90
+ error: push('error'), trace: push('trace'), fatal: push('fatal'),
91
+ };
92
+ log.child = () => log;
93
+ return log;
94
+ }
95
+ // A CONSUMER PROJECT ON REAL DISK, not in memfs.
96
+ //
97
+ // It has to be real: `requirePath` resolves a component with an actual Node
98
+ // `require` against `<root>/.sdk/dist/cmp/...`, and components read sibling
99
+ // fragment files off disk relative to their own `__dirname`. A memfs project
100
+ // can exercise the add pipeline (sdkgen's own action suites do) but can never
101
+ // RUN what it installed, which is the half a package author most needs.
102
+ function stageConsumer(opts = {}) {
103
+ // THE ROOT IS USED VERBATIM. Do not "canonicalise" it.
104
+ //
105
+ // This exact string is handed to `generate()` as its output folder, and
106
+ // `generateInto` strips it back off to key the result. Those two uses only
107
+ // agree while it is ONE string, so any transformation here would have to be
108
+ // one jostraca performs too — and `realpathSync` is not.
109
+ //
110
+ // It WAS resolved here briefly, added as hardening for a macOS case
111
+ // (`/var/folders` vs `/private/var/folders`) that was measured beforehand
112
+ // and does not arise: jostraca realpaths a copy's SOURCE, not the output
113
+ // folder. Hardening against a hazard that was not there is its own reason
114
+ // to revert.
115
+ //
116
+ // (It was also blamed, wrongly, for the Windows failure that followed. That
117
+ // was the drive letter — see `volkey` in generateInto — and the short name
118
+ // in `C:\Users\RUNNER~1\…` was a coincidence of the runner's paths, present
119
+ // on both sides of the comparison and never the difference.)
120
+ const root = opts.dir ?? node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'sdkgen-consumer-'));
121
+ const sdk = node_path_1.default.join(root, '.sdk');
122
+ node_fs_1.default.mkdirSync(node_path_1.default.join(sdk, 'model', 'target'), { recursive: true });
123
+ node_fs_1.default.mkdirSync(node_path_1.default.join(sdk, 'model', 'feature'), { recursive: true });
124
+ node_fs_1.default.writeFileSync(node_path_1.default.join(sdk, 'model', 'target', 'target-index.aontu'), '# Targets\n');
125
+ node_fs_1.default.writeFileSync(node_path_1.default.join(sdk, 'model', 'feature', 'feature-index.aontu'), '# Features\n');
126
+ const name = opts.name ?? 'demo';
127
+ // The project's OWN model, written once by create-sdkgen at init. It
128
+ // includes the indexes of the kinds that existed THEN — which is why a
129
+ // consumer staged here can show what an existing project does when a new
130
+ // kind arrives, rather than assuming every index is already wired.
131
+ node_fs_1.default.writeFileSync(node_path_1.default.join(sdk, 'model', 'sdk.aontu'), "name: '" + name + "'\n" +
132
+ '@"target/target-index.aontu"\n' +
133
+ '@"feature/feature-index.aontu"\n' +
134
+ (opts.extra ? opts.extra + '\n' : ''));
135
+ // `@voxgig/sdkgen` has to be resolvable FROM THE CONSUMER, because the
136
+ // feature fan-out reads the bundled feature models through the path a
137
+ // consumer sees them at (`node_modules/@voxgig/sdkgen/...`, relative to the
138
+ // project) and scaffold components `require('@voxgig/sdkgen')` by name.
139
+ //
140
+ // 'junction' is the portable spelling: on Windows it creates a directory
141
+ // junction, which needs no elevation, and on POSIX the type argument is
142
+ // ignored and an ordinary symlink results. A copy would work too and costs
143
+ // the whole 27-target template tree per staged consumer.
144
+ // NODE_MODULES LIVES INSIDE `.sdk`, NOT BESIDE IT.
145
+ //
146
+ // A generated SDK's `.sdk` is itself an npm package root — it has its own
147
+ // package.json and its own install — so that is where a consumer's
148
+ // `@voxgig/sdkgen` actually sits. It matters because the feature fan-out
149
+ // composes the path from the ACTION FOLDER (`<.sdk>/node_modules/...`)
150
+ // rather than resolving it upward; put the link one level too high and
151
+ // every bundled feature reports `feature-source-unresolved` while
152
+ // `require('@voxgig/sdkgen')` from a component keeps working, because
153
+ // Node's upward search finds either. One of the two readers is forgiving
154
+ // and the other is not.
155
+ const modules = node_path_1.default.join(sdk, 'node_modules');
156
+ const links = [linkModule(modules, '@voxgig/sdkgen', SDKGEN_ROOT)];
157
+ // ...and sdkgen's PEERS, because the base model schema a consumer compiles
158
+ // against pulls in `@voxgig/apidef/model/apidef.aontu` by package name. A
159
+ // consumer that really installed sdkgen has these; a staged one has to be
160
+ // given them, or every model compile here fails on an include that resolves
161
+ // fine everywhere else.
162
+ for (const dep of peerNames()) {
163
+ const from = peerRoot(dep);
164
+ if (null != from) {
165
+ links.push(linkModule(modules, dep, from));
166
+ }
167
+ }
168
+ const lines = [];
169
+ const log = makeLog(opts.recordLog ? lines : undefined);
170
+ const actx = {
171
+ fs: () => node_fs_1.default,
172
+ log,
173
+ folder: sdk,
174
+ model: {
175
+ const: { name, Name: name.charAt(0).toUpperCase() + name.slice(1) },
176
+ main: {
177
+ [types_1.KIT]: { feature: {}, entity: {}, target: {} },
178
+ },
179
+ },
180
+ url: node_path_1.default.join(sdk, 'model', 'sdk.aontu'),
181
+ jostraca: (0, jostraca_1.Jostraca)({ existing: { txt: { write: true, merge: false } } }),
182
+ opts: { dryrun: false },
183
+ };
184
+ const files = () => walk(sdk)
185
+ .map((p) => node_path_1.default.relative(sdk, p).split(node_path_1.default.sep).join('/'))
186
+ .filter((p) => !p.startsWith('.jostraca/') && !p.includes('/.jostraca/'))
187
+ .sort();
188
+ return {
189
+ root, sdk, actx, log,
190
+ addPackage: async (ref, flags = {}) => {
191
+ actx.flags = flags;
192
+ return (0, package_1.package_add)([ref], actx);
193
+ },
194
+ // Through `ACTION_MAP`, which is the SAME dispatch the CLI uses — so a
195
+ // kind registered later is installable here with no change to the kit,
196
+ // and a kind whose action is missing fails the way the CLI fails.
197
+ add: async (kind, ref, flags = {}) => {
198
+ const action = dispatch_1.ACTION_MAP[kind];
199
+ if (null == action) {
200
+ throw new Error('testkit: no such kind: ' + kind +
201
+ ' (known: ' + Object.keys(dispatch_1.ACTION_MAP).sort().join(', ') + ')');
202
+ }
203
+ actx.flags = flags;
204
+ // `[kind, cmd, ...refs]` — the CLI's own argv shape, which is what
205
+ // `action_<kind>` parses (it reads the verb at args[1]).
206
+ return action([kind, 'add', ref], actx);
207
+ },
208
+ // CONSUMER-RELATIVE, not absolute, and that is not a stylistic choice.
209
+ //
210
+ // `target add` records `base` as the resolved source folder, so an
211
+ // absolute ref writes THIS MACHINE'S path into the installed model. Two
212
+ // things then go wrong at once: the copy is not reproducible across
213
+ // machines, and the feature fan-out compares the target's own tm folder
214
+ // against the one it reaches through `node_modules` — textually different
215
+ // paths for the same tree, which it reports as a shadowing overlay. Both
216
+ // disappear when the ref is spelled the way a consumer spells it.
217
+ bundledRef: (kind, name) => 'target' === kind ? 'node_modules/@voxgig/sdkgen/project/' + name : name,
218
+ setModel: (model) => { actx.model = model; },
219
+ // AWAITS A PROMISE-RETURNING CALLBACK before restoring the directory.
220
+ //
221
+ // A synchronous `finally` would put the cwd back the moment `fn` RETURNS,
222
+ // which for an async callback is its first `await` — so the generation it
223
+ // was wrapping would do most of its work, including every CWD-relative
224
+ // template copy and the later docs and out-of-tree passes, from the
225
+ // caller's directory. The wrapper would look correct and protect almost
226
+ // nothing.
227
+ inSdk: (fn) => {
228
+ const prev = process.cwd();
229
+ process.chdir(sdk);
230
+ let out;
231
+ try {
232
+ out = fn();
233
+ }
234
+ catch (err) {
235
+ process.chdir(prev);
236
+ throw err;
237
+ }
238
+ if (null != out && 'function' === typeof out.then) {
239
+ return out.then((v) => { process.chdir(prev); return v; }, (err) => { process.chdir(prev); throw err; });
240
+ }
241
+ process.chdir(prev);
242
+ return out;
243
+ },
244
+ compile: (copts = {}) => compileComponents(sdk, copts.transform),
245
+ files,
246
+ cleanup: () => {
247
+ if (null == opts.dir) {
248
+ // The links go FIRST, explicitly. `rmSync` would remove them without
249
+ // following (measured), so this is belt and braces for the walk — but
250
+ // it is NOT redundant on Windows, where a junction refuses `unlink`
251
+ // and `force: true` forgives only ENOENT, so leaving it to the walk
252
+ // can throw.
253
+ for (const link of links) {
254
+ unlink(link);
255
+ }
256
+ node_fs_1.default.rmSync(root, { recursive: true, force: true });
257
+ }
258
+ },
259
+ };
260
+ }
261
+ // REMOVE A LINK, WHICHEVER KIND IT TURNED OUT TO BE.
262
+ //
263
+ // `linkModule` makes a symlink on POSIX and a directory JUNCTION on Windows,
264
+ // and falls back to a real directory holding a re-export shim. `unlinkSync`
265
+ // removes the first, fails with EPERM on the second, and cannot remove the
266
+ // third.
267
+ //
268
+ // WHAT THIS IS AND IS NOT FOR. Node's recursive `rmSync` does NOT follow a
269
+ // symlink or a junction — it removes the link itself — so the tree walk that
270
+ // follows is not going to reach through into the sdkgen checkout. That was
271
+ // measured, not assumed. What this function buys is narrower and still worth
272
+ // having: cleanup that does not throw on Windows (a junction refuses
273
+ // `unlink`, and `force: true` forgives only ENOENT), and not depending on
274
+ // that `rmSync` behaviour holding forever for an operation whose blast radius
275
+ // would be a developer's checkout.
276
+ function unlink(link) {
277
+ let stat;
278
+ try {
279
+ stat = node_fs_1.default.lstatSync(link);
280
+ }
281
+ catch (err) {
282
+ return;
283
+ }
284
+ // A LINK OF SOME KIND — never recurse. `lstat` reports a Windows junction as
285
+ // a symbolic link, which is exactly the case a recursive remove must not
286
+ // reach: it would delete the sdkgen checkout on the other side.
287
+ //
288
+ // `unlink` removes a POSIX symlink and refuses a junction (EPERM); `rmdir`
289
+ // removes a junction and refuses a symlink. Try both rather than branch on
290
+ // the platform.
291
+ if (stat.isSymbolicLink()) {
292
+ try {
293
+ node_fs_1.default.unlinkSync(link);
294
+ return;
295
+ }
296
+ catch (err) { /* junction */ }
297
+ try {
298
+ node_fs_1.default.rmdirSync(link);
299
+ }
300
+ catch (err) { /* already gone */ }
301
+ return;
302
+ }
303
+ // Not a link: the re-export shim `linkModule` writes when symlinking is
304
+ // refused. A real directory we created, so removing its contents is safe.
305
+ try {
306
+ node_fs_1.default.rmSync(link, { recursive: true, force: true });
307
+ }
308
+ catch (err) { /* already gone */ }
309
+ }
310
+ // A path as an in-memory VOLUME spells it: forward slashes, no drive letter.
311
+ //
312
+ // memfs stores `/a/b`, never `C:/a/b`, so a Windows path has to lose both its
313
+ // separators and its drive before it can be compared with a volume key. This
314
+ // has now caused two Windows failures — the second one silently, because
315
+ // `Path.relative` between a drive-less key and a real root does not fail, it
316
+ // resolves the key against the CWD and returns a confidently wrong answer.
317
+ //
318
+ // Backslashes are replaced unconditionally rather than via `Path.sep`, so the
319
+ // rule is the same function on every platform and can be tested anywhere. A
320
+ // Windows filename cannot contain a backslash, so nothing legitimate is lost.
321
+ function volumeKey(p) {
322
+ return p.replace(/\\/g, '/').replace(/^[A-Za-z]:/, '');
323
+ }
324
+ // The peer packages a consumer necessarily has installed alongside sdkgen.
325
+ // Read from the manifest rather than listed here, so a peer added later is
326
+ // linked without anyone remembering to.
327
+ function peerNames() {
328
+ try {
329
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(SDKGEN_ROOT, 'package.json'), 'utf8'));
330
+ return Object.keys(pkg.peerDependencies ?? {});
331
+ }
332
+ catch (err) {
333
+ return [];
334
+ }
335
+ }
336
+ // WHERE A PEER ACTUALLY LIVES — asked of Node, not guessed from a path.
337
+ //
338
+ // The tempting version is `<SDKGEN_ROOT>/node_modules/<dep>`, and it is right
339
+ // only in this checkout. npm HOISTS: in a real installation sdkgen's peers are
340
+ // siblings of `@voxgig/sdkgen` under the host project's `node_modules`, not
341
+ // children of it. So the guessed path exists here, misses everywhere else, and
342
+ // the failure is silent — the peer is skipped, the staged consumer has no
343
+ // `@voxgig/apidef`, and the model compile fails on an include that resolves
344
+ // fine in every other context.
345
+ //
346
+ // `require.resolve` with `paths` walks the real chain, hoisted or not. The
347
+ // package.json is resolved rather than the entry point because a peer may not
348
+ // export one, and its directory is what has to be linked.
349
+ function peerRoot(dep) {
350
+ try {
351
+ return node_path_1.default.dirname(require.resolve(dep + '/package.json', { paths: [SDKGEN_ROOT] }));
352
+ }
353
+ catch (err) {
354
+ // Some packages restrict `exports` and refuse the package.json subpath.
355
+ // Fall back to the entry point and climb to the directory that holds one.
356
+ try {
357
+ let dir = node_path_1.default.dirname(require.resolve(dep, { paths: [SDKGEN_ROOT] }));
358
+ for (let up = 0; up < 8; up++) {
359
+ if (node_fs_1.default.existsSync(node_path_1.default.join(dir, 'package.json')))
360
+ return dir;
361
+ const parent = node_path_1.default.dirname(dir);
362
+ if (parent === dir)
363
+ break;
364
+ dir = parent;
365
+ }
366
+ }
367
+ catch (err2) { /* genuinely not installed */ }
368
+ return undefined;
369
+ }
370
+ }
371
+ // One `node_modules/<name>` entry pointing at an existing package directory.
372
+ //
373
+ // 'junction' is the portable spelling: on Windows it creates a directory
374
+ // junction, which needs no elevation, and on POSIX the type argument is
375
+ // ignored and an ordinary symlink results. A copy would work too and costs the
376
+ // whole 27-target template tree per staged consumer.
377
+ function linkModule(modules, name, from) {
378
+ const link = node_path_1.default.join(modules, ...name.split('/'));
379
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(link), { recursive: true });
380
+ if (node_fs_1.default.existsSync(link))
381
+ return link;
382
+ try {
383
+ node_fs_1.default.symlinkSync(from, link, 'junction');
384
+ }
385
+ catch (err) {
386
+ // Last resort: a re-export shim. It satisfies `require('<name>')` but NOT
387
+ // deep file reads under the package, so a model include would still fail
388
+ // — which is why this is the fallback and not the mechanism.
389
+ node_fs_1.default.mkdirSync(link, { recursive: true });
390
+ node_fs_1.default.writeFileSync(node_path_1.default.join(link, 'package.json'), JSON.stringify({ name, version: '0.0.0', main: 'index.js' }) + '\n');
391
+ node_fs_1.default.writeFileSync(node_path_1.default.join(link, 'index.js'), 'module.exports = require(' + JSON.stringify(from) + ')\n');
392
+ }
393
+ return link;
394
+ }
395
+ function walk(dir) {
396
+ if (!node_fs_1.default.existsSync(dir))
397
+ return [];
398
+ const out = [];
399
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
400
+ const full = node_path_1.default.join(dir, entry.name);
401
+ if (entry.isDirectory())
402
+ out.push(...walk(full));
403
+ else
404
+ out.push(full);
405
+ }
406
+ return out;
407
+ }
408
+ // TRANSPILE, DO NOT TYPE-CHECK.
409
+ //
410
+ // Type-checking a package's components is a BUILD-time gate (the package runs
411
+ // `tsc --noEmit` over its own `src/cmp/**`, the way this repo's
412
+ // `check-scaffold` does). Doing it again per staged consumer would add seconds
413
+ // to every test for an answer the build already has. What the kit needs here
414
+ // is only executable JS at the path `requirePath` reads.
415
+ //
416
+ // Neither transpiler is a dependency of this package, which has none. They are
417
+ // looked up at call time, and if neither is present the error names both
418
+ // rather than failing later as a missing module inside `requirePath`.
419
+ function compileComponents(sdk, transform) {
420
+ const srcdir = node_path_1.default.join(sdk, 'src', 'cmp');
421
+ const outdir = node_path_1.default.join(sdk, 'dist', 'cmp');
422
+ if (!node_fs_1.default.existsSync(srcdir))
423
+ return 0;
424
+ const xform = transform ?? defaultTransform();
425
+ let count = 0;
426
+ for (const file of walk(srcdir)) {
427
+ const rel = node_path_1.default.relative(srcdir, file);
428
+ // Fragments are template source carrying placeholder tokens, not valid
429
+ // standalone modules — the same exclusion `tsconfig.scaffold.json` makes.
430
+ if (rel.split(node_path_1.default.sep).includes('fragment'))
431
+ continue;
432
+ const out = node_path_1.default.join(outdir, rel.replace(/\.ts$/, '.js'));
433
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(out), { recursive: true });
434
+ if (!file.endsWith('.ts')) {
435
+ // Components read sibling non-TS files (fragments, docs) relative to
436
+ // their own __dirname, so those have to arrive in dist too.
437
+ node_fs_1.default.copyFileSync(file, node_path_1.default.join(outdir, rel));
438
+ continue;
439
+ }
440
+ node_fs_1.default.writeFileSync(out, xform(node_fs_1.default.readFileSync(file, 'utf8'), file));
441
+ count++;
442
+ }
443
+ return count;
444
+ }
445
+ function defaultTransform() {
446
+ const tried = [];
447
+ try {
448
+ tried.push('sucrase');
449
+ const sucrase = require('sucrase');
450
+ return (src, file) => sucrase.transform(src, {
451
+ transforms: ['typescript', 'imports'],
452
+ filePath: file,
453
+ }).code;
454
+ }
455
+ catch (err) { /* fall through to typescript */ }
456
+ try {
457
+ tried.push('typescript');
458
+ const ts = require('typescript');
459
+ return (src, file) => ts.transpileModule(src, {
460
+ fileName: file,
461
+ compilerOptions: {
462
+ target: ts.ScriptTarget.ES2021,
463
+ module: ts.ModuleKind.CommonJS,
464
+ esModuleInterop: true,
465
+ },
466
+ }).outputText;
467
+ }
468
+ catch (err) { /* fall through to the error */ }
469
+ throw new Error('testkit: no TypeScript transpiler found (looked for: ' +
470
+ tried.join(', ') + '). Add one as a devDependency, or pass ' +
471
+ '`compile({ transform })` with your own.');
472
+ }
473
+ // GENERATE INTO MEMORY, from a consumer staged on disk.
474
+ //
475
+ // The split matters: the project (components, templates, model) is real,
476
+ // because that is what generation READS; the output is a memfs volume,
477
+ // because a test wants to assert on it rather than clean it up.
478
+ async function generateInto(consumer, opts) {
479
+ // memfs is a devDependency of whoever is testing, not of this package.
480
+ let memfs;
481
+ try {
482
+ memfs = require('memfs').memfs;
483
+ }
484
+ catch (err) {
485
+ throw new Error('testkit: generateInto needs `memfs` — add it as a devDependency');
486
+ }
487
+ const { fs, vol } = memfs({});
488
+ const sdkgen = (0, sdkgen_1.SdkGen)({
489
+ fs: layeredFs(fs),
490
+ folder: consumer.root,
491
+ root: '',
492
+ pino: consumer.log,
493
+ });
494
+ // GENERATION RUNS FROM `.sdk`.
495
+ //
496
+ // Components copy their template tree with a CWD-RELATIVE path
497
+ // (`Copy({ from: 'tm/<lang>' })`), because that is how a consumer runs it —
498
+ // `npm run generate` from the `.sdk` directory. Run it from anywhere else
499
+ // and the first feature copy fails on a `tm/...` path that does not exist
500
+ // relative to the caller's cwd, naming the template rather than the reason.
501
+ //
502
+ // The chdir is restored even when generation throws, because a test runner
503
+ // shares one process across suites and a leaked cwd breaks whatever runs
504
+ // next, somewhere else entirely.
505
+ const prevcwd = process.cwd();
506
+ process.chdir(consumer.sdk);
507
+ let res;
508
+ try {
509
+ res = await sdkgen.generate({
510
+ model: opts.model,
511
+ root: opts.root ?? defaultRoot(),
512
+ });
513
+ }
514
+ finally {
515
+ process.chdir(prevcwd);
516
+ }
517
+ if (true !== res.ok) {
518
+ throw new Error('testkit: generation failed: ' + JSON.stringify(res));
519
+ }
520
+ // KEYS ARE COMPARED AS THE VOLUME SPELLS THEM, NOT AS THE OS DOES.
521
+ //
522
+ // memfs is a POSIX volume: it stores `/a/b`, never `C:/a/b`. So a generated
523
+ // path comes back with its DRIVE LETTER DROPPED, while `consumer.root` — a
524
+ // real Windows path — still has one. `Path.relative` between the two does
525
+ // not merely fail; it resolves the drive-less path against the CWD and
526
+ // returns something confidently wrong (`D:\Users\…` for a root on `C:`).
527
+ //
528
+ // `external.test.ts` hit this first and its `norm` is the rule being reused
529
+ // here: lose the separators AND the drive letter before comparing. Prefix
530
+ // arithmetic rather than `Path.relative`, because the whole problem is that
531
+ // these are volume keys and not OS paths.
532
+ //
533
+ // On POSIX both transformations are identity, so this changes nothing there.
534
+ const rootkey = volumeKey(consumer.root);
535
+ const files = {};
536
+ for (const [path, content] of Object.entries(vol.toJSON())) {
537
+ const key = volumeKey(path);
538
+ // A KEY OUTSIDE THE ROOT IS A BUG HERE, NOT A RESULT.
539
+ //
540
+ // It means the string generation wrote under and the string being stripped
541
+ // off have diverged, and the caller would otherwise get a file map keyed
542
+ // by absolute path — which reads as "my component never ran" rather than
543
+ // as a path problem, and is exactly how this presented on Windows.
544
+ if (key !== rootkey && !key.startsWith(rootkey + '/')) {
545
+ throw new Error('testkit: generated path is not under the consumer root, so the ' +
546
+ 'result cannot be keyed.\n root: ' + consumer.root +
547
+ '\n path: ' + path +
548
+ '\n compared as: ' + rootkey + ' vs ' + key +
549
+ '\nThese must agree once separators and any drive letter are ' +
550
+ 'normalised — memfs stores volume keys, not OS paths.');
551
+ }
552
+ const rel = key === rootkey ? '' : key.slice(rootkey.length + 1);
553
+ if (rel.startsWith('.jostraca/') || rel.includes('/.jostraca/'))
554
+ continue;
555
+ files[rel] = content;
556
+ }
557
+ const allow = opts.allowPlaceholder ?? (() => false);
558
+ const leaks = [];
559
+ for (const [path, content] of Object.entries(files)) {
560
+ if ('string' !== typeof content)
561
+ continue;
562
+ for (const token of PLACEHOLDERS) {
563
+ if (content.includes(token) && !allow(path, token)) {
564
+ leaks.push(path + ': ' + token);
565
+ }
566
+ }
567
+ const ref = content.match(PLACEHOLDER_REF);
568
+ if (null != ref && !allow(path, ref[0])) {
569
+ leaks.push(path + ': ' + ref[0]);
570
+ }
571
+ }
572
+ return { files, leaks: leaks.sort() };
573
+ }
574
+ // Write to memfs, read through to the real project.
575
+ function layeredFs(mem) {
576
+ const readThrough = (name) => (path, ...rest) => {
577
+ const target = mem.existsSync(path) ? mem : node_fs_1.default;
578
+ return target[name](path, ...rest);
579
+ };
580
+ return {
581
+ ...mem,
582
+ existsSync: (path) => mem.existsSync(path) || node_fs_1.default.existsSync(path),
583
+ readFileSync: readThrough('readFileSync'),
584
+ readdirSync: readThrough('readdirSync'),
585
+ statSync: readThrough('statSync'),
586
+ realpathSync: readThrough('realpathSync'),
587
+ };
588
+ }
589
+ // The Root a create-sdkgen consumer has: per target, a folder holding the
590
+ // entity, feature, main, readme, agentguide and test phases.
591
+ //
592
+ // Deliberately minimal. A package author testing their own target wants to
593
+ // know that THEIR components ran, not to re-test the scaffold's Root — and a
594
+ // kit that shipped an elaborate Root would make its own behaviour part of
595
+ // every package's test result.
596
+ function defaultRoot() {
597
+ const { cmp, each, names, Project, Folder } = require('jostraca');
598
+ const { Main, Entity, Feature, Test, Readme, AgentGuide } = require('./sdkgen');
599
+ return cmp(function Root(props) {
600
+ const { model, ctx$ } = props;
601
+ model.const = model.const || { name: model.name };
602
+ names(model.const, model.name);
603
+ if (null == model.const.year)
604
+ model.const.year = new Date().getFullYear();
605
+ names(model, model.name);
606
+ ctx$.model = model;
607
+ ctx$.stdrep = ctx$.stdrep || {};
608
+ names(ctx$.stdrep, model.Name, 'Project' + 'Name');
609
+ const target = model.main[types_1.KIT].target || {};
610
+ const feature = model.main[types_1.KIT].feature || {};
611
+ const entity = model.main[types_1.KIT].entity || {};
612
+ Project({}, () => {
613
+ each(target)
614
+ .filter((t) => t && false !== t.active)
615
+ .map((t) => {
616
+ names(t, t.name);
617
+ const phase = t.phase || {};
618
+ const on = (n) => false !== (phase[n] && phase[n].active);
619
+ Folder({ name: t.name }, () => {
620
+ if (on('entity')) {
621
+ each(entity)
622
+ .filter((e) => e && false !== e.active)
623
+ .map((e) => {
624
+ names(e, e.name);
625
+ Entity({ target: t, entity: e });
626
+ });
627
+ }
628
+ if (on('feature')) {
629
+ each(feature)
630
+ .filter((f) => f && f.active)
631
+ .map((f) => {
632
+ names(f, f.name);
633
+ Feature({ target: t, feature: f });
634
+ });
635
+ }
636
+ Main({ target: t });
637
+ if (on('readme'))
638
+ Readme({ target: t });
639
+ if (on('agentguide'))
640
+ AgentGuide({ target: t });
641
+ if (on('test'))
642
+ Test({ target: t });
643
+ });
644
+ });
645
+ });
646
+ });
647
+ }
648
+ // THE PARITY TIER A PACKAGE DECLARES, from its manifest.
649
+ //
650
+ // `ts/test/parity.test.ts` owns the tier declaration for BUNDLED targets, and
651
+ // §18.4a refused to duplicate that map into the bundled manifest for exactly
652
+ // the reason this function exists to eventually resolve: the manifest should
653
+ // become the source and the parity suite should read it, not the reverse. For
654
+ // an EXTERNAL package there is no such conflict — its manifest is the only
655
+ // place its tier can live, so reading it here is the whole mechanism.
656
+ function manifestParity(pkgRoot) {
657
+ const file = node_path_1.default.join(pkgRoot, 'sdkgen-package.json');
658
+ if (!node_fs_1.default.existsSync(file))
659
+ return {};
660
+ const manifest = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
661
+ return manifest.parity ?? {};
662
+ }
663
+ //# sourceMappingURL=testkit.js.map