@pikku/inspector 0.12.16 → 0.12.18

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## 0.12.18
2
+
3
+ ### Patch Changes
4
+
5
+ - 20750fd: feat(workflow): decide step dispatch purely per-function
6
+
7
+ Workflow step execution (inline vs queue dispatch) is now decided entirely by
8
+ the step's function `inline` flag — the workflow-level / run-level `inline`
9
+ meta no longer participates in per-step dispatch.
10
+ - Steps default to **inline**, so a normally-started (queue-backed) workflow
11
+ runs its whole chain in one orchestrator pass instead of one queue
12
+ round-trip per step.
13
+ - A function marked `inline: false` is dispatched via the queue (its own
14
+ worker, retry isolation). When `inline: false` but no `queueService` is
15
+ configured, the step falls back to inline and emits a `logger.warn` instead
16
+ of silently swallowing the misconfiguration.
17
+ - Removed the now-unused workflow-level `inline` from `WorkflowsMeta` /
18
+ `WorkflowRuntimeMeta`, the inspector's workflow extraction, the DSL→graph
19
+ converter, and the deploy analyzer / service inference (which now key off
20
+ the per-function flag). Run-level `inline` is retained: it still controls
21
+ whether a whole run executes in-process without queue infrastructure.
22
+
23
+ - Updated dependencies [cd101a5]
24
+ - Updated dependencies [ac16265]
25
+ - Updated dependencies [a05e864]
26
+ - Updated dependencies [20750fd]
27
+ - @pikku/core@0.12.30
28
+
29
+ ## 0.12.17
30
+
31
+ ### Patch Changes
32
+
33
+ - 2cf67be: Add inline option to pikkuFunc/pikkuSessionlessFunc for workflow step dispatch
34
+
35
+ By default, workflow steps now run inline (no queue hop). Set inline: false on a function to force dispatch through the queue for that step.
36
+
37
+ - Updated dependencies [2cf67be]
38
+ - @pikku/core@0.12.28
39
+
1
40
  ## 0.12.16
2
41
 
3
42
  ### Patch Changes
@@ -277,6 +277,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
277
277
  let deploy;
278
278
  let approvalRequired;
279
279
  let approvalDescription;
280
+ let inline;
280
281
  let version;
281
282
  let objectNode;
282
283
  let nodeDisplayName = null;
@@ -344,6 +345,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
344
345
  readonly_ = getPropertyValue(firstArg, 'readonly');
345
346
  deploy = getPropertyValue(firstArg, 'deploy');
346
347
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
348
+ inline = getPropertyValue(firstArg, 'inline');
347
349
  // Extract approvalDescription identifier reference
348
350
  for (const prop of firstArg.properties) {
349
351
  if (ts.isPropertyAssignment(prop) &&
@@ -486,6 +488,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
486
488
  const genericTypes = (typeArguments ?? [])
487
489
  .map((tn) => checker.getTypeFromTypeNode(tn))
488
490
  .map((t) => unwrapPromise(checker, t));
491
+ // pikkuChannelConnectionFunc<Out> declares a single generic that is the
492
+ // OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
493
+ // Every other wrapper reads generic[0] as INPUT, so without this guard the
494
+ // connect handler's output generic is mis-recorded as inputSchemaName and the
495
+ // empty WS handshake fails input validation at connect (1008/403).
496
+ const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text);
489
497
  const capitalizedName = funcIdToTypeName(name);
490
498
  // --- Input Extraction ---
491
499
  let inputNames = [];
@@ -511,7 +519,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
511
519
  inputTypes = [filterType];
512
520
  }
513
521
  }
514
- else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
522
+ else if (!isChannelConnectionFunc &&
523
+ !isListFunc &&
524
+ genericTypes.length >= 1 &&
525
+ genericTypes[0]) {
515
526
  // Fall back to extracting from generic type arguments
516
527
  const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
517
528
  inputNames = result.names;
@@ -716,6 +727,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
716
727
  deploy: deploy || undefined,
717
728
  approvalRequired: approvalRequired || undefined,
718
729
  approvalDescription: approvalDescription || undefined,
730
+ inline: inline === false ? false : undefined,
719
731
  implementationHash,
720
732
  version,
721
733
  title,
@@ -2,6 +2,7 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue, assertStringLiteralProperty, } from '../utils/get-property-value.js';
3
3
  import { ErrorCode } from '../error-codes.js';
4
4
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
5
+ import { parseDurationString } from '@pikku/core';
5
6
  export const createAddKeyedWiring = (config) => {
6
7
  return (logger, node, checker, state, _options) => {
7
8
  if (!ts.isCallExpression(node)) {
@@ -24,6 +25,7 @@ export const createAddKeyedWiring = (config) => {
24
25
  const nameValue = getPropertyValue(obj, 'name');
25
26
  const displayNameValue = getPropertyValue(obj, 'displayName');
26
27
  const descriptionValue = getPropertyValue(obj, 'description');
28
+ const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod');
27
29
  const idValue = getPropertyValue(obj, config.idField);
28
30
  let schemaVariableName = null;
29
31
  let schemaSourceFile = null;
@@ -74,6 +76,15 @@ export const createAddKeyedWiring = (config) => {
74
76
  logger.critical(ErrorCode.MISSING_NAME, `${config.label} '${nameValue}' is missing the required 'schema' property or schema is not a variable reference.`);
75
77
  return;
76
78
  }
79
+ if (rotationPeriodValue) {
80
+ try {
81
+ parseDurationString(rotationPeriodValue);
82
+ }
83
+ catch {
84
+ logger.critical(ErrorCode.INVALID_VALUE, `${config.label} '${nameValue}' has an invalid 'rotationPeriod': '${rotationPeriodValue}'. Use a duration like '1d', '30day', or '1w'.`);
85
+ return;
86
+ }
87
+ }
77
88
  const sourceFile = node.getSourceFile().fileName;
78
89
  const wiringState = config.getState(state);
79
90
  wiringState.files.add(sourceFile);
@@ -90,6 +101,7 @@ export const createAddKeyedWiring = (config) => {
90
101
  name: nameValue,
91
102
  displayName: displayNameValue,
92
103
  description: descriptionValue || undefined,
104
+ rotationPeriod: rotationPeriodValue || undefined,
93
105
  [config.idField]: idValue,
94
106
  schema: schemaLookupName,
95
107
  sourceFile,
@@ -182,7 +182,6 @@ export const addWorkflow = (logger, node, checker, state) => {
182
182
  let summary;
183
183
  let description;
184
184
  let errors;
185
- let inline;
186
185
  let expose;
187
186
  if (ts.isObjectLiteralExpression(firstArg)) {
188
187
  const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger, checker);
@@ -192,10 +191,6 @@ export const addWorkflow = (logger, node, checker, state) => {
192
191
  summary = metadata.summary;
193
192
  description = metadata.description;
194
193
  errors = metadata.errors;
195
- const inlineProp = getPropertyValue(firstArg, 'inline');
196
- if (inlineProp === true) {
197
- inline = true;
198
- }
199
194
  expose = getPropertyValue(firstArg, 'expose');
200
195
  }
201
196
  // Validate that we got a valid function
@@ -279,7 +274,6 @@ export const addWorkflow = (logger, node, checker, state) => {
279
274
  description,
280
275
  errors,
281
276
  tags,
282
- inline,
283
277
  expose,
284
278
  };
285
279
  // Workflow functions require platform services that aren't visible
@@ -7,6 +7,53 @@
7
7
  export function sanitizeTypeName(name) {
8
8
  return name.replace(/[^a-zA-Z0-9_$]/g, '_');
9
9
  }
10
+ const CLASSIFICATION_WRAPPERS = new Set(['Private', 'Pii', 'Secret']);
11
+ function findMatchingAngleBracket(type, startIndex) {
12
+ let depth = 0;
13
+ for (let i = startIndex; i < type.length; i += 1) {
14
+ const char = type[i];
15
+ if (char === '<') {
16
+ depth += 1;
17
+ continue;
18
+ }
19
+ if (char === '>' && (i === 0 || type[i - 1] !== '=')) {
20
+ depth -= 1;
21
+ if (depth === 0) {
22
+ return i;
23
+ }
24
+ }
25
+ }
26
+ return -1;
27
+ }
28
+ function stripClassificationWrappers(type) {
29
+ let output = '';
30
+ let index = 0;
31
+ while (index < type.length) {
32
+ const char = type[index];
33
+ if (!/[A-Za-z_$]/.test(char)) {
34
+ output += char;
35
+ index += 1;
36
+ continue;
37
+ }
38
+ let end = index + 1;
39
+ while (end < type.length && /[A-Za-z0-9_$]/.test(type[end])) {
40
+ end += 1;
41
+ }
42
+ const identifier = type.slice(index, end);
43
+ if (CLASSIFICATION_WRAPPERS.has(identifier) && type[end] === '<') {
44
+ const closingIndex = findMatchingAngleBracket(type, end);
45
+ if (closingIndex !== -1) {
46
+ const inner = type.slice(end + 1, closingIndex);
47
+ output += stripClassificationWrappers(inner);
48
+ index = closingIndex + 1;
49
+ continue;
50
+ }
51
+ }
52
+ output += identifier;
53
+ index = end;
54
+ }
55
+ return output;
56
+ }
10
57
  export function generateCustomTypes(typesMap, requiredTypes) {
11
58
  const typeDeclarations = Array.from(typesMap.customTypes.entries())
12
59
  .sort(([a], [b]) => a.localeCompare(b))
@@ -17,11 +64,13 @@ export function generateCustomTypes(typesMap, requiredTypes) {
17
64
  .map(([originalName, { type, references }]) => {
18
65
  const name = sanitizeTypeName(originalName);
19
66
  references.forEach((refName) => {
20
- if (refName !== '__object' && !refName.startsWith('__object_')) {
67
+ if (refName !== '__object' &&
68
+ !refName.startsWith('__object_') &&
69
+ !CLASSIFICATION_WRAPPERS.has(refName)) {
21
70
  requiredTypes.add(refName);
22
71
  }
23
72
  });
24
- const typeString = type;
73
+ const typeString = stripClassificationWrappers(type);
25
74
  const typeNameRegex = /\b[A-Z][a-zA-Z0-9]*\b/g;
26
75
  const potentialTypes = typeString.match(typeNameRegex) || [];
27
76
  potentialTypes.forEach((typeName) => {
@@ -46,12 +95,14 @@ export function generateCustomTypes(typesMap, requiredTypes) {
46
95
  // Type not found in map (ambient/builtin type)
47
96
  }
48
97
  });
49
- if (name === type)
98
+ if (name === typeString)
50
99
  return null;
51
- return `export type ${name} = ${type}`;
100
+ return `export type ${name} = ${typeString}`;
52
101
  });
53
102
  const importsByPath = new Map();
54
103
  for (const typeName of requiredTypes) {
104
+ if (CLASSIFICATION_WRAPPERS.has(typeName))
105
+ continue;
55
106
  try {
56
107
  const typeMeta = typesMap.getTypeMeta(typeName);
57
108
  if (typeMeta.path) {
@@ -760,10 +760,15 @@ export function filterInspectorState(state, filters, logger) {
760
760
  }
761
761
  filteredState.requiredSchemas = prunedSchemas;
762
762
  }
763
- // If any surviving function is a non-inline workflow step, the unit needs
764
- // workflowService + queueService even though the function doesn't use them.
765
- // Check the ORIGINAL graph meta (before filtering pruned it).
763
+ // Step dispatch is decided purely per-function: a workflow step runs via the
764
+ // queue only when its function opts out of inline execution (inline: false).
765
+ // Such a unit needs workflowService + queueService injected even though the
766
+ // function itself doesn't reference them. Check the ORIGINAL graph meta
767
+ // (before filtering pruned it).
766
768
  const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta));
769
+ const resolveFuncId = (rpcName) => filteredState.rpc.internalMeta[rpcName] ??
770
+ filteredState.rpc.exposedMeta[rpcName] ??
771
+ rpcName;
767
772
  // Use the snapshot taken before filtering
768
773
  for (const graph of Object.values(originalGraphMeta)) {
769
774
  if (!graph.nodes)
@@ -772,11 +777,12 @@ export function filterInspectorState(state, filters, logger) {
772
777
  if (!('rpcName' in node) || !node.rpcName)
773
778
  continue;
774
779
  const rpcName = node.rpcName;
775
- if (!survivingFuncIds.has(rpcName))
780
+ const funcId = resolveFuncId(rpcName);
781
+ if (!survivingFuncIds.has(funcId) && !survivingFuncIds.has(rpcName))
776
782
  continue;
777
- const isInline = node.options?.async !== true &&
778
- graph.inline === true;
779
- if (!isInline) {
783
+ const funcMeta = (filteredState.functions.meta[funcId] ??
784
+ filteredState.functions.meta[rpcName]);
785
+ if (funcMeta?.inline === false) {
780
786
  filteredState.serviceAggregation.requiredServices.add('workflowService');
781
787
  filteredState.serviceAggregation.requiredServices.add('queueService');
782
788
  }
@@ -212,7 +212,9 @@ async function batchImportWithRegister(logger, sourceFiles) {
212
212
  return null;
213
213
  }
214
214
  finally {
215
- await unregister?.();
215
+ void Promise.resolve(unregister?.()).catch((e) => {
216
+ logger.debug(`tsx unregister() failed: ${e.message}`);
217
+ });
216
218
  }
217
219
  }
218
220
  async function importWithRegister(sourceFile) {
@@ -221,7 +223,7 @@ async function importWithRegister(sourceFile) {
221
223
  return await import(sourceFile);
222
224
  }
223
225
  finally {
224
- await unregister();
226
+ void Promise.resolve(unregister()).catch(() => { });
225
227
  }
226
228
  }
227
229
  function processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger) {
@@ -1040,6 +1040,29 @@ function extractReturn(statement, context) {
1040
1040
  if (!statement.expression) {
1041
1041
  return null;
1042
1042
  }
1043
+ if (ts.isAwaitExpression(statement.expression) &&
1044
+ ts.isCallExpression(statement.expression.expression)) {
1045
+ const call = statement.expression.expression;
1046
+ if (isWorkflowDoCall(call, context.checker)) {
1047
+ return isInlineDoCall(call)
1048
+ ? extractInlineStep(call, context)
1049
+ : extractRpcStep(call, context);
1050
+ }
1051
+ if (isWorkflowSleepCall(call, context.checker)) {
1052
+ return extractSleepStep(call, context);
1053
+ }
1054
+ }
1055
+ if (ts.isCallExpression(statement.expression)) {
1056
+ const call = statement.expression;
1057
+ if (isWorkflowDoCall(call, context.checker)) {
1058
+ return isInlineDoCall(call)
1059
+ ? extractInlineStep(call, context)
1060
+ : extractRpcStep(call, context);
1061
+ }
1062
+ if (isWorkflowSleepCall(call, context.checker)) {
1063
+ return extractSleepStep(call, context);
1064
+ }
1065
+ }
1043
1066
  if (!ts.isObjectLiteralExpression(statement.expression)) {
1044
1067
  return null;
1045
1068
  }
@@ -301,7 +301,6 @@ export function convertDslToGraph(workflowName, meta) {
301
301
  source,
302
302
  description: meta.description,
303
303
  tags: meta.tags,
304
- inline: meta.inline,
305
304
  context: meta.context,
306
305
  nodes: nodesRecord,
307
306
  entryNodeIds,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.16",
3
+ "version": "0.12.18",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.27",
38
+ "@pikku/core": "^0.12.30",
39
39
  "path-to-regexp": "^8.3.0",
40
40
  "ts-json-schema-generator": "^2.5.0",
41
41
  "tsx": "^4.21.0",
@@ -316,3 +316,55 @@ describe('addFunctions implementationHash', () => {
316
316
  }
317
317
  })
318
318
  })
319
+
320
+ describe('pikkuChannelConnectionFunc generic mapping', () => {
321
+ // Regression: pikkuChannelConnectionFunc<Out> has a single generic that is the
322
+ // OUTPUT type (input is always void). The inspector must NOT record that generic
323
+ // as inputSchemaName — otherwise the empty WS handshake is validated against an
324
+ // input schema requiring the send-payload shape and the connect is rejected 403.
325
+ test('does not map the output generic to inputSchemaName', async () => {
326
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-channel-connect-'))
327
+ const file = join(rootDir, 'channel.ts')
328
+
329
+ await writeFile(
330
+ file,
331
+ [
332
+ 'type Sessionless<In, Out> = (',
333
+ ' services: any,',
334
+ ' data: In,',
335
+ ' interaction: any',
336
+ ') => Promise<Out>',
337
+ 'export const pikkuChannelConnectionFunc = <Out = unknown>(',
338
+ ' func: Sessionless<void, Out>',
339
+ ') => ({ func })',
340
+ 'export const onCardsConnect = pikkuChannelConnectionFunc<{',
341
+ " type: 'hello'",
342
+ ' count: number',
343
+ '}>(async (_services, _data, _interaction) => {})',
344
+ ].join('\n')
345
+ )
346
+
347
+ const logger: InspectorLogger = {
348
+ debug: () => {},
349
+ info: () => {},
350
+ warn: () => {},
351
+ error: () => {},
352
+ critical: () => {},
353
+ hasCriticalErrors: () => false,
354
+ }
355
+
356
+ try {
357
+ const state = await inspect(logger, [file], { rootDir })
358
+ const meta = state.functions.meta['onCardsConnect']
359
+ assert.ok(meta, 'onCardsConnect meta should exist')
360
+ assert.strictEqual(
361
+ meta!.inputSchemaName,
362
+ null,
363
+ 'connect input must be void (no input schema), not the output generic'
364
+ )
365
+ assert.deepStrictEqual(meta!.inputs, [])
366
+ } finally {
367
+ await rm(rootDir, { recursive: true, force: true })
368
+ }
369
+ })
370
+ })
@@ -392,6 +392,7 @@ export const addFunctions: AddWiring = (
392
392
  let deploy: 'serverless' | 'server' | 'auto' | undefined
393
393
  let approvalRequired: boolean | undefined
394
394
  let approvalDescription: string | undefined
395
+ let inline: boolean | undefined
395
396
  let version: number | undefined
396
397
  let objectNode: ts.ObjectLiteralExpression | undefined
397
398
  let nodeDisplayName: string | null = null
@@ -487,6 +488,7 @@ export const addFunctions: AddWiring = (
487
488
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
488
489
  | boolean
489
490
  | undefined
491
+ inline = getPropertyValue(firstArg, 'inline') as boolean | undefined
490
492
 
491
493
  // Extract approvalDescription identifier reference
492
494
  for (const prop of firstArg.properties) {
@@ -669,6 +671,13 @@ export const addFunctions: AddWiring = (
669
671
  .map((tn) => checker.getTypeFromTypeNode(tn))
670
672
  .map((t) => unwrapPromise(checker, t))
671
673
 
674
+ // pikkuChannelConnectionFunc<Out> declares a single generic that is the
675
+ // OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
676
+ // Every other wrapper reads generic[0] as INPUT, so without this guard the
677
+ // connect handler's output generic is mis-recorded as inputSchemaName and the
678
+ // empty WS handshake fails input validation at connect (1008/403).
679
+ const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text)
680
+
672
681
  const capitalizedName = funcIdToTypeName(name)
673
682
 
674
683
  // --- Input Extraction ---
@@ -706,7 +715,12 @@ export const addFunctions: AddWiring = (
706
715
  } else {
707
716
  inputTypes = [filterType]
708
717
  }
709
- } else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
718
+ } else if (
719
+ !isChannelConnectionFunc &&
720
+ !isListFunc &&
721
+ genericTypes.length >= 1 &&
722
+ genericTypes[0]
723
+ ) {
710
724
  // Fall back to extracting from generic type arguments
711
725
  const result = getNamesAndTypes(
712
726
  checker,
@@ -1004,6 +1018,7 @@ export const addFunctions: AddWiring = (
1004
1018
  deploy: deploy || undefined,
1005
1019
  approvalRequired: approvalRequired || undefined,
1006
1020
  approvalDescription: approvalDescription || undefined,
1021
+ inline: inline === false ? false : undefined,
1007
1022
  implementationHash,
1008
1023
  version,
1009
1024
  title,
@@ -6,6 +6,7 @@ import {
6
6
  import type { AddWiring, InspectorState } from '../types.js'
7
7
  import { ErrorCode } from '../error-codes.js'
8
8
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
9
+ import { parseDurationString } from '@pikku/core'
9
10
 
10
11
  export interface KeyedWiringConfig {
11
12
  functionName: string
@@ -52,6 +53,9 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
52
53
  const descriptionValue = getPropertyValue(obj, 'description') as
53
54
  | string
54
55
  | null
56
+ const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod') as
57
+ | string
58
+ | null
55
59
  const idValue = getPropertyValue(obj, config.idField) as string | null
56
60
 
57
61
  let schemaVariableName: string | null = null
@@ -123,6 +127,18 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
123
127
  return
124
128
  }
125
129
 
130
+ if (rotationPeriodValue) {
131
+ try {
132
+ parseDurationString(rotationPeriodValue)
133
+ } catch {
134
+ logger.critical(
135
+ ErrorCode.INVALID_VALUE,
136
+ `${config.label} '${nameValue}' has an invalid 'rotationPeriod': '${rotationPeriodValue}'. Use a duration like '1d', '30day', or '1w'.`
137
+ )
138
+ return
139
+ }
140
+ }
141
+
126
142
  const sourceFile = node.getSourceFile().fileName
127
143
 
128
144
  const wiringState = config.getState(state)
@@ -148,6 +164,7 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
148
164
  name: nameValue,
149
165
  displayName: displayNameValue,
150
166
  description: descriptionValue || undefined,
167
+ rotationPeriod: rotationPeriodValue || undefined,
151
168
  [config.idField]: idValue,
152
169
  schema: schemaLookupName,
153
170
  sourceFile,
@@ -209,7 +209,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
209
209
  let summary: string | undefined
210
210
  let description: string | undefined
211
211
  let errors: string[] | undefined
212
- let inline: boolean | undefined
213
212
  let expose: boolean | undefined
214
213
 
215
214
  if (ts.isObjectLiteralExpression(firstArg)) {
@@ -226,11 +225,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
226
225
  description = metadata.description
227
226
  errors = metadata.errors
228
227
 
229
- const inlineProp = getPropertyValue(firstArg, 'inline')
230
- if (inlineProp === true) {
231
- inline = true
232
- }
233
-
234
228
  expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
235
229
  }
236
230
 
@@ -337,7 +331,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
337
331
  description,
338
332
  errors,
339
333
  tags,
340
- inline,
341
334
  expose,
342
335
  }
343
336
 
@@ -0,0 +1,99 @@
1
+ import { test, describe } from 'node:test'
2
+ import { strict as assert } from 'node:assert'
3
+ import { generateCustomTypes } from './custom-types-generator.js'
4
+ import { TypesMap } from '../types-map.js'
5
+
6
+ function makeTypesMap(
7
+ entries: Record<string, { type: string; references: string[] }>
8
+ ): TypesMap {
9
+ const tm = new TypesMap()
10
+ for (const [name, { type, references }] of Object.entries(entries)) {
11
+ tm.addCustomType(name, type, references)
12
+ }
13
+ return tm
14
+ }
15
+
16
+ describe('generateCustomTypes — classification wrapper stripping', () => {
17
+ test('strips Private<T> wrapper from type alias', () => {
18
+ const tm = makeTypesMap({
19
+ UserEmail: { type: 'Private<string>', references: ['Private'] },
20
+ })
21
+ const result = generateCustomTypes(tm, new Set())
22
+ assert.match(result, /UserEmail/, 'should emit UserEmail alias')
23
+ assert.match(
24
+ result,
25
+ /= string/,
26
+ 'Private<string> should be stripped to string'
27
+ )
28
+ assert.doesNotMatch(result, /Private/, 'Private wrapper must be removed')
29
+ })
30
+
31
+ test('strips Secret<T> wrapper from type alias', () => {
32
+ const tm = makeTypesMap({
33
+ HashedPw: { type: 'Secret<string>', references: ['Secret'] },
34
+ })
35
+ const result = generateCustomTypes(tm, new Set())
36
+ assert.match(result, /HashedPw/)
37
+ assert.match(
38
+ result,
39
+ /= string/,
40
+ 'Secret<string> should be stripped to string'
41
+ )
42
+ assert.doesNotMatch(result, /Secret/, 'Secret wrapper must be removed')
43
+ })
44
+
45
+ test('strips Pii<T> wrapper from type alias', () => {
46
+ const tm = makeTypesMap({
47
+ UserPhone: { type: 'Pii<string>', references: ['Pii'] },
48
+ })
49
+ const result = generateCustomTypes(tm, new Set())
50
+ assert.match(result, /UserPhone/)
51
+ assert.match(result, /= string/, 'Pii<string> should be stripped to string')
52
+ assert.doesNotMatch(result, /Pii/, 'Pii wrapper must be removed')
53
+ })
54
+
55
+ test('strips nested classification wrappers', () => {
56
+ const tm = makeTypesMap({
57
+ Combo: {
58
+ type: 'Private<Secret<string>>',
59
+ references: ['Private', 'Secret'],
60
+ },
61
+ })
62
+ const result = generateCustomTypes(tm, new Set())
63
+ assert.match(result, /Combo/)
64
+ assert.match(
65
+ result,
66
+ /= string/,
67
+ 'nested wrappers should resolve to inner type'
68
+ )
69
+ assert.doesNotMatch(
70
+ result,
71
+ /Private|Secret/,
72
+ 'all wrappers must be removed'
73
+ )
74
+ })
75
+
76
+ test('does not strip non-classification type names', () => {
77
+ const tm = makeTypesMap({
78
+ MyType: { type: 'SomeOtherType', references: ['SomeOtherType'] },
79
+ })
80
+ const required = new Set<string>()
81
+ generateCustomTypes(tm, required)
82
+ assert.ok(
83
+ required.has('SomeOtherType'),
84
+ 'non-classification references must remain in requiredTypes'
85
+ )
86
+ })
87
+
88
+ test('classification wrapper references are not added to requiredTypes', () => {
89
+ const tm = makeTypesMap({
90
+ SensitiveField: { type: 'Private<string>', references: ['Private'] },
91
+ })
92
+ const required = new Set<string>()
93
+ generateCustomTypes(tm, required)
94
+ assert.ok(
95
+ !required.has('Private'),
96
+ 'Private must not be added to requiredTypes'
97
+ )
98
+ })
99
+ })