@voxgig/sdkgen-infrapack 0.0.12 → 0.0.13

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.
@@ -21,36 +21,8 @@ import {
21
21
  import { Gitignore } from './Gitignore_seneca-provider'
22
22
 
23
23
 
24
- // The `seneca-provider` target: a Seneca plugin exposing this API's entities
25
- // as Seneca entities (`provider/<name>/<entity>`), layered on the sibling
26
- // `ts` SDK.
27
- //
28
- // A consumer target in the go-cli / py-data mould — every standard phase is
29
- // off in model/target/seneca-provider.aon and this component emits the
30
- // whole package. It differs from those in one way that shapes everything
31
- // here: it generates into ITS OWN REPO (`output: path`), depends on the SDK
32
- // as a PUBLISHED npm package rather than by path, and therefore carries a
33
- // repo's worth of furniture rather than a subfolder's.
34
- //
35
- // SHAPE OF THE MAPPING
36
- //
37
- // Seneca's store commands are list / load / save / remove. The SDK's are
38
- // list / load / create / update / remove. `save` is the one that is not
39
- // one-to-one: Seneca's convention is that an entity carrying an id is an
40
- // update and one without is a create, so `save` dispatches on `data.id`.
41
- //
42
- // Everything else follows from the model:
43
- // - which cmds exist at all, from the entity's declared ops;
44
- // - the required path params of each op, which become argument guards (a
45
- // nested entity like `moon` under `/planet/{planet_id}/moon` cannot build
46
- // its URL without the parent id, and an opaque 404 from a half-built URL
47
- // is a bad error message);
48
- // - the SDK accessor and entity class names, from the same helpers the ts
49
- // target uses, so the two cannot drift.
50
-
51
-
52
- // Seneca store cmd -> the SDK ops it needs. `save` needs BOTH create and
53
- // update; it is emitted when either is present and dispatches on the id.
24
+
25
+
54
26
  const CMD_OPS: Record<string, string[]> = {
55
27
  list: ['list'],
56
28
  load: ['load'],
@@ -59,28 +31,6 @@ const CMD_OPS: Record<string, string[]> = {
59
31
  }
60
32
 
61
33
 
62
- // The custom ACTIONS one Seneca cmd can reach, as action name -> SDK op.
63
- //
64
- // apidef folds a non-CRUD verb into an ordinary op as an extra point marked
65
- // `select.$action`: GitHub's `PUT /repos/{owner}/{repo}/pulls/{n}/merge` is a
66
- // second point of `pull.update`, beside the canonical `PATCH`. The SDK
67
- // selects one with `$action` in the call's argument; without this map the
68
- // provider has no way to name one at all, and `merge` is simply unreachable
69
- // through a generated plugin.
70
- //
71
- // KEYED BY CMD, NOT BY OP, and that is the whole point of the function.
72
- // `save` covers create AND update, so an action folded into `create` arrives
73
- // through `save$` exactly as one folded into `update` does — assuming
74
- // `update` would send `upload_image` (petstore's
75
- // `POST /pet/{petId}/uploadImage`, a create point) to the wrong endpoint, and
76
- // the SDK would then refuse it as an invalid action on an operation the
77
- // caller never named.
78
- //
79
- // A name claimed by an earlier op WINS: `entityActions` walks the op map in
80
- // sorted-key order, so for `save` that is create before update. Two ops of
81
- // one entity sharing an action name is not something apidef produces from a
82
- // spec — the name comes from the route — and if it ever does, a stable choice
83
- // beats a last-writer-wins one.
84
34
  function cmdActions(ent: any, cmd: string): Record<string, string> {
85
35
  const ops = CMD_OPS[cmd] || []
86
36
 
@@ -107,21 +57,13 @@ function cmdActions(ent: any, cmd: string): Record<string, string> {
107
57
  }
108
58
 
109
59
 
110
- // The ops that have a route of their OWN, as opposed to nothing but folded-in
111
- // actions. An op whose every point is an action point has no plain call: a
112
- // `save$` naming no action still reaches the SDK's `update`, but the SDK finds
113
- // one point and takes it, so the "canonical" update IS the action's route.
114
- //
115
- // Which is what the generated tests need to know before writing a plain,
116
- // id-bearing save: without this they were emitted for an entity that has no
117
- // such call to make.
118
60
  function canonicalOps(ent: any): string[] {
119
61
  return entityOps(ent).filter((opname: string) => {
120
62
  const op = (ent.op || {})[opname]
121
63
  const points: any[] = (op && op.points) || []
122
64
 
123
65
  return points.some((pt: any) =>
124
- null == (pt && pt.select && pt.select['$action']))
66
+ null == (pt && pt.q && pt.q['$action']))
125
67
  })
126
68
  }
127
69
 
@@ -146,9 +88,6 @@ function entityActionList(ent: any):
146
88
  }
147
89
 
148
90
 
149
- // The required (non-optional) request keys of an op, id first. These are what
150
- // the SDK needs to build the path, so they are what the provider must have
151
- // before it calls.
152
91
  function requiredKeys(ent: any, opname: string): string[] {
153
92
  const idf = entityIdField(ent)
154
93
  return opRequestShape(ent, opname).items
@@ -167,13 +106,6 @@ function addressKeys(ent: any, opname: string): string[] {
167
106
  const parts = idParts(ent)
168
107
 
169
108
  if (0 < parts.length) {
170
- // EVERY PART, required or not — for the same reason a single record key
171
- // always travels. github's api_insights_summary_stat is keyed
172
- // `actor_type/actor_id` and declares both OPTIONAL, because the op also
173
- // covers routes that take neither; only `min_timestamp` came through as
174
- // required. So the call sent the timestamp alone, the split id went
175
- // nowhere, and every read answered with the same record — the composite
176
- // half of the defect fixed below for single keys.
177
109
  const shape = opRequestShape(ent, opname).items
178
110
  for (const part of parts) {
179
111
  if (!keys.includes(part) &&
@@ -194,33 +126,6 @@ function addressKeys(ent: any, opname: string): string[] {
194
126
  }
195
127
 
196
128
 
197
- // The key that addresses ONE record.
198
- //
199
- // `entityIdField` answers whenever the model declares one, which apidef does
200
- // for any entity carrying a field literally named `id` — and it renames an
201
- // `<entity>_id` path param to `id` besides, which is why most APIs never reach
202
- // the fallback.
203
- //
204
- // When it does NOT answer, the record key is the LAST path param of the
205
- // op's own point, in PATH order — a route addresses parents first and the
206
- // record last, by construction. Without this the key was simply unknown,
207
- // so a param named `code` failed the `!== idf` test in opParentKeys and
208
- // was classified as a PARENT — `load$('SAVE20')` threw "coupon load: code
209
- // is required" instead of loading anything, and the entity was treated as
210
- // nested throughout.
211
- //
212
- // opParams(op) is NOT the source here: it alphabetizes params for output
213
- // stability, which loses path order on a 3+-param route — Airtable's
214
- // record (base_id, table_id, record_id) alphabetizes with table_id last,
215
- // so the old `params[params.length - 1]` picked the wrong parent as the
216
- // record's own key. The point's own `parts` still has the true order.
217
- // The path parameters that TOGETHER name one record, when no single one does.
218
- //
219
- // apidef sets `id.parts` for an API that addresses a record by several
220
- // adjacent path parameters — github needs {owner} AND {repo} to name a
221
- // repository — and `id.sep` (a slash) joins them into the ONE id a Seneca
222
- // entity carries. Empty for the ordinary single-key entity, which is most of
223
- // them, so every caller can branch on `0 < parts.length`.
224
129
  function idParts(ent: any): string[] {
225
130
  const parts = ent?.id?.parts
226
131
  return Array.isArray(parts) && 1 < parts.length ?
@@ -234,18 +139,6 @@ function idSep(ent: any): string {
234
139
  }
235
140
 
236
141
 
237
- // THE ONE DESCRIPTION OF AN ENTITY'S ID, or null when Seneca's `id` and the
238
- // API's key already agree and nothing needs translating.
239
- //
240
- // It unifies the two cases that used to be handled by separate emitters. A
241
- // compound key (`{owner}/{repo}`) has several parts; an API that simply calls
242
- // its key something else (`repo`, `number`) has exactly one. Neither needs a
243
- // different algorithm — a one-part split is the identity, and a one-part join
244
- // is the old carry-across — so both come through the same table and the same
245
- // two functions.
246
- //
247
- // `from` is passed through as the model states it: which response field
248
- // carries each part. apidef derives it and guide.aon can correct it.
249
142
  function idSpec(ent: any): { parts: string[], sep: string, from?: Record<string, string> } | null {
250
143
  const parts = idParts(ent)
251
144
  if (0 < parts.length) {
@@ -262,8 +155,6 @@ function idSpec(ent: any): { parts: string[], sep: string, from?: Record<string,
262
155
  return null
263
156
  }
264
157
 
265
- // A single-key entity whose key is not `id`. `from` defaults to the key's
266
- // own name, which is what the old carry-across read.
267
158
  return { parts: [rk], sep: idSep(ent) }
268
159
  }
269
160
 
@@ -281,11 +172,8 @@ function recordKey(ent: any): string {
281
172
  }
282
173
 
283
174
  const canonical = op.points.filter((pt: any) =>
284
- null == (pt && pt.select && pt.select['$action']))
175
+ null == (pt && pt.q && pt.q['$action']))
285
176
  const point = ownPoint(0 < canonical.length ? canonical : op.points)
286
- // The LAST variable segment names the record's key. apidef states which
287
- // segments are variables (its ADR-003), so this reads the name off the
288
- // vector rather than finding a `{` and slicing the braces back off.
289
177
  const vars = pointSegments(point)
290
178
  .filter((seg: any) => null != seg.var)
291
179
 
@@ -293,12 +181,10 @@ function recordKey(ent: any): string {
293
181
  return String(vars[vars.length - 1].var)
294
182
  }
295
183
 
296
- // No path param at all (e.g. GET /scan/async/result?trace_id=...): the
297
- // record's own key can still be a single required QUERY param.
298
- const query = (point && point.args && point.args.query) || []
299
- const reqdQuery = query.filter((q: any) => false !== q.reqd)
184
+ const query = (point && point.g && point.g.query) || []
185
+ const reqdQuery = query.filter((q: any) => false !== q.r)
300
186
  if (1 === reqdQuery.length) {
301
- return String(reqdQuery[0].name)
187
+ return String(reqdQuery[0].n)
302
188
  }
303
189
  }
304
190
 
@@ -306,25 +192,12 @@ function recordKey(ent: any): string {
306
192
  }
307
193
 
308
194
 
309
- // The guard function's name. Spec-derived param names are not constrained to
310
- // identifiers — Evervault's `/payments/3ds-sessions/{3ds_session_id}` is the
311
- // standing example — and a name is a DECLARATION here, so it cannot be
312
- // bracket-quoted the way a property access can. Non-identifier characters are
313
- // replaced rather than dropped, so `a-b` and `a_b` cannot collide.
314
195
  function guardName(e: any, key: string): string {
315
196
  return `need_${e.name}_${String(key).replace(/[^A-Za-z0-9_$]/g, '_')}`
316
197
  .replace(/^need_(\d)/, 'need__$1')
317
198
  }
318
199
 
319
200
 
320
- // The parent PATH params of ONE op: `moon`'s load under
321
- // `/planet/{planet_id}/moon/{id}` yields ['planet_id']. These are the keys a
322
- // caller can forget, so each gets a guard.
323
- //
324
- // From opParams — the op's declared path params — NOT from opRequestShape.
325
- // For create/update the latter also returns the request BODY fields, so
326
- // reading it here generated a "planet name is required" guard for every
327
- // writable field on every entity.
328
201
  function opParentKeys(ent: any, opname: string): string[] {
329
202
  const rk = recordKey(ent)
330
203
  const op = (ent.op || {})[opname]
@@ -336,8 +209,8 @@ function opParentKeys(ent: any, opname: string): string[] {
336
209
  const seen = new Set<string>()
337
210
 
338
211
  for (const p of opParams(op)) {
339
- const name = String((p as any).name)
340
- if (false !== (p as any).reqd && name !== rk && name !== 'id') {
212
+ const name = String((p as any).n)
213
+ if (false !== (p as any).r && name !== rk && name !== 'id') {
341
214
  seen.add(name)
342
215
  }
343
216
  }
@@ -346,13 +219,6 @@ function opParentKeys(ent: any, opname: string): string[] {
346
219
  }
347
220
 
348
221
 
349
- // Every parent key the entity has, across its ACTIVE ops — for the seed data
350
- // and the docs, which describe the entity rather than one call.
351
- //
352
- // `entityOps` and not `Object.keys(ent.op)`: an op the model marks
353
- // `active: false` generates no SDK method, so letting it contribute a key
354
- // here put a mandatory guard for a parameter of a call that does not exist
355
- // onto every cmd that does.
356
222
  function parentKeys(ent: any): string[] {
357
223
  const seen = new Set<string>()
358
224
 
@@ -366,20 +232,6 @@ function parentKeys(ent: any): string[] {
366
232
  }
367
233
 
368
234
 
369
- // A model field's broad shape, for generating seed data that reads as data.
370
- // The model carries canon strings (`\`$STRING\``), not JS types.
371
- //
372
- // ORDER MATTERS and the tests are substring tests, so the container kinds are
373
- // checked FIRST: a multi-type field's sentinel is the ARRAY
374
- // `['`$ONE`', [members...]]`, and String() flattens it to a comma-joined
375
- // string — so a `$ONE` of string|number matched `includes('NUMBER')` and was
376
- // seeded as a bare number. A union is not a number; it is whatever its first
377
- // member is, and falling back to a string is the safe answer.
378
- //
379
- // `$ARRAY` and `$OBJECT` are in the sentinel vocabulary (see
380
- // helpers/canonType.ts) and used to fall through to 'string', which put
381
- // `tags: 'quick-tags'` into test/quick.js — a type-incorrect body that a
382
- // validating server rejects.
383
235
  function fieldKind(type: any): string {
384
236
  if (Array.isArray(type)) {
385
237
  return 'string'
@@ -396,21 +248,12 @@ function fieldKind(type: any): string {
396
248
  }
397
249
 
398
250
 
399
- // Which entity a parent path param refers to: `planet_id` -> `planet`, but
400
- // only when an entity of that name actually exists. A key that names no
401
- // entity gets no cross-reference, and the seed falls back to a plain string.
402
251
  function parentEntityOf(key: string, names: string[]): string {
403
252
  const stem = key.replace(/_id$/, '')
404
253
  return names.includes(stem) ? stem : ''
405
254
  }
406
255
 
407
256
 
408
- // The repo this provider is released from — NOT the SDK's. A provider is its
409
- // own package in its own repo, so its manifest's homepage/repository must
410
- // point there; deriving them from `main: kit: repo` sends every link in the
411
- // published package to the SDK instead.
412
- //
413
- // Order: the project's `output: repo`, else the Seneca convention.
414
257
  function providerRepo(model: any, lower: string, tname: string):
415
258
  { url: string, path: string } {
416
259
  const host = model?.main?.[KIT]?.repo?.host || 'github.com'
@@ -437,9 +280,6 @@ const Main = cmp(function Main(props: any) {
437
280
  const { target, ctx$ } = props
438
281
  const { model } = ctx$
439
282
 
440
- // HARD REQUIREMENT: this plugin imports the TypeScript SDK. Generating it
441
- // without `ts` produces a package whose every import fails, so fail at
442
- // GENERATE time with an actionable message instead.
443
283
  const targets = model.main[KIT].target || {}
444
284
  if (null == targets.ts) {
445
285
  throw new SdkGenError(
@@ -449,12 +289,12 @@ const Main = cmp(function Main(props: any) {
449
289
  'then regenerate.')
450
290
  }
451
291
 
452
- const Name = model.const.Name // Solardemo
453
- const lower = String(model.const.name) // solardemo
454
- const ENV = envName(model) // SOLARDEMO
455
- const sdkClass = `${Name}SDK` // SolardemoSDK
456
- const pluginName = `${Name}Provider` // SolardemoProvider
457
- const fileBase = `${lower}-provider` // solardemo-provider
292
+ const Name = model.const.Name
293
+ const lower = String(model.const.name)
294
+ const ENV = envName(model)
295
+ const sdkClass = `${Name}SDK`
296
+ const pluginName = `${Name}Provider`
297
+ const fileBase = `${lower}-provider`
458
298
 
459
299
  // The SDK is a PUBLISHED dependency, not a path: this package lives in its
460
300
  // own repo. Its name is whatever the ts target publishes under, pin
@@ -464,45 +304,23 @@ const Main = cmp(function Main(props: any) {
464
304
  const sdkPkg = packageName(model, 'npm')
465
305
  const sdkVersion = packageVersion(model, 'ts')
466
306
 
467
- // UNFILTERED by design — see the AGENTS.md sharp edge: entityCollection is
468
- // the resolver every component must use (getModelPath rebuilds its container
469
- // per call, defeating the class-name memo), and it deliberately includes
470
- // inactive entities because the typed-model emitters need them.
471
- //
472
- // Which makes filtering `active` the CALLER's job, and this component was
473
- // the one consumer target that skipped it — go-cli, go-mcp and py-data all
474
- // re-filter. The cost: MainEntity_ts emits an accessor only for an ACTIVE
475
- // entity, so an inactive one produced `this.shared.sdk.Ghost()` in the
476
- // provider and an `assert.equal(typeof sdk.Ghost, 'function')` in its tests,
477
- // against a method the SDK does not have.
478
307
  const entityColl = entityCollection(model)
479
308
 
480
309
  const activeEntities = Object.keys(entityColl).sort()
481
310
  .map((key: string) => entityColl[key])
482
311
  .filter((ent: any) => false !== ent.active)
483
312
 
484
- // Entities this provider can serve: those with at least one op that maps to
485
- // a Seneca store cmd. An entity with no such op would produce an empty cmd
486
- // map, which seneca-entity treats as a store that answers nothing.
487
313
  const entityNames = activeEntities.map((ent: any) => ent.name)
488
314
 
489
315
  const entities = activeEntities
490
316
  .map((ent: any) => {
491
317
  const parents = parentKeys(ent)
492
318
 
493
- // The parent entity PER KEY. Deriving it from `parents[0]` alone left an
494
- // entity nested two levels deep with no cross-reference for its outer
495
- // parents, so their seed values came out as the literal '0'.
496
319
  const parentOf: Record<string, string> = {}
497
320
  for (const key of parents) {
498
321
  parentOf[key] = parentEntityOf(key, entityNames)
499
322
  }
500
323
 
501
- // The parent keys of each op SEPARATELY. The guards used to be the union
502
- // across all ops, applied to every cmd alike, while the argument handed
503
- // to the SDK was computed per op — so an entity whose routes are not
504
- // uniformly nested (a flat `load`, a nested `create`) demanded a
505
- // parameter its own call would never use.
506
324
  const opParents: Record<string, string[]> = {}
507
325
  for (const opname of entityOps(ent)) {
508
326
  opParents[opname] = opParentKeys(ent, opname)
@@ -511,27 +329,11 @@ const Main = cmp(function Main(props: any) {
511
329
  return {
512
330
  ent,
513
331
  name: ent.name,
514
- // The SDK ACCESSOR on the client (`client.Moon()`), which is the
515
- // entity's PascalCase name — NOT entityClassName, which is the
516
- // collision-safe CLASS name the accessor constructs (`MoonEntity`).
517
- // Calling the class name reads plausibly and fails at runtime with
518
- // "sdk.MoonEntity is not a function". MainEntity_ts is the authority
519
- // for this: it declares the method as `${entity.Name}()`.
520
332
  acc: ent.Name,
521
- // The entity's canonical route, used to probe the live server for
522
- // liveness. Path params are left in place only if the route has
523
- // them — a collection route (the `list` op's) has none, which is why
524
- // entityPath prefers it.
525
333
  path: entityPath(ent),
526
334
  cls: entityClassName(ent, entityColl),
527
335
  ops: entityOps(ent),
528
336
  idf: entityIdField(ent),
529
- // The field this API's routes actually ADDRESS a record by, which is
530
- // not always `id` and is not always what entityIdField answers (that
531
- // returns null when the load match has no `id`, leaving recordKey to
532
- // read the route's last variable segment). The doc and test emitters
533
- // in Extras need the same answer the handler emitters use, or the
534
- // seed they build is keyed by a field the routes never look at.
535
337
  rk: recordKey(ent),
536
338
  // The composite key, when this API addresses a record by several
537
339
  // path params at once. The doc and test emitters in Extras need the
@@ -540,41 +342,6 @@ const Main = cmp(function Main(props: any) {
540
342
  // rejects.
541
343
  idparts: idParts(ent),
542
344
  idsep: idSep(ent),
543
- // DOES EACH SINGLE-RECORD OP ACTUALLY CARRY THE RECORD'S KEY?
544
- //
545
- // Only then can a wrong id miss on a LOAD: github's `interaction`
546
- // reads `/user/interaction-limits` — a singleton, called as
547
- // `load({})` — so `load$('no-such-id')` correctly returns the one
548
- // record there is, and a not-found test against it asserts the
549
- // opposite of the truth.
550
- //
551
- // And only then can a REMOVE delete what a create just made. A
552
- // tag-bucket entity can have ops addressing different resources
553
- // entirely: github's `action` is keyed `archive_format` from its
554
- // download route while its remove takes `hosted_runner_id` and
555
- // `org_id`, so the remove addressed by parent scope alone — it
556
- // deleted whichever record the store happened to yield first, which
557
- // was usually a SEEDED one, and the round-trip failed on the record
558
- // it had created surviving. Intermittently: the created record's id
559
- // is random, so where it falls in iteration order decides.
560
- //
561
- // Parent keys alone do not distinguish records, which is why this
562
- // asks for the record's key or every composite part rather than
563
- // merely for "the route has a parameter".
564
- // WHICH SINGLE-RECORD OPS ADDRESS A DIFFERENT RESOURCE ENTIRELY.
565
- //
566
- // Not merely "cannot address the record": an op that addresses
567
- // NOTHING is a singleton read, and github's `interaction`
568
- // (`/user/interaction-limits`) is a real one. This is the other case
569
- // — the op addresses something, and it is not this record. Every one
570
- // is a tag-derived entity whose ops were gathered from unrelated
571
- // routes: `migration`'s remove takes `owner` and `repo` and deletes
572
- // a repository's migration archive, `user`'s deletes a GPG KEY, and
573
- // `pull`'s deletes a review COMMENT. The id the caller passed is
574
- // dropped, and the request goes anyway.
575
- //
576
- // Reads are untouched across the whole of github — this is 7 removes
577
- // and 5 updates, and both are writes.
578
345
  idmisaddressed: ['remove', 'update'].reduce(
579
346
  (acc: Record<string, boolean>, opname: string) => {
580
347
  acc[opname] = null != (ent.op || {})[opname] &&
@@ -594,8 +361,6 @@ const Main = cmp(function Main(props: any) {
594
361
  keys.includes(recordKey(ent))
595
362
  return acc
596
363
  }, {}),
597
- // Where each part is carried in a response. The test emitter needs
598
- // it to know whether a created record's id can be rebuilt at all.
599
364
  idfrom: (ent?.id?.from) || {},
600
365
  parents,
601
366
  parentOf,
@@ -613,30 +378,15 @@ const Main = cmp(function Main(props: any) {
613
378
  acc[cmd] = cmdActions(ent, cmd)
614
379
  return acc
615
380
  }, {}),
616
- // The same information flattened, for the README and the generated
617
- // tests: [{ cmd, op, action, path }].
618
381
  actionList: entityActionList(ent),
619
- // The ops with a route of their own. `ops` minus those that are
620
- // nothing but folded-in actions — what the generated tests consult
621
- // before assuming a plain call exists.
622
382
  canonicalOps: canonicalOps(ent),
623
- // Required fields only: a seed record has to satisfy the shape the
624
- // SDK will hand back, and optional noise makes the assertions
625
- // harder to read.
626
- //
627
- // A parent path param is then FORCED IN even when the entity's own
628
- // schema omits it or marks it optional. A path param is a routing key,
629
- // not necessarily a response field: when the child's schema left it
630
- // out the seeded record had no link back to its parent, and the mock's
631
- // match found nothing — which the nested `load` test reported as a
632
- // TypeError and the nested `list` test reported as a pass.
633
383
  fields: (() => {
634
- const req = (ent.fields || [])
635
- .filter((f: any) => false !== f.req)
384
+ const req = (Object.values(ent.fields || {}))
385
+ .filter((f: any) => false !== f.r)
636
386
  .map((f: any) => ({
637
- name: f.name,
638
- kind: fieldKind(f.type),
639
- parentEntity: parentEntityOf(f.name, entityNames),
387
+ name: f.n,
388
+ kind: fieldKind(f.t),
389
+ parentEntity: parentEntityOf(f.n, entityNames),
640
390
  }))
641
391
 
642
392
  const have = new Set(req.map((f: any) => f.name))
@@ -677,27 +427,6 @@ const Main = cmp(function Main(props: any) {
677
427
  // once by the external pass — see cmp/ExternalTarget.
678
428
  const sdkrel = ctx$.sdkrelpath || '..'
679
429
 
680
- // WHERE THE SDK SOURCE IS, AND WHY IT IS NOT `sdkrel`.
681
- //
682
- // `sdkrel` is a walk back through the FILESYSTEM, and it is committed here
683
- // — into the docs, the live-test headers and the develop-locally recipe.
684
- // That makes generated content depend on one machine's directory layout.
685
- // voxgig-solardemo-sdk committed '../../voxgig-sdk/voxgig-solardemo-sdk',
686
- // where `voxgig-sdk` is the name of a workspace directory on one laptop and
687
- // no part of any model; a second developer with both repos under a
688
- // differently named parent regenerates a diff in tracked files and follows
689
- // instructions that are wrong on one of the two machines.
690
- //
691
- // So the SDK is named by its PIN instead — repository and release tag, both
692
- // facts about the SDK rather than about anyone's disk — and fetched to a
693
- // fixed path INSIDE this repo. `make sdk-src` puts it there. The path is
694
- // then the same string on every machine and under every layout, which is
695
- // what lets this repo regenerate itself from a checkout it fetched (see
696
- // SdkPin and the Makefile).
697
- //
698
- // The tag is `v<version>`: the SDK's publish workflow cuts exactly that for
699
- // its primary npm target, so the pin needs no separate bookkeeping and
700
- // cannot drift from the dependency version.
701
430
  const sdkRepoUrl = String(repoInfo(model).repoUrl || '')
702
431
  const sdkTag = 'v' + sdkVersion
703
432
  const sdkRepoDir = sdkRepoUrl.replace(/[/]+$/, '').split('/').pop() || 'sdk'
@@ -708,22 +437,6 @@ const Main = cmp(function Main(props: any) {
708
437
  const sdkPinned = '' !== sdkRepoUrl
709
438
  const sdkSrc = sdkPinned ? SDK_SRC_DIR + '/' + sdkRepoDir : sdkrel
710
439
 
711
- // Where a live run points — and whether there is anything honest to point
712
- // it at.
713
- //
714
- // NOT simply `servers[0].url`. For an OpenAPI-derived model that is the
715
- // PRODUCTION host of a third-party API, and everything gated on it aims
716
- // there: the `describe('live')` block, which `npm test` runs on every CI
717
- // push on three operating systems; the `serverUp` probe in front of it; and
718
- // test/quick.js, whose header says "start the companion server first" while
719
- // BASE silently defaults to production and whose body is a create / update /
720
- // remove cycle. A live suite that reaches a stranger's API from CI is not a
721
- // live suite, it is traffic — and unauthenticated traffic at that.
722
- //
723
- // So a live base is taken only when it is unambiguously OURS: declared
724
- // outright by the project, or a loopback address, which no third party can
725
- // be behind. Anything else leaves live testing ungenerated, which is the
726
- // honest answer for an API nobody here runs.
727
440
  const live = model?.main?.[KIT]?.test?.live || {}
728
441
  const servers = (model?.main?.[KIT]?.info?.servers || [])
729
442
  const specBase = 0 < servers.length ? String(servers[0].url || '') : ''
@@ -748,9 +461,6 @@ const Main = cmp(function Main(props: any) {
748
461
  const provider = {
749
462
  Name, lower, ENV, sdkClass, pluginName, fileBase,
750
463
  sdkPkg, sdkVersion, entities,
751
- // The SDK dependency, resolved ONCE. PackageJson emits it and the CI
752
- // note describes it, and computing it twice is how the second caller
753
- // came to pass a half-built provider object into it.
754
464
  sdkDep,
755
465
  // Whether that dependency comes from outside a registry. The generated
756
466
  // CI note says so, because "npm install is all you need" stops being
@@ -761,60 +471,28 @@ const Main = cmp(function Main(props: any) {
761
471
  // that says the wrong one is worse than no note.
762
472
  sdkDepKind: sdkDep.startsWith('github:') ? 'git' :
763
473
  (sdkDep.startsWith('http') ? 'release' : 'npm'),
764
- // THE OPT-IN A NON-REGISTRY DEPENDENCY NEEDS, or ''.
765
- //
766
- // npm 12 defaults `allow-git` and `allow-remote` to "none" and REFUSES
767
- // such a dependency outright — EALLOWGIT / EALLOWREMOTE, before it
768
- // fetches anything. Older npm allowed both, so a workflow that passes
769
- // today fails the moment its runner image updates; this provider's own
770
- // publish run failed that way within hours of the build run passing,
771
- // because publishing upgrades npm first.
772
- //
773
- // Emitted ONLY where the project chose such a dependency: it is the
774
- // project opting into what it already declared, not a default weakened
775
- // for everyone. Publishing the SDK remains the durable answer, and this
776
- // flag exists so an unpublished one is workable meanwhile.
777
474
  sdkInstallFlag: sdkDep.startsWith('github:') ? ' --allow-git=all' :
778
475
  (sdkDep.startsWith('http') ? ' --allow-remote=all' : ''),
779
476
  repoUrl: repo.url,
780
- // The SDK's own repo, for pointing at the companion test server which is
781
- // only distributed in source.
782
477
  sdkRepoUrl,
783
478
  sdkTag,
784
479
  sdkRepoDir,
785
480
  sdkPinned,
786
- // Where the SDK SOURCE is reached, as every generated file spells it.
787
- // Layout-independent when the SDK has a repo to pin. See above.
788
481
  sdkSrc,
789
482
  api: apiName(model),
790
483
  version: packageVersion(model, target.name),
791
484
  liveBase,
792
485
  liveApp,
793
- // The sponsor line. @seneca/maintain's `content_readme` check requires
794
- // the publisher's name in the README, so this is load-bearing rather
795
- // than decorative — the generated `maintain` test fails without it.
796
486
  publisher: PUBLISHER,
797
487
  publisherUrl: PUBLISHER_URL,
798
- // A route with NO path params, so the probe is a plain GET. An entity
799
- // whose every route is parameterised gives none, and then the probe
800
- // falls back to the base URL itself.
801
488
  probePath: (entities.find((e: any) =>
802
489
  '' !== e.path && !e.path.includes('{')) || { path: '' }).path,
803
- // The provider's OWN published name. Not derived from the SDK slug the
804
- // way a language target's is — a provider is `@seneca/<name>-provider` —
805
- // so read the project's pin directly and default to that shape.
806
490
  pkgName: providerPackage(model, lower, target.name),
807
491
  // Whether this API authenticates at all. Decides if the plugin plumbs a
808
492
  // credential: the SDK's auth stage emits nothing for an auth-inactive
809
493
  // model and strips the authorization header regardless of options, so
810
494
  // plumbing one anyway produces a credential path that cannot work.
811
495
  authActive: isAuthActive(model),
812
- // Whether this API's scheme is genuine HTTP Basic Auth (two credentials,
813
- // base64-joined), matching the SDK's own auth.basic signal. Decides
814
- // whether the plugin also plumbs a `secret` alongside `apikey` — a
815
- // Basic-Auth SDK's own auth stage deletes the header when either is
816
- // missing, so a provider forwarding apikey alone could never
817
- // authenticate, the same class of gap `apikey`-only forwarding was.
818
496
  authBasic: isAuthActive(model) && isHttpBasicAuth(model),
819
497
  }
820
498
 
@@ -824,8 +502,6 @@ const Main = cmp(function Main(props: any) {
824
502
  // target calls its own.
825
503
  Gitignore({})
826
504
 
827
- // Static furniture: LICENSE, CODE_OF_CONDUCT, Makefile, tsfmt.json and
828
- // both tsconfigs. Same for every provider.
829
505
  Copy({
830
506
  from: 'tm/' + target.name,
831
507
  replace: { ...ctx$.stdrep },
@@ -843,36 +519,6 @@ const Main = cmp(function Main(props: any) {
843
519
  })
844
520
 
845
521
 
846
- // HOW THE PROVIDER DEPENDS ON THE SDK IT WRAPS, as one dependency value.
847
- //
848
- // Default: the PUBLISHED package pinned to the version the `ts` target
849
- // publishes, so the two can never disagree. Right whenever the SDK is on a
850
- // registry — and wrong when it is not. An SDK for a private API, or one not
851
- // published yet, leaves the provider unable to `npm install` at all: the
852
- // dependency 404s, so the package cannot be built, tested or released. That
853
- // is not hypothetical; it is why @seneca/github-provider could not be
854
- // regenerated and released for weeks.
855
- //
856
- // `kind: 'git'` points at a GIT TAG instead, which needs no registry.
857
- //
858
- // NPM RESOLVES A GIT DEPENDENCY AGAINST THE REPOSITORY ROOT, and sdkgen
859
- // generates the TypeScript SDK into `ts/`. So `kind: 'git'` suits an SDK
860
- // whose package.json IS the repository root, and NOT the layout this
861
- // toolchain produces.
862
- //
863
- // THE `::path:` SUBDIRECTORY SYNTAX DOES NOT WORK, and it looks like it
864
- // does. npm-package-arg parses `#<ref>::path:ts` and reports
865
- // `gitSubdir: /ts`, so a spec built that way reads as correct — but the
866
- // INSTALLER ignores it: npm clones the repository and opens package.json at
867
- // the clone root, failing with ENOENT on linux, macOS and Windows alike.
868
- // That was measured, on all three, after the parser had said otherwise.
869
- //
870
- // `kind: 'release'` is the form that works for a package in a subdirectory:
871
- // a GitHub release asset, which is `npm pack` output attached to the tag.
872
- // npm installs an https tarball natively and never looks at the repository
873
- // layout at all.
874
- //
875
- // `spec` still wins over all of it, for anything the shorthand cannot say.
876
522
  function sdkDependency(model: any, target: any, provider: any): string {
877
523
  const dep = model?.main?.[KIT]?.target?.[target.name]?.sdk?.dep || {}
878
524
 
@@ -913,8 +559,6 @@ function sdkDependency(model: any, target: any, provider: any): string {
913
559
  }
914
560
 
915
561
  if ('release' === kind) {
916
- // The asset `npm pack` produces: scope and name flattened, then the
917
- // version. `asset` overrides it for a project that names its own.
918
562
  const asset = String(dep.asset || '').trim() || (
919
563
  provider.sdkPkg.replace(/^@/, '').replace(/\//g, '-') +
920
564
  '-' + provider.sdkVersion + '.tgz')
@@ -926,7 +570,6 @@ function sdkDependency(model: any, target: any, provider: any): string {
926
570
  }
927
571
 
928
572
 
929
- // --- package.json -----------------------------------------------------------
930
573
 
931
574
  const PackageJson = cmp(function PackageJson(props: any) {
932
575
  const { provider, target } = props
@@ -934,19 +577,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
934
577
 
935
578
  const deps = collectDeps(model, target.name, target.deps, props.ctx$.log)
936
579
 
937
- // collectDeps returns { name, version, source, raw } — the DECLARED kind
938
- // (prod/peer/dev) is on `raw`, not hoisted. Reading `d.kind` instead
939
- // silently yields an empty manifest section, which is how the first cut of
940
- // this component shipped a package.json with no seneca peers at all.
941
- //
942
- // `kind` is a COMMA-SEPARATED LIST, so one dependency can land in more than
943
- // one manifest section. That is the Seneca plugin convention and not an
944
- // embellishment: `seneca` is a PEER (the plugin must run inside the host's
945
- // instance, never its own bundled copy) and also a DEV dependency (the test
946
- // suite does `require('seneca')` directly, so a bare `npm install` in a
947
- // clean checkout has to produce it). collectDeps deduplicates by package
948
- // name and the model's dep map is keyed by name, so the same package cannot
949
- // be declared twice — the kind has to carry the list instead.
950
580
  const dep = (kind: string) => {
951
581
  const out: Record<string, string> = {}
952
582
  const kinds = (d: any) => String((d.raw && d.raw.kind) || '')
@@ -978,9 +608,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
978
608
  keywords: ['seneca', provider.lower, `${provider.lower}-provider`,
979
609
  provider.publisher.toLowerCase(), 'sdk'],
980
610
  author,
981
- // Omitted entirely when the project names none, rather than emitted as an
982
- // empty array — npm treats `"contributors": []` as a declaration that
983
- // there are none, which is a different claim from not having said.
984
611
  ...(0 < contributors.length ? { contributors } : {}),
985
612
  license: 'MIT',
986
613
  repository: { type: 'git', url: `git+${provider.repoUrl}.git` },
@@ -1012,17 +639,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
1012
639
  'npm run build && npm run test && npm run repo-tag && ' +
1013
640
  'npm publish --access public --registry https://registry.npmjs.org',
1014
641
  },
1015
- // What actually ships. Without `files`, `npm publish` packs the test
1016
- // suite and build output into the tarball.
1017
- //
1018
- // `doc` IS PART OF THE PACKAGE. The generated README links to
1019
- // doc/tutorial.md, doc/how-to.md, doc/reference.md and
1020
- // doc/explanation.md with relative paths, so omitting it published a
1021
- // README whose every documentation link 404s for anyone reading the
1022
- // installed package rather than the repository. Either the links become
1023
- // absolute repository URLs or the files ship; they ship, because the
1024
- // docs describe the exact version installed and a URL would drift to
1025
- // whatever main says later.
1026
642
  files: ['dist', 'doc', 'src/**/*.ts', 'LICENSE'],
1027
643
  engines: { node: '>=24' },
1028
644
  dependencies: {
@@ -1042,7 +658,6 @@ const PackageJson = cmp(function PackageJson(props: any) {
1042
658
  })
1043
659
 
1044
660
 
1045
- // --- src/<name>-provider.ts -------------------------------------------------
1046
661
 
1047
662
  const ProviderSource = cmp(function ProviderSource(props: any) {
1048
663
  const { provider } = props
@@ -1212,26 +827,6 @@ function ${provider.pluginName}(this: any, options: ${provider.pluginName}Option
1212
827
  })
1213
828
  }
1214
829
 
1215
- // Seneca's own entity key is ALWAYS literally `id` — `load$('x')` sets
1216
- // `q = { id: 'x' }`, and the id of the entity it builds comes off
1217
- // `data.id`. An API that addresses a record by anything else therefore
1218
- // needs translating in both directions, or `load$` requests a record
1219
- // keyed `undefined` and every entity handed back has no id at all — one
1220
- // that cannot then be saved or removed.
1221
- // ID TRANSLATION, ONE ALGORITHM AND A TABLE.
1222
- //
1223
- // Seneca entities carry exactly one `id`. Plenty of APIs do not: they
1224
- // key a record by a differently-named field (`repo`, `number`), or by
1225
- // SEVERAL path parameters at once with no single one that is the id
1226
- // (github's `/repos/{owner}/{repo}`). Both need translating in both
1227
- // directions, or `load$` asks for a record keyed `undefined` and every
1228
- // record handed back has no id to save or remove it by.
1229
- //
1230
- // The LOGIC is the same for every API; only which parameters, which
1231
- // separator and where they live in a response differ, and those are
1232
- // model data. So this emits one `splitid`, one `joinid` and a table —
1233
- // not a bespoke pair of functions per entity, which is what it used to
1234
- // do and which duplicated the same algorithm N times over.
1235
830
  const translated = provider.entities.filter((e: any) => null != idSpec(e.ent))
1236
831
 
1237
832
  if (0 < translated.length) {
@@ -1349,14 +944,6 @@ ${rows}
1349
944
  `)
1350
945
  }
1351
946
 
1352
- // WHICH SDK OP SERVES EACH `action$`, per entity and per cmd.
1353
- //
1354
- // Emitted for EVERY cmd, including the ones with no actions at all.
1355
- // That empty map is not waste: it is what lets `actionop` refuse an
1356
- // `action$` on an entity that has none, instead of ignoring the key
1357
- // and performing an ordinary call. Passing `action$` and getting a
1358
- // plain save is the failure this whole mechanism exists to prevent —
1359
- // it is how GitHub's `merge` silently became an "update".
1360
947
  Content(` // The custom actions each cmd can reach, as action -> SDK op. An action
1361
948
  // is an alternative POINT of an ordinary op (\`select.$action\` in the API
1362
949
  // model), so \`save$\` routes by this map rather than assuming update: an
@@ -1370,12 +957,6 @@ ${rows}
1370
957
  const name = String(cmd.val$ ?? cmd)
1371
958
  const map = e.actions[name] || {}
1372
959
  const names = Object.keys(map).sort()
1373
- // COMPUTED KEYS, not `jsKey`. An action named `__proto__` written
1374
- // as a plain (or quoted) object-literal key SETS THE PROTOTYPE
1375
- // instead of creating a property, so the action would vanish from
1376
- // its own map and be unreachable. A computed key always defines an
1377
- // own property. apidef derives an action name from a route segment,
1378
- // and `__proto__` is a legal one.
1379
960
  Content(` ${name}: {${names.map((a: string) =>
1380
961
  ` [${JSON.stringify(a)}]: '${map[a]}'`).join(',')}${0 < names.length ? ' ' : ''}},
1381
962
  `)
@@ -1450,8 +1031,6 @@ ${rows}
1450
1031
 
1451
1032
  `)
1452
1033
 
1453
- // The cmd map, declared up front so every action is attached to a
1454
- // shape seneca-entity can read before the actions are defined.
1455
1034
  Content(` const entity: any = {
1456
1035
  `)
1457
1036
  each(provider.entities, (e: any) => {
@@ -1472,22 +1051,7 @@ ${rows}
1472
1051
  `)
1473
1052
 
1474
1053
  each(provider.entities, (e: any) => {
1475
- // The guard set is PER OP, not the union across the entity's ops. An
1476
- // entity whose routes are not uniformly nested — a flat `load`, a
1477
- // nested `create` — used to demand the parent id on the flat call too,
1478
- // an argument its own SDK request would then never use.
1479
- //
1480
- // `save` guards the union of create's and update's keys: one action
1481
- // serves both and dispatches at runtime, so it cannot know which set
1482
- // applies until it has the data.
1483
- // A COMPOSITE-KEY ENTITY GUARDS NOTHING SEPARATELY. Its parent keys
1484
- // travel INSIDE the id, so demanding `q.owner` as well would reject
1485
- // `load$('octocat/hello-world')` — the very call the composite id
1486
- // exists to allow. splitid() does the checking instead, and refuses
1487
- // a wrong part count by name.
1488
1054
  const eparts = idParts(e.ent)
1489
- // The whole id description for this entity, or null when none is
1490
- // needed. `out` branches on it rather than on the record key.
1491
1055
  const espec = idSpec(e.ent)
1492
1056
  const guard = (cmd: string, src: string) => {
1493
1057
  const keys = 'save' === cmd ?
@@ -1501,12 +1065,6 @@ ${rows}
1501
1065
  .join('')
1502
1066
  }
1503
1067
 
1504
- // THE REFUSAL LINE, for a cmd whose only route addresses a
1505
- // different resource. Placed AFTER the action branch — an action
1506
- // route is named explicitly and is exactly how the real operation is
1507
- // reached — and BEFORE the guards, so the caller is told the cmd
1508
- // does not exist for this entity rather than being asked for a
1509
- // parameter that would not have helped.
1510
1068
  const refuse = (cmd: string) => {
1511
1069
  if (true !== (e.idmisaddressed || {})[cmd]) {
1512
1070
  return ''
@@ -1520,26 +1078,8 @@ ${rows}
1520
1078
  `
1521
1079
  }
1522
1080
 
1523
- // Reading the record's own key off the Seneca query, which always
1524
- // spells it `id`, and every other required key off its own name.
1525
1081
  const rk = recordKey(e.ent)
1526
1082
 
1527
- // For a composite key the parameters come out of the split, which the
1528
- // handler binds to `key` before the call. Any required key the id
1529
- // does NOT carry still comes off the query.
1530
- // THE RECORD'S OWN KEY IS ALWAYS SENT, whether or not the match
1531
- // declares it required.
1532
- //
1533
- // An op gathers several routes, and a parameter only one of them
1534
- // uses comes through OPTIONAL: github's IssueLoadMatch has `owner`
1535
- // and `repo` required but `id` optional, because the op also covers
1536
- // `/repos/{owner}/{repo}/issues/comments/{comment_id}`. Sending only
1537
- // the required keys called `Issue().load({owner, repo})` — the id
1538
- // the caller passed to `load$` went nowhere, so every read of any
1539
- // issue in that repo answered with the same record and
1540
- // `load$('no-such-issue')` returned one. Ten entities failed their
1541
- // not-found test on it, and the ones that "passed" were passing for
1542
- // the wrong reason. addressKeys is what the test emitter reads too.
1543
1083
  const sdkArg = (opname: string) => {
1544
1084
  const keys = addressKeys(e.ent, opname)
1545
1085
  if (0 === keys.length) {
@@ -1571,24 +1111,6 @@ ${rows}
1571
1111
  null == espec ? `plain(${expr})` :
1572
1112
  `joinid('${e.name}', plain(${expr})${null == vals ? '' : ', ' + vals})`
1573
1113
 
1574
- // The action branch, emitted for every cmd whether or not this entity
1575
- // has actions. `actionop` is what refuses an unknown name, so leaving
1576
- // it out where the map is empty would restore the silent drop for
1577
- // exactly the entities most likely to be typed at by mistake.
1578
- //
1579
- // IT COMES BEFORE THE PARENT GUARDS, and that ordering is the whole
1580
- // of its correctness. The guards describe the CANONICAL route —
1581
- // opParams drops action points when computing them — and an action
1582
- // route need not be nested the same way: Zoom's canonical update is
1583
- // `/user/{user_id}/meeting/{id}` while its status action hangs off
1584
- // `/meeting/{id}`. Guarding first rejected that action for want of a
1585
- // `user_id` its own URL has no segment for.
1586
- //
1587
- // The action path is not left unguarded, it is guarded by the RIGHT
1588
- // thing: the SDK builds the action's own point and refuses an
1589
- // unbuildable one with `point_action_invalid`, naming the op and the
1590
- // action. A call naming no action falls through to the guards exactly
1591
- // as before.
1592
1114
  const actionBranch = (cmd: string, call: string) => ` const action$ = actionOf(msg)
1593
1115
  if (null != action$) {
1594
1116
  const op$ = actionop(action$, '${e.name}', '${cmd}')
@@ -1630,15 +1152,6 @@ ${actionBranch('load',
1630
1152
  const hasCreate = e.ops.includes('create')
1631
1153
  const hasUpdate = e.ops.includes('update')
1632
1154
 
1633
- // Dispatch on the SENECA key. The record arriving here is a Seneca
1634
- // entity's data, so its id lives at `id` whatever the API calls it —
1635
- // dispatching on the API's key sent every save to `create`, leaving
1636
- // update unreachable.
1637
- // A MISADDRESSED UPDATE IS REFUSED, BUT ONLY ON THE UPDATE LEG. A
1638
- // create needs no record key — the API assigns one — so an entity
1639
- // whose update route addresses a different resource can still be
1640
- // created. Dispatch is on `data.id`, so the refusal goes exactly
1641
- // where the update would have.
1642
1155
  const refuseUpdate = true === (e.idmisaddressed || {}).update ?
1643
1156
  refuse('update') : ''
1644
1157
 
@@ -1654,25 +1167,6 @@ ${actionBranch('load',
1654
1167
  ? ` const res = await sdk.${e.acc}(${entArg}).create(data)`
1655
1168
  : `${refuseUpdate} const res = await sdk.${e.acc}(${entArg}).update(data)`
1656
1169
 
1657
- // ... and hand the API back its own key, which Seneca does not know
1658
- // to send.
1659
- //
1660
- // A COMPOSITE KEY IS UNPACKED ONTO THE DATA. The write goes out as
1661
- // a body plus path parameters, and the SDK reads those parameters
1662
- // off the same object — so the parts have to be present under
1663
- // their own names, not fused into `id`. On a create there is no id
1664
- // yet and the caller supplies the parts directly, which is why this
1665
- // only runs when an id is there.
1666
- // THE COMPOSITE SPLIT CANNOT LIVE HERE, before the action branch,
1667
- // even though that is where the single-key alias sits. The alias
1668
- // only ever assigns; the split THROWS on an id that is not all its
1669
- // parts, and an `action$` call is entitled to an id shaped however
1670
- // that action's own route wants. Running it first turned
1671
- // `action$: 'no_such_action'` into an id complaint, hiding the
1672
- // error the caller needed. It is emitted after the action branch
1673
- // instead — see compositeSave below, spliced where the parent
1674
- // guards go, which is exactly the position the component already
1675
- // documents as "after the action branch".
1676
1170
  const compositeSave = 0 < eparts.length ? `
1677
1171
  // Seneca carries this ${e.name}'s key as one \`id\`; the API addresses
1678
1172
  // the record by ${eparts.map((p: string) => '`' + p + '`').join(' and ')}.
@@ -1746,20 +1240,6 @@ ${actionBranch('save',
1746
1240
  }
1747
1241
 
1748
1242
  if (e.cmds.includes('remove')) {
1749
- // A REMOVE ACTION ANSWERS FOR ITSELF. The canonical remove has
1750
- // nothing to hand back — the record is gone — so its handler
1751
- // returns null and takes no `entize`. An action folded into
1752
- // `remove` is a different endpoint with a response of its own
1753
- // (`/meeting/{id}/archive` answers with the archive record), and
1754
- // the other three cmds all pass an action's response through.
1755
- // Applying the canonical "return null" to it threw that away, so
1756
- // the action appeared to succeed and yielded nothing.
1757
- //
1758
- // `entize` is therefore named, never `_entize`. Naming it
1759
- // conditionally on the entity HAVING a remove action does not work
1760
- // and the type-check says so: the action branch is emitted for
1761
- // every entity — that is what refuses an unknown `action$` on one
1762
- // with no actions — so the parameter is always referenced.
1763
1243
  Content(`
1764
1244
  ${jsProp('entity', e.name)}.cmd.remove.action =
1765
1245
  async function remove_${e.name}(this: any, entize: any, msg: any) {
@@ -1883,7 +1363,6 @@ if ('undefined' !== typeof module) {
1883
1363
  })
1884
1364
 
1885
1365
 
1886
- // --- src/<Name>Provider-doc.ts ----------------------------------------------
1887
1366
 
1888
1367
  const ProviderDoc = cmp(function ProviderDoc(props: any) {
1889
1368
  const { provider } = props