ts-procedures 8.4.0 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/build/codegen/bin/cli.d.ts +4 -0
  2. package/build/codegen/bin/cli.js +16 -0
  3. package/build/codegen/bin/cli.js.map +1 -1
  4. package/build/codegen/bin/cli.test.js +18 -0
  5. package/build/codegen/bin/cli.test.js.map +1 -1
  6. package/build/codegen/bin/flag-specs.js +2 -0
  7. package/build/codegen/bin/flag-specs.js.map +1 -1
  8. package/build/codegen/bin/flag-specs.test.js +9 -0
  9. package/build/codegen/bin/flag-specs.test.js.map +1 -1
  10. package/build/codegen/collect-models.d.ts +14 -3
  11. package/build/codegen/collect-models.js +15 -5
  12. package/build/codegen/collect-models.js.map +1 -1
  13. package/build/codegen/collect-models.test.js +21 -2
  14. package/build/codegen/collect-models.test.js.map +1 -1
  15. package/build/codegen/index.d.ts +10 -0
  16. package/build/codegen/index.js +3 -0
  17. package/build/codegen/index.js.map +1 -1
  18. package/build/codegen/pipeline.d.ts +4 -0
  19. package/build/codegen/pipeline.js +3 -0
  20. package/build/codegen/pipeline.js.map +1 -1
  21. package/build/codegen/targets/_shared/target-run.d.ts +10 -0
  22. package/build/codegen/targets/ts/run.js +11 -2
  23. package/build/codegen/targets/ts/run.js.map +1 -1
  24. package/build/codegen/targets/ts/shared-models.test.js +97 -1
  25. package/build/codegen/targets/ts/shared-models.test.js.map +1 -1
  26. package/docs/client-and-codegen.md +62 -0
  27. package/docs/handoffs/shared-models-auto-resolve-response.md +181 -0
  28. package/docs/superpowers/plans/2026-06-06-shared-models-convention-and-diagnostics.md +659 -0
  29. package/package.json +1 -1
  30. package/src/codegen/bin/cli.test.ts +27 -0
  31. package/src/codegen/bin/cli.ts +18 -0
  32. package/src/codegen/bin/flag-specs.test.ts +11 -0
  33. package/src/codegen/bin/flag-specs.ts +2 -0
  34. package/src/codegen/collect-models.test.ts +24 -2
  35. package/src/codegen/collect-models.ts +22 -5
  36. package/src/codegen/index.ts +13 -0
  37. package/src/codegen/pipeline.ts +7 -0
  38. package/src/codegen/targets/_shared/target-run.ts +10 -0
  39. package/src/codegen/targets/ts/run.ts +18 -1
  40. package/src/codegen/targets/ts/shared-models.test.ts +109 -1
@@ -555,3 +555,30 @@ describe('--share-models', () => {
555
555
  expect(parseArgs(['--out', 'g', '--url', 'u'], cfg as CodegenConfig).sharedTypesImport).toEqual(cfg.sharedTypesImport)
556
556
  })
557
557
  })
558
+
559
+ describe('--shared-models-module and --strict-shared-models', () => {
560
+ it('parses --shared-models-module into sharedModelsModule', () => {
561
+ const parsed = parseArgs(['--out', 'gen', '--file', 'e.json', '--shared-models-module', '@app/schemas'])
562
+ expect(parsed.sharedModelsModule).toBe('@app/schemas')
563
+ })
564
+
565
+ it('parses --strict-shared-models as a boolean (default false)', () => {
566
+ expect(parseArgs(['--out', 'gen', '--file', 'e.json']).strictSharedModels).toBe(false)
567
+ expect(
568
+ parseArgs(['--out', 'gen', '--file', 'e.json', '--strict-shared-models']).strictSharedModels,
569
+ ).toBe(true)
570
+ })
571
+
572
+ it('CLI --shared-models-module overrides a config value', () => {
573
+ const parsed = parseArgs(
574
+ ['--out', 'gen', '--file', 'e.json', '--shared-models-module', '@cli/pkg'],
575
+ { sharedModelsModule: '@config/pkg' },
576
+ )
577
+ expect(parsed.sharedModelsModule).toBe('@cli/pkg')
578
+ })
579
+
580
+ it('strictSharedModels is seeded from config when the flag is absent', () => {
581
+ const parsed = parseArgs(['--out', 'gen', '--file', 'e.json'], { strictSharedModels: true })
582
+ expect(parsed.strictSharedModels).toBe(true)
583
+ })
584
+ })
@@ -30,6 +30,8 @@ export interface CodegenConfig {
30
30
  unsupportedUnions?: 'throw' | 'fallback'
31
31
  shareModels?: boolean
32
32
  sharedTypesImport?: SharedTypesImportMap
33
+ sharedModelsModule?: string
34
+ strictSharedModels?: boolean
33
35
  }
34
36
 
35
37
  export interface ParsedArgs {
@@ -51,6 +53,8 @@ export interface ParsedArgs {
51
53
  unsupportedUnions?: 'throw' | 'fallback'
52
54
  shareModels: boolean
53
55
  sharedTypesImport?: SharedTypesImportMap
56
+ sharedModelsModule?: string
57
+ strictSharedModels: boolean
54
58
  }
55
59
 
56
60
  // ---------------------------------------------------------------------------
@@ -170,6 +174,8 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
170
174
  let unsupportedUnions: 'throw' | 'fallback' | undefined = config?.unsupportedUnions
171
175
  let shareModels = config?.shareModels ?? true
172
176
  const sharedTypesImport = config?.sharedTypesImport
177
+ let sharedModelsModule: string | undefined = config?.sharedModelsModule
178
+ let strictSharedModels = config?.strictSharedModels ?? false
173
179
  let configPath: string | undefined
174
180
 
175
181
  for (let i = 0; i < argv.length; i++) {
@@ -260,6 +266,10 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
260
266
  shareModels = true
261
267
  } else if (arg === '--no-share-models') {
262
268
  shareModels = false
269
+ } else if (arg === '--shared-models-module') {
270
+ sharedModelsModule = argv[++i]
271
+ } else if (arg === '--strict-shared-models') {
272
+ strictSharedModels = true
263
273
  } else if (arg === '--config') {
264
274
  configPath = argv[++i]
265
275
  } else if (arg !== undefined && arg.startsWith('--')) {
@@ -336,6 +346,8 @@ export function parseArgs(argv: string[], config?: CodegenConfig): ParsedArgs {
336
346
  ...(unsupportedUnions !== undefined ? { unsupportedUnions } : {}),
337
347
  shareModels,
338
348
  ...(sharedTypesImport !== undefined ? { sharedTypesImport } : {}),
349
+ ...(sharedModelsModule !== undefined ? { sharedModelsModule } : {}),
350
+ strictSharedModels,
339
351
  }
340
352
  }
341
353
 
@@ -425,6 +437,9 @@ async function runWithWatch(parsed: ParsedArgs): Promise<void> {
425
437
  ...(parsed.swift?.accessLevel !== undefined ? { swiftAccessLevel: parsed.swift.accessLevel } : {}),
426
438
  shareModels: parsed.shareModels,
427
439
  ...(parsed.sharedTypesImport !== undefined ? { sharedTypesImport: parsed.sharedTypesImport } : {}),
440
+ ...(parsed.sharedModelsModule !== undefined ? { sharedModelsModule: parsed.sharedModelsModule } : {}),
441
+ strictSharedModels: parsed.strictSharedModels,
442
+ logger: (message: string) => { console.log(message) },
428
443
  ...kotlinWiring,
429
444
  ...swiftWiring,
430
445
  })
@@ -555,6 +570,9 @@ async function main(): Promise<void> {
555
570
  ...(parsed.swift?.accessLevel !== undefined ? { swiftAccessLevel: parsed.swift.accessLevel } : {}),
556
571
  shareModels: parsed.shareModels,
557
572
  ...(parsed.sharedTypesImport !== undefined ? { sharedTypesImport: parsed.sharedTypesImport } : {}),
573
+ ...(parsed.sharedModelsModule !== undefined ? { sharedModelsModule: parsed.sharedModelsModule } : {}),
574
+ strictSharedModels: parsed.strictSharedModels,
575
+ logger: (message: string) => { console.log(message) },
558
576
  ...kotlinWiring,
559
577
  ...swiftWiring,
560
578
  })
@@ -24,4 +24,15 @@ describe('flag-specs', () => {
24
24
  expect(KNOWN_FLAGS).not.toContain('--help')
25
25
  expect(KNOWN_FLAGS).not.toContain('-h')
26
26
  })
27
+
28
+ it('catalogs the shared-models convention + strict flags', () => {
29
+ expect(KNOWN_FLAGS).toContain('--shared-models-module')
30
+ expect(KNOWN_FLAGS).toContain('--strict-shared-models')
31
+ })
32
+
33
+ it('documents the new flags in --help output', () => {
34
+ const help = formatHelp()
35
+ expect(help).toContain('--shared-models-module')
36
+ expect(help).toContain('--strict-shared-models')
37
+ })
27
38
  })
@@ -27,6 +27,8 @@ export const FLAG_SPECS: readonly FlagSpec[] = [
27
27
  { name: '--client-import-path', arg: '<path>', description: 'Override the client runtime import path', group: 'Codegen' },
28
28
  { name: '--share-models', description: 'Hoist $id-bearing schemas into a shared _models.ts', group: 'Codegen', default: 'on' },
29
29
  { name: '--no-share-models', description: 'Inline every type per route (legacy behaviour)', group: 'Codegen' },
30
+ { name: '--shared-models-module', arg: '<module>', description: 'Re-export every $id model from one module (convention; sharedTypesImport overrides)', group: 'Codegen' },
31
+ { name: '--strict-shared-models', description: 'Fail if any $id model would be generated as a local twin', group: 'Codegen' },
30
32
  { name: '--jsdoc', description: 'Emit JSDoc on generated types', group: 'Codegen', default: 'on' },
31
33
  { name: '--no-jsdoc', description: 'Suppress JSDoc', group: 'Codegen' },
32
34
  { name: '--enum-style', arg: '<union|enum>', description: 'How to emit enums (namespace mode)', group: 'Codegen' },
@@ -40,7 +40,29 @@ it('does NOT throw for ordinary schemas without the reserved prefix', () => {
40
40
 
41
41
  it('resolveModelImports tags mapped models and leaves others generated', () => {
42
42
  const models = [{ id: 'urn:msg', name: 'Message', schema: {} as any }]
43
- const mapped = resolveModelImports(models, { 'urn:msg': { module: '@shared/schemas', name: 'Message' } })
43
+ const mapped = resolveModelImports(models, {
44
+ sharedTypesImport: { 'urn:msg': { module: '@shared/schemas', name: 'Message' } },
45
+ })
44
46
  expect(mapped[0]?.import).toEqual({ module: '@shared/schemas', name: 'Message' })
45
- expect(resolveModelImports(models, {})[0]?.import).toBeUndefined()
47
+ expect(resolveModelImports(models)[0]?.import).toBeUndefined()
48
+ })
49
+
50
+ it('resolveModelImports falls back to sharedModelsModule when no map entry matches', () => {
51
+ const models = [{ id: 'urn:msg', name: 'Message', schema: {} as any }]
52
+ const resolved = resolveModelImports(models, { sharedModelsModule: '@app/schemas' })
53
+ expect(resolved[0]?.import).toEqual({ module: '@app/schemas', name: 'Message' })
54
+ })
55
+
56
+ it('resolveModelImports: explicit map entry wins over the convention', () => {
57
+ const models = [{ id: 'urn:msg', name: 'Message', schema: {} as any }]
58
+ const resolved = resolveModelImports(models, {
59
+ sharedTypesImport: { 'urn:msg': { module: '@override/pkg', name: 'Msg' } },
60
+ sharedModelsModule: '@app/schemas',
61
+ })
62
+ expect(resolved[0]?.import).toEqual({ module: '@override/pkg', name: 'Msg' })
63
+ })
64
+
65
+ it('resolveModelImports: empty-string convention is treated as unset (generated locally)', () => {
66
+ const models = [{ id: 'urn:msg', name: 'Message', schema: {} as any }]
67
+ expect(resolveModelImports(models, { sharedModelsModule: '' })[0]?.import).toBeUndefined()
46
68
  })
@@ -93,16 +93,33 @@ export function collectModels(routes: AnyHttpRouteDoc[]): CollectedModel[] {
93
93
  })
94
94
  }
95
95
 
96
+ /** Options controlling how collected models are resolved to external imports. */
97
+ export interface ResolveModelImportsOptions {
98
+ /** Per-`$id` external import. A matching entry wins over `sharedModelsModule`. */
99
+ sharedTypesImport?: SharedTypesImportMap
100
+ /** Single module every otherwise-unmapped `$id` model re-exports from (convention). */
101
+ sharedModelsModule?: string
102
+ }
103
+
96
104
  /**
97
- * Tags each collected model with its external import when its `$id` is a key in
98
- * the import map; models without a mapping keep `import` undefined (generated locally).
105
+ * Tags each collected model with its external import. Precedence:
106
+ * 1. an explicit `sharedTypesImport[$id]` entry (per-type override / rename),
107
+ * 2. otherwise, when `sharedModelsModule` is set, the convention
108
+ * `{ module: sharedModelsModule, name: model.name }` — every shared model
109
+ * re-exports from one module under its derived name (`$id`/`title` === export),
110
+ * 3. otherwise `import` stays undefined and the model is generated locally.
99
111
  */
100
112
  export function resolveModelImports(
101
113
  models: CollectedModel[],
102
- map: SharedTypesImportMap = {}
114
+ options: ResolveModelImportsOptions = {},
103
115
  ): ResolvedModel[] {
116
+ const { sharedTypesImport = {}, sharedModelsModule } = options
104
117
  return models.map((model) => {
105
- const mapped = map[model.id]
106
- return mapped ? { ...model, import: mapped } : { ...model }
118
+ const mapped = sharedTypesImport[model.id]
119
+ if (mapped) return { ...model, import: mapped }
120
+ if (sharedModelsModule != null && sharedModelsModule !== '') {
121
+ return { ...model, import: { module: sharedModelsModule, name: model.name } }
122
+ }
123
+ return { ...model }
107
124
  })
108
125
  }
@@ -18,6 +18,16 @@ export interface GenerateClientOptions extends ResolveInput {
18
18
  shareModels?: boolean
19
19
  /** Maps a model `$id` to an external import so a shared model is re-exported rather than generated. */
20
20
  sharedTypesImport?: SharedTypesImportMap
21
+ /** Single module every $id-bearing model re-exports from (convention; `sharedTypesImport` entries override). */
22
+ sharedModelsModule?: string
23
+ /** Hard-fail codegen if any $id-bearing model would be generated as a local structural twin. */
24
+ strictSharedModels?: boolean
25
+ /**
26
+ * Sink for non-error progress messages (e.g. the shared-models summary).
27
+ * Omitted by default so programmatic callers produce no console output; pass
28
+ * `console.log` to opt in. The CLI wires this to stdout.
29
+ */
30
+ logger?: (message: string) => void
21
31
  target?: 'ts' | 'kotlin' | 'swift'
22
32
  kotlinPackage?: string
23
33
  kotlinSerializer?: 'kotlinx' | 'none'
@@ -44,6 +54,9 @@ export async function generateClient(options: GenerateClientOptions): Promise<Ge
44
54
  cleanOutDir: options.cleanOutDir,
45
55
  shareModels: options.shareModels,
46
56
  sharedTypesImport: options.sharedTypesImport,
57
+ sharedModelsModule: options.sharedModelsModule,
58
+ strictSharedModels: options.strictSharedModels,
59
+ logger: options.logger,
47
60
  target: options.target,
48
61
  kotlinPackage: options.kotlinPackage,
49
62
  kotlinSerializer: options.kotlinSerializer,
@@ -23,6 +23,10 @@ export interface PipelineOptions {
23
23
  cleanOutDir?: boolean
24
24
  shareModels?: boolean
25
25
  sharedTypesImport?: SharedTypesImportMap
26
+ sharedModelsModule?: string
27
+ strictSharedModels?: boolean
28
+ /** Sink for non-error progress messages (shared-models summary). Defaults to silent. */
29
+ logger?: (message: string) => void
26
30
  target?: 'ts' | 'kotlin' | 'swift'
27
31
  kotlinPackage?: string
28
32
  kotlinSerializer?: 'kotlinx' | 'none'
@@ -72,6 +76,9 @@ export async function runPipeline(options: PipelineOptions): Promise<GeneratedFi
72
76
  cleanOutDir,
73
77
  shareModels,
74
78
  sharedTypesImport: options.sharedTypesImport,
79
+ sharedModelsModule: options.sharedModelsModule,
80
+ strictSharedModels: options.strictSharedModels,
81
+ logger: options.logger,
75
82
  }
76
83
 
77
84
  switch (options.target) {
@@ -30,4 +30,14 @@ export interface TargetRunInput {
30
30
  shareModels?: boolean
31
31
  /** Maps a model `$id` to an external import (re-exported instead of generated). */
32
32
  sharedTypesImport?: SharedTypesImportMap
33
+ /** Single module every $id-bearing model re-exports from (convention; map entries override). */
34
+ sharedModelsModule?: string
35
+ /** When true, hard-fail if any $id-bearing model has no shared-type mapping (would be a local twin). */
36
+ strictSharedModels?: boolean
37
+ /**
38
+ * Sink for non-error progress messages (e.g. the shared-models summary).
39
+ * Defaults to undefined — programmatic callers stay silent; the CLI injects
40
+ * `console.log`. Output belongs to the caller, not the run module.
41
+ */
42
+ logger?: (message: string) => void
33
43
  }
@@ -33,6 +33,9 @@ export async function runTsPipeline(input: TsRunInput): Promise<GeneratedFile[]>
33
33
  selfContained = false,
34
34
  cleanOutDir = false,
35
35
  sharedTypesImport,
36
+ sharedModelsModule,
37
+ strictSharedModels = false,
38
+ logger,
36
39
  } = input
37
40
  const shareModels = input.shareModels ?? false
38
41
 
@@ -60,7 +63,7 @@ export async function runTsPipeline(input: TsRunInput): Promise<GeneratedFile[]>
60
63
  // covers ALL models (generated AND externally-imported) since scopes always
61
64
  // import shared types from `./_models`.
62
65
  const models = shareModels
63
- ? resolveModelImports(collectModels(envelope.routes), sharedTypesImport)
66
+ ? resolveModelImports(collectModels(envelope.routes), { sharedTypesImport, sharedModelsModule })
64
67
  : []
65
68
  const idToModelName = new Map(models.map((m) => [m.id, m.name]))
66
69
 
@@ -74,6 +77,20 @@ export async function runTsPipeline(input: TsRunInput): Promise<GeneratedFile[]>
74
77
  }
75
78
  }
76
79
 
80
+ if (shareModels && models.length > 0) {
81
+ const generated = models.filter((m) => m.import == null)
82
+ if (strictSharedModels && generated.length > 0) {
83
+ const list = generated.map((m) => `"${m.id}" (${m.name})`).join(', ')
84
+ throw new Error(
85
+ `[ts-procedures-codegen] --strict-shared-models: ${generated.length} $id-bearing schema(s) have no shared-type mapping and would be generated as local structural twins: ${list}. Add a sharedTypesImport entry, set sharedModelsModule, or drop --strict-shared-models.`,
86
+ )
87
+ }
88
+ const reExported = models.length - generated.length
89
+ logger?.(
90
+ `[ts-procedures-codegen] Shared models: ${models.length} total — ${reExported} re-exported, ${generated.length} generated locally.`,
91
+ )
92
+ }
93
+
77
94
  const files: GeneratedFile[] = []
78
95
 
79
96
  for (const group of groups) {
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, it, expect, vi } from 'vitest'
2
2
  import { runPipeline } from '../../pipeline.js'
3
3
  import type { DocEnvelope } from '../../../implementations/types.js'
4
4
  import type { GeneratedFile } from '../_shared/write-files.js'
@@ -233,6 +233,114 @@ describe('shared models (TS target, ajsc x-named-type)', () => {
233
233
  expect(messages.code).toContain("import type { Message } from './_models'")
234
234
  })
235
235
 
236
+ it('sharedModelsModule re-exports every $id model from the convention module', async () => {
237
+ const files = await runPipeline({
238
+ envelope: modelEnvelope(),
239
+ outDir: 'out',
240
+ dryRun: true,
241
+ shareModels: true,
242
+ namespaceTypes: true,
243
+ selfContained: false,
244
+ sharedModelsModule: '@app/schemas',
245
+ })
246
+
247
+ const modelsFile = findFile(files, '_models.ts')!
248
+ // Re-exported, not generated.
249
+ expect(modelsFile.code).toContain("export { Message } from '@app/schemas'")
250
+ expect(countMatches(modelsFile.code, /export type Message =/g)).toBe(0)
251
+
252
+ // Scopes still import the shared name from ./_models.
253
+ expect(findFile(files, 'messages.ts')!.code).toContain("from './_models'")
254
+ })
255
+
256
+ it('explicit sharedTypesImport overrides the sharedModelsModule convention per $id', async () => {
257
+ const files = await runPipeline({
258
+ envelope: modelEnvelope(),
259
+ outDir: 'out',
260
+ dryRun: true,
261
+ shareModels: true,
262
+ namespaceTypes: true,
263
+ selfContained: false,
264
+ sharedModelsModule: '@app/schemas',
265
+ sharedTypesImport: { 'urn:msg': { module: '@override/pkg', name: 'Message' } },
266
+ })
267
+ expect(findFile(files, '_models.ts')!.code).toContain("export { Message } from '@override/pkg'")
268
+ })
269
+
270
+ it('emits a neutral summary of the re-exported / generated split via the injected logger', async () => {
271
+ const lines: string[] = []
272
+ await runPipeline({
273
+ envelope: modelEnvelope(),
274
+ outDir: 'out',
275
+ dryRun: true,
276
+ shareModels: true,
277
+ namespaceTypes: true,
278
+ selfContained: false,
279
+ logger: (m) => lines.push(m),
280
+ })
281
+ expect(lines.join('\n')).toContain('Shared models: 1 total — 0 re-exported, 1 generated locally.')
282
+ })
283
+
284
+ it('does not emit a summary when there are no $id-bearing models', async () => {
285
+ const lines: string[] = []
286
+ await runPipeline({
287
+ envelope: noModelEnvelope(),
288
+ outDir: 'out',
289
+ dryRun: true,
290
+ shareModels: true,
291
+ namespaceTypes: true,
292
+ selfContained: false,
293
+ logger: (m) => lines.push(m),
294
+ })
295
+ expect(lines.join('\n')).not.toContain('Shared models:')
296
+ })
297
+
298
+ it('stays silent (no console output) when no logger is injected', async () => {
299
+ const log = vi.spyOn(console, 'log').mockImplementation(() => {})
300
+ try {
301
+ await runPipeline({
302
+ envelope: modelEnvelope(),
303
+ outDir: 'out',
304
+ dryRun: true,
305
+ shareModels: true,
306
+ namespaceTypes: true,
307
+ selfContained: false,
308
+ })
309
+ expect(log.mock.calls.flat().join('\n')).not.toContain('Shared models:')
310
+ } finally {
311
+ log.mockRestore()
312
+ }
313
+ })
314
+
315
+ it('strictSharedModels throws listing the offending $id and derived name', async () => {
316
+ await expect(
317
+ runPipeline({
318
+ envelope: modelEnvelope(),
319
+ outDir: 'out',
320
+ dryRun: true,
321
+ shareModels: true,
322
+ namespaceTypes: true,
323
+ selfContained: false,
324
+ strictSharedModels: true,
325
+ }),
326
+ ).rejects.toThrow(/--strict-shared-models[\s\S]*urn:msg[\s\S]*Message/)
327
+ })
328
+
329
+ it('strictSharedModels passes once every model is covered by the convention', async () => {
330
+ await expect(
331
+ runPipeline({
332
+ envelope: modelEnvelope(),
333
+ outDir: 'out',
334
+ dryRun: true,
335
+ shareModels: true,
336
+ namespaceTypes: true,
337
+ selfContained: false,
338
+ strictSharedModels: true,
339
+ sharedModelsModule: '@app/schemas',
340
+ }),
341
+ ).resolves.toBeDefined()
342
+ })
343
+
236
344
  it('fails loudly when a shared model name collides with a property-derived sub-type', async () => {
237
345
  // `latest` references the Message model; a sibling property literally named
238
346
  // `message` would make ajsc extract a structural sub-type ALSO named