@voxgig/sdkgen 3.4.6 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/bin/voxgig-sdkgen +78 -10
  2. package/dist/action/action.d.ts +5 -2
  3. package/dist/action/action.js +113 -6
  4. package/dist/action/action.js.map +1 -1
  5. package/dist/action/check.d.ts +15 -0
  6. package/dist/action/check.js +347 -0
  7. package/dist/action/check.js.map +1 -0
  8. package/dist/action/dispatch.d.ts +8 -0
  9. package/dist/action/dispatch.js +74 -0
  10. package/dist/action/dispatch.js.map +1 -0
  11. package/dist/action/docs.d.ts +4 -0
  12. package/dist/action/docs.js +236 -0
  13. package/dist/action/docs.js.map +1 -0
  14. package/dist/action/doctor.d.ts +5 -2
  15. package/dist/action/doctor.js +526 -74
  16. package/dist/action/doctor.js.map +1 -1
  17. package/dist/action/feature.js +96 -17
  18. package/dist/action/feature.js.map +1 -1
  19. package/dist/action/kind.d.ts +30 -0
  20. package/dist/action/kind.js +258 -0
  21. package/dist/action/kind.js.map +1 -0
  22. package/dist/action/package.d.ts +25 -0
  23. package/dist/action/package.js +852 -0
  24. package/dist/action/package.js.map +1 -0
  25. package/dist/action/resolve.d.ts +18 -0
  26. package/dist/action/resolve.js +280 -0
  27. package/dist/action/resolve.js.map +1 -0
  28. package/dist/action/target.d.ts +7 -1
  29. package/dist/action/target.js +240 -97
  30. package/dist/action/target.js.map +1 -1
  31. package/dist/cmp/Docs.d.ts +4 -0
  32. package/dist/cmp/Docs.js +98 -0
  33. package/dist/cmp/Docs.js.map +1 -0
  34. package/dist/cmp/ExternalDocs.d.ts +2 -0
  35. package/dist/cmp/ExternalDocs.js +43 -0
  36. package/dist/cmp/ExternalDocs.js.map +1 -0
  37. package/dist/cmp/ReadmeTop.js +5 -1
  38. package/dist/cmp/ReadmeTop.js.map +1 -1
  39. package/dist/helpers/definition.d.ts +5 -0
  40. package/dist/helpers/definition.js +72 -0
  41. package/dist/helpers/definition.js.map +1 -0
  42. package/dist/helpers/featureSource.d.ts +8 -2
  43. package/dist/helpers/featureSource.js +61 -12
  44. package/dist/helpers/featureSource.js.map +1 -1
  45. package/dist/helpers/junk.d.ts +11 -0
  46. package/dist/helpers/junk.js +149 -0
  47. package/dist/helpers/junk.js.map +1 -0
  48. package/dist/helpers/manifest.d.ts +50 -0
  49. package/dist/helpers/manifest.js +328 -0
  50. package/dist/helpers/manifest.js.map +1 -0
  51. package/dist/helpers/modelcheck.d.ts +22 -0
  52. package/dist/helpers/modelcheck.js +203 -0
  53. package/dist/helpers/modelcheck.js.map +1 -0
  54. package/dist/helpers/semver.d.ts +4 -0
  55. package/dist/helpers/semver.js +174 -0
  56. package/dist/helpers/semver.js.map +1 -0
  57. package/dist/helpers/shipped.d.ts +4 -0
  58. package/dist/helpers/shipped.js +36 -0
  59. package/dist/helpers/shipped.js.map +1 -0
  60. package/dist/helpers/stdrep.d.ts +9 -1
  61. package/dist/helpers/stdrep.js +22 -0
  62. package/dist/helpers/stdrep.js.map +1 -1
  63. package/dist/sdkgen.d.ts +15 -3
  64. package/dist/sdkgen.js +233 -67
  65. package/dist/sdkgen.js.map +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/dist/types.d.ts +10 -2
  68. package/dist/utility.js +17 -0
  69. package/dist/utility.js.map +1 -1
  70. package/model/sdkgen.aontu +69 -0
  71. package/package.json +3 -2
  72. package/project/.sdk/model/feature/audit.aontu +1 -0
  73. package/project/.sdk/model/feature/cache.aontu +1 -0
  74. package/project/.sdk/model/feature/clienttrack.aontu +1 -0
  75. package/project/.sdk/model/feature/debug.aontu +1 -0
  76. package/project/.sdk/model/feature/idempotency.aontu +1 -0
  77. package/project/.sdk/model/feature/log.aontu +1 -0
  78. package/project/.sdk/model/feature/metrics.aontu +1 -0
  79. package/project/.sdk/model/feature/netsim.aontu +1 -0
  80. package/project/.sdk/model/feature/paging.aontu +1 -0
  81. package/project/.sdk/model/feature/proxy.aontu +1 -0
  82. package/project/.sdk/model/feature/ratelimit.aontu +1 -0
  83. package/project/.sdk/model/feature/rbac.aontu +1 -0
  84. package/project/.sdk/model/feature/retry.aontu +1 -0
  85. package/project/.sdk/model/feature/streaming.aontu +1 -0
  86. package/project/.sdk/model/feature/telemetry.aontu +1 -0
  87. package/project/.sdk/model/feature/test.aontu +1 -0
  88. package/project/.sdk/model/feature/timeout.aontu +1 -0
  89. package/project/.sdk/model/target/c.aontu +1 -0
  90. package/project/.sdk/model/target/clojure.aontu +1 -0
  91. package/project/.sdk/model/target/cpp.aontu +1 -0
  92. package/project/.sdk/model/target/dart.aontu +1 -0
  93. package/project/.sdk/model/target/elixir.aontu +1 -0
  94. package/project/.sdk/model/target/go-cli.aontu +1 -0
  95. package/project/.sdk/model/target/go-mcp.aontu +1 -0
  96. package/project/.sdk/model/target/go.aontu +1 -0
  97. package/project/.sdk/model/target/haskell.aontu +1 -0
  98. package/project/.sdk/model/target/java.aontu +1 -0
  99. package/project/.sdk/model/target/js.aontu +1 -0
  100. package/project/.sdk/model/target/kotlin.aontu +1 -0
  101. package/project/.sdk/model/target/lean.aontu +1 -0
  102. package/project/.sdk/model/target/lua.aontu +1 -0
  103. package/project/.sdk/model/target/ocaml.aontu +1 -0
  104. package/project/.sdk/model/target/perl.aontu +1 -0
  105. package/project/.sdk/model/target/php.aontu +1 -0
  106. package/project/.sdk/model/target/py-data.aontu +1 -0
  107. package/project/.sdk/model/target/py.aontu +1 -0
  108. package/project/.sdk/model/target/rb.aontu +1 -0
  109. package/project/.sdk/model/target/rust.aontu +1 -0
  110. package/project/.sdk/model/target/scala.aontu +1 -0
  111. package/project/.sdk/model/target/seneca-provider.aontu +1 -0
  112. package/project/.sdk/model/target/zig.aontu +1 -0
  113. package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
  114. package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
  115. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
  116. package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
  117. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
  118. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
  119. package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
  120. package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
  121. package/project/.sdk/src/cmp/dart/Config_dart.ts +56 -3
  122. package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
  123. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
  124. package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
  125. package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
  126. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
  127. package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
  128. package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
  129. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
  130. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
  131. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
  132. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
  133. package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
  134. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
  135. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
  136. package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
  137. package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
  138. package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
  139. package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
  140. package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
  141. package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
  142. package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
  143. package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
  144. package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
  145. package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +31 -18
  146. package/project/.sdk/tm/c/core/sdk.h +1 -0
  147. package/project/.sdk/tm/c/utility/make_options.c +5 -1
  148. package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
  149. package/project/.sdk/tm/cpp/core/types.hpp +4 -1
  150. package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
  151. package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
  152. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
  153. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
  154. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
  155. package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
  156. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
  157. package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
  158. package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
  159. package/project/.sdk/tm/java/core/SdkClient.java +3 -1
  160. package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
  161. package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
  162. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
  163. package/project/.sdk/tm/ocaml/Makefile +3 -2
  164. package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
  165. package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
  166. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
  167. package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
  168. package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
  169. package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
  170. package/project/.sdk/tm/scala/utility/Make.scala +5 -1
  171. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
  172. package/project/.sdk/tm/zig/core/utility.zig +85 -1
  173. package/project/.sdk/tm/zig/test/gotcha_test.zig +8 -8
  174. package/project/sdkgen-package.json +57 -0
  175. package/src/action/action.ts +136 -6
  176. package/src/action/check.ts +495 -0
  177. package/src/action/dispatch.ts +105 -0
  178. package/src/action/docs.ts +310 -0
  179. package/src/action/doctor.ts +692 -81
  180. package/src/action/feature.ts +109 -17
  181. package/src/action/kind.ts +375 -0
  182. package/src/action/package.ts +1173 -0
  183. package/src/action/resolve.ts +362 -0
  184. package/src/action/target.ts +290 -108
  185. package/src/cmp/Docs.ts +125 -0
  186. package/src/cmp/ExternalDocs.ts +54 -0
  187. package/src/cmp/ReadmeTop.ts +6 -1
  188. package/src/helpers/definition.ts +82 -0
  189. package/src/helpers/featureSource.ts +83 -13
  190. package/src/helpers/junk.ts +168 -0
  191. package/src/helpers/manifest.ts +474 -0
  192. package/src/helpers/modelcheck.ts +262 -0
  193. package/src/helpers/semver.ts +211 -0
  194. package/src/helpers/shipped.ts +41 -0
  195. package/src/helpers/stdrep.ts +70 -0
  196. package/src/sdkgen.ts +294 -66
  197. package/src/types.ts +33 -3
  198. package/src/utility.ts +17 -0
@@ -0,0 +1,495 @@
1
+ // `package check` — the AUTHOR-side battery. Design §14.
2
+ //
3
+ // WHAT IT IS FOR
4
+ //
5
+ // Every other verb here acts on a project. This one acts on a PACKAGE, before
6
+ // anyone installs it, and its whole value is moving a failure from the
7
+ // consumer's build back to the author's terminal. The failures worth moving
8
+ // are the ones a package cannot detect by using itself:
9
+ //
10
+ // - a model file that compiles for the author and not for a consumer (the
11
+ // `//` comment asymmetry — seven shipped targets went out that way);
12
+ // - a key the base schema requires and the file omits, which nothing
13
+ // notices until a consumer unifies the whole model;
14
+ // - a key the PROJECT owns that the package pinned, which fails in the
15
+ // consumer's own file and reads as the consumer's mistake;
16
+ // - a manifest that disagrees with the disk, in either direction;
17
+ // - feature source the trim step will never recognise, so it ships to every
18
+ // project whatever the model selected.
19
+ //
20
+ // WHY IT REUSES EVERYTHING
21
+ //
22
+ // The battery asserts nothing of its own. Manifest agreement is
23
+ // `validateManifest`, the trees a kind needs are the kind registry's
24
+ // `requires`, the feature catalogue is `availableFeatures`, the source walk is
25
+ // `findFeatureEntries`, the model rules are `helpers/modelcheck`. A check that
26
+ // restated any of those would be a second copy of a rule the installer
27
+ // already has — and this workstream's most repeated defect is exactly that,
28
+ // two copies of one rule drifting apart. Where the check and the installer
29
+ // disagree, the check is wrong by construction.
30
+ //
31
+ // NO MANIFEST IS NOT A REFUSAL. `package add` requires one; here its absence
32
+ // is the first finding and the rest of the battery still runs, because an
33
+ // author who has not written it yet is precisely who needs the rest.
34
+
35
+ import Path from 'node:path'
36
+
37
+ import { KIT } from '../types'
38
+
39
+ import type {
40
+ ActionContext,
41
+ ActionResult,
42
+ } from '../types'
43
+
44
+ import { SdkGenError } from '../utility'
45
+
46
+ import {
47
+ MANIFEST,
48
+ probePackage,
49
+ validateManifest,
50
+ } from '../helpers/manifest'
51
+
52
+ import type { Finding, Manifest } from '../helpers/manifest'
53
+
54
+ import { definitionPath, definitionNames } from '../helpers/definition'
55
+
56
+ import {
57
+ ANCHOR,
58
+ ANCHOR_RE,
59
+ compileModel,
60
+ publishOverrideProbe,
61
+ slashComments,
62
+ } from '../helpers/modelcheck'
63
+
64
+ import {
65
+ BASE_FEATURE,
66
+ availableFeatures,
67
+ findFeatureEntries,
68
+ findFeatureSources,
69
+ } from '../helpers/featureSource'
70
+
71
+ import { scaffoldFolder } from '../helpers/shipped'
72
+
73
+ import { KINDS } from './kind'
74
+
75
+
76
+ // How many lines of one file's worth of the same problem to name before
77
+ // saying "and N more". A file with a hundred `//` lines is one mistake, and
78
+ // printing a hundred findings buries the other nine checks.
79
+ const SAME_FILE_LIMIT = 5
80
+
81
+
82
+ // A LIST OF NAMES from a manifest map, or nothing.
83
+ //
84
+ // The manifest is the one input here that is neither this generator's nor
85
+ // validated before it is read: `provides: { target: 42 }` is a manifest a
86
+ // person can write, `checkShape` already reports it, and spreading it then
87
+ // threw `TypeError: claimed is not iterable` — turning an actionable finding
88
+ // into a stack trace, from the verb whose entire job is actionable findings.
89
+ //
90
+ // Malformed reads as EMPTY rather than as its own finding: the shape checks
91
+ // have already said what is wrong with it, and saying so twice in different
92
+ // words helps nobody.
93
+ function claims(map: any, key: string): string[] {
94
+ const value = map?.[key]
95
+
96
+ return Array.isArray(value) ?
97
+ value.filter((n: any) => 'string' === typeof n && '' !== n) : []
98
+ }
99
+
100
+
101
+ type CheckReport = {
102
+ ok: boolean
103
+ ref: string
104
+ root: string
105
+ errors: number
106
+ warnings: number
107
+ findings: Finding[]
108
+ summary: string
109
+ }
110
+
111
+
112
+ async function cmd_package_check(
113
+ args: string[], actx: ActionContext,
114
+ ): Promise<ActionResult> {
115
+ const refs = args.slice(2).flatMap(
116
+ (a: any) => 'string' === typeof a ? a.split(',') : a)
117
+ .filter((r: any) => null != r && '' !== r)
118
+
119
+ // No ref means "the package I am standing in", which is where an author
120
+ // runs it.
121
+ const reports = (0 === refs.length ? ['.'] : refs)
122
+ .map((ref: string) => checkPackage(ref, actx))
123
+
124
+ const ok = reports.every((r: CheckReport) => r.ok)
125
+
126
+ return {
127
+ report: {
128
+ ok,
129
+ packages: reports,
130
+ // The one line the CLI prints when it exits non-zero. Built here
131
+ // because the counts are here; `bin/voxgig-sdkgen` must not have to
132
+ // know what a check finding is.
133
+ summary: reports.map((r: CheckReport) => r.summary).join('\n'),
134
+ },
135
+ }
136
+ }
137
+
138
+
139
+ // The battery, for one package. Logs as it goes and returns what it found, so
140
+ // a caller in a test can assert on findings without parsing log lines.
141
+ function checkPackage(ref: string, actx: ActionContext): CheckReport {
142
+ const fs = actx.fs()
143
+ const log = actx.log
144
+
145
+ const { found, search } = probePackage(fs, actx.folder ?? '.', ref)
146
+
147
+ // Nothing to check is not a finding — it is a wrong invocation, and the
148
+ // caller needs to know which paths were tried.
149
+ if (null == found) {
150
+ throw new SdkGenError(
151
+ 'Package not found: ' + ref + '\n looked for a `.sdk` folder in:\n ' +
152
+ search.join('\n '))
153
+ }
154
+
155
+ const { root, sdk, read } = found
156
+
157
+ log.info({
158
+ point: 'package-check-start', package: ref, root,
159
+ note: ref + ': checking ' + root
160
+ })
161
+
162
+ const findings: Finding[] = [
163
+ ...checkManifest(fs, sdk, read),
164
+ ...checkItems(fs, sdk, read.manifest),
165
+ ...checkFeatureSource(fs, sdk, read.manifest),
166
+ ]
167
+
168
+ for (const f of findings) {
169
+ const level = 'error' === f.level ? 'error' : f.level
170
+ log[level](f)
171
+ }
172
+
173
+ const errors = findings.filter((f: Finding) => 'error' === f.level).length
174
+ const warnings = findings.filter((f: Finding) => 'warn' === f.level).length
175
+
176
+ const summary = ref + ': ' + (0 === errors ?
177
+ (0 === warnings ? 'no findings' : warnings + ' warning(s)') :
178
+ errors + ' error(s), ' + warnings + ' warning(s)')
179
+
180
+ log.info({
181
+ point: 'package-check-end', package: ref, root, errors, warnings,
182
+ note: summary
183
+ })
184
+
185
+ return { ok: 0 === errors, ref, root, errors, warnings, findings, summary }
186
+ }
187
+
188
+
189
+ // The manifest, and its agreement with the disk in both directions.
190
+ function checkManifest(fs: any, sdk: string, read: any): Finding[] {
191
+ if (null != read.err) {
192
+ return [{
193
+ level: 'error', point: 'manifest-unreadable', file: read.file,
194
+ note: read.file + ': ' + read.err
195
+ }]
196
+ }
197
+
198
+ if (null == read.manifest) {
199
+ // A WARNING, not an error: the `.sdk` may be perfectly good, and a direct
200
+ // `target add <path>/<name>` installs from it today. What it cannot do is
201
+ // be installed as a package, which is what the manifest is for.
202
+ return [{
203
+ level: 'warn', point: 'manifest-absent', file: read.file,
204
+ note: read.file + ': no manifest, so `package add` cannot install this' +
205
+ ' — its items can still be added directly by path'
206
+ }]
207
+ }
208
+
209
+ return validateManifest(fs, sdk, read.manifest, KINDS)
210
+ }
211
+
212
+
213
+ // Every item's definition file: the anchor, the comment dialect, the parse,
214
+ // the schema, and the kind-specific probes.
215
+ //
216
+ // The items are the manifest's claims UNION what is on disk, so a definition
217
+ // nobody claims is still checked (its being unclaimed is `validateManifest`'s
218
+ // finding, not a reason to skip it) and a claim with no file is skipped here
219
+ // (its absence is also already reported, and there is nothing to compile).
220
+ function checkItems(fs: any, sdk: string, manifest?: Manifest): Finding[] {
221
+ const found: Finding[] = []
222
+
223
+ for (const kind of Object.keys(KINDS).sort()) {
224
+ const claimed = claims(manifest?.provides, kind)
225
+ const ondisk = definitionNames(fs, sdk, kind)
226
+
227
+ const names = Array.from(new Set([...claimed, ...ondisk])).sort()
228
+
229
+ for (const name of names) {
230
+ const file = definitionPath(sdk, kind, name)
231
+
232
+ if (!fs.existsSync(file)) {
233
+ continue
234
+ }
235
+
236
+ found.push(...checkDefinition(fs, kind, name, file))
237
+ }
238
+ }
239
+
240
+ return found
241
+ }
242
+
243
+
244
+ function checkDefinition(
245
+ fs: any, kind: string, name: string, file: string,
246
+ ): Finding[] {
247
+ const found: Finding[] = []
248
+ const src = String(fs.readFileSync(file, 'utf8'))
249
+
250
+ const at = (level: 'error' | 'warn', point: string, note: string): Finding =>
251
+ ({ level, point, kind, name, file, note: file + ': ' + note })
252
+
253
+ // 1. The provenance anchor. Its absence costs nothing at add time and
254
+ // everything afterwards: the copy records no source, so `doctor` and
255
+ // `package update` cannot find where it came from.
256
+ if (!ANCHOR_RE.test(src)) {
257
+ found.push(at('error', 'model-anchor-missing',
258
+ 'no `' + ANCHOR + '` line — the copy would record no provenance, so ' +
259
+ '`package update` and `doctor` could never locate its source'))
260
+ }
261
+
262
+ // 2. The comment dialect. Reported before the parse so the finding names
263
+ // the LINE rather than aontu's view of where the parse gave up.
264
+ const slashes = slashComments(src)
265
+
266
+ for (const s of slashes.slice(0, SAME_FILE_LIMIT)) {
267
+ found.push(at('error', 'model-slash-comment',
268
+ s.line + ': `' + s.text + '` — aontu takes `#` comments only; a `//` ' +
269
+ 'or `/* */` line is a parse error in a consumer, which configures the ' +
270
+ 'parser strictly even though a bare Aontu() accepts it'))
271
+ }
272
+
273
+ if (SAME_FILE_LIMIT < slashes.length) {
274
+ found.push(at('error', 'model-slash-comment',
275
+ 'and ' + (slashes.length - SAME_FILE_LIMIT) + ' more slash-comment line(s)'))
276
+ }
277
+
278
+ // A slash comment IS the parse failure below, and saying it twice — once
279
+ // with the line, once as aontu's `unexpected character(s): //` — reads as
280
+ // two problems. Everything after it would be reporting on a file the author
281
+ // is about to change anyway.
282
+ if (0 < slashes.length) {
283
+ return found
284
+ }
285
+
286
+ // 3. The parse, as a CONSUMER's parser sees it.
287
+ const strict = compileModel(src, file)
288
+
289
+ if (0 < strict.errors.length) {
290
+ // Which parser rejected it changes what the author must do, so say. A
291
+ // file that a bare Aontu() accepts and the strict one rejects is almost
292
+ // always the comment dialect above.
293
+ const bare = compileModel(src, file, { strict: false })
294
+
295
+ found.push(at('error', 'model-parse',
296
+ strict.errors.join(' | ') +
297
+ (0 === bare.errors.length ?
298
+ ' (it DOES compile under a bare Aontu() — the difference is the ' +
299
+ 'comment dialect a consumer configures)' : '')))
300
+
301
+ // Nothing below can run on a file that does not compile.
302
+ return found
303
+ }
304
+
305
+ // 4. Does it declare the key its FILENAME promises? `model/target/iot-go.aontu`
306
+ // is copied to that name and included by it, so a file still declaring
307
+ // `main: kit: target: go:` installs an item the consumer's model never
308
+ // sees — the exact mistake a package author makes copying a bundled
309
+ // target as a starting point.
310
+ const declared = strict.model?.main?.[KIT]?.[kind]?.[name]
311
+
312
+ if (null == declared || 'object' !== typeof declared) {
313
+ found.push(at('error', 'model-key-missing',
314
+ 'declares no `main: kit: ' + kind + ': ' + name + ':` block — the file ' +
315
+ 'is installed and included under its own name, so nothing it declares ' +
316
+ 'under another name is reachable'))
317
+ }
318
+
319
+ // 5. The base schema. A non-defaulted key the file omits (`ext`,
320
+ // `comment.line`, `module.name`, a feature's `title`) compiles fine
321
+ // alone and fails the consumer's whole model.
322
+ const unified = compileModel(src, file, { schema: true })
323
+
324
+ for (const err of unified.errors.slice(0, SAME_FILE_LIMIT)) {
325
+ found.push(at('error', 'model-schema',
326
+ err + ' (unified with the base schema — this is what a consumer compiles)'))
327
+ }
328
+
329
+ if ('target' === kind) {
330
+ found.push(...checkTargetModel(src, name, file, at))
331
+ }
332
+
333
+ if ('feature' === kind) {
334
+ found.push(...checkFeatureModel(strict.model, name, at))
335
+ }
336
+
337
+ return found
338
+ }
339
+
340
+
341
+ type At = (level: 'error' | 'warn', point: string, note: string) => Finding
342
+
343
+
344
+ // The publish-override probe: unify the target model with a project that sets
345
+ // the keys a project owns. Concrete-vs-concrete is a conflict in aontu, so a
346
+ // package that pins one makes it impossible for the consumer to set it — and
347
+ // the consumer's error names the CONSUMER's file.
348
+ function checkTargetModel(
349
+ src: string, name: string, file: string, at: At,
350
+ ): Finding[] {
351
+ const probe = publishOverrideProbe(src, file, name)
352
+
353
+ if (0 === probe.errors.length) {
354
+ return []
355
+ }
356
+
357
+ return [at('error', 'target-publish-pinned',
358
+ 'a project cannot override its publication values: ' +
359
+ probe.errors.join(' | ') +
360
+ ' (the target model sets a key the schema already defaults — leave ' +
361
+ '`publish.version`, `publish.registry.package`, `state` and `active` ' +
362
+ 'unset; registry identity is yours, versions and names are the ' +
363
+ "project's)")]
364
+ }
365
+
366
+
367
+ // Per-target dependencies belong DIRECTLY under the feature
368
+ // (`deps: <target>: {…}`), which is the only path `collectDeps` reads. The
369
+ // other spelling — `feature.<f>.target.<t>.deps` — is schema-legal, because
370
+ // every target model declares that slot, so nothing errors and the
371
+ // dependency simply never reaches the generated manifest.
372
+ function checkFeatureModel(model: any, name: string, at: At): Finding[] {
373
+ const targets = model?.main?.[KIT]?.feature?.[name]?.target
374
+
375
+ if (null == targets || 'object' !== typeof targets) {
376
+ return []
377
+ }
378
+
379
+ const found: Finding[] = []
380
+
381
+ for (const tname of Object.keys(targets).sort()) {
382
+ const deps = targets[tname]?.deps
383
+
384
+ if (null == deps || 'object' !== typeof deps) {
385
+ continue
386
+ }
387
+
388
+ // The SLOT itself is legal and empty; only concrete entries are the
389
+ // mistake. An entry is concrete when it declares anything of its own.
390
+ const named = Object.keys(deps).filter((d: string) =>
391
+ null != deps[d] && 'object' === typeof deps[d] &&
392
+ 0 < Object.keys(deps[d]).length)
393
+
394
+ if (0 === named.length) {
395
+ continue
396
+ }
397
+
398
+ found.push(at('warn', 'feature-deps-misplaced',
399
+ 'declares ' + named.join(', ') + ' under `feature: ' + name +
400
+ ': target: ' + tname + ': deps:` — nothing reads that path. ' +
401
+ 'Per-target dependencies go directly under the feature: `deps: ' +
402
+ tname + ': { ' + named[0] + ': {…} }`'))
403
+ }
404
+
405
+ return found
406
+ }
407
+
408
+
409
+ // The template trees: does a feature's declared coverage exist, and is
410
+ // anything in a target's tree feature-shaped but invisible to the trim?
411
+ function checkFeatureSource(
412
+ fs: any, sdk: string, manifest?: Manifest,
413
+ ): Finding[] {
414
+ const found: Finding[] = []
415
+ const file = Path.join(sdk, '..', MANIFEST)
416
+
417
+ // The catalogue a CONSUMER will have: this generator's own features, plus
418
+ // the package's. A package shipping a copy of a bundled target ships source
419
+ // for features it does not provide, and those are not strays.
420
+ const bundled = availableFeatures(fs, scaffoldFolder())
421
+
422
+ const known = new Set([...bundled, ...availableFeatures(fs, sdk)])
423
+
424
+ // Declared coverage. `targetsSupported: { <feature>: [<target>, …] }` is
425
+ // the author's statement about which targets a feature ships source for, so
426
+ // it is checkable — unlike its absence, which means nothing either way.
427
+ const supported: any = manifest?.targetsSupported
428
+
429
+ for (const fname of Object.keys(supported ?? {}).sort()) {
430
+ for (const tname of claims(supported, fname)) {
431
+ const tm = Path.join(sdk, 'tm', tname)
432
+
433
+ if (0 === findFeatureSources(fs, tm, [fname.toLowerCase()]).length) {
434
+ found.push({
435
+ level: 'warn', point: 'feature-source-undelivered', file,
436
+ kind: 'feature', name: fname,
437
+ note: file + ': `targetsSupported.' + fname + '` claims ' + tname +
438
+ ', but tm/' + tname + ' holds no source this generator can find ' +
439
+ 'for it — see the naming conventions in ' +
440
+ 'docs/how-to/add-a-feature.md'
441
+ })
442
+ }
443
+ }
444
+ }
445
+
446
+ // Strays: feature-shaped entries in a provided TARGET's tree whose name is
447
+ // in no catalogue. They are copied into every project regardless of what
448
+ // its model selects, because trim only drops what it can recognise.
449
+ //
450
+ // NOT REPORTED AT ALL if this generator's own feature definitions cannot be
451
+ // read: every bundled feature's source would then look like a stray, and a
452
+ // check that fires on a correct package because of a problem at ITS end is
453
+ // worse than one that stays quiet.
454
+ if (0 === bundled.length) {
455
+ return found
456
+ }
457
+
458
+ const targets = new Set([
459
+ ...claims(manifest?.provides, 'target'),
460
+ ...definitionNames(fs, sdk, 'target'),
461
+ ])
462
+
463
+ for (const tname of Array.from(targets).sort()) {
464
+ const tm = Path.join(sdk, 'tm', tname)
465
+
466
+ const strays = findFeatureEntries(fs, tm, known)
467
+ .filter((e) => e.shaped && !known.has(e.name) && BASE_FEATURE !== e.name)
468
+
469
+ if (0 === strays.length) {
470
+ continue
471
+ }
472
+
473
+ found.push({
474
+ level: 'warn', point: 'feature-source-unrecognised', file,
475
+ kind: 'target', name: tname,
476
+ note: 'tm/' + tname + ': ' + strays.map((e) => e.path).join(', ') +
477
+ ' — named like feature source, but no `model/feature/<name>.aontu` ' +
478
+ 'declares ' + strays.map((e) => e.name).join(', ') +
479
+ ', so the trim cannot recognise them and every project gets them ' +
480
+ 'whatever its model selects'
481
+ })
482
+ }
483
+
484
+ return found
485
+ }
486
+
487
+
488
+ export type {
489
+ CheckReport,
490
+ }
491
+
492
+ export {
493
+ cmd_package_check,
494
+ checkPackage,
495
+ }
@@ -0,0 +1,105 @@
1
+ // WHAT `voxgig-sdkgen <action> …` CAN BE.
2
+ //
3
+ // Built FROM THE KIND REGISTRY rather than hand-listed, so registering a kind
4
+ // is the only edit a new kind needs — `docs add …` costs no dispatch code
5
+ // (docs/design/sdkgen-packages.md §9). The `package` and `doctor` verbs are
6
+ // not kinds and are added beside it.
7
+ //
8
+ // It lives in its own module because `package.ts` needs the per-kind add
9
+ // functions and `target.ts` imports `feature.ts` which imports `kind.ts`;
10
+ // wiring them together anywhere in that chain is a require cycle. Here,
11
+ // nothing imports back.
12
+ //
13
+ // NULL-PROTOTYPE, and this is not decoration. A plain object literal inherits
14
+ // Object.prototype, so `voxgig-sdkgen toString` (or `constructor`, `valueOf`,
15
+ // …) resolved to an inherited function, passed the `null == actionFunc`
16
+ // guard, and got CALLED with the action arguments instead of reporting an
17
+ // unknown action.
18
+
19
+ import type {
20
+ ActionContext,
21
+ ActionResult,
22
+ } from '../types'
23
+
24
+ import { KINDS } from './kind'
25
+
26
+ import { action_target, target_add } from './target'
27
+ import { action_feature, feature_add } from './feature'
28
+ import { action_docs, docs_add } from './docs'
29
+ import { action_doctor } from './doctor'
30
+ import { action_package, registerAdder } from './package'
31
+
32
+
33
+ type ActionFunc =
34
+ (args: string[], actx: ActionContext) => Promise<ActionResult>
35
+
36
+
37
+ // The per-kind entry points, by kind name. A kind in the registry with no
38
+ // entry here is one nothing can install yet — reported when something asks
39
+ // for it, never silently ignored.
40
+ const KIND_ACTIONS: Record<string, ActionFunc> = Object.assign(
41
+ Object.create(null), {
42
+ target: action_target,
43
+ feature: action_feature,
44
+ docs: action_docs,
45
+ })
46
+
47
+
48
+ // The same functions, as the direct `(refs, actx)` calls `package add` loops
49
+ // over. Registered rather than imported by `package.ts`, for the cycle above.
50
+ registerAdder('target', target_add)
51
+ registerAdder('feature', feature_add)
52
+ registerAdder('docs', docs_add)
53
+
54
+
55
+ function actionMap(): Record<string, ActionFunc> {
56
+ const map: Record<string, ActionFunc> = Object.create(null)
57
+
58
+ for (const kind of Object.keys(KINDS)) {
59
+ if (null != KIND_ACTIONS[kind]) {
60
+ map[kind] = KIND_ACTIONS[kind]
61
+ }
62
+ }
63
+
64
+ map.package = action_package
65
+ map.doctor = action_doctor
66
+
67
+ return map
68
+ }
69
+
70
+
71
+ const ACTION_MAP = actionMap()
72
+
73
+
74
+ // The verbs, for help text and for an unknown-action message that says what
75
+ // IS available rather than only what is not.
76
+ function actionNames(): string[] {
77
+ return Object.keys(ACTION_MAP).sort()
78
+ }
79
+
80
+
81
+ // Does this invocation need the PROJECT's model?
82
+ //
83
+ // Every verb but one operates on a project, and compiling `model/sdk.aontu`
84
+ // before dispatch is what makes them fail early and clearly when run from the
85
+ // wrong directory. `package check` is the exception on purpose: it operates
86
+ // on a PACKAGE, and an author runs it in their package root, where there is
87
+ // no project model to compile and nothing is wrong with that.
88
+ //
89
+ // Stated here, with the verb table, rather than in the CLI or in `sdkgen.ts`:
90
+ // it is a fact about the verb.
91
+ function needsModel(args: string[]): boolean {
92
+ return !('package' === args[0] && 'check' === args[1])
93
+ }
94
+
95
+
96
+ export type {
97
+ ActionFunc,
98
+ }
99
+
100
+ export {
101
+ ACTION_MAP,
102
+ actionMap,
103
+ actionNames,
104
+ needsModel,
105
+ }