@voxgig/sdkgen 4.15.0 → 4.17.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 (104) hide show
  1. package/README.md +8 -0
  2. package/bin/voxgig-sdkgen +2 -2
  3. package/dist/action/action.js +2 -2
  4. package/dist/action/action.js.map +1 -1
  5. package/dist/action/check.js +9 -8
  6. package/dist/action/check.js.map +1 -1
  7. package/dist/action/dispatch.js +4 -4
  8. package/dist/action/dispatch.js.map +1 -1
  9. package/dist/action/doctor.js +16 -15
  10. package/dist/action/doctor.js.map +1 -1
  11. package/dist/action/edition.d.ts +4 -0
  12. package/dist/action/{docs.js → edition.js} +57 -54
  13. package/dist/action/edition.js.map +1 -0
  14. package/dist/action/kind.d.ts +1 -2
  15. package/dist/action/kind.js +22 -38
  16. package/dist/action/kind.js.map +1 -1
  17. package/dist/action/package.js +17 -16
  18. package/dist/action/package.js.map +1 -1
  19. package/dist/action/resolve.d.ts +3 -1
  20. package/dist/action/resolve.js +29 -11
  21. package/dist/action/resolve.js.map +1 -1
  22. package/dist/admin/status.d.ts +43 -0
  23. package/dist/admin/status.js +161 -0
  24. package/dist/admin/status.js.map +1 -0
  25. package/dist/cmp/Test.js +11 -0
  26. package/dist/cmp/Test.js.map +1 -1
  27. package/dist/helpers/kindCollection.d.ts +1 -0
  28. package/dist/helpers/kindCollection.js +14 -0
  29. package/dist/helpers/kindCollection.js.map +1 -0
  30. package/dist/helpers/modelcheck.d.ts +1 -1
  31. package/dist/helpers/modelcheck.js +13 -3
  32. package/dist/helpers/modelcheck.js.map +1 -1
  33. package/dist/helpers/shipped.js +2 -3
  34. package/dist/helpers/shipped.js.map +1 -1
  35. package/dist/helpers/testPolicy.d.ts +1 -1
  36. package/dist/helpers/testPolicy.js +6 -37
  37. package/dist/helpers/testPolicy.js.map +1 -1
  38. package/dist/sdkgen.d.ts +7 -1
  39. package/dist/sdkgen.js +146 -89
  40. package/dist/sdkgen.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/utility.js +9 -1
  43. package/dist/utility.js.map +1 -1
  44. package/model/sdkgen.aon +5 -52
  45. package/package.json +1 -1
  46. package/project/.sdk/src/cmp/go/Config_go.ts +4 -4
  47. package/project/.sdk/src/cmp/go/utility_go.ts +10 -2
  48. package/project/.sdk/src/cmp/js/Package_js.ts +5 -1
  49. package/project/.sdk/src/cmp/js/TestDirect_js.ts +11 -7
  50. package/project/.sdk/src/cmp/js/TestEntity_js.ts +16 -2
  51. package/project/.sdk/src/cmp/js/TestLive_js.ts +32 -0
  52. package/project/.sdk/src/cmp/js/Test_js.ts +2 -0
  53. package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +1 -1
  54. package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +4 -2
  55. package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +30 -9
  56. package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +8 -3
  57. package/project/.sdk/src/cmp/php/ReadmeQuick_php.ts +5 -4
  58. package/project/.sdk/src/cmp/php/ReadmeTopQuick_php.ts +2 -2
  59. package/project/.sdk/src/cmp/ts/Package_ts.ts +5 -1
  60. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +5 -0
  61. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +15 -11
  62. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +13 -16
  63. package/project/.sdk/src/cmp/ts/TestLive_ts.ts +32 -0
  64. package/project/.sdk/src/cmp/ts/Test_ts.ts +2 -0
  65. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +1 -0
  66. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +2 -0
  67. package/project/.sdk/tm/csharp/test/feature/secrets/SecretsFeatureTest.cs +9 -2
  68. package/project/.sdk/tm/js/test/live-contract.js +183 -0
  69. package/project/.sdk/tm/js/test/live-entity.js +169 -0
  70. package/project/.sdk/tm/js/test/live-runner.js +129 -0
  71. package/project/.sdk/tm/js/test/live-scenarios.js +142 -0
  72. package/project/.sdk/tm/js/test/utility.js +84 -0
  73. package/project/.sdk/tm/ts/test/live-contract.ts +104 -0
  74. package/project/.sdk/tm/ts/test/live-entity.ts +142 -0
  75. package/project/.sdk/tm/ts/test/live-runner.ts +166 -0
  76. package/project/.sdk/tm/ts/test/live-scenarios.ts +112 -0
  77. package/project/.sdk/tm/ts/test/utility.ts +1 -2
  78. package/project/sdkgen-package.json +1 -1
  79. package/src/action/action.ts +2 -2
  80. package/src/action/check.ts +3 -2
  81. package/src/action/dispatch.ts +4 -4
  82. package/src/action/doctor.ts +10 -9
  83. package/src/action/{docs.ts → edition.ts} +56 -53
  84. package/src/action/kind.ts +12 -33
  85. package/src/action/package.ts +4 -3
  86. package/src/action/resolve.ts +26 -2
  87. package/src/admin/status.ts +124 -0
  88. package/src/cmp/Test.ts +13 -1
  89. package/src/helpers/kindCollection.ts +11 -0
  90. package/src/helpers/modelcheck.ts +10 -3
  91. package/src/helpers/shipped.ts +2 -3
  92. package/src/helpers/testPolicy.ts +7 -45
  93. package/src/sdkgen.ts +189 -93
  94. package/src/utility.ts +8 -2
  95. package/dist/action/docs.d.ts +0 -4
  96. package/dist/action/docs.js.map +0 -1
  97. package/dist/cmp/Docs.d.ts +0 -4
  98. package/dist/cmp/Docs.js +0 -98
  99. package/dist/cmp/Docs.js.map +0 -1
  100. package/dist/cmp/ExternalDocs.d.ts +0 -2
  101. package/dist/cmp/ExternalDocs.js +0 -43
  102. package/dist/cmp/ExternalDocs.js.map +0 -1
  103. package/src/cmp/Docs.ts +0 -125
  104. package/src/cmp/ExternalDocs.ts +0 -54
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert/strict'
2
+ import { LiveBlocked, runLiveSteps, assertLiveReport, createLiveTransport } from './live-runner'
3
+ import { requestContract, synthesizeInput, validateContract } from './live-contract'
4
+ import { liveClientOptions, liveDelayMs, isControlSkipped } from './utility'
5
+
6
+ const at = (value: any, path?: string): any => (path || '').split('.').filter(Boolean).reduce((v, k) => v?.[k], value)
7
+ export function resolveRecipe(recipe: any, values: Map<string, any>): any {
8
+ if (Array.isArray(recipe)) return recipe.map(v => resolveRecipe(v, values))
9
+ if (recipe && typeof recipe === 'object') {
10
+ if (recipe.from) {
11
+ if (!values.has(recipe.from)) throw new LiveBlocked('Missing recipe output: ' + recipe.from)
12
+ let value = values.get(recipe.from)
13
+ if (recipe.where) {
14
+ if (!Array.isArray(value)) throw new LiveBlocked('Discovery output is not a list')
15
+ value = value.find((item: any) => Object.entries(recipe.where).every(([k, v]) => at(item, k) === v) &&
16
+ (!recipe.related || (values.get(recipe.related.from) || []).some((other: any) =>
17
+ at(item, recipe.related.local) === at(other, recipe.related.foreign) &&
18
+ Object.entries(recipe.related.where || {}).every(([k, v]) => at(other, k) === v))))
19
+ }
20
+ value = at(value, recipe.path)
21
+ if (value === undefined || value === null) throw new LiveBlocked('Discovery found no compatible value')
22
+ return value
23
+ }
24
+ return Object.fromEntries(Object.entries(recipe).map(([k, v]) => [k, resolveRecipe(v, values)]))
25
+ }
26
+ return recipe
27
+ }
28
+
29
+ export function recipeNeeds(value: any): string[] {
30
+ if (!value || typeof value !== 'object') return []
31
+ return [...new Set([...(typeof value.from === 'string' ? [value.from] : []), ...Object.values(value).flatMap(recipeNeeds)])]
32
+ }
33
+
34
+ export async function runLiveScenarios(SDK: any, plan: any[], envPrefix: string, liveDefaults: any = {}) {
35
+ const transport = createLiveTransport()
36
+ const steps = plan.map(point => {
37
+ const hint = point.facts.live || {}
38
+ const control = isControlSkipped('entityOp', point.entity + '.' + point.op, 'live')
39
+ return { role: hint.auth || (point.facts.security?.length === 0 || point.facts.securitySource === 'unspecified' ? 'public' : 'account'), retention: hint.retention, id: hint.id || point.id, needs: [...new Set([...(hint.needs || []), ...recipeNeeds(hint.input), ...recipeNeeds(hint.credential), ...recipeNeeds(hint.assert)])] as string[],
40
+ cleanup: !!hint.cleanup,
41
+ excluded: control.skip ? control.reason || 'Excluded by test control' : hint.excluded,
42
+ run: async (ctx: any) => {
43
+ transport.enter(ctx)
44
+ if (point.contractVersion && point.contractVersion !== 1) throw new LiveBlocked('Unsupported operation contract version')
45
+ if (point.op === 'remove' || hint.cleanup) {
46
+ const owned = recipeNeeds(hint.input).some(id => plan.some(source =>
47
+ (source.facts.live?.id || source.id) === id && source.entity === point.entity && source.op === 'create'))
48
+ if (!owned) throw new LiveBlocked('Cleanup input is not bound to a resource created by this run')
49
+ }
50
+ if (!point.reachable) throw new LiveBlocked('Point selector is indistinguishable; add a guide action')
51
+ const request = requestContract(point.facts)
52
+ const explicit = hint.input === undefined ? request.example : resolveRecipe(hint.input, ctx.values)
53
+ let input = request.schema ? synthesizeInput(request.schema, explicit) : explicit ?? {}
54
+ for (const kind of ['params', 'query', 'header', 'cookie']) for (const arg of point.args?.[kind] || []) {
55
+ if (arg.reqd && input[arg.name] === undefined) {
56
+ if (arg.example === undefined) throw new LiveBlocked('Missing required argument: ' + arg.name)
57
+ input[arg.name] = arg.example
58
+ }
59
+ }
60
+ const role = hint.auth || (point.facts.security?.length === 0 || point.facts.securitySource === 'unspecified' ? 'public' : 'account')
61
+ let apikey = role === 'public' ? null : role === 'issued' ? resolveRecipe(hint.credential, ctx.values) : process.env[envPrefix + '_APIKEY']
62
+ if (role === 'issued' && (typeof apikey !== 'string' || !apikey)) throw new LiveBlocked('Issued credential unavailable')
63
+ let wire: any
64
+ let wireSchema: any
65
+ const options = liveClientOptions()
66
+ const client = new SDK({ ...options, ...liveDefaults, apikey,
67
+ feature: { ...options.feature, test: { active: false }, retry: { active: false },
68
+ secrets: { ...options.feature?.secrets, active: role === 'account' && (!apikey || options.feature?.secrets?.active === true) } },
69
+ system: { ...options.system, fetch: async (url: any, init: any) => {
70
+ const headers = new Headers(init?.headers)
71
+ if (role === 'public') assert(!headers.has('authorization'), 'Public request carries authentication')
72
+ else if (!headers.get('authorization')) throw new LiveBlocked('Credential unavailable for ' + role + ' request')
73
+ const response = await transport.fetch(url, { ...init, redirect: 'error' })
74
+ const expectedPath = point.path.replace(/\{([^}]+)\}/g, (_: string, name: string) => encodeURIComponent(input[point.rename?.param?.[name] || name] ?? input[name]))
75
+ if (point.kind === 'graphql') {
76
+ const payload = JSON.parse(init.body)
77
+ assert.equal(payload.query, point.graphql.doc, 'Entity selected wrong GraphQL operation')
78
+ } else assert.equal(new URL(String(url)).pathname, expectedPath, 'Entity selected wrong route')
79
+ assert.equal(init?.method || 'GET', point.method)
80
+ assert(response.status >= 200 && response.status < 300, 'Unsuccessful HTTP response')
81
+ if (point.kind === 'graphql') wire = await response.clone().json()
82
+ const def = point.facts.responses?.[response.status] || point.facts.responses?.[String(response.status)[0] + 'XX'] || point.facts.responses?.default
83
+ if (point.facts.responses && !def) throw new Error('Undeclared response status')
84
+ if (def) {
85
+ const schema = def.content?.['application/json']?.schema ?? def.schema
86
+ if (schema && response.status !== 204) { wireSchema = schema; wire = await response.clone().json() }
87
+ }
88
+ return response
89
+ } } })
90
+ if (point.action) input = { ...input, $action: point.action }
91
+ assert(JSON.stringify(input).length <= 1024 * 1024, 'Request exceeds test payload limit')
92
+ const result = await client[point.accessor]()[point.op](input)
93
+ const data = Array.isArray(result) ? result.map(item => { assert.equal(typeof item?.data, 'function'); return item.data() }) : (assert.equal(typeof result?.data, 'function'), result.data())
94
+ ctx.publish(data)
95
+ if (wireSchema) validateContract(wireSchema, wire, 'response')
96
+ if (Array.isArray(wire?.errors) && wire.errors.length) throw new Error('GraphQL operation reported errors')
97
+ if (wire?.success === false) throw new Error('API reported unsuccessful operation')
98
+ const checks = resolveRecipe(hint.assert || {}, ctx.values)
99
+ for (const [path, expected] of Object.entries(checks.equal || {})) assert.deepEqual(at(data, path), expected)
100
+ for (const path of checks.nonempty || []) assert(at(data, path)?.length > 0, 'Empty required output')
101
+ if (checks.vectors) {
102
+ const vectors = at(data, checks.vectors.path)
103
+ assert(Array.isArray(vectors) && vectors.length === checks.vectors.count)
104
+ for (const vector of vectors) assert(Array.isArray(vector) && vector.length === checks.vectors.dimension && vector.every((n: any) => typeof n === 'number' && Number.isFinite(n)), 'Invalid vector shape')
105
+ }
106
+ } }
107
+ })
108
+ const report = await runLiveSteps(steps, { delayMs: liveDelayMs(), report: result => console.log('LIVE STEP ' + JSON.stringify(result)) })
109
+ console.log('LIVE SUMMARY ' + JSON.stringify(report))
110
+ assertLiveReport(report)
111
+ return report
112
+ }
@@ -161,8 +161,7 @@ function skipIfMissingIds(t: any, setup: any, requiredKeys: string[]): boolean {
161
161
  if (!setup.live) return false
162
162
  const missing = requiredKeys.filter(k => null == setup.idmap?.[k])
163
163
  if (missing.length > 0) {
164
- t.skip(`live test needs ${missing.join(', ')} via *_ENTID env var (synthetic IDs only)`)
165
- return true
164
+ throw new Error(`Live test blocked: needs ${missing.join(', ')} via *_ENTID env var`)
166
165
  }
167
166
  return false
168
167
  }
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.15.0",
6
+ "version": "4.17.0",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",
@@ -102,7 +102,7 @@ function parseAddNames(args: any[]): string[] {
102
102
  // `seed` IS THE UPGRADE PATH. A project scaffolded before a kind existed has
103
103
  // no `model/<kind>/<kind>-index.aon` — every project alive today is in
104
104
  // exactly that position for `docs` — and reading it unguarded made the FIRST
105
- // `docs add` in any existing project fail on ENOENT before it wrote anything.
105
+ // `edition add` in any existing project fail on ENOENT before it wrote anything.
106
106
  //
107
107
  // Seeded per call rather than defaulted for every kind: a missing
108
108
  // `target-index.aon` in a scaffolded project is a broken project, and
@@ -136,7 +136,7 @@ function loadContent(
136
136
  // indexes of the kinds that existed then. So a project scaffolded before a
137
137
  // kind existed — which is every project alive today, for `docs` — never
138
138
  // includes its index, and the item's model file is an orphan: it is on disk,
139
- // `<kind>-index.aon` includes it, and NOTHING includes that. `main.kit.docs`
139
+ // `<kind>-index.aon` includes it, and NOTHING includes that. `main.kit.doc.edition`
140
140
  // is then absent from the compiled model, so `package list`, `package update`
141
141
  // and `doctor` cannot see the item at all.
142
142
  //
@@ -1,3 +1,4 @@
1
+ import { kindCollection } from '../helpers/kindCollection'
1
2
  // `package check` — the AUTHOR-side battery. Design §14.
2
3
  //
3
4
  // WHAT IT IS FOR
@@ -308,11 +309,11 @@ function checkDefinition(
308
309
  // `main: kit: target: go:` installs an item the consumer's model never
309
310
  // sees — the exact mistake a package author makes copying a bundled
310
311
  // target as a starting point.
311
- const declared = strict.model?.main?.[KIT]?.[kind]?.[name]
312
+ const declared = kindCollection(strict.model, kind)?.[name]
312
313
 
313
314
  if (null == declared || 'object' !== typeof declared) {
314
315
  found.push(at('error', 'model-key-missing',
315
- 'declares no `main: kit: ' + kind + ': ' + name + ':` block — the file ' +
316
+ 'declares no `main: kit: ' + (kind === 'edition' ? 'doc: edition' : kind) + ': ' + name + ':` block — the file ' +
316
317
  'is installed and included under its own name, so nothing it declares ' +
317
318
  'under another name is reachable'))
318
319
  }
@@ -1,7 +1,7 @@
1
1
  // WHAT `voxgig-sdkgen <action> …` CAN BE.
2
2
  //
3
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
4
+ // is the only edit a new kind needs — `edition add …` costs no dispatch code
5
5
  // (docs/design/sdkgen-packages.md §9). The `package` and `doctor` verbs are
6
6
  // not kinds and are added beside it.
7
7
  //
@@ -25,7 +25,7 @@ import { KINDS } from './kind'
25
25
 
26
26
  import { action_target, target_add } from './target'
27
27
  import { action_feature, feature_add } from './feature'
28
- import { action_docs, docs_add } from './docs'
28
+ import { action_edition, edition_add } from './edition'
29
29
  import { action_doctor } from './doctor'
30
30
  import { action_package, registerAdder } from './package'
31
31
 
@@ -41,7 +41,7 @@ const KIND_ACTIONS: Record<string, ActionFunc> = Object.assign(
41
41
  Object.create(null), {
42
42
  target: action_target,
43
43
  feature: action_feature,
44
- docs: action_docs,
44
+ edition: action_edition,
45
45
  })
46
46
 
47
47
 
@@ -49,7 +49,7 @@ const KIND_ACTIONS: Record<string, ActionFunc> = Object.assign(
49
49
  // over. Registered rather than imported by `package.ts`, for the cycle above.
50
50
  registerAdder('target', target_add)
51
51
  registerAdder('feature', feature_add)
52
- registerAdder('docs', docs_add)
52
+ registerAdder('edition', edition_add)
53
53
 
54
54
 
55
55
  function actionMap(): Record<string, ActionFunc> {
@@ -1,3 +1,4 @@
1
+ import { kindCollection } from '../helpers/kindCollection'
1
2
  // `voxgig-sdkgen doctor` — does this project's `.sdk/` still match the
2
3
  // scaffold?
3
4
  //
@@ -276,7 +277,7 @@ async function doctor(
276
277
 
277
278
  const counts: Record<string, number> = {}
278
279
  for (const kind of kinds) {
279
- counts[kind] = Object.keys((model as any)?.main?.[KIT]?.[kind] ?? {}).length
280
+ counts[kind] = Object.keys(kindCollection(model, kind) ?? {}).length
280
281
  }
281
282
 
282
283
  log.info({ point: 'doctor-start', targets: counts.target ?? 0, ...counts })
@@ -303,7 +304,7 @@ async function doctor(
303
304
  }
304
305
 
305
306
  for (const kind of kinds) {
306
- const items = Object.keys((model as any)?.main?.[KIT]?.[kind] ?? {}).sort()
307
+ const items = Object.keys(kindCollection(model, kind) ?? {}).sort()
307
308
 
308
309
  for (const name of items) {
309
310
  if (null != scope && !scope(kind, name)) {
@@ -321,8 +322,8 @@ async function doctor(
321
322
  checkTarget(actx, source, report)
322
323
  }
323
324
 
324
- if ('docs' === kind) {
325
- checkDocs(actx, source, report)
325
+ if ('edition' === kind) {
326
+ checkEdition(actx, source, report)
326
327
  }
327
328
 
328
329
  // Only an ACTIVE feature has source copied out; what an inactive one
@@ -433,7 +434,7 @@ function checkWiring(actx: ActionContext, report: DoctorReport) {
433
434
  function resolveDeclared(
434
435
  kind: string, name: string, actx: ActionContext,
435
436
  ): Source | undefined {
436
- const declared: any = (actx.model as any)?.main?.[KIT]?.[kind]?.[name]
437
+ const declared: any = kindCollection(actx.model, kind)?.[name]
437
438
  const ref = recordedRef(declared, name) || name
438
439
 
439
440
  try {
@@ -659,8 +660,8 @@ function compareTrees(
659
660
  // rather than being spelled a second time.
660
661
  //
661
662
  // The optional template tree is skipped when the SOURCE does not ship one:
662
- // `docs add` did not copy it, so the project is right not to have it.
663
- function checkDocs(
663
+ // `edition add` did not copy it, so the project is right not to have it.
664
+ function checkEdition(
664
665
  actx: ActionContext, resolved: Source, report: DoctorReport,
665
666
  ) {
666
667
  const fs = actx.fs()
@@ -669,8 +670,8 @@ function checkDocs(
669
670
  const origname = resolved.origname
670
671
  const aliased = name !== origname
671
672
 
672
- const dest = kindTrees('docs', name)
673
- const from = kindTrees('docs', origname)
673
+ const dest = kindTrees('edition', name)
674
+ const from = kindTrees('edition', origname)
674
675
 
675
676
  const trees: TreeCompare[] = dest.flatMap((tree: TreeDef, i: number) => {
676
677
  const scaffold = Path.join(resolved.folder, ...from[i].path.split('/'))
@@ -1,4 +1,5 @@
1
- // `docs add <ref>` the third kind. See docs/design/sdkgen-packages.md §20.
1
+ import { kindCollection } from '../helpers/kindCollection'
2
+ // `edition add <ref>` — the third kind. See edition/design/sdkgen-packages.md §20.
2
3
  //
3
4
  // WHAT A DOCS ITEM IS
4
5
  //
@@ -12,11 +13,11 @@
12
13
  //
13
14
  // Three reasons, argued in §20.2 and worth restating where the code is:
14
15
  //
15
- // - a docs item's INPUT is the target collection — a page per SDK, the
16
- // package table, per-language tabs — so a docs item inside
16
+ // - a edition item's INPUT is the target collection — a page per SDK, the
17
+ // package table, per-language tabs — so a edition item inside
17
18
  // `main.kit.target` would enumerate itself;
18
19
  // - `action/feature.ts` fans out with `each(target, …)` and warns
19
- // `feature-source-missing` per target with no source, so a docs item in
20
+ // `feature-source-missing` per target with no source, so a edition item in
20
21
  // that collection would collect one warning per feature, forever
21
22
  // (`srcfeature: false` does not help — that flag is read at generate time
22
23
  // and never by the add-time fan-out);
@@ -30,7 +31,7 @@
30
31
  // aliasing and the index come along unchanged — and the kind's trees, whose
31
32
  // paths come from the registry rather than being spelled here.
32
33
  //
33
- // It does NOT fan out over targets, trim, or prune. A docs item reads the
34
+ // It does NOT fan out over targets, trim, or prune. A edition item reads the
34
35
  // target collection at GENERATE time, not at add time: the opposite direction
35
36
  // and the opposite moment from a feature's fan-out, so sharing that machinery
36
37
  // would have been a false economy.
@@ -58,26 +59,26 @@ import { aliasCmpTree, pruneStaleTemplates } from './target'
58
59
  import { parseAddNames, loadContent, ensureModelInclude } from './action'
59
60
 
60
61
 
61
- // The PREFIX of a docs item's component tree (`src/cmp/docs/`), for the
62
+ // The PREFIX of a edition item's component tree (`src/cmp/edition/`), for the
62
63
  // alias rewrite. Derived from the registry's declaration rather than written
63
- // out a second time, so the two cannot disagree about where docs components
64
+ // out a second time, so the two cannot disagree about where edition components
64
65
  // live.
65
66
  const NAME_MARK = '\u0001name\u0001'
66
67
 
67
68
  function cmpBase(): string {
68
- const cmp = kindTrees('docs', NAME_MARK)
69
+ const cmp = kindTrees('edition', NAME_MARK)
69
70
  .find((t: TreeDef) => 'none' === t.replace)
70
71
 
71
- return null == cmp ? 'src/cmp/docs/' : cmp.path.split(NAME_MARK)[0]
72
+ return null == cmp ? 'src/cmp/edition/' : cmp.path.split(NAME_MARK)[0]
72
73
  }
73
74
 
74
75
 
75
76
  const CMD_MAP: any = Object.assign(Object.create(null), {
76
- add: cmd_docs_add,
77
+ add: cmd_edition_add,
77
78
  })
78
79
 
79
80
 
80
- async function action_docs(
81
+ async function action_edition(
81
82
  args: string[], actx: ActionContext,
82
83
  ): Promise<ActionResult> {
83
84
  const cmdname = args[1]
@@ -85,7 +86,7 @@ async function action_docs(
85
86
 
86
87
  if (null == cmd) {
87
88
  throw new SdkGenError(
88
- 'Unknown docs cmd: ' + cmdname + ' (expected: ' +
89
+ 'Unknown edition cmd: ' + cmdname + ' (expected: ' +
89
90
  Object.keys(CMD_MAP).sort().join(', ') + ')')
90
91
  }
91
92
 
@@ -93,17 +94,19 @@ async function action_docs(
93
94
  }
94
95
 
95
96
 
96
- async function cmd_docs_add(
97
+ async function cmd_edition_add(
97
98
  args: string[], actx: ActionContext,
98
99
  ): Promise<ActionResult> {
99
- return docs_add(parseAddNames(args), actx)
100
+ return edition_add(parseAddNames(args), actx)
100
101
  }
101
102
 
102
103
 
103
104
  // Code API.
104
- async function docs_add(
105
- docs: string[], actx: ActionContext,
105
+ async function edition_add(
106
+ edition: string[], actx: ActionContext,
106
107
  ): Promise<ActionResult> {
108
+ edition = edition.map(ref => !ref.includes('/') && !ref.includes('\\') &&
109
+ !kindCollection(actx.model, 'edition')[ref.split('~')[0]] ? '@voxgig/docgen/project/' + ref : ref)
107
110
  const jostraca = actx.jostraca
108
111
 
109
112
  const opts = {
@@ -112,9 +115,9 @@ async function docs_add(
112
115
  log: actx.log.child({ cmp: 'jostraca' }),
113
116
  meta: {
114
117
  url: actx.url,
115
- // Seeded: no project scaffolded before the docs kind existed has a
116
- // docs index, and every project alive today is in that position.
117
- content: loadContent(actx, 'docs', { docs: '# Docs\n' }),
118
+ // Seeded: no project scaffolded before the edition kind existed has a
119
+ // edition index, and every project alive today is in that position.
120
+ content: loadContent(actx, 'edition', { edition: '# Docs\n' }),
118
121
  },
119
122
  model: actx.model,
120
123
  // Per-call, never left to the Jostraca instance: `generate` runs its own
@@ -130,7 +133,7 @@ async function docs_add(
130
133
  }
131
134
 
132
135
  opts.log.info({
133
- point: 'docs-start',
136
+ point: 'edition-start',
134
137
  note: (actx.opts.dryrun ? '** DRY RUN **' : '')
135
138
  })
136
139
 
@@ -138,26 +141,26 @@ async function docs_add(
138
141
  //
139
142
  // The write pass emits the definition and its index entry before it copies
140
143
  // the trees, so a ref whose definition exists but whose required components
141
- // do not left the command failed AND the project carrying a docs item with
144
+ // do not left the command failed AND the project carrying a edition item with
142
145
  // no implementation — which the next model compile then reads as real.
143
146
  // `package add` already validates a whole package up front for exactly this
144
- // reason; a direct `docs add` needs the same guarantee.
145
- preflight(docs, actx)
147
+ // reason; a direct `edition add` needs the same guarantee.
148
+ preflight(edition, actx)
146
149
 
147
- // The project's own model must INCLUDE the docs index, or everything below
150
+ // The project's own model must INCLUDE the edition index, or everything below
148
151
  // is invisible: no project scaffolded before this kind existed includes it,
149
- // and `main.kit.docs` would simply be absent from the next compile.
150
- ensureModelInclude(actx, 'docs')
152
+ // and `main.kit.edition` would simply be absent from the next compile.
153
+ ensureModelInclude(actx, 'edition')
151
154
 
152
155
  // Into the IN-MEMORY model before anything reads it. Nothing recompiles
153
- // `model/sdk.aontu` mid-process, so without this a second docs item in the
156
+ // `model/sdk.aontu` mid-process, so without this a second edition item in the
154
157
  // same command — and anything else later in it — behaves as if the first
155
158
  // was never installed. One definition of what gets recorded, shared with
156
159
  // `target add` and `package add`.
157
- registerInstalled('docs', docs, actx)
160
+ registerInstalled('edition', edition, actx)
158
161
 
159
162
  const jres = await jostraca.generate(opts, () =>
160
- DocsRoot({ docs, actx }))
163
+ EditionRoot({ edition, actx }))
161
164
 
162
165
  return { jres }
163
166
  }
@@ -166,13 +169,13 @@ async function docs_add(
166
169
  // Resolve every ref and check every REQUIRED tree, throwing before anything
167
170
  // is written. Resolution itself is the other half: a ref that names no
168
171
  // definition fails here rather than partway through the pass.
169
- function preflight(docs: string[], actx: ActionContext) {
172
+ function preflight(edition: string[], actx: ActionContext) {
170
173
  const fs = actx.fs()
171
174
 
172
- for (const ref of docs) {
173
- const source = resolveKind(ref, 'docs', actx as any)
175
+ for (const ref of edition) {
176
+ const source = resolveKind(ref, 'edition', actx as any)
174
177
 
175
- for (const tree of kindTrees('docs', source.origname)) {
178
+ for (const tree of kindTrees('edition', source.origname)) {
176
179
  if (!tree.required) {
177
180
  continue
178
181
  }
@@ -182,7 +185,7 @@ function preflight(docs: string[], actx: ActionContext) {
182
185
  if (!fs.existsSync(from)) {
183
186
  throw new SdkGenError(
184
187
  'Docs ' + source.name + ': required tree not found: ' + from +
185
- '\n a docs item needs its components (' + tree.path +
188
+ '\n a edition item needs its components (' + tree.path +
186
189
  '); nothing has been written')
187
190
  }
188
191
  }
@@ -190,8 +193,8 @@ function preflight(docs: string[], actx: ActionContext) {
190
193
  }
191
194
 
192
195
 
193
- const DocsRoot = cmp(function DocsRoot(props: any) {
194
- const { ctx$, docs } = props
196
+ const EditionRoot = cmp(function EditionRoot(props: any) {
197
+ const { ctx$, edition } = props
195
198
  const { log } = ctx$
196
199
 
197
200
  Project({}, () => {
@@ -200,29 +203,29 @@ const DocsRoot = cmp(function DocsRoot(props: any) {
200
203
  // carry all of them.
201
204
  const dnames: string[] = []
202
205
 
203
- each(docs, (n: any) => {
206
+ each(edition, (n: any) => {
204
207
  const dref = n.val$
205
208
 
206
- log.info({ point: 'docs-build', docs: dref, note: dref })
209
+ log.info({ point: 'edition-build', edition: dref, note: dref })
207
210
 
208
211
  // The shared spine: a BARE name resolves against what the model
209
- // RECORDS, so a docs item installed from a package resolves back to
212
+ // RECORDS, so a edition item installed from a package resolves back to
210
213
  // that package on its next add rather than to the bundled scaffold.
211
- const source = resolveKind(dref, 'docs', ctx$)
214
+ const source = resolveKind(dref, 'edition', ctx$)
212
215
 
213
216
  dnames.push(source.name)
214
217
 
215
218
  log.info({
216
- point: 'docs-name', docs: source.name, folder: source.folder, ref: dref,
219
+ point: 'edition-name', edition: source.name, folder: source.folder, ref: dref,
217
220
  note: source.name +
218
221
  (source.name !== source.origname ?
219
222
  ' (from ' + source.origname + ')' : '') +
220
223
  ' from:' + source.folder
221
224
  })
222
225
 
223
- Folder({ name: 'model/docs' }, () => kindModel({
224
- ctx$, kind: 'docs', source, names: dnames,
225
- content: ctx$.meta.content.docs_index,
226
+ Folder({ name: 'model/edition' }, () => kindModel({
227
+ ctx$, kind: 'edition', source, names: dnames,
228
+ content: ctx$.meta.content.edition_index,
226
229
  }))
227
230
 
228
231
  // Both ends of every tree come from the registry's ONE declaration,
@@ -230,8 +233,8 @@ const DocsRoot = cmp(function DocsRoot(props: any) {
230
233
  // the installed one. Deriving the source path by substituting inside
231
234
  // the destination path would corrupt any item whose name also appears
232
235
  // in the fixed part of the path.
233
- const dest = kindTrees('docs', source.name)
234
- const from = kindTrees('docs', source.origname)
236
+ const dest = kindTrees('edition', source.name)
237
+ const from = kindTrees('edition', source.origname)
235
238
 
236
239
  dest.forEach((tree: TreeDef, i: number) => {
237
240
  // Copy only ADDS and overwrites. A newer version of the package that
@@ -252,7 +255,7 @@ const DocsRoot = cmp(function DocsRoot(props: any) {
252
255
  copyTree(ctx$, source, tree, from[i].path)
253
256
  })
254
257
 
255
- log.info({ point: 'docs-done', docs: source.name, note: source.name })
258
+ log.info({ point: 'edition-done', edition: source.name, note: source.name })
256
259
  })
257
260
  })
258
261
  })
@@ -261,7 +264,7 @@ const DocsRoot = cmp(function DocsRoot(props: any) {
261
264
  // One tree, copied from the origin path to the installed one.
262
265
  //
263
266
  // An optional tree the source does not ship is simply not copied — that is
264
- // what `required: false` means, and a docs item whose every byte is generated
267
+ // what `required: false` means, and a edition item whose every byte is generated
265
268
  // legitimately has no template tree.
266
269
  function copyTree(ctx$: any, source: any, tree: TreeDef, frompath: string) {
267
270
  const fs = ctx$.fs()
@@ -274,7 +277,7 @@ function copyTree(ctx$: any, source: any, tree: TreeDef, frompath: string) {
274
277
  }
275
278
 
276
279
  ctx$.log.info({
277
- point: 'docs-tree-absent', docs: source.name, tree: tree.path, from,
280
+ point: 'edition-tree-absent', edition: source.name, tree: tree.path, from,
278
281
  note: source.name + ': the source ships no ' + frompath +
279
282
  ', nothing to copy'
280
283
  })
@@ -282,8 +285,8 @@ function copyTree(ctx$: any, source: any, tree: TreeDef, frompath: string) {
282
285
  }
283
286
 
284
287
  // An ALIASED component tree cannot be copied verbatim: components are
285
- // dispatched by the convention `cmp/docs/<n>/Main_<n>`, so files keeping
286
- // the origin suffix resolve nothing — `Main_apidocs.ts` is invisible to a
288
+ // dispatched by the convention `cmp/edition/<n>/Main_<n>`, so files keeping
289
+ // the origin suffix resolve nothing — `Main_summary.ts` is invisible to a
287
290
  // lookup for `Main_portal`. Same rule as a target's, so the same function
288
291
  // does it; jostraca's tree Copy has no per-entry rename hook, which is why
289
292
  // an aliased tree is emitted file by file.
@@ -305,6 +308,6 @@ function copyTree(ctx$: any, source: any, tree: TreeDef, frompath: string) {
305
308
 
306
309
 
307
310
  export {
308
- action_docs,
309
- docs_add,
311
+ action_edition,
312
+ edition_add,
310
313
  }
@@ -1,3 +1,4 @@
1
+ import { kindCollection } from '../helpers/kindCollection'
1
2
  // KINDS: the things an `add` can install.
2
3
  //
3
4
  // `target` and `feature` are two of them, `docs` and others are meant to
@@ -20,6 +21,7 @@
20
21
  // are genuinely different work, not the same work with different strings, and
21
22
  // pretending otherwise would buy generality nobody can use.
22
23
 
24
+
23
25
  import Path from 'node:path'
24
26
 
25
27
  import { File, Copy, Content, template } from 'jostraca'
@@ -30,7 +32,7 @@ import { SdkGenError } from '../utility'
30
32
 
31
33
  import { provenanceReplace } from '../helpers/stdrep'
32
34
 
33
- import { resolveSource } from './resolve'
35
+ import { resolveSource, recordedRef, isBare } from './resolve'
34
36
  import type { Source } from './resolve'
35
37
 
36
38
  import { UpdateIndex } from './action'
@@ -92,7 +94,7 @@ type TreeDef = {
92
94
 
93
95
  // Rewrite the ITEM KEY in a copied model file, for an aliased install.
94
96
  //
95
- // Parameterised by kind because `main: kit: docs: <n>:` is the same rewrite
97
+ // Parameterised by kind because `main: kit: doc: edition: <n>:` is the same rewrite
96
98
  // with a different word, and a second copy of this regex is exactly the
97
99
  // same-rule-written-twice defect the registry exists to prevent.
98
100
  //
@@ -156,21 +158,21 @@ const KINDS: Record<string, KindDef> = Object.assign(Object.create(null), {
156
158
 
157
159
  // DOCS — the third kind. See docs/design/sdkgen-packages.md §20.
158
160
  //
159
- // Its trees are NESTED under the kind name (`src/cmp/docs/<n>`, not
161
+ // Its trees are NESTED under the kind name (`src/cmp/edition/<n>`, not
160
162
  // `src/cmp/<n>`) so a docs item and a target may share a name without
161
163
  // sharing a directory. Everything that composes those paths takes them
162
164
  // from here.
163
165
  //
164
- // `tm/docs/{name}` is NOT required: a docs item whose every emitted byte
166
+ // `tm/edition/{name}` is NOT required: a docs item whose every emitted byte
165
167
  // depends on the API — a catalogue entry, a config file — legitimately
166
168
  // ships no template tree, while a static site needs one. So it is
167
169
  // copy-if-present, and `package check` does not demand it.
168
- docs: {
169
- name: 'docs', alias: true, ownedWhenAliased: true,
170
- rename: aliasModelKey('docs'),
170
+ edition: {
171
+ name: 'edition', alias: true, ownedWhenAliased: true,
172
+ rename: aliasModelKey('edition'),
171
173
  trees: [
172
- { path: 'src/cmp/docs/{name}', replace: 'none', required: true },
173
- { path: 'tm/docs/{name}', replace: 'template', required: false },
174
+ { path: 'src/cmp/edition/{name}', replace: 'none', required: true },
175
+ { path: 'tm/edition/{name}', replace: 'template', required: false },
174
176
  ],
175
177
  },
176
178
  })
@@ -208,12 +210,7 @@ function kindDef(kind: string): KindDef {
208
210
  // source.
209
211
  function resolveKind(ref: string, kind: string, ctx$: any): Source {
210
212
  const def = kindDef(kind)
211
- const model = ctx$.model
212
-
213
- const declared: any = model?.main?.[KIT]?.[kind]?.[ref]
214
- const recorded = (isBare(ref) && recordedRef(declared, ref)) || ref
215
-
216
- const source = resolveSource(recorded, kind, ctx$)
213
+ const source = resolveSource(ref, kind, ctx$)
217
214
 
218
215
  // Asked of the RESOLVER rather than by re-reading the ref. `~` separates an
219
216
  // alias only in the last segment, and a check that looked for one anywhere
@@ -334,24 +331,6 @@ function kindModel(props: {
334
331
  // ONE definition, used by the add actions and by doctor. This reconstruction
335
332
  // was written twice and the two copies had already diverged on exactly this
336
333
  // point, which is the drift the kind spine exists to end.
337
- function recordedRef(declared: any, name: string): string | undefined {
338
- if (null == declared?.base || '' === declared.base) {
339
- return undefined
340
- }
341
-
342
- const origname = declared.origname || name
343
-
344
- return Path.join(declared.base, '..', origname) +
345
- (origname === name ? '' : '~' + name)
346
- }
347
-
348
-
349
- // A bare NAME, as opposed to a ref that locates a source.
350
- function isBare(ref: string): boolean {
351
- return !ref.includes('/') && !ref.includes(Path.sep)
352
- }
353
-
354
-
355
334
  function capitalise(s: string): string {
356
335
  return s.charAt(0).toUpperCase() + s.slice(1)
357
336
  }