@voxgig/apidef 8.15.1 → 8.16.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 (45) hide show
  1. package/dist/builder/entity/entity.js +8 -5
  2. package/dist/builder/entity/entity.js.map +1 -1
  3. package/dist/builder/entity/info.js +1 -1
  4. package/dist/builder/entity/info.js.map +1 -1
  5. package/dist/builder/flow.js +3 -3
  6. package/dist/builder/flow.js.map +1 -1
  7. package/dist/cli.js +8 -10
  8. package/dist/cli.js.map +1 -1
  9. package/dist/guide/guide.d.ts +14 -1
  10. package/dist/guide/guide.js +104 -62
  11. package/dist/guide/guide.js.map +1 -1
  12. package/dist/guide/heuristic01.js +105 -34
  13. package/dist/guide/heuristic01.js.map +1 -1
  14. package/dist/parse.d.ts +2 -1
  15. package/dist/parse.js +42 -43
  16. package/dist/parse.js.map +1 -1
  17. package/dist/refcount.d.ts +6 -0
  18. package/dist/refcount.js +215 -0
  19. package/dist/refcount.js.map +1 -0
  20. package/dist/transform/field.js +1 -1
  21. package/dist/transform/field.js.map +1 -1
  22. package/dist/transform/operation.js +1 -1
  23. package/dist/transform/operation.js.map +1 -1
  24. package/dist/transform/top.js +1 -1
  25. package/dist/transform/top.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/utility.d.ts +2 -1
  28. package/dist/utility.js +42 -1
  29. package/dist/utility.js.map +1 -1
  30. package/model/{guide.aon → guide.aontu} +1 -1
  31. package/package.json +3 -3
  32. package/src/builder/entity/entity.ts +8 -5
  33. package/src/builder/entity/info.ts +1 -1
  34. package/src/builder/flow.ts +3 -3
  35. package/src/cli.ts +8 -10
  36. package/src/guide/guide.ts +133 -71
  37. package/src/guide/heuristic01.ts +136 -38
  38. package/src/parse.ts +64 -38
  39. package/src/refcount.ts +244 -0
  40. package/src/transform/field.ts +1 -1
  41. package/src/transform/operation.ts +1 -1
  42. package/src/transform/top.ts +1 -1
  43. package/src/types.ts +2 -2
  44. package/src/utility.ts +54 -1
  45. /package/model/{apidef.aon → apidef.aontu} +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import Path from 'node:path'
4
4
 
5
- import { Jostraca, Project, File, Content, each } from 'jostraca'
5
+ import { each } from 'jostraca'
6
6
 
7
7
  import { Aontu } from 'aontu'
8
8
 
@@ -46,47 +46,79 @@ const aontu = new Aontu()
46
46
 
47
47
 
48
48
 
49
- function migrateLegacyGuide(fs: any, folder: string, guideprefix: string): boolean {
50
- const guidepath = Path.join(folder, 'guide', guideprefix + 'guide.aon')
51
- const legacyguide = Path.join(folder, 'guide', guideprefix + 'guide.aontu')
52
-
53
- if (fs.existsSync(guidepath) || !fs.existsSync(legacyguide)) {
54
- return false
55
- }
56
-
57
- let migrated = String(fs.readFileSync(legacyguide, 'utf8'))
58
- .replace(/@"@voxgig\/apidef\/model\/guide\.aontu"/g,
59
- '@"@voxgig/apidef/model/guide.aon"')
49
+ function migrateGuideIncludes(src: string, guideprefix: string): string {
50
+ let migrated = src
51
+ .replace(/@"@voxgig\/apidef\/model\/guide\.aon"/g,
52
+ '@"@voxgig/apidef/model/guide.aontu"')
60
53
 
61
54
  // The sibling include is written bare or with `./`; both name this file.
62
55
  for (const dir of ['', './']) {
63
56
  migrated = migrated
64
- .split('@"' + dir + guideprefix + 'base-guide.aontu"')
65
- .join('@"' + dir + guideprefix + 'base-guide.aon"')
57
+ .split('@"' + dir + guideprefix + 'base-guide.aon"')
58
+ .join('@"' + dir + guideprefix + 'base-guide.aontu"')
66
59
  }
67
60
 
68
- fs.writeFileSync(guidepath, migrated)
61
+ return migrated
62
+ }
63
+
64
+
65
+ // aontu refuses a bare sibling include, so it gains the `./` it needs.
66
+ function prefixGuideInclude(src: string, guideprefix: string): string {
67
+ return src
68
+ .split('@"' + guideprefix + 'base-guide.aontu"')
69
+ .join('@"./' + guideprefix + 'base-guide.aontu"')
70
+ }
71
+
72
+
73
+ // A `.aon` entry file is unresolvable: aontu reads only `.aontu` as source.
74
+ // So this renames AND rewrites both includes — a repair, not a convenience.
75
+ function migrateLegacyGuide(fs: any, folder: string, guideprefix: string): boolean {
76
+ const guidepath = Path.join(folder, 'guide', guideprefix + 'guide.aontu')
77
+ const legacyguide = Path.join(folder, 'guide', guideprefix + 'guide.aon')
78
+
79
+ if (fs.existsSync(guidepath) || !fs.existsSync(legacyguide)) {
80
+ return false
81
+ }
82
+
83
+ fs.writeFileSync(guidepath,
84
+ migrateGuideIncludes(String(fs.readFileSync(legacyguide, 'utf8')), guideprefix))
69
85
  try { fs.unlinkSync(legacyguide) } catch (_err: any) { }
70
86
 
71
87
  return true
72
88
  }
73
89
 
90
+
91
+ // A `.aontu` entry file may still include a `.aon` sibling, so the rename
92
+ // above never fires for it while its include still names an absent file.
93
+ function migrateLegacyGuideInclude(
94
+ fs: any, guidepath: string, guideprefix: string
95
+ ): boolean {
96
+ return rewriteGuide(fs, guidepath, (src) => migrateGuideIncludes(src, guideprefix))
97
+ }
98
+
99
+
74
100
  function migrateGuideIncludePrefix(
75
101
  fs: any, guidepath: string, guideprefix: string
102
+ ): boolean {
103
+ return rewriteGuide(fs, guidepath, (src) => prefixGuideInclude(src, guideprefix))
104
+ }
105
+
106
+
107
+ function rewriteGuide(
108
+ fs: any, guidepath: string, rewrite: (src: string) => string
76
109
  ): boolean {
77
110
  if (!fs.existsSync(guidepath)) {
78
111
  return false
79
112
  }
80
113
 
81
- const bare = '@"' + guideprefix + 'base-guide.aon"'
82
114
  const src = String(fs.readFileSync(guidepath, 'utf8'))
115
+ const migrated = rewrite(src)
83
116
 
84
- if (!src.includes(bare)) {
117
+ if (migrated === src) {
85
118
  return false
86
119
  }
87
120
 
88
- fs.writeFileSync(guidepath,
89
- src.split(bare).join('@"./' + guideprefix + 'base-guide.aon"'))
121
+ fs.writeFileSync(guidepath, migrated)
90
122
 
91
123
  return true
92
124
  }
@@ -94,6 +126,26 @@ function migrateGuideIncludePrefix(
94
126
 
95
127
 
96
128
 
129
+ function guideConflictMessage(
130
+ path: string, conflict: { line: number, text: string }
131
+ ): string {
132
+ return `@voxgig/apidef: guide: unresolved merge conflict at ${path}:${conflict.line}\n` +
133
+ ` ${conflict.text}\n` +
134
+ `Resolve the marked block in ${path}.`
135
+ }
136
+
137
+
138
+ function missingGuideMessage(path: string, guideprefix: string): string {
139
+ return `@voxgig/apidef: guide: ${path} defines no guide map; it needs the include ` +
140
+ `@"./${guideprefix}base-guide.aontu".`
141
+ }
142
+
143
+
144
+ function isPlainObject(val: any): boolean {
145
+ return null != val && 'object' === typeof val && !Array.isArray(val)
146
+ }
147
+
148
+
97
149
  function findConflict(src: string): { line: number, text: string } | null {
98
150
  const lines = String(src || '').split('\n')
99
151
 
@@ -115,7 +167,7 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
115
167
  const folder = Path.resolve(ctx.opts.folder)
116
168
 
117
169
  try {
118
- const basejres = await buildBaseGuide(ctx)
170
+ await buildBaseGuide(ctx)
119
171
  }
120
172
  catch (err: any) {
121
173
  errs.push(err)
@@ -125,16 +177,23 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
125
177
 
126
178
  let src = ''
127
179
  const guideprefix = null == ctx.opts.outprefix ? '' : ctx.opts.outprefix
128
- let guidepath = Path.join(folder, 'guide', guideprefix + 'guide.aon')
180
+ let guidepath = Path.join(folder, 'guide', guideprefix + 'guide.aontu')
129
181
 
130
182
  if (migrateLegacyGuide(ctx.fs, folder, guideprefix)) {
131
- log.info({ point: 'migrate-guide', note: 'guide.aontu -> guide.aon' })
183
+ log.info({ point: 'migrate-guide', note: 'guide.aon -> guide.aontu' })
184
+ }
185
+
186
+ if (migrateLegacyGuideInclude(ctx.fs, guidepath, guideprefix)) {
187
+ log.info({
188
+ point: 'migrate-guide-include',
189
+ note: 'base-guide.aon -> base-guide.aontu'
190
+ })
132
191
  }
133
192
 
134
193
  if (migrateGuideIncludePrefix(ctx.fs, guidepath, guideprefix)) {
135
194
  log.info({
136
195
  point: 'migrate-guide-prefix',
137
- note: 'base-guide.aon -> ./base-guide.aon'
196
+ note: 'base-guide.aontu -> ./base-guide.aontu'
138
197
  })
139
198
  }
140
199
 
@@ -154,35 +213,10 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
154
213
 
155
214
  handleErrors(ctx, errs)
156
215
 
157
- const basepath = Path.join(folder, 'guide', guideprefix + 'base-guide.aon')
158
- for (const checkpath of [guidepath, basepath]) {
159
- let checksrc = ''
160
- try {
161
- checksrc = checkpath === guidepath ? src : String(ctx.fs.readFileSync(checkpath, 'utf8'))
162
- }
163
- catch (_err: any) {
164
- continue
165
- }
166
-
167
- const conflict = findConflict(checksrc)
168
- if (null != conflict) {
169
- errs.push(new Error(
170
- `@voxgig/apidef: guide: unresolved merge conflict at ${
171
- relativizePath(checkpath)}:${conflict.line}\n` +
172
- ` ${conflict.text}\n` +
173
- `A guide is merged, not overwritten, so an edit the regenerated base\n` +
174
- `guide contradicts is left for a human to settle. Resolve the marked\n` +
175
- `block` +
176
- // DELETING ONLY HELPS FOR THE BASE GUIDE. Regeneration rewrites that
177
- // file, while the top-level entry guide is the user's own and is read
178
- // back unchanged — so advising its deletion would send a reader in a
179
- // circle, failing this same check on the next build.
180
- (checkpath === basepath ?
181
- `, or delete ${guideprefix}base-guide.aon to regenerate it from the\n` +
182
- `specification and re-apply the edit afterwards.` :
183
- ` in ${relativizePath(checkpath)}.`)))
184
- break
185
- }
216
+ // Only the entry file: the base guide was just rewritten from the spec.
217
+ const conflict = findConflict(src)
218
+ if (null != conflict) {
219
+ errs.push(new Error(guideConflictMessage(relativizePath(guidepath), conflict)))
186
220
  }
187
221
 
188
222
  handleErrors(ctx, errs)
@@ -194,7 +228,7 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
194
228
 
195
229
  const opts: any = {
196
230
  path: guidepath,
197
- errs,
231
+ errfs: ctx.fs,
198
232
  }
199
233
 
200
234
  if (ctx.fsInjected) {
@@ -203,7 +237,19 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
203
237
 
204
238
  ctx.work.guideAontuFs = undefined !== opts.fs
205
239
 
206
- const guideModel = aontu.generate(src, opts)
240
+ // One AontuError carries every aontu failure, formatted: collect mode
241
+ // leaves a generation-time failure's message empty.
242
+ let guideModel: any
243
+ try {
244
+ guideModel = aontu.generate(src, opts)
245
+ }
246
+ catch (err: any) {
247
+ errs.push(err)
248
+ }
249
+
250
+ if (0 === errs.length && !isPlainObject(guideModel?.guide)) {
251
+ errs.push(new Error(missingGuideMessage(relativizePath(guidepath), guideprefix)))
252
+ }
207
253
 
208
254
  handleErrors(ctx, errs)
209
255
 
@@ -256,8 +302,10 @@ async function buildBaseGuide(ctx: ApiDefContext) {
256
302
  throw new Error('Unknown guide strategy: ' + ctx.opts.strategy)
257
303
  }
258
304
 
305
+ const guideprefix = null == ctx.opts.outprefix ? '' : ctx.opts.outprefix
306
+
259
307
  const guideBlocks = [
260
- '# Guide',
308
+ ...baseGuideHeader(guideprefix),
261
309
  '',
262
310
  'guide: {',
263
311
  ]
@@ -371,7 +419,7 @@ async function buildBaseGuide(ctx: ApiDefContext) {
371
419
  guideBlocks.push(
372
420
  ` # Deactivated by the heuristic` +
373
421
  (null == why ? '' : ` (${why})`) + `. Set` +
374
- ` \`active: true\` here in guide.aon to generate it as an entity.`)
422
+ ` \`active: true\` here in guide.aontu to generate it as an entity.`)
375
423
  guideBlocks.push(` active: *false`)
376
424
  }
377
425
 
@@ -392,25 +440,32 @@ async function buildBaseGuide(ctx: ApiDefContext) {
392
440
 
393
441
  ctx.note.guide = { base: guideSrc }
394
442
 
395
- const baseGuideFileName =
396
- (null == ctx.opts.outprefix ? '' : ctx.opts.outprefix) + 'base-guide.aon'
443
+ const guidefolder = Path.join(ctx.opts.folder, 'guide')
444
+ ctx.fs.mkdirSync(guidefolder, { recursive: true })
445
+ ctx.fs.writeFileSync(Path.join(guidefolder, guideprefix + 'base-guide.aontu'), guideSrc)
446
+ }
397
447
 
398
- const jostraca = Jostraca({
399
- folder: ctx.opts.folder + '/guide',
400
- now: ctx.spec.now,
401
- fs: () => ctx.fs,
402
- log: ctx.log,
403
- })
404
448
 
405
- const root = () => Project({ folder: '.' }, async () => {
406
- File({ name: baseGuideFileName }, () => Content(guideSrc))
407
- })
449
+ // The entry file a project writes once and owns; the CLI prints it.
450
+ function guideEntrySource(guideprefix: string): string[] {
451
+ return [
452
+ '# Guide entry file: put customizations below the includes. The base guide',
453
+ '# it includes is generated and overwritten on every build.',
454
+ '@"@voxgig/apidef/model/guide.aontu"',
455
+ '@"./' + guideprefix + 'base-guide.aontu"',
456
+ ]
457
+ }
408
458
 
409
- const jres = await jostraca.generate({
410
- existing: { txt: { merge: true } }
411
- }, root)
412
459
 
413
- return jres
460
+ // Written into every base guide, so a reader of the file learns where an
461
+ // edit belongs before making one there.
462
+ function baseGuideHeader(guideprefix: string): string[] {
463
+ return [
464
+ '# Generated from the API definition and overwritten on every build: do not',
465
+ '# edit this file. Put customizations in the guide entry file, which includes',
466
+ '# this one and overrides its defaults:',
467
+ '# ' + guideprefix + 'guide.aontu',
468
+ ]
414
469
  }
415
470
 
416
471
 
@@ -555,6 +610,13 @@ function validateBaseBuide(ctx: ApiDefContext, baseguide: any) {
555
610
 
556
611
 
557
612
  export {
613
+ migrateGuideIncludes,
614
+ prefixGuideInclude,
615
+ findConflict,
616
+ guideConflictMessage,
617
+ missingGuideMessage,
618
+ baseGuideHeader,
619
+ guideEntrySource,
558
620
  migrateLegacyGuide,
559
621
  migrateGuideIncludePrefix,
560
622
  buildGuide
@@ -8,7 +8,7 @@ import { each } from 'jostraca'
8
8
  import { size, merge, getelem, isempty, items, keysof } from '@voxgig/struct'
9
9
 
10
10
  import {
11
- isEntityWrapperProp, envelopeProp, closedBodyTransform,
11
+ isEntityWrapperProp, envelopeProp, envelopeItemRef, closedBodyTransform,
12
12
  authExchangeOp, specSecuredByDefault,
13
13
  } from '../utility'
14
14
 
@@ -66,6 +66,8 @@ import { snakify } from 'jostraca'
66
66
 
67
67
  import { mergeCollectionPaths } from '../transform/entity'
68
68
 
69
+ import { byCodePoint, countRefs, satAdd } from '../refcount'
70
+
69
71
  import type {
70
72
  PathMatch
71
73
  } from '../utility'
@@ -77,8 +79,8 @@ const KONSOLE_LOG = console['log']
77
79
  // Log non - fatal wierdness.
78
80
  const dlog = getdlog('apidef', __filename)
79
81
 
80
- // Schema components that occur less than this rate(over total method count) qualify
81
- // as unique entities, not shared schemas
82
+ // A schema whose per-use occurrences, over the method count or over the path
83
+ // count, fall below these rates names an entity rather than a shared shape.
82
84
  const IS_ENTCMP_METHOD_RATE = 0.21
83
85
  const IS_ENTCMP_PATH_RATE = 0.41
84
86
 
@@ -98,6 +100,9 @@ const METHOD_IDOP: Record<string, string> = {
98
100
  OPTIONS: 'OPTIONS',
99
101
  }
100
102
 
103
+ // Tried in order: the first shape a path matches decides how its entity is named.
104
+ const ENTITY_PATH_SHAPES = ['t/p/t/', 't/p/', 'p/t/', 't/', 't/p/p']
105
+
101
106
  const METHOD_CONSIDER_ORDER: Record<string, number> = {
102
107
  'GET': 100,
103
108
  'QUERY': 150,
@@ -123,6 +128,8 @@ async function heuristic01(ctx: ApiDefContext): Promise<Guide> {
123
128
  ]
124
129
  },
125
130
  { select: selectCmpXrefs, apply: MeasureRef },
131
+ { select: selectAllMethods, apply: MeasureEnvelope },
132
+ MeasureEnvelopeItems,
126
133
  {
127
134
  select: selectAllMethods, apply: [
128
135
  ResolveEntityComponent,
@@ -206,6 +213,7 @@ function Prepare(spec: TaskSpec) {
206
213
  work: {
207
214
  pathmap: {},
208
215
  entmap: {},
216
+ envelope: {},
209
217
  entity: {
210
218
  count: {
211
219
  seen: 0,
@@ -279,10 +287,11 @@ function PreparePath(spec: TaskSpec) {
279
287
 
280
288
 
281
289
  function selectCmpXrefs(_source: any, spec: TaskSpec) {
282
- const out = find(spec.ctx.def, 'x-ref')
283
- .filter(xref => xref.val.match(/\/(components\/schemas|definitions)\//))
284
-
285
- return out
290
+ const counts = countRefs(spec.ctx.def)
291
+ return Object.keys(counts)
292
+ .sort(byCodePoint)
293
+ .filter(val => val.match(/\/(components\/schemas|definitions)\//))
294
+ .map(val => ({ val, count: counts[val] }))
286
295
  }
287
296
 
288
297
 
@@ -290,14 +299,16 @@ function MeasureRef(spec: TaskSpec) {
290
299
  const guide = spec.data.guide
291
300
  const metrics = guide.metrics
292
301
 
293
- let m = spec.node.val.val.match(/\/(components\/schemas|definitions)\/(.+)$/)
302
+ const xref = spec.node.val
303
+ let m = xref.val.match(/\/(components\/schemas|definitions)\/(.+)$/)
294
304
  if (m) {
295
305
  const name = canonizeCmpName(m[2])
296
306
  if (null == metrics.count.origcmprefs[name]) {
297
307
  metrics.count.cmp++
298
308
  metrics.count.origcmprefs[name] = 0
299
309
  }
300
- metrics.count.origcmprefs[name]++
310
+ metrics.count.origcmprefs[name] =
311
+ satAdd(metrics.count.origcmprefs[name], xref.count)
301
312
 
302
313
  if (null == metrics.found.cmp[name]) {
303
314
  metrics.found.cmp[name] = { orig: m[2] }
@@ -306,6 +317,44 @@ function MeasureRef(spec: TaskSpec) {
306
317
  }
307
318
 
308
319
 
320
+ // Being an envelope belongs to the component, not to one operation: it names
321
+ // through the record it carries only when every operation answering with it
322
+ // unwraps it, so the operations on one resource are never split between the
323
+ // record's name and the envelope's. An operation unwraps only the response
324
+ // ResolveTransform reads. The entry is the item's reference, or '' for none.
325
+ function MeasureEnvelope(spec: TaskSpec) {
326
+ const work = spec.data.work
327
+ const mdesc = spec.node.val
328
+ const opname = methodOpname(mdesc, matchEntityPath(work.pathmap[mdesc.path].parts), [])
329
+ const unwrapref = getResponseSchema(successResponse(mdesc.responses))?.['x-ref']
330
+
331
+ for (const schema of successSchemas(mdesc.responses)) {
332
+ const xref = schema['x-ref']
333
+ if (null != xref) {
334
+ const itemref = null == opname || xref !== unwrapref ? null :
335
+ envelopeItemRef(schema, opname)
336
+ work.envelope[xref] = '' === work.envelope[xref] || null == itemref ? '' : itemref
337
+ }
338
+ }
339
+ }
340
+
341
+
342
+ // An item carried by more than one envelope is named by none of them: the
343
+ // envelopes' own names are then what tell the resources apart.
344
+ function MeasureEnvelopeItems(spec: TaskSpec) {
345
+ const envelope: Record<string, string> = spec.data.work.envelope
346
+ const carriers: Record<string, number> = {}
347
+ for (const itemref of Object.values(envelope)) {
348
+ carriers[itemref] = (carriers[itemref] ?? 0) + 1
349
+ }
350
+ for (const xref of Object.keys(envelope)) {
351
+ if (1 < carriers[envelope[xref]]) {
352
+ envelope[xref] = ''
353
+ }
354
+ }
355
+ }
356
+
357
+
309
358
  function selectAllMethods(_source: any, spec: TaskSpec): MethodDesc[] {
310
359
  const ctx = spec.ctx
311
360
 
@@ -370,9 +419,10 @@ function ResolveEntityComponent(spec: TaskSpec) {
370
419
 
371
420
  let responses = methodDef.responses
372
421
 
373
- let origxrefs: any[] = findPotentialSchemaRefs(pathStr, methodName, responses).map(val => ({
374
- val
375
- }))
422
+ let origxrefs: any[] = findPotentialSchemaRefs(
423
+ pathStr, methodName, responses, work.envelope, why_cmp).map(val => ({
424
+ val
425
+ }))
376
426
 
377
427
  let cmpxrefs = origxrefs
378
428
  .filter(xref => xref.val.includes('schema') || xref.val.includes('definitions'))
@@ -544,25 +594,25 @@ function ResolveEntityName(spec: TaskSpec) {
544
594
 
545
595
  let entname
546
596
 
547
- let pm = undefined
597
+ const pm = matchEntityPath(parts)
548
598
 
549
- if (pm = pathMatch(parts, 't/p/t/')) {
599
+ if ('t/p/t/' === pm?.expr) {
550
600
  entname = entityPathMatch_tpte(data, pm, mdesc, why_path)
551
601
  }
552
602
 
553
- else if (pm = pathMatch(parts, 't/p/')) {
603
+ else if ('t/p/' === pm?.expr) {
554
604
  entname = entityPathMatch_tpe(data, pm, mdesc, why_path)
555
605
  }
556
606
 
557
- else if (pm = pathMatch(parts, 'p/t/')) {
607
+ else if ('p/t/' === pm?.expr) {
558
608
  entname = entityPathMatch_pte(data, pm, mdesc, why_path)
559
609
  }
560
610
 
561
- else if (pm = pathMatch(parts, 't/')) {
611
+ else if ('t/' === pm?.expr) {
562
612
  entname = entityPathMatch_te(data, pm, mdesc, why_path)
563
613
  }
564
614
 
565
- else if (pm = pathMatch(parts, 't/p/p')) {
615
+ else if ('t/p/p' === pm?.expr) {
566
616
  entname = entityPathMatch_tpp(data, pm, mdesc, why_path)
567
617
  }
568
618
 
@@ -1004,7 +1054,7 @@ function ResolveOperation(spec: TaskSpec) {
1004
1054
 
1005
1055
 
1006
1056
  if ('load' === standard_opname) {
1007
- const islist = isListResponse(mdesc, pathStr, why_op)
1057
+ const islist = isListResponse(mdesc, ment.pm, pathStr, why_op)
1008
1058
  opname = islist ? 'list' : opname
1009
1059
  }
1010
1060
 
@@ -1074,8 +1124,7 @@ function ResolveTransform(spec: TaskSpec) {
1074
1124
  res: undefined,
1075
1125
  }
1076
1126
 
1077
- const resokdef = mdesc.responses?.[200] || mdesc.responses?.[201]
1078
- const resprops = getResponseSchema(resokdef)?.properties
1127
+ const resprops = getResponseSchema(successResponse(mdesc.responses))?.properties
1079
1128
  debugpath(pathStr, methodName, 'TRANSFORM-RES', keysof(resprops))
1080
1129
 
1081
1130
  if (resprops) {
@@ -1418,6 +1467,21 @@ function getRequestBodySchema(requestBody: any) {
1418
1467
  requestBody?.schema
1419
1468
  }
1420
1469
 
1470
+ // The response an operation's result is read from.
1471
+ function successResponse(responses: any): any {
1472
+ return responses?.[200] ?? responses?.[201]
1473
+ }
1474
+
1475
+
1476
+ // The response schemas an operation answers with when it succeeds, in the
1477
+ // order they are tried.
1478
+ function successSchemas(responses: any): any[] {
1479
+ return ['200', '201']
1480
+ .map((rescode) => getResponseSchema(responses?.[rescode]))
1481
+ .filter((schema) => null != schema)
1482
+ }
1483
+
1484
+
1421
1485
  function getResponseSchema(response: any) {
1422
1486
  return response?.content?.['application/json']?.schema ??
1423
1487
  response?.schema
@@ -1634,14 +1698,35 @@ function cmpOccursInPath(data: { def: any, work: any }, cmpname: string): boolea
1634
1698
 
1635
1699
 
1636
1700
 
1701
+ function matchEntityPath(parts: string[]): PathMatch | null {
1702
+ for (const shape of ENTITY_PATH_SHAPES) {
1703
+ const pm = pathMatch(parts, shape)
1704
+ if (null != pm) {
1705
+ return pm
1706
+ }
1707
+ }
1708
+ return null
1709
+ }
1710
+
1711
+
1712
+ // The operation ResolveOperation will assign, needed before the entity is
1713
+ // named: whether a response unwraps as an envelope depends on it.
1714
+ function methodOpname(
1715
+ mdesc: Record<string, any>,
1716
+ pm: PathMatch | null,
1717
+ why: string[]
1718
+ ): string | undefined {
1719
+ const opname = METHOD_IDOP[mdesc.method]
1720
+ return 'load' === opname && isListResponse(mdesc, pm, mdesc.path, why) ? 'list' : opname
1721
+ }
1722
+
1723
+
1637
1724
  function isListResponse(
1638
1725
  mdesc: Record<string, any>,
1726
+ pm: PathMatch | null | undefined,
1639
1727
  pathStr: string,
1640
1728
  why: string[]
1641
1729
  ): boolean {
1642
- const ment = mdesc.MethodEntity
1643
- const pm = ment.pm
1644
-
1645
1730
  let islist = false
1646
1731
  let schema
1647
1732
 
@@ -1652,8 +1737,7 @@ function isListResponse(
1652
1737
  why.push('end-param')
1653
1738
  }
1654
1739
  else {
1655
- const response = mdesc.responses?.[200] ?? mdesc.responses?.[201]
1656
- schema = getResponseSchema(response)
1740
+ schema = getResponseSchema(successResponse(mdesc.responses))
1657
1741
 
1658
1742
  if (null == schema) {
1659
1743
  why.push('no-schema')
@@ -1875,22 +1959,30 @@ function makeMethodEntityDesc(desc: Record<string, any>): MethodEntityDesc {
1875
1959
  }
1876
1960
 
1877
1961
 
1878
- function findPotentialSchemaRefs(pathStr: string, methodName: string, responses: any) {
1962
+ function findPotentialSchemaRefs(
1963
+ pathStr: string,
1964
+ methodName: string,
1965
+ responses: any,
1966
+ envelope: Record<string, string>,
1967
+ why: string[],
1968
+ ) {
1879
1969
  const xrefs: string[] = []
1880
- if (null == responses) {
1881
- return xrefs
1882
- }
1883
- const rescodes = ['200', '201']
1884
- for (let rescode of rescodes) {
1885
- const schema = getResponseSchema(responses[rescode])
1886
- if (null != schema) {
1887
- if (null != schema['x-ref']) {
1888
- xrefs.push(schema['x-ref'])
1970
+ for (const schema of successSchemas(responses)) {
1971
+ if (null != schema['x-ref']) {
1972
+ // An envelope component names its wrapping, not the entity: the
1973
+ // component it carries takes its place.
1974
+ const itemref = envelope[schema['x-ref']]
1975
+ if ('' !== itemref) {
1976
+ why.push('envelope=' + cmpRefName(schema['x-ref']))
1977
+ xrefs.push(itemref)
1889
1978
  }
1890
- else if ('array' === schema.type && null != schema.items?.['x-ref']) {
1891
- xrefs.push(schema.items?.['x-ref'])
1979
+ else {
1980
+ xrefs.push(schema['x-ref'])
1892
1981
  }
1893
1982
  }
1983
+ else if ('array' === schema.type && null != schema.items?.['x-ref']) {
1984
+ xrefs.push(schema.items?.['x-ref'])
1985
+ }
1894
1986
  }
1895
1987
 
1896
1988
  debugpath(pathStr, methodName, 'POTENTIAL-SCHEMA-REFS', xrefs)
@@ -1898,6 +1990,12 @@ function findPotentialSchemaRefs(pathStr: string, methodName: string, responses:
1898
1990
  }
1899
1991
 
1900
1992
 
1993
+ function cmpRefName(xref: string): string {
1994
+ const m = xref.match(/\/(components\/schemas|definitions)\/(.+)$/)
1995
+ return null == m ? xref : canonizeCmpName(m[2])
1996
+ }
1997
+
1998
+
1901
1999
  function hasMethod(def: any, pathStr: string, methodName: string) {
1902
2000
  const pathDef = def?.paths?.[pathStr]
1903
2001
  const found = (