@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
package/src/testkit.ts ADDED
@@ -0,0 +1,863 @@
1
+ /* Copyright (c) 2024-2026 Voxgig Ltd, MIT License */
2
+
3
+ // THE TEST KIT: `@voxgig/sdkgen/testkit`.
4
+ //
5
+ // See §14 of docs/design/sdkgen-packages.md.
6
+ //
7
+ // WHY THIS EXISTS
8
+ //
9
+ // sdkgen's own suites are CLOSED. `parity.test.ts` and `featuremodel.test.ts`
10
+ // derive their sets from `ts/project/.sdk` listings; `generate.test.ts` runs
11
+ // the per-language components out of a staged copy of that same tree. All of
12
+ // it is excellent coverage that an external package gets exactly none of —
13
+ // and an external package is where the coverage is needed most, because its
14
+ // content reaches a consumer through the same add pipeline with none of the
15
+ // same review.
16
+ //
17
+ // So the machinery is parameterised rather than reimplemented: this module is
18
+ // the staging in `build/scaffold-stage.js` and the generation harness in
19
+ // `ts/test/generateharness.ts`, with the paths taken as arguments instead of
20
+ // hardcoded to the bundled scaffold.
21
+ //
22
+ // WHAT A PACKAGE AUTHOR DOES WITH IT
23
+ //
24
+ // const consumer = stageConsumer()
25
+ // await consumer.addPackage(__dirname + '/..') // the package under test
26
+ // consumer.compile() // as a consumer's build does
27
+ // const { files, leaks } = await generateInto(consumer, { model })
28
+ //
29
+ // That runs the REAL add pipeline and the REAL generation, so provenance,
30
+ // index handling, the feature fan-out and the trim catalogue are all exercised
31
+ // against the package as published rather than as described.
32
+ //
33
+ // NO RUNTIME DEPENDENCIES. This package has none and the test kit does not
34
+ // introduce any: `compile()` reaches for a transpiler at call time and says
35
+ // which ones it looked for if it finds none.
36
+
37
+ import Fs from 'node:fs'
38
+ import Os from 'node:os'
39
+ import Path from 'node:path'
40
+
41
+ import { Jostraca } from 'jostraca'
42
+
43
+ import { SdkGen } from './sdkgen'
44
+
45
+ import { package_add } from './action/package'
46
+ import { ACTION_MAP } from './action/dispatch'
47
+
48
+ import { KIT } from './types'
49
+
50
+
51
+ // This package's own root — `<...>/node_modules/@voxgig/sdkgen` for a
52
+ // consumer, or the checkout when sdkgen tests itself. Computed from this
53
+ // module's location rather than by `require.resolve`, which would go through
54
+ // the `exports` map and answer with `dist/sdkgen.js` instead of the root.
55
+ const SDKGEN_ROOT = Path.resolve(__dirname, '..')
56
+
57
+
58
+ // The placeholder tokens template substitution is supposed to replace. One
59
+ // surviving into generated output means a replace map did not reach a file —
60
+ // the failure mode `generate.test.ts` scans the bundled targets for, made
61
+ // available to packages that ship template trees of their own.
62
+ //
63
+ // KEPT IN STEP WITH `generate.test.ts` DELIBERATELY. `PROJECTENV` and
64
+ // `PROJECTVERSION` are added by `ensureStdrep` / `templateReplacements`, not
65
+ // by the name map, and a kit that scanned only for the name tokens would let a
66
+ // package ship an unsubstituted version string and still report `leaks: []`.
67
+ const PLACEHOLDERS = [
68
+ 'ProjectName', 'PROJECTNAME', 'PROJECTENV', 'PROJECTVERSION', 'GOMODULE',
69
+ ]
70
+
71
+
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
+
82
+
83
+ type StageOptions = {
84
+ // Where to build the consumer. A fresh temp directory by default.
85
+ dir?: string
86
+
87
+ // The project's API name, which every derived name comes from.
88
+ name?: string
89
+
90
+ // The project's OWN model text, appended to `model/sdk.aontu`.
91
+ //
92
+ // THIS WRITES A FILE. It does NOT reach the action context — see
93
+ // `setModel` on the consumer, and use that if an add needs to SEE what is
94
+ // declared here. The two are separate on purpose rather than by oversight:
95
+ // nothing recompiles a model mid-process, so there is no honest way to make
96
+ // a string written here appear in an action's `actx.model` automatically.
97
+ extra?: string
98
+
99
+ // Record log lines instead of discarding them, for assertions about what an
100
+ // action reported. The array is exposed as `consumer.log.lines`.
101
+ recordLog?: boolean
102
+ }
103
+
104
+
105
+ type Consumer = {
106
+ // The project root — the directory that HOLDS `.sdk`. Generation is rooted
107
+ // here; an action's folder is `.sdk` itself. Conflating the two is the
108
+ // mistake this pair of fields exists to prevent.
109
+ root: string
110
+ sdk: string
111
+
112
+ actx: any
113
+ log: any
114
+
115
+ // Install a package by ref, through the real `package add`.
116
+ addPackage: (ref: string, flags?: any) => Promise<any>
117
+
118
+ // Install one item of one kind, through the real `<kind> add`.
119
+ add: (kind: string, ref: string, flags?: any) => Promise<any>
120
+
121
+ // A bundled target/feature ref, for a consumer that wants one alongside the
122
+ // package's own content — `wfeat`'s overlay for `ts` needs `ts` present.
123
+ bundledRef: (kind: string, name: string) => string
124
+
125
+ // Install a compiled model as the one the ADD ACTIONS see.
126
+ //
127
+ // Needed because an action reads `actx.model`, which is compiled from
128
+ // `model/sdk.aontu` BEFORE the run — nothing recompiles mid-process. The
129
+ // CLI does that compile per invocation; a kit staging several adds in one
130
+ // process does not, so a feature declared in the project's model is
131
+ // invisible to a later `target add` unless it is installed here. That
132
+ // matters: `target add` TRIMS feature source down to what the model
133
+ // selects, so a target added against an empty model ships none of it.
134
+ //
135
+ // `package add` handles its own within-run sequencing (it teaches the
136
+ // in-memory model about each kind's items as it installs them), so this is
137
+ // for adds the caller sequences itself.
138
+ setModel: (model: any) => void
139
+
140
+ // Run `fn` with the working directory set to `.sdk`, which is where a
141
+ // consumer runs generation from. Exposed because a caller doing its own
142
+ // generate() needs the same contract. A promise-returning `fn` is awaited
143
+ // before the directory is restored.
144
+ inSdk: (<T>(fn: () => Promise<T>) => Promise<T>) & (<T>(fn: () => T) => T)
145
+
146
+ // Compile `.sdk/src/cmp/**` to `.sdk/dist/cmp/**`, which is what a
147
+ // consumer's own `npm run build` does and what `requirePath` reads.
148
+ compile: (opts?: { transform?: (src: string, file: string) => string }) => number
149
+
150
+ // Paths written so far, relative to `.sdk`, sorted.
151
+ files: () => string[]
152
+
153
+ cleanup: () => void
154
+ }
155
+
156
+
157
+ const noop = () => { }
158
+
159
+ function makeLog(lines?: any[]): any {
160
+ const push = (level: string) => (entry: any) => {
161
+ if (lines) lines.push({ level, ...entry })
162
+ }
163
+ const log: any = {
164
+ lines,
165
+ info: push('info'), debug: push('debug'), warn: push('warn'),
166
+ error: push('error'), trace: push('trace'), fatal: push('fatal'),
167
+ }
168
+ log.child = () => log
169
+ return log
170
+ }
171
+
172
+
173
+ // A CONSUMER PROJECT ON REAL DISK, not in memfs.
174
+ //
175
+ // It has to be real: `requirePath` resolves a component with an actual Node
176
+ // `require` against `<root>/.sdk/dist/cmp/...`, and components read sibling
177
+ // fragment files off disk relative to their own `__dirname`. A memfs project
178
+ // can exercise the add pipeline (sdkgen's own action suites do) but can never
179
+ // RUN what it installed, which is the half a package author most needs.
180
+ function stageConsumer(opts: StageOptions = {}): Consumer {
181
+ // THE ROOT IS USED VERBATIM. Do not "canonicalise" it.
182
+ //
183
+ // This exact string is handed to `generate()` as its output folder, and
184
+ // `generateInto` strips it back off to key the result. Those two uses only
185
+ // agree while it is ONE string, so any transformation here would have to be
186
+ // one jostraca performs too — and `realpathSync` is not.
187
+ //
188
+ // It WAS resolved here briefly, added as hardening for a macOS case
189
+ // (`/var/folders` vs `/private/var/folders`) that was measured beforehand
190
+ // and does not arise: jostraca realpaths a copy's SOURCE, not the output
191
+ // folder. Hardening against a hazard that was not there is its own reason
192
+ // to revert.
193
+ //
194
+ // (It was also blamed, wrongly, for the Windows failure that followed. That
195
+ // was the drive letter — see `volkey` in generateInto — and the short name
196
+ // in `C:\Users\RUNNER~1\…` was a coincidence of the runner's paths, present
197
+ // on both sides of the comparison and never the difference.)
198
+ const root = opts.dir ?? Fs.mkdtempSync(Path.join(Os.tmpdir(), 'sdkgen-consumer-'))
199
+ const sdk = Path.join(root, '.sdk')
200
+
201
+ Fs.mkdirSync(Path.join(sdk, 'model', 'target'), { recursive: true })
202
+ Fs.mkdirSync(Path.join(sdk, 'model', 'feature'), { recursive: true })
203
+
204
+ Fs.writeFileSync(Path.join(sdk, 'model', 'target', 'target-index.aontu'),
205
+ '# Targets\n')
206
+ Fs.writeFileSync(Path.join(sdk, 'model', 'feature', 'feature-index.aontu'),
207
+ '# Features\n')
208
+
209
+ const name = opts.name ?? 'demo'
210
+
211
+ // The project's OWN model, written once by create-sdkgen at init. It
212
+ // includes the indexes of the kinds that existed THEN — which is why a
213
+ // consumer staged here can show what an existing project does when a new
214
+ // kind arrives, rather than assuming every index is already wired.
215
+ Fs.writeFileSync(Path.join(sdk, 'model', 'sdk.aontu'),
216
+ "name: '" + name + "'\n" +
217
+ '@"target/target-index.aontu"\n' +
218
+ '@"feature/feature-index.aontu"\n' +
219
+ (opts.extra ? opts.extra + '\n' : ''))
220
+
221
+ // `@voxgig/sdkgen` has to be resolvable FROM THE CONSUMER, because the
222
+ // feature fan-out reads the bundled feature models through the path a
223
+ // consumer sees them at (`node_modules/@voxgig/sdkgen/...`, relative to the
224
+ // project) and scaffold components `require('@voxgig/sdkgen')` by name.
225
+ //
226
+ // 'junction' is the portable spelling: on Windows it creates a directory
227
+ // junction, which needs no elevation, and on POSIX the type argument is
228
+ // ignored and an ordinary symlink results. A copy would work too and costs
229
+ // the whole 27-target template tree per staged consumer.
230
+ // NODE_MODULES LIVES INSIDE `.sdk`, NOT BESIDE IT.
231
+ //
232
+ // A generated SDK's `.sdk` is itself an npm package root — it has its own
233
+ // package.json and its own install — so that is where a consumer's
234
+ // `@voxgig/sdkgen` actually sits. It matters because the feature fan-out
235
+ // composes the path from the ACTION FOLDER (`<.sdk>/node_modules/...`)
236
+ // rather than resolving it upward; put the link one level too high and
237
+ // every bundled feature reports `feature-source-unresolved` while
238
+ // `require('@voxgig/sdkgen')` from a component keeps working, because
239
+ // Node's upward search finds either. One of the two readers is forgiving
240
+ // and the other is not.
241
+ const modules = Path.join(sdk, 'node_modules')
242
+ const links = [linkModule(modules, '@voxgig/sdkgen', SDKGEN_ROOT)]
243
+
244
+ // ...and sdkgen's PEERS, because the base model schema a consumer compiles
245
+ // against pulls in `@voxgig/apidef/model/apidef.aontu` by package name. A
246
+ // consumer that really installed sdkgen has these; a staged one has to be
247
+ // given them, or every model compile here fails on an include that resolves
248
+ // fine everywhere else.
249
+ for (const dep of peerNames()) {
250
+ const from = peerRoot(dep)
251
+ if (null != from) {
252
+ links.push(linkModule(modules, dep, from))
253
+ }
254
+ }
255
+
256
+ const lines: any[] = []
257
+ const log = makeLog(opts.recordLog ? lines : undefined)
258
+
259
+ const actx: any = {
260
+ fs: () => Fs,
261
+ log,
262
+ folder: sdk,
263
+ model: {
264
+ const: { name, Name: name.charAt(0).toUpperCase() + name.slice(1) },
265
+ main: {
266
+ [KIT]: { feature: {}, entity: {}, target: {} },
267
+ },
268
+ },
269
+ url: Path.join(sdk, 'model', 'sdk.aontu'),
270
+ jostraca: Jostraca({ existing: { txt: { write: true, merge: false } } }),
271
+ opts: { dryrun: false },
272
+ }
273
+
274
+ const files = () => walk(sdk)
275
+ .map((p: string) => Path.relative(sdk, p).split(Path.sep).join('/'))
276
+ .filter((p: string) => !p.startsWith('.jostraca/') && !p.includes('/.jostraca/'))
277
+ .sort()
278
+
279
+ return {
280
+ root, sdk, actx, log,
281
+
282
+ addPackage: async (ref: string, flags: any = {}) => {
283
+ actx.flags = flags
284
+ return package_add([ref], actx)
285
+ },
286
+
287
+ // Through `ACTION_MAP`, which is the SAME dispatch the CLI uses — so a
288
+ // kind registered later is installable here with no change to the kit,
289
+ // and a kind whose action is missing fails the way the CLI fails.
290
+ add: async (kind: string, ref: string, flags: any = {}) => {
291
+ const action = (ACTION_MAP as any)[kind]
292
+ if (null == action) {
293
+ throw new Error('testkit: no such kind: ' + kind +
294
+ ' (known: ' + Object.keys(ACTION_MAP).sort().join(', ') + ')')
295
+ }
296
+ actx.flags = flags
297
+ // `[kind, cmd, ...refs]` — the CLI's own argv shape, which is what
298
+ // `action_<kind>` parses (it reads the verb at args[1]).
299
+ return action([kind, 'add', ref], actx)
300
+ },
301
+
302
+ // CONSUMER-RELATIVE, not absolute, and that is not a stylistic choice.
303
+ //
304
+ // `target add` records `base` as the resolved source folder, so an
305
+ // absolute ref writes THIS MACHINE'S path into the installed model. Two
306
+ // things then go wrong at once: the copy is not reproducible across
307
+ // machines, and the feature fan-out compares the target's own tm folder
308
+ // against the one it reaches through `node_modules` — textually different
309
+ // paths for the same tree, which it reports as a shadowing overlay. Both
310
+ // disappear when the ref is spelled the way a consumer spells it.
311
+ bundledRef: (kind: string, name: string) =>
312
+ 'target' === kind ? 'node_modules/@voxgig/sdkgen/project/' + name : name,
313
+
314
+ setModel: (model: any) => { actx.model = model },
315
+
316
+ // AWAITS A PROMISE-RETURNING CALLBACK before restoring the directory.
317
+ //
318
+ // A synchronous `finally` would put the cwd back the moment `fn` RETURNS,
319
+ // which for an async callback is its first `await` — so the generation it
320
+ // was wrapping would do most of its work, including every CWD-relative
321
+ // template copy and the later docs and out-of-tree passes, from the
322
+ // caller's directory. The wrapper would look correct and protect almost
323
+ // nothing.
324
+ inSdk: (fn: any): any => {
325
+ const prev = process.cwd()
326
+ process.chdir(sdk)
327
+
328
+ let out: any
329
+ try {
330
+ out = fn()
331
+ }
332
+ catch (err) {
333
+ process.chdir(prev)
334
+ throw err
335
+ }
336
+
337
+ if (null != out && 'function' === typeof out.then) {
338
+ return out.then(
339
+ (v: any) => { process.chdir(prev); return v },
340
+ (err: any) => { process.chdir(prev); throw err })
341
+ }
342
+
343
+ process.chdir(prev)
344
+ return out
345
+ },
346
+
347
+ compile: (copts = {}) => compileComponents(sdk, copts.transform),
348
+
349
+ files,
350
+
351
+ cleanup: () => {
352
+ if (null == opts.dir) {
353
+ // The links go FIRST, explicitly. `rmSync` would remove them without
354
+ // following (measured), so this is belt and braces for the walk — but
355
+ // it is NOT redundant on Windows, where a junction refuses `unlink`
356
+ // and `force: true` forgives only ENOENT, so leaving it to the walk
357
+ // can throw.
358
+ for (const link of links) {
359
+ unlink(link)
360
+ }
361
+ Fs.rmSync(root, { recursive: true, force: true })
362
+ }
363
+ },
364
+ }
365
+ }
366
+
367
+
368
+ // REMOVE A LINK, WHICHEVER KIND IT TURNED OUT TO BE.
369
+ //
370
+ // `linkModule` makes a symlink on POSIX and a directory JUNCTION on Windows,
371
+ // and falls back to a real directory holding a re-export shim. `unlinkSync`
372
+ // removes the first, fails with EPERM on the second, and cannot remove the
373
+ // third.
374
+ //
375
+ // WHAT THIS IS AND IS NOT FOR. Node's recursive `rmSync` does NOT follow a
376
+ // symlink or a junction — it removes the link itself — so the tree walk that
377
+ // follows is not going to reach through into the sdkgen checkout. That was
378
+ // measured, not assumed. What this function buys is narrower and still worth
379
+ // having: cleanup that does not throw on Windows (a junction refuses
380
+ // `unlink`, and `force: true` forgives only ENOENT), and not depending on
381
+ // that `rmSync` behaviour holding forever for an operation whose blast radius
382
+ // would be a developer's checkout.
383
+ function unlink(link: string): void {
384
+ let stat: any
385
+ try {
386
+ stat = Fs.lstatSync(link)
387
+ }
388
+ catch (err) {
389
+ return
390
+ }
391
+
392
+ // A LINK OF SOME KIND — never recurse. `lstat` reports a Windows junction as
393
+ // a symbolic link, which is exactly the case a recursive remove must not
394
+ // reach: it would delete the sdkgen checkout on the other side.
395
+ //
396
+ // `unlink` removes a POSIX symlink and refuses a junction (EPERM); `rmdir`
397
+ // removes a junction and refuses a symlink. Try both rather than branch on
398
+ // the platform.
399
+ if (stat.isSymbolicLink()) {
400
+ try { Fs.unlinkSync(link); return } catch (err) { /* junction */ }
401
+ try { Fs.rmdirSync(link) } catch (err) { /* already gone */ }
402
+ return
403
+ }
404
+
405
+ // Not a link: the re-export shim `linkModule` writes when symlinking is
406
+ // refused. A real directory we created, so removing its contents is safe.
407
+ try {
408
+ Fs.rmSync(link, { recursive: true, force: true })
409
+ }
410
+ catch (err) { /* already gone */ }
411
+ }
412
+
413
+
414
+ // A path as an in-memory VOLUME spells it: forward slashes, no drive letter.
415
+ //
416
+ // memfs stores `/a/b`, never `C:/a/b`, so a Windows path has to lose both its
417
+ // separators and its drive before it can be compared with a volume key. This
418
+ // has now caused two Windows failures — the second one silently, because
419
+ // `Path.relative` between a drive-less key and a real root does not fail, it
420
+ // resolves the key against the CWD and returns a confidently wrong answer.
421
+ //
422
+ // Backslashes are replaced unconditionally rather than via `Path.sep`, so the
423
+ // rule is the same function on every platform and can be tested anywhere. A
424
+ // Windows filename cannot contain a backslash, so nothing legitimate is lost.
425
+ function volumeKey(p: string): string {
426
+ return p.replace(/\\/g, '/').replace(/^[A-Za-z]:/, '')
427
+ }
428
+
429
+
430
+ // The peer packages a consumer necessarily has installed alongside sdkgen.
431
+ // Read from the manifest rather than listed here, so a peer added later is
432
+ // linked without anyone remembering to.
433
+ function peerNames(): string[] {
434
+ try {
435
+ const pkg = JSON.parse(
436
+ Fs.readFileSync(Path.join(SDKGEN_ROOT, 'package.json'), 'utf8'))
437
+ return Object.keys(pkg.peerDependencies ?? {})
438
+ }
439
+ catch (err) {
440
+ return []
441
+ }
442
+ }
443
+
444
+
445
+ // WHERE A PEER ACTUALLY LIVES — asked of Node, not guessed from a path.
446
+ //
447
+ // The tempting version is `<SDKGEN_ROOT>/node_modules/<dep>`, and it is right
448
+ // only in this checkout. npm HOISTS: in a real installation sdkgen's peers are
449
+ // siblings of `@voxgig/sdkgen` under the host project's `node_modules`, not
450
+ // children of it. So the guessed path exists here, misses everywhere else, and
451
+ // the failure is silent — the peer is skipped, the staged consumer has no
452
+ // `@voxgig/apidef`, and the model compile fails on an include that resolves
453
+ // fine in every other context.
454
+ //
455
+ // `require.resolve` with `paths` walks the real chain, hoisted or not. The
456
+ // package.json is resolved rather than the entry point because a peer may not
457
+ // export one, and its directory is what has to be linked.
458
+ function peerRoot(dep: string): string | undefined {
459
+ try {
460
+ return Path.dirname(
461
+ require.resolve(dep + '/package.json', { paths: [SDKGEN_ROOT] }))
462
+ }
463
+ catch (err) {
464
+ // Some packages restrict `exports` and refuse the package.json subpath.
465
+ // Fall back to the entry point and climb to the directory that holds one.
466
+ try {
467
+ let dir = Path.dirname(require.resolve(dep, { paths: [SDKGEN_ROOT] }))
468
+ for (let up = 0; up < 8; up++) {
469
+ if (Fs.existsSync(Path.join(dir, 'package.json'))) return dir
470
+ const parent = Path.dirname(dir)
471
+ if (parent === dir) break
472
+ dir = parent
473
+ }
474
+ }
475
+ catch (err2) { /* genuinely not installed */ }
476
+
477
+ return undefined
478
+ }
479
+ }
480
+
481
+
482
+ // One `node_modules/<name>` entry pointing at an existing package directory.
483
+ //
484
+ // 'junction' is the portable spelling: on Windows it creates a directory
485
+ // junction, which needs no elevation, and on POSIX the type argument is
486
+ // ignored and an ordinary symlink results. A copy would work too and costs the
487
+ // whole 27-target template tree per staged consumer.
488
+ function linkModule(modules: string, name: string, from: string): string {
489
+ const link = Path.join(modules, ...name.split('/'))
490
+ Fs.mkdirSync(Path.dirname(link), { recursive: true })
491
+
492
+ if (Fs.existsSync(link)) return link
493
+
494
+ try {
495
+ Fs.symlinkSync(from, link, 'junction')
496
+ }
497
+ catch (err: any) {
498
+ // Last resort: a re-export shim. It satisfies `require('<name>')` but NOT
499
+ // deep file reads under the package, so a model include would still fail
500
+ // — which is why this is the fallback and not the mechanism.
501
+ Fs.mkdirSync(link, { recursive: true })
502
+ Fs.writeFileSync(Path.join(link, 'package.json'),
503
+ JSON.stringify({ name, version: '0.0.0', main: 'index.js' }) + '\n')
504
+ Fs.writeFileSync(Path.join(link, 'index.js'),
505
+ 'module.exports = require(' + JSON.stringify(from) + ')\n')
506
+ }
507
+
508
+ return link
509
+ }
510
+
511
+
512
+ function walk(dir: string): string[] {
513
+ if (!Fs.existsSync(dir)) return []
514
+ const out: string[] = []
515
+ for (const entry of Fs.readdirSync(dir, { withFileTypes: true })) {
516
+ const full = Path.join(dir, entry.name)
517
+ if (entry.isDirectory()) out.push(...walk(full))
518
+ else out.push(full)
519
+ }
520
+ return out
521
+ }
522
+
523
+
524
+ // TRANSPILE, DO NOT TYPE-CHECK.
525
+ //
526
+ // Type-checking a package's components is a BUILD-time gate (the package runs
527
+ // `tsc --noEmit` over its own `src/cmp/**`, the way this repo's
528
+ // `check-scaffold` does). Doing it again per staged consumer would add seconds
529
+ // to every test for an answer the build already has. What the kit needs here
530
+ // is only executable JS at the path `requirePath` reads.
531
+ //
532
+ // Neither transpiler is a dependency of this package, which has none. They are
533
+ // looked up at call time, and if neither is present the error names both
534
+ // rather than failing later as a missing module inside `requirePath`.
535
+ function compileComponents(
536
+ sdk: string,
537
+ transform?: (src: string, file: string) => string,
538
+ ): number {
539
+ const srcdir = Path.join(sdk, 'src', 'cmp')
540
+ const outdir = Path.join(sdk, 'dist', 'cmp')
541
+
542
+ if (!Fs.existsSync(srcdir)) return 0
543
+
544
+ const xform = transform ?? defaultTransform()
545
+
546
+ let count = 0
547
+ for (const file of walk(srcdir)) {
548
+ const rel = Path.relative(srcdir, file)
549
+
550
+ // Fragments are template source carrying placeholder tokens, not valid
551
+ // standalone modules — the same exclusion `tsconfig.scaffold.json` makes.
552
+ if (rel.split(Path.sep).includes('fragment')) continue
553
+
554
+ const out = Path.join(outdir, rel.replace(/\.ts$/, '.js'))
555
+ Fs.mkdirSync(Path.dirname(out), { recursive: true })
556
+
557
+ if (!file.endsWith('.ts')) {
558
+ // Components read sibling non-TS files (fragments, docs) relative to
559
+ // their own __dirname, so those have to arrive in dist too.
560
+ Fs.copyFileSync(file, Path.join(outdir, rel))
561
+ continue
562
+ }
563
+
564
+ Fs.writeFileSync(out, xform(Fs.readFileSync(file, 'utf8'), file))
565
+ count++
566
+ }
567
+
568
+ return count
569
+ }
570
+
571
+
572
+ function defaultTransform(): (src: string, file: string) => string {
573
+ const tried: string[] = []
574
+
575
+ try {
576
+ tried.push('sucrase')
577
+ const sucrase = require('sucrase')
578
+ return (src: string, file: string) => sucrase.transform(src, {
579
+ transforms: ['typescript', 'imports'],
580
+ filePath: file,
581
+ }).code
582
+ }
583
+ catch (err) { /* fall through to typescript */ }
584
+
585
+ try {
586
+ tried.push('typescript')
587
+ const ts = require('typescript')
588
+ return (src: string, file: string) => ts.transpileModule(src, {
589
+ fileName: file,
590
+ compilerOptions: {
591
+ target: ts.ScriptTarget.ES2021,
592
+ module: ts.ModuleKind.CommonJS,
593
+ esModuleInterop: true,
594
+ },
595
+ }).outputText
596
+ }
597
+ catch (err) { /* fall through to the error */ }
598
+
599
+ throw new Error(
600
+ 'testkit: no TypeScript transpiler found (looked for: ' +
601
+ tried.join(', ') + '). Add one as a devDependency, or pass ' +
602
+ '`compile({ transform })` with your own.')
603
+ }
604
+
605
+
606
+ type GenerateOptions = {
607
+ // The compiled model. A caller with aontu source unifies it themselves —
608
+ // the kit does not choose a model-compilation strategy for a package.
609
+ model: any
610
+
611
+ // The Root component. Defaults to one that renders every active target the
612
+ // way a create-sdkgen consumer's Root does.
613
+ root?: any
614
+
615
+ // Placeholder mentions that are NOT leaks, exactly as `parity.test.ts`
616
+ // keeps its list: a stated policy, not a mute button.
617
+ allowPlaceholder?: (path: string, token: string) => boolean
618
+ }
619
+
620
+
621
+ type GenerateResult = {
622
+ // Every generated path, relative to the consumer root, mapped to content.
623
+ files: Record<string, string>
624
+
625
+ // `<path>: <token>` for each surviving placeholder.
626
+ leaks: string[]
627
+ }
628
+
629
+
630
+ // GENERATE INTO MEMORY, from a consumer staged on disk.
631
+ //
632
+ // The split matters: the project (components, templates, model) is real,
633
+ // because that is what generation READS; the output is a memfs volume,
634
+ // because a test wants to assert on it rather than clean it up.
635
+ async function generateInto(
636
+ consumer: Consumer, opts: GenerateOptions,
637
+ ): Promise<GenerateResult> {
638
+ // memfs is a devDependency of whoever is testing, not of this package.
639
+ let memfs: any
640
+ try {
641
+ memfs = require('memfs').memfs
642
+ }
643
+ catch (err) {
644
+ throw new Error('testkit: generateInto needs `memfs` — add it as a devDependency')
645
+ }
646
+
647
+ const { fs, vol } = memfs({})
648
+
649
+ const sdkgen = SdkGen({
650
+ fs: layeredFs(fs),
651
+ folder: consumer.root,
652
+ root: '',
653
+ pino: consumer.log,
654
+ })
655
+
656
+ // GENERATION RUNS FROM `.sdk`.
657
+ //
658
+ // Components copy their template tree with a CWD-RELATIVE path
659
+ // (`Copy({ from: 'tm/<lang>' })`), because that is how a consumer runs it —
660
+ // `npm run generate` from the `.sdk` directory. Run it from anywhere else
661
+ // and the first feature copy fails on a `tm/...` path that does not exist
662
+ // relative to the caller's cwd, naming the template rather than the reason.
663
+ //
664
+ // The chdir is restored even when generation throws, because a test runner
665
+ // shares one process across suites and a leaked cwd breaks whatever runs
666
+ // next, somewhere else entirely.
667
+ const prevcwd = process.cwd()
668
+ process.chdir(consumer.sdk)
669
+
670
+ let res: any
671
+ try {
672
+ res = await sdkgen.generate({
673
+ model: opts.model,
674
+ root: opts.root ?? defaultRoot(),
675
+ })
676
+ }
677
+ finally {
678
+ process.chdir(prevcwd)
679
+ }
680
+
681
+ if (true !== res.ok) {
682
+ throw new Error('testkit: generation failed: ' + JSON.stringify(res))
683
+ }
684
+
685
+ // KEYS ARE COMPARED AS THE VOLUME SPELLS THEM, NOT AS THE OS DOES.
686
+ //
687
+ // memfs is a POSIX volume: it stores `/a/b`, never `C:/a/b`. So a generated
688
+ // path comes back with its DRIVE LETTER DROPPED, while `consumer.root` — a
689
+ // real Windows path — still has one. `Path.relative` between the two does
690
+ // not merely fail; it resolves the drive-less path against the CWD and
691
+ // returns something confidently wrong (`D:\Users\…` for a root on `C:`).
692
+ //
693
+ // `external.test.ts` hit this first and its `norm` is the rule being reused
694
+ // here: lose the separators AND the drive letter before comparing. Prefix
695
+ // arithmetic rather than `Path.relative`, because the whole problem is that
696
+ // these are volume keys and not OS paths.
697
+ //
698
+ // On POSIX both transformations are identity, so this changes nothing there.
699
+ const rootkey = volumeKey(consumer.root)
700
+
701
+ const files: Record<string, string> = {}
702
+ for (const [path, content] of Object.entries(vol.toJSON() as Record<string, string>)) {
703
+ const key = volumeKey(path)
704
+
705
+ // A KEY OUTSIDE THE ROOT IS A BUG HERE, NOT A RESULT.
706
+ //
707
+ // It means the string generation wrote under and the string being stripped
708
+ // off have diverged, and the caller would otherwise get a file map keyed
709
+ // by absolute path — which reads as "my component never ran" rather than
710
+ // as a path problem, and is exactly how this presented on Windows.
711
+ if (key !== rootkey && !key.startsWith(rootkey + '/')) {
712
+ throw new Error(
713
+ 'testkit: generated path is not under the consumer root, so the ' +
714
+ 'result cannot be keyed.\n root: ' + consumer.root +
715
+ '\n path: ' + path +
716
+ '\n compared as: ' + rootkey + ' vs ' + key +
717
+ '\nThese must agree once separators and any drive letter are ' +
718
+ 'normalised — memfs stores volume keys, not OS paths.')
719
+ }
720
+
721
+ const rel = key === rootkey ? '' : key.slice(rootkey.length + 1)
722
+
723
+ if (rel.startsWith('.jostraca/') || rel.includes('/.jostraca/')) continue
724
+ files[rel] = content
725
+ }
726
+
727
+ const allow = opts.allowPlaceholder ?? (() => false)
728
+ const leaks: string[] = []
729
+ for (const [path, content] of Object.entries(files)) {
730
+ if ('string' !== typeof content) continue
731
+
732
+ for (const token of PLACEHOLDERS) {
733
+ if (content.includes(token) && !allow(path, token)) {
734
+ leaks.push(path + ': ' + token)
735
+ }
736
+ }
737
+
738
+ const ref = content.match(PLACEHOLDER_REF)
739
+ if (null != ref && !allow(path, ref[0])) {
740
+ leaks.push(path + ': ' + ref[0])
741
+ }
742
+ }
743
+
744
+ return { files, leaks: leaks.sort() }
745
+ }
746
+
747
+
748
+ // Write to memfs, read through to the real project.
749
+ function layeredFs(mem: any): any {
750
+ const readThrough = (name: string) => (path: any, ...rest: any[]) => {
751
+ const target = mem.existsSync(path) ? mem : Fs
752
+ return (target as any)[name](path, ...rest)
753
+ }
754
+
755
+ return {
756
+ ...mem,
757
+ existsSync: (path: any) => mem.existsSync(path) || Fs.existsSync(path),
758
+ readFileSync: readThrough('readFileSync'),
759
+ readdirSync: readThrough('readdirSync'),
760
+ statSync: readThrough('statSync'),
761
+ realpathSync: readThrough('realpathSync'),
762
+ }
763
+ }
764
+
765
+
766
+ // The Root a create-sdkgen consumer has: per target, a folder holding the
767
+ // entity, feature, main, readme, agentguide and test phases.
768
+ //
769
+ // Deliberately minimal. A package author testing their own target wants to
770
+ // know that THEIR components ran, not to re-test the scaffold's Root — and a
771
+ // kit that shipped an elaborate Root would make its own behaviour part of
772
+ // every package's test result.
773
+ function defaultRoot(): any {
774
+ const { cmp, each, names, Project, Folder } = require('jostraca')
775
+ const { Main, Entity, Feature, Test, Readme, AgentGuide } = require('./sdkgen')
776
+
777
+ return cmp(function Root(props: any) {
778
+ const { model, ctx$ } = props
779
+
780
+ model.const = model.const || { name: model.name }
781
+ names(model.const, model.name)
782
+ if (null == model.const.year) model.const.year = new Date().getFullYear()
783
+ names(model, model.name)
784
+
785
+ ctx$.model = model
786
+ ctx$.stdrep = ctx$.stdrep || {}
787
+ names(ctx$.stdrep, model.Name, 'Project' + 'Name')
788
+
789
+ const target = model.main[KIT].target || {}
790
+ const feature = model.main[KIT].feature || {}
791
+ const entity = model.main[KIT].entity || {}
792
+
793
+ Project({}, () => {
794
+ each(target)
795
+ .filter((t: any) => t && false !== t.active)
796
+ .map((t: any) => {
797
+ names(t, t.name)
798
+
799
+ const phase = t.phase || {}
800
+ const on = (n: string) => false !== (phase[n] && phase[n].active)
801
+
802
+ Folder({ name: t.name }, () => {
803
+ if (on('entity')) {
804
+ each(entity)
805
+ .filter((e: any) => e && false !== e.active)
806
+ .map((e: any) => {
807
+ names(e, e.name)
808
+ Entity({ target: t, entity: e })
809
+ })
810
+ }
811
+
812
+ if (on('feature')) {
813
+ each(feature)
814
+ .filter((f: any) => f && f.active)
815
+ .map((f: any) => {
816
+ names(f, f.name)
817
+ Feature({ target: t, feature: f })
818
+ })
819
+ }
820
+
821
+ Main({ target: t })
822
+
823
+ if (on('readme')) Readme({ target: t })
824
+ if (on('agentguide')) AgentGuide({ target: t })
825
+ if (on('test')) Test({ target: t })
826
+ })
827
+ })
828
+ })
829
+ })
830
+ }
831
+
832
+
833
+ // THE PARITY TIER A PACKAGE DECLARES, from its manifest.
834
+ //
835
+ // `ts/test/parity.test.ts` owns the tier declaration for BUNDLED targets, and
836
+ // §18.4a refused to duplicate that map into the bundled manifest for exactly
837
+ // the reason this function exists to eventually resolve: the manifest should
838
+ // become the source and the parity suite should read it, not the reverse. For
839
+ // an EXTERNAL package there is no such conflict — its manifest is the only
840
+ // place its tier can live, so reading it here is the whole mechanism.
841
+ function manifestParity(pkgRoot: string): Record<string, string> {
842
+ const file = Path.join(pkgRoot, 'sdkgen-package.json')
843
+ if (!Fs.existsSync(file)) return {}
844
+ const manifest = JSON.parse(Fs.readFileSync(file, 'utf8'))
845
+ return manifest.parity ?? {}
846
+ }
847
+
848
+
849
+ export type {
850
+ Consumer,
851
+ StageOptions,
852
+ GenerateOptions,
853
+ GenerateResult,
854
+ }
855
+
856
+ export {
857
+ PLACEHOLDERS,
858
+ SDKGEN_ROOT,
859
+ volumeKey,
860
+ stageConsumer,
861
+ generateInto,
862
+ manifestParity,
863
+ }