@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,54 @@
1
+ // The Root for a DOCS item that generates outside the SDK repo
2
+ // (`main: kit: docs: <n>: output: path`).
3
+ //
4
+ // Out of tree is the NORMAL case for a docs item, not the exception it is for
5
+ // a target: a documentation site usually has its own repo, and in-tree it
6
+ // would collide with the SDK repo's own README and CI workflows.
7
+ //
8
+ // The two facts that make this a separate `generate()` pass rather than a
9
+ // folder are the ones `ExternalTarget` documents at length: jostraca's
10
+ // `FileHandler.validName` refuses a `..` segment in a folder name (the guard
11
+ // that keeps generation inside the tree it was pointed at, not an oversight
12
+ // to route around), and the output root is `jopts.folder` PER CALL — so
13
+ // writing somewhere else means another call.
14
+ //
15
+ // No Folder wraps the item: the destination IS the site, so its files go at
16
+ // the root of the output path.
17
+
18
+ import { cmp, names, Project } from 'jostraca'
19
+
20
+ import { KIT } from '../types'
21
+
22
+ import { DocsItem, prepareModel } from './Docs'
23
+
24
+
25
+ const ExternalDocs = cmp(function ExternalDocs(props: any) {
26
+ const { model, item, cmpfolder, sdkrelpath } = props
27
+ const ctx$ = props.ctx$
28
+
29
+ // Components live in the PROJECT, not in the repo being written to. This
30
+ // pass has retargeted jostraca's output folder, which is what requirePath
31
+ // otherwise resolves against — see utility.resolvePath. Losing this line
32
+ // makes the pass look for components under the DESTINATION.
33
+ ctx$.cmpfolder = cmpfolder
34
+
35
+ // The path from the destination back to the SDK project, for a site that
36
+ // sits beside the SDK in a known layout and needs to name it.
37
+ ctx$.sdkrelpath = sdkrelpath
38
+
39
+ // The same preamble the in-tree pass performs — one definition, both
40
+ // callers, because a project whose ONLY docs item is out of tree must
41
+ // generate exactly as one with both.
42
+ prepareModel(model, ctx$)
43
+
44
+ Project({}, () => {
45
+ names(item, item.name ?? item.key$)
46
+
47
+ DocsItem({ item })
48
+ })
49
+ })
50
+
51
+
52
+ export {
53
+ ExternalDocs
54
+ }
@@ -261,8 +261,13 @@ ${aboutMd.trim()}
261
261
  exCall = `const ${exLower} = await client.${ex}().load(${exLoadArg})`
262
262
  } else if ('create' === primaryOp || 'update' === primaryOp) {
263
263
  const exIdF = entityIdField(exEnt)
264
+ // Drop the id only when the request shape says it is OPTIONAL. It is
265
+ // server-assigned on a normal create, but an op whose id comes from a
266
+ // PATH PARAMETER requires it, and the typed CreateData then rejects a
267
+ // body without it. Same rule as dataArg in helpers/opExample.
264
268
  const shapeItems = opRequestShape(exEnt, primaryOp).items
265
- .filter((it: any) => it.name !== exIdF && it.name !== 'id')
269
+ .filter((it: any) =>
270
+ (it.name !== exIdF && it.name !== 'id') || !it.optional)
266
271
  const required = shapeItems.filter((it: any) => !it.optional)
267
272
  // ALL required fields must appear or the literal is not assignable to
268
273
  // the typed CreateData/UpdateData; cap only the optional fallback.
@@ -0,0 +1,82 @@
1
+ // WHERE A KIND'S DEFINITION FILES LIVE.
2
+ //
3
+ // `model/<kind>/<name>.aontu`, with `model/<kind>/<kind>-index.aontu` as the
4
+ // include list beside them. One line of path-building — and it was written out
5
+ // longhand in three places (the resolver, the feature catalogue, doctor), each
6
+ // with its own idea of which files in that directory count.
7
+ //
8
+ // That is the same shape of defect this workstream has now fixed four times:
9
+ // the same rule expressed twice, drifting. The manifest makes it four callers,
10
+ // so it becomes one function first.
11
+ //
12
+ // The EXCLUSIONS matter as much as the path. A `model/<kind>/` directory holds
13
+ // the index file (a list of includes, not a definition) and, in the shipped
14
+ // scaffold, a `README.md`. Neither is an item, and a caller that forgets one
15
+ // invents a `feature-index` feature.
16
+
17
+ import Path from 'node:path'
18
+
19
+ import { isJunk } from './junk'
20
+
21
+
22
+ // The definition file for one item.
23
+ function definitionPath(sdkfolder: string, kind: string, name: string): string {
24
+ return Path.join(sdkfolder, 'model', kind, name + '.aontu')
25
+ }
26
+
27
+
28
+ // The directory holding a kind's definitions.
29
+ function definitionFolder(sdkfolder: string, kind: string): string {
30
+ return Path.join(sdkfolder, 'model', kind)
31
+ }
32
+
33
+
34
+ // The include list beside them.
35
+ function indexName(kind: string): string {
36
+ return kind + '-index.aontu'
37
+ }
38
+
39
+
40
+ // Every item of `kind` a `.sdk` folder DEFINES, sorted. Sorted because it
41
+ // feeds an exact-set comparison against a manifest and, through
42
+ // `availableFeatures`, the feature-trim catalogue — both of which are compared
43
+ // as text, so a readdir order that varies by filesystem would vary the result.
44
+ //
45
+ // An absent directory is not an error: a package providing only features has
46
+ // no `model/target/`, and that is the normal shape of a feature package.
47
+ function definitionNames(fs: any, sdkfolder: string, kind: string): string[] {
48
+ const dir = definitionFolder(sdkfolder, kind)
49
+ const index = indexName(kind)
50
+
51
+ // `existsSync` is true for a REGULAR FILE and for a directory this process
52
+ // cannot read, so guarding on it and then calling `readdirSync` let a bare
53
+ // `ENOTDIR`/`EACCES` escape to callers that have no catch — including
54
+ // `featureCatalogue`, which runs on every ordinary `target add`, so the
55
+ // whole add aborted with an errno instead of a diagnostic. Not-a-readable-
56
+ // directory is the same answer as not-there: this kind defines nothing
57
+ // here.
58
+ let entries: string[]
59
+ try {
60
+ entries = fs.readdirSync(dir)
61
+ }
62
+ catch (err: any) {
63
+ return []
64
+ }
65
+
66
+ // The `.aontu` suffix is not enough on its own: an emacs lock link is named
67
+ // `.#target.aontu` and a merge leaves `target.aontu.orig`, so an editor open
68
+ // in the wrong window invents an item called `.#target`, which then fails to
69
+ // resolve everywhere it is named. See helpers/junk.
70
+ return entries
71
+ .filter((n: string) => n.endsWith('.aontu') && index !== n && !isJunk(n))
72
+ .map((n: string) => n.replace(/\.aontu$/, ''))
73
+ .sort()
74
+ }
75
+
76
+
77
+ export {
78
+ definitionPath,
79
+ definitionFolder,
80
+ definitionNames,
81
+ indexName,
82
+ }
@@ -37,6 +37,10 @@ import Path from 'node:path'
37
37
 
38
38
  import { KIT, getModelPath } from '@voxgig/apidef'
39
39
 
40
+ import { definitionNames } from './definition'
41
+
42
+ import { isJunk } from './junk'
43
+
40
44
 
41
45
  // One feature's source within a target template tree.
42
46
  type FeatureSource = {
@@ -52,12 +56,25 @@ type FeatureSource = {
52
56
  }
53
57
 
54
58
 
59
+ // One entry inside a feature container, before the catalogue has had its say.
60
+ type FeatureEntry = FeatureSource & {
61
+ // Does it follow a per-feature NAMING convention? See `featureShaped`.
62
+ shaped: boolean
63
+ }
64
+
65
+
55
66
  // Directory name that marks a feature container. Kept exact (not a substring
56
67
  // match) so `utility/feature_add.go` and `test/feature_test.go` — which are
57
68
  // shared machinery, not per-feature source — are never treated as features.
58
69
  const FEATURE_DIR = 'feature'
59
70
 
60
71
 
72
+ // The always-present foundation every other feature builds on. It has no
73
+ // model file, so it is in no catalogue, and it must never be trimmed or
74
+ // reported as an unrecognised stray — every target ships one.
75
+ const BASE_FEATURE = 'base'
76
+
77
+
61
78
  // Derive the feature name an entry inside a feature directory belongs to.
62
79
  // Returns the lowercased name, which the caller then checks against the
63
80
  // available set.
@@ -88,25 +105,51 @@ function featureOf(entry: string, folder: boolean): string {
88
105
  // Feature names this generator can supply, read from the scaffold's
89
106
  // `model/feature/*.aontu`. This is the authoritative catalogue: a name not in
90
107
  // it is not a feature, so nothing outside it is ever excluded.
108
+ //
109
+ // LOWERCASED, unlike `definitionNames`, because these are matched against
110
+ // names DERIVED FROM FILENAMES (`RetryFeature.swift` -> `retry`), and the
111
+ // languages disagree about case. The manifest compares definition names as
112
+ // written, so the two callers cannot share the lowercasing.
91
113
  function availableFeatures(fs: any, sdkfolder: string): string[] {
92
- const dir = Path.join(sdkfolder, 'model', 'feature')
114
+ return definitionNames(fs, sdkfolder, 'feature')
115
+ .map((n: string) => n.toLowerCase())
116
+ .sort()
117
+ }
118
+
93
119
 
94
- if (!fs.existsSync(dir)) {
95
- return []
120
+ // Does the entry LOOK like per-feature source, whatever it is called?
121
+ //
122
+ // The naming conventions carry the intent: `<name>_feature.<ext>`,
123
+ // `<Name>Feature.<ext>`, and a directory named for the feature. A bare
124
+ // `<name>.<ext>` does NOT — `retry.rs` and `support.rs` are written the same
125
+ // way, so shape cannot separate rust's feature source from rust's shared
126
+ // machinery, and only the catalogue can. That is the deliberate blind spot of
127
+ // `package check`'s unrecognised-source finding: it reports what it can prove
128
+ // looks like a feature, and stays quiet where it would have to guess.
129
+ function featureShaped(entry: string, folder: boolean): boolean {
130
+ if (folder) {
131
+ return true
96
132
  }
97
133
 
98
- return fs.readdirSync(dir)
99
- .filter((n: string) => n.endsWith('.aontu') && 'feature-index.aontu' !== n)
100
- .map((n: string) => n.replace(/\.aontu$/, '').toLowerCase())
101
- .sort()
134
+ const stem = entry.replace(/\.[^.]+$/, '')
135
+
136
+ return /_feature$/i.test(stem) || /Feature$/.test(stem)
102
137
  }
103
138
 
104
139
 
105
- // Walk a target's template tree and return every per-feature source entry.
106
- // `tmfolder` is the target template root (`<sdk>/tm/<lang>`).
107
- function findFeatureSources(fs: any, tmfolder: string, available: string[]): FeatureSource[] {
108
- const known = new Set(available)
109
- const found: FeatureSource[] = []
140
+ // Every entry inside a feature container of a target's template tree
141
+ // whether or not its derived name is a feature this generator knows.
142
+ //
143
+ // `known` decides DESCENT, not membership: a directory that names a known
144
+ // feature is the whole feature, so the walk stops there, and one that does
145
+ // not is walked through (`src/feature/base/` holds `BaseFeature.ts`, and the
146
+ // container rule below then ignores it because its parent is not `feature`).
147
+ // The unknown entries are what `package check` reports and what
148
+ // `findFeatureSources` drops; both need the same walk, so there is one.
149
+ function findFeatureEntries(
150
+ fs: any, tmfolder: string, known: Set<string>,
151
+ ): FeatureEntry[] {
152
+ const found: FeatureEntry[] = []
110
153
 
111
154
  if (!fs.existsSync(tmfolder)) {
112
155
  return found
@@ -118,6 +161,14 @@ function findFeatureSources(fs: any, tmfolder: string, available: string[]): Fea
118
161
  const entries = fs.readdirSync(abs).sort()
119
162
 
120
163
  for (const entry of entries) {
164
+ // A `__pycache__` inside `src/feature/` is not a feature — but every
165
+ // rule here derives a feature NAME from an entry name, so without this
166
+ // it becomes one: reported by `package check` as an unknown feature, and
167
+ // trimmed (or not) as if it were source. See helpers/junk.
168
+ if (isJunk(entry)) {
169
+ continue
170
+ }
171
+
121
172
  const entryrel = '' === rel ? entry : rel + '/' + entry
122
173
  const folder = fs.statSync(Path.join(tmfolder, entryrel)).isDirectory()
123
174
 
@@ -126,8 +177,12 @@ function findFeatureSources(fs: any, tmfolder: string, available: string[]): Fea
126
177
  // looking for more.
127
178
  if (FEATURE_DIR === Path.basename(rel)) {
128
179
  const name = featureOf(entry, folder)
180
+
181
+ found.push({
182
+ name, path: entryrel, folder, shaped: featureShaped(entry, folder),
183
+ })
184
+
129
185
  if (known.has(name)) {
130
- found.push({ name, path: entryrel, folder })
131
186
  continue
132
187
  }
133
188
  }
@@ -144,6 +199,17 @@ function findFeatureSources(fs: any, tmfolder: string, available: string[]): Fea
144
199
  }
145
200
 
146
201
 
202
+ // Walk a target's template tree and return every per-feature source entry.
203
+ // `tmfolder` is the target template root (`<sdk>/tm/<lang>`).
204
+ function findFeatureSources(fs: any, tmfolder: string, available: string[]): FeatureSource[] {
205
+ const known = new Set(available)
206
+
207
+ return findFeatureEntries(fs, tmfolder, known)
208
+ .filter((e: FeatureEntry) => known.has(e.name))
209
+ .map(({ name, path, folder }: FeatureEntry) => ({ name, path, folder }))
210
+ }
211
+
212
+
147
213
  // Turn discovered sources into path patterns for a jostraca `Copy` exclude.
148
214
  //
149
215
  // jostraca tests each candidate against the path built up during its walk,
@@ -196,11 +262,15 @@ function srcFeatureExcludes(model: any): RegExp[] {
196
262
 
197
263
  export type {
198
264
  FeatureSource,
265
+ FeatureEntry,
199
266
  }
200
267
 
201
268
  export {
269
+ BASE_FEATURE,
202
270
  featureOf,
271
+ featureShaped,
203
272
  availableFeatures,
273
+ findFeatureEntries,
204
274
  findFeatureSources,
205
275
  featureExcludes,
206
276
  fullsetExcludes,
@@ -0,0 +1,168 @@
1
+ // WHAT NEVER COUNTS AS CONTENT.
2
+ //
3
+ // Every tree sdkgen reads — the scaffold's `tm/<lang>` and `src/cmp/<lang>`, an
4
+ // external package's `.sdk`, a project's own copies of both — is a DIRECTORY ON
5
+ // A DEVELOPER'S MACHINE, and a developer's machine drops things in directories:
6
+ // `.DS_Store` from the Finder, `__pycache__` from running a python module,
7
+ // `node_modules` from an `npm install` in the wrong shell, a `~` backup from an
8
+ // editor. None of it is authored, none of it is tracked (it is all gitignored),
9
+ // and all of it is invisible in `git status` — so the first thing that notices
10
+ // is whatever compares two trees.
11
+ //
12
+ // It bit as: a stray `tm/py/utility/__pycache__/make_options.cpython-314.pyc`,
13
+ // created in August by running that module in place, copied verbatim by
14
+ // `target add py` into the add output, and reported by the golden-manifest test
15
+ // as toolchain drift. The copy is the real problem — the test only saw it. Left
16
+ // alone, that `.pyc` ships into every consumer's SDK repo, compiled against
17
+ // whatever python the maintainer happened to have.
18
+ //
19
+ // The rule is one list, in one place, applied by everything that walks a tree:
20
+ // the WRITER (jostraca's Copy, through `cmp.Copy.ignore`, plus the two walks
21
+ // target-add does itself) and every READER that compares against what the
22
+ // writer produced (doctor's drift walk, the feature-source scan, the definition
23
+ // listing). A writer and a reader that disagree about what counts report the
24
+ // whole tree as a fork — the same discipline `templateReplacements` and
25
+ // `aliasCmpName` are held to, and for the same reason.
26
+ //
27
+ // MEMBERSHIP IS A PROMISE that no template will ever legitimately carry that
28
+ // name, so this list holds only what a TOOL creates and a person does not: no
29
+ // `build`, `dist`, `bin` or `out` (a template may well ship one), and
30
+ // emphatically no `target` — sdkgen's own model directory is called that, and
31
+ // it is also where cargo and maven put their output. `test/junk.test.ts` checks
32
+ // the list against every name in the shipped scaffold, so an over-broad pattern
33
+ // fails there rather than by silently dropping a file from someone's SDK.
34
+
35
+ // All of these are matched against the BARE NAME of a directory entry, not its
36
+ // path — the same way jostraca's `cmp.Copy.ignore` is applied, so that naming a
37
+ // directory prunes its whole subtree without every caller having to spell out
38
+ // what is inside it.
39
+
40
+
41
+ // Droppings that are not output of anything: an editor, the Finder, or a
42
+ // version control system left them, and they occupy no space in the meaning of
43
+ // the tree. Kept separate from BUILD because "would we copy this" and "is this
44
+ // directory empty" are different questions, and only this half answers both —
45
+ // a destination holding `node_modules` is not empty by any reading.
46
+ const NOISE: RegExp[] = [
47
+ // Editors and merges. `~` duplicates jostraca's built-in `IGNORED_RE`,
48
+ // deliberately: that one governs its Copy walk alone, and the walks sdkgen
49
+ // does itself (the prune, the feature scan) have to agree with it.
50
+ /~$/, // editor backup
51
+ /^\.#/, // emacs lock link
52
+ /^#.*#$/, // emacs autosave
53
+ /\.sw[a-p]$/, // vim swap
54
+ /\.(bak|orig|rej)$/, // backups, and what a conflicted merge leaves behind
55
+
56
+ // Operating systems.
57
+ /^\.DS_Store$/,
58
+ /^__MACOSX$/,
59
+ /^Thumbs\.db$/i,
60
+ /^desktop\.ini$/i,
61
+
62
+ // Version control. A nested checkout inside a template tree is never the
63
+ // payload, and copying one produces a project git cannot make sense of.
64
+ /^\.git$/,
65
+ /^\.svn$/,
66
+ /^\.hg$/,
67
+ /^\.idea$/, // not `.vscode` — an SDK may ship editor settings
68
+ ]
69
+
70
+
71
+ // Toolchain output, per language. Each of these appears exactly when someone
72
+ // runs that language's tools inside a template tree, which is the normal way to
73
+ // check that a template actually works — and none of them is ever the payload.
74
+ const BUILD: RegExp[] = [
75
+ /^node_modules$/,
76
+ /^__pycache__$/,
77
+ /\.py[co]$/,
78
+ /^\.(pytest|mypy|ruff)_cache$/,
79
+ /^\.tox$/,
80
+ /^\.venv$/,
81
+ /^\.gradle$/,
82
+ /\.class$/,
83
+ /^\.dart_tool$/,
84
+ /^\.?zig-cache$/,
85
+ /^zig-out$/,
86
+ /^_build$/, // mix, dune
87
+ /^\.elixir_ls$/,
88
+ /^\.cpcache$/, // clojure
89
+ /^\.stack-work$/,
90
+ /^dist-newstyle$/, // cabal
91
+ /^\.cargo$/,
92
+ ]
93
+
94
+
95
+ const JUNK: RegExp[] = [...NOISE, ...BUILD]
96
+
97
+
98
+ // Is this directory entry something no tree operation should see?
99
+ function isJunk(name: string): boolean {
100
+ return matches(name, JUNK)
101
+ }
102
+
103
+
104
+ // Weaker: does this entry leave a directory as good as empty? Toolchain output
105
+ // does NOT — it is junk to copy and content to find.
106
+ function isNoise(name: string): boolean {
107
+ return matches(name, NOISE)
108
+ }
109
+
110
+
111
+ function matches(name: string, patterns: RegExp[]): boolean {
112
+ for (const re of patterns) {
113
+ // These are module-level regexes reused across every entry of every tree.
114
+ // None carries `g` or `y` today, but a `test` on a stateful regex resumes
115
+ // from the previous match and would skip every other file — the exact bug
116
+ // jostraca's own `excluded()` carries a comment about.
117
+ re.lastIndex = 0
118
+
119
+ if (re.test(name)) {
120
+ return true
121
+ }
122
+ }
123
+
124
+ return false
125
+ }
126
+
127
+
128
+ // The jostraca `cmp` option block, which is how the WRITER side of the rule is
129
+ // applied: jostraca tests `cmp.Copy.ignore` against the bare name of every
130
+ // entry it walks, directories included, and prunes what matches.
131
+ //
132
+ // Passed PER CALL by each action, not left to the Jostraca instance — the same
133
+ // discipline `control.dryrun` is held to, and for a related reason: the actions
134
+ // take whatever instance their caller hands them (`actx.jostraca`), and a
135
+ // consumer that builds its own would otherwise silently lose the filter. A
136
+ // per-call value is merged last and wins.
137
+ //
138
+ // `/~$/` is first, and must stay first while any supported jostraca is at or
139
+ // below 0.31.0. That merge takes this list over its own default of `[/~$/]`
140
+ // with `deep()`, which recursed INTO index 0 — two RegExps are both objects, so
141
+ // it iterated the incoming one's enumerable properties, of which a RegExp has
142
+ // none, and kept the default. Whatever sat at index 0 was silently discarded.
143
+ // Leading with the value already there makes that a no-op instead of a hole,
144
+ // and jostraca's built-in `IGNORED_RE` covers `~` anyway.
145
+ //
146
+ // FIXED UPSTREAM (jostraca `deep` now replaces a custom-constructor value
147
+ // rather than walking into it), so this is compatibility, not a live
148
+ // workaround: harmless on a fixed jostraca, load-bearing on an older one.
149
+ //
150
+ // A FRESH object each call: `deep` mutates its base, and jostraca's merge order
151
+ // leaves these values reachable from options it keeps.
152
+ function copyOpts(): { Copy: { ignore: RegExp[] } } {
153
+ return {
154
+ Copy: {
155
+ ignore: [/~$/, ...JUNK]
156
+ }
157
+ }
158
+ }
159
+
160
+
161
+ export {
162
+ JUNK,
163
+ NOISE,
164
+ BUILD,
165
+ isJunk,
166
+ isNoise,
167
+ copyOpts,
168
+ }