x-openapi-flow 1.4.4 → 1.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.
@@ -12,13 +12,17 @@ const {
12
12
  detectDuplicateTransitions,
13
13
  detectInvalidOperationReferences,
14
14
  detectTerminalCoverage,
15
+ detectSemanticModelingWarnings,
16
+ computeQualityReport,
15
17
  } = require("../lib/validator");
18
+ const { CODES } = require("../lib/error-codes");
16
19
  const { generateSdk } = require("../lib/sdk-generator");
17
20
  const {
18
21
  exportDocFlows,
19
22
  generatePostmanCollection,
20
23
  generateInsomniaWorkspace,
21
24
  generateRedocPackage,
25
+ generateFlowTests,
22
26
  } = require("../adapters/flow-output-adapters");
23
27
  const pkg = require("../package.json");
24
28
 
@@ -26,16 +30,19 @@ const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
26
30
  const DEFAULT_FLOWS_FILE = "x-openapi-flow.flows.yaml";
27
31
  const KNOWN_COMMANDS = [
28
32
  "validate",
33
+ "quickstart",
29
34
  "init",
30
35
  "apply",
31
36
  "diff",
32
37
  "lint",
33
38
  "analyze",
39
+ "quality-report",
34
40
  "generate-sdk",
35
41
  "export-doc-flows",
36
42
  "generate-postman",
37
43
  "generate-insomnia",
38
44
  "generate-redoc",
45
+ "generate-flow-tests",
39
46
  "graph",
40
47
  "doctor",
41
48
  "completion",
@@ -43,7 +50,7 @@ const KNOWN_COMMANDS = [
43
50
 
44
51
  const COMMAND_SNIPPETS = {
45
52
  validate: {
46
- usage: "x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]",
53
+ usage: "x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--semantic] [--config path]",
47
54
  examples: [
48
55
  "x-openapi-flow validate examples/order-api.yaml",
49
56
  "x-openapi-flow validate examples/order-api.yaml --profile relaxed",
@@ -59,6 +66,15 @@ const COMMAND_SNIPPETS = {
59
66
  "x-openapi-flow init openapi.yaml --dry-run",
60
67
  ],
61
68
  },
69
+ quickstart: {
70
+ usage: "x-openapi-flow quickstart [--dir path] [--runtime express|fastify] [--force]",
71
+ examples: [
72
+ "x-openapi-flow quickstart",
73
+ "x-openapi-flow quickstart --dir ./my-flow-demo",
74
+ "x-openapi-flow quickstart --runtime fastify",
75
+ "x-openapi-flow quickstart --dir ./my-flow-demo --force",
76
+ ],
77
+ },
62
78
  apply: {
63
79
  usage: "x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]",
64
80
  examples: [
@@ -75,12 +91,20 @@ const COMMAND_SNIPPETS = {
75
91
  ],
76
92
  },
77
93
  lint: {
78
- usage: "x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]",
94
+ usage: "x-openapi-flow lint [openapi-file] [--format pretty|json] [--semantic] [--config path]",
79
95
  examples: [
80
96
  "x-openapi-flow lint openapi.yaml",
81
97
  "x-openapi-flow lint openapi.yaml --format json",
82
98
  ],
83
99
  },
100
+ "quality-report": {
101
+ usage: "x-openapi-flow quality-report <openapi-file> [--profile core|relaxed|strict] [--semantic] [--output path]",
102
+ examples: [
103
+ "x-openapi-flow quality-report openapi.flow.yaml",
104
+ "x-openapi-flow quality-report openapi.flow.yaml --profile strict --semantic",
105
+ "x-openapi-flow quality-report openapi.flow.yaml --output quality-report.json",
106
+ ],
107
+ },
84
108
  analyze: {
85
109
  usage: "x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]",
86
110
  examples: [
@@ -119,6 +143,14 @@ const COMMAND_SNIPPETS = {
119
143
  "x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow",
120
144
  ],
121
145
  },
146
+ "generate-flow-tests": {
147
+ usage: "x-openapi-flow generate-flow-tests [openapi-file] [--format jest|vitest|postman] [--output path] [--with-scripts]",
148
+ examples: [
149
+ "x-openapi-flow generate-flow-tests openapi.flow.yaml --format jest --output ./flow.generated.test.js",
150
+ "x-openapi-flow generate-flow-tests openapi.flow.yaml --format vitest --output ./flow.generated.vitest.test.js",
151
+ "x-openapi-flow generate-flow-tests openapi.flow.yaml --format postman --output ./x-openapi-flow.flow-tests.postman_collection.json --with-scripts",
152
+ ],
153
+ },
122
154
  graph: {
123
155
  usage: "x-openapi-flow graph <openapi-file> [--format mermaid|json]",
124
156
  examples: [
@@ -183,11 +215,14 @@ _x_openapi_flow() {
183
215
 
184
216
  case "$words[2]" in
185
217
  validate)
186
- _values 'options' --format --profile --strict-quality --config --help
218
+ _values 'options' --format --profile --strict-quality --semantic --config --help
187
219
  ;;
188
220
  init)
189
221
  _values 'options' --flows --force --dry-run --help
190
222
  ;;
223
+ quickstart)
224
+ _values 'options' --dir --runtime --force --help
225
+ ;;
191
226
  apply)
192
227
  _values 'options' --flows --out --in-place --help
193
228
  ;;
@@ -195,7 +230,10 @@ _x_openapi_flow() {
195
230
  _values 'options' --flows --format --help
196
231
  ;;
197
232
  lint)
198
- _values 'options' --format --config --help
233
+ _values 'options' --format --semantic --config --help
234
+ ;;
235
+ quality-report)
236
+ _values 'options' --profile --semantic --output --help
199
237
  ;;
200
238
  analyze)
201
239
  _values 'options' --format --out --merge --flows --help
@@ -215,6 +253,9 @@ _x_openapi_flow() {
215
253
  generate-redoc)
216
254
  _values 'options' --output --help
217
255
  ;;
256
+ generate-flow-tests)
257
+ _values 'options' --format --output --with-scripts --help
258
+ ;;
218
259
  graph)
219
260
  _values 'options' --format --help
220
261
  ;;
@@ -248,11 +289,14 @@ compdef _x_openapi_flow x-openapi-flow
248
289
 
249
290
  case "\${COMP_WORDS[1]}" in
250
291
  validate)
251
- COMPREPLY=( $(compgen -W "--format --profile --strict-quality --config --help --verbose" -- "\$cur") )
292
+ COMPREPLY=( $(compgen -W "--format --profile --strict-quality --semantic --config --help --verbose" -- "\$cur") )
252
293
  ;;
253
294
  init)
254
295
  COMPREPLY=( $(compgen -W "--flows --force --dry-run --help --verbose" -- "\$cur") )
255
296
  ;;
297
+ quickstart)
298
+ COMPREPLY=( $(compgen -W "--dir --runtime --force --help --verbose" -- "\$cur") )
299
+ ;;
256
300
  apply)
257
301
  COMPREPLY=( $(compgen -W "--flows --out --in-place --help --verbose" -- "\$cur") )
258
302
  ;;
@@ -260,7 +304,10 @@ compdef _x_openapi_flow x-openapi-flow
260
304
  COMPREPLY=( $(compgen -W "--flows --format --help --verbose" -- "\$cur") )
261
305
  ;;
262
306
  lint)
263
- COMPREPLY=( $(compgen -W "--format --config --help --verbose" -- "\$cur") )
307
+ COMPREPLY=( $(compgen -W "--format --semantic --config --help --verbose" -- "\$cur") )
308
+ ;;
309
+ quality-report)
310
+ COMPREPLY=( $(compgen -W "--profile --semantic --output --help --verbose" -- "\$cur") )
264
311
  ;;
265
312
  analyze)
266
313
  COMPREPLY=( $(compgen -W "--format --out --merge --flows --help --verbose" -- "\$cur") )
@@ -280,6 +327,9 @@ compdef _x_openapi_flow x-openapi-flow
280
327
  generate-redoc)
281
328
  COMPREPLY=( $(compgen -W "--output --help --verbose" -- "\$cur") )
282
329
  ;;
330
+ generate-flow-tests)
331
+ COMPREPLY=( $(compgen -W "--format --output --with-scripts --help --verbose" -- "\$cur") )
332
+ ;;
283
333
  graph)
284
334
  COMPREPLY=( $(compgen -W "--format --help --verbose" -- "\$cur") )
285
335
  ;;
@@ -418,17 +468,20 @@ Global options:
418
468
 
419
469
  Usage:
420
470
  x-openapi-flow <command> [options]
421
- x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
471
+ x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--semantic] [--config path]
472
+ x-openapi-flow quickstart [--dir path] [--runtime express|fastify] [--force]
422
473
  x-openapi-flow init [openapi-file] [--flows path] [--force] [--dry-run]
423
474
  x-openapi-flow apply [openapi-file] [--flows path] [--out path] [--in-place]
424
475
  x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
425
- x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]
476
+ x-openapi-flow lint [openapi-file] [--format pretty|json] [--semantic] [--config path]
426
477
  x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]
478
+ x-openapi-flow quality-report <openapi-file> [--profile core|relaxed|strict] [--semantic] [--output path]
427
479
  x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]
428
480
  x-openapi-flow export-doc-flows [openapi-file] [--output path] [--format markdown|json]
429
481
  x-openapi-flow generate-postman [openapi-file] [--output path] [--with-scripts]
430
482
  x-openapi-flow generate-insomnia [openapi-file] [--output path]
431
483
  x-openapi-flow generate-redoc [openapi-file] [--output path]
484
+ x-openapi-flow generate-flow-tests [openapi-file] [--format jest|vitest|postman] [--output path] [--with-scripts]
432
485
  x-openapi-flow graph <openapi-file> [--format mermaid|json]
433
486
  x-openapi-flow doctor [--config path]
434
487
  x-openapi-flow completion [bash|zsh]
@@ -441,6 +494,10 @@ Examples:
441
494
  x-openapi-flow validate examples/order-api.yaml
442
495
  x-openapi-flow validate examples/order-api.yaml --profile relaxed
443
496
  x-openapi-flow validate examples/order-api.yaml --strict-quality
497
+ x-openapi-flow validate examples/order-api.yaml --semantic
498
+ x-openapi-flow quickstart
499
+ x-openapi-flow quickstart --dir ./my-flow-demo
500
+ x-openapi-flow quickstart --runtime fastify
444
501
  x-openapi-flow init openapi.yaml --flows openapi.x.yaml
445
502
  x-openapi-flow init openapi.yaml --force
446
503
  x-openapi-flow init openapi.yaml --dry-run
@@ -452,6 +509,10 @@ Examples:
452
509
  x-openapi-flow diff openapi.yaml --format json
453
510
  x-openapi-flow lint openapi.yaml
454
511
  x-openapi-flow lint openapi.yaml --format json
512
+ x-openapi-flow lint openapi.yaml --semantic
513
+ x-openapi-flow quality-report openapi.flow.yaml
514
+ x-openapi-flow quality-report openapi.flow.yaml --profile strict --semantic
515
+ x-openapi-flow quality-report openapi.flow.yaml --output quality-report.json
455
516
  x-openapi-flow analyze openapi.yaml
456
517
  x-openapi-flow analyze openapi.yaml --out openapi.x.yaml
457
518
  x-openapi-flow analyze openapi.yaml --format json
@@ -461,10 +522,14 @@ Examples:
461
522
  x-openapi-flow generate-postman openapi.yaml --output ./x-openapi-flow.postman_collection.json --with-scripts
462
523
  x-openapi-flow generate-insomnia openapi.yaml --output ./x-openapi-flow.insomnia.json
463
524
  x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow
525
+ x-openapi-flow generate-flow-tests openapi.flow.yaml --format jest --output ./flow.generated.test.js
464
526
  x-openapi-flow graph examples/order-api.yaml
465
527
  x-openapi-flow doctor
466
528
 
467
529
  Quick Start:
530
+ # 0) Create a runnable starter project (recommended for first contact)
531
+ x-openapi-flow quickstart
532
+
468
533
  # 1) Initialize sidecar from your OpenAPI file
469
534
  x-openapi-flow init
470
535
 
@@ -598,7 +663,7 @@ function parseValidateArgs(args) {
598
663
  const unknown = findUnknownOptions(
599
664
  args,
600
665
  ["--format", "--profile", "--config"],
601
- ["--strict-quality"]
666
+ ["--strict-quality", "--semantic"]
602
667
  );
603
668
  if (unknown) {
604
669
  return { error: `Unknown option: ${unknown}` };
@@ -620,6 +685,7 @@ function parseValidateArgs(args) {
620
685
  }
621
686
 
622
687
  const strictQuality = args.includes("--strict-quality");
688
+ const semantic = args.includes("--semantic");
623
689
  const format = formatOpt.found ? formatOpt.value : undefined;
624
690
  const profile = profileOpt.found ? profileOpt.value : undefined;
625
691
 
@@ -657,6 +723,7 @@ function parseValidateArgs(args) {
657
723
  return {
658
724
  filePath: path.resolve(positional[0]),
659
725
  strictQuality,
726
+ semantic,
660
727
  format,
661
728
  profile,
662
729
  configPath: configOpt.found ? configOpt.value : undefined,
@@ -702,6 +769,52 @@ function parseInitArgs(args) {
702
769
  };
703
770
  }
704
771
 
772
+ function parseQuickstartArgs(args) {
773
+ const unknown = findUnknownOptions(args, ["--dir", "--runtime"], ["--force"]);
774
+ if (unknown) {
775
+ return { error: `Unknown option: ${unknown}` };
776
+ }
777
+
778
+ const dirOpt = getOptionValue(args, "--dir");
779
+ if (dirOpt.error) {
780
+ return { error: dirOpt.error };
781
+ }
782
+
783
+ const runtimeOpt = getOptionValue(args, "--runtime");
784
+ if (runtimeOpt.error) {
785
+ return { error: `${runtimeOpt.error} Use 'express' or 'fastify'.` };
786
+ }
787
+
788
+ const positional = args.filter((token, index) => {
789
+ if (token === "--dir" || token === "--runtime" || token === "--force") {
790
+ return false;
791
+ }
792
+ if (index > 0 && (args[index - 1] === "--dir" || args[index - 1] === "--runtime")) {
793
+ return false;
794
+ }
795
+ return !token.startsWith("--");
796
+ });
797
+
798
+ if (positional.length > 1) {
799
+ return { error: `Unexpected argument: ${positional[1]}` };
800
+ }
801
+
802
+ const targetDirRaw = dirOpt.found
803
+ ? dirOpt.value
804
+ : (positional[0] || "x-openapi-flow-quickstart");
805
+
806
+ const runtime = runtimeOpt.found ? String(runtimeOpt.value).toLowerCase() : "express";
807
+ if (!["express", "fastify"].includes(runtime)) {
808
+ return { error: `Invalid --runtime '${runtime}'. Use 'express' or 'fastify'.` };
809
+ }
810
+
811
+ return {
812
+ targetDir: path.resolve(targetDirRaw),
813
+ runtime,
814
+ force: args.includes("--force"),
815
+ };
816
+ }
817
+
705
818
  function summarizeSidecarDiff(existingFlowsDoc, mergedFlowsDoc) {
706
819
  const existingOps = new Map();
707
820
  for (const entry of (existingFlowsDoc && existingFlowsDoc.operations) || []) {
@@ -853,7 +966,7 @@ function parseDiffArgs(args) {
853
966
  }
854
967
 
855
968
  function parseLintArgs(args) {
856
- const unknown = findUnknownOptions(args, ["--format", "--config"], []);
969
+ const unknown = findUnknownOptions(args, ["--format", "--config"], ["--semantic"]);
857
970
  if (unknown) {
858
971
  return { error: `Unknown option: ${unknown}` };
859
972
  }
@@ -893,10 +1006,54 @@ function parseLintArgs(args) {
893
1006
  return {
894
1007
  openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
895
1008
  format,
1009
+ semantic: args.includes("--semantic"),
896
1010
  configPath: configOpt.found ? configOpt.value : undefined,
897
1011
  };
898
1012
  }
899
1013
 
1014
+ function parseQualityReportArgs(args) {
1015
+ const unknown = findUnknownOptions(args, ["--profile", "--output"], ["--semantic"]);
1016
+ if (unknown) {
1017
+ return { error: `Unknown option: ${unknown}` };
1018
+ }
1019
+
1020
+ const profileOpt = getOptionValue(args, "--profile");
1021
+ if (profileOpt.error) {
1022
+ return { error: `${profileOpt.error} Use 'core', 'relaxed', or 'strict'.` };
1023
+ }
1024
+
1025
+ const outputOpt = getOptionValue(args, "--output");
1026
+ if (outputOpt.error) {
1027
+ return { error: outputOpt.error };
1028
+ }
1029
+
1030
+ const profile = profileOpt.found ? profileOpt.value : undefined;
1031
+ if (profile && !["core", "relaxed", "strict"].includes(profile)) {
1032
+ return { error: `Invalid --profile '${profile}'. Use 'core', 'relaxed', or 'strict'.` };
1033
+ }
1034
+
1035
+ const positional = args.filter((token, index) => {
1036
+ if (["--profile", "--output"].includes(token)) return false;
1037
+ if (index > 0 && ["--profile", "--output"].includes(args[index - 1])) return false;
1038
+ return !token.startsWith("--") || token === "-";
1039
+ });
1040
+
1041
+ if (positional.length === 0) {
1042
+ return { error: "Missing OpenAPI file path. Usage: x-openapi-flow quality-report <openapi-file>" };
1043
+ }
1044
+
1045
+ if (positional.length > 1) {
1046
+ return { error: `Unexpected argument: ${positional[1]}` };
1047
+ }
1048
+
1049
+ return {
1050
+ filePath: path.resolve(positional[0]),
1051
+ profile,
1052
+ semantic: args.includes("--semantic"),
1053
+ outputPath: outputOpt.found ? path.resolve(outputOpt.value) : undefined,
1054
+ };
1055
+ }
1056
+
900
1057
  function parseApplyArgs(args) {
901
1058
  const unknown = findUnknownOptions(args, ["--flows", "--out"], ["--in-place"]);
902
1059
  if (unknown) {
@@ -1215,6 +1372,51 @@ function parseGenerateRedocArgs(args) {
1215
1372
  };
1216
1373
  }
1217
1374
 
1375
+ function parseGenerateFlowTestsArgs(args) {
1376
+ const unknown = findUnknownOptions(args, ["--format", "--output"], ["--with-scripts"]);
1377
+ if (unknown) {
1378
+ return { error: `Unknown option: ${unknown}` };
1379
+ }
1380
+
1381
+ const formatOpt = getOptionValue(args, "--format");
1382
+ if (formatOpt.error) {
1383
+ return { error: `${formatOpt.error} Use 'jest', 'vitest', or 'postman'.` };
1384
+ }
1385
+
1386
+ const outputOpt = getOptionValue(args, "--output");
1387
+ if (outputOpt.error) {
1388
+ return { error: outputOpt.error };
1389
+ }
1390
+
1391
+ const format = formatOpt.found ? String(formatOpt.value || "").toLowerCase() : "jest";
1392
+ if (!["jest", "vitest", "postman"].includes(format)) {
1393
+ return { error: `Invalid --format '${format}'. Use 'jest', 'vitest', or 'postman'.` };
1394
+ }
1395
+
1396
+ const positional = args.filter((token, index) => {
1397
+ if (token === "--format" || token === "--output" || token === "--with-scripts") {
1398
+ return false;
1399
+ }
1400
+
1401
+ if (index > 0 && (args[index - 1] === "--format" || args[index - 1] === "--output")) {
1402
+ return false;
1403
+ }
1404
+
1405
+ return !token.startsWith("--");
1406
+ });
1407
+
1408
+ if (positional.length > 1) {
1409
+ return { error: `Unexpected argument: ${positional[1]}` };
1410
+ }
1411
+
1412
+ return {
1413
+ openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
1414
+ format,
1415
+ outputPath: outputOpt.found ? path.resolve(outputOpt.value) : undefined,
1416
+ withScripts: args.includes("--with-scripts") ? true : undefined,
1417
+ };
1418
+ }
1419
+
1218
1420
  function parseArgs(argv) {
1219
1421
  const stripped = stripGlobalFlags(argv.slice(2));
1220
1422
  const args = stripped.args;
@@ -1257,6 +1459,11 @@ function parseArgs(argv) {
1257
1459
  return withVerbose(parsed.error ? parsed : { command, ...parsed });
1258
1460
  }
1259
1461
 
1462
+ if (command === "quickstart") {
1463
+ const parsed = parseQuickstartArgs(commandArgs);
1464
+ return withVerbose(parsed.error ? parsed : { command, ...parsed });
1465
+ }
1466
+
1260
1467
  if (command === "graph") {
1261
1468
  const parsed = parseGraphArgs(commandArgs);
1262
1469
  return withVerbose(parsed.error ? parsed : { command, ...parsed });
@@ -1282,6 +1489,11 @@ function parseArgs(argv) {
1282
1489
  return withVerbose(parsed.error ? parsed : { command, ...parsed });
1283
1490
  }
1284
1491
 
1492
+ if (command === "quality-report") {
1493
+ const parsed = parseQualityReportArgs(commandArgs);
1494
+ return withVerbose(parsed.error ? parsed : { command, ...parsed });
1495
+ }
1496
+
1285
1497
  if (command === "doctor") {
1286
1498
  const parsed = parseDoctorArgs(commandArgs);
1287
1499
  return withVerbose(parsed.error ? parsed : { command, ...parsed });
@@ -1312,6 +1524,11 @@ function parseArgs(argv) {
1312
1524
  return withVerbose(parsed.error ? parsed : { command, ...parsed });
1313
1525
  }
1314
1526
 
1527
+ if (command === "generate-flow-tests") {
1528
+ const parsed = parseGenerateFlowTestsArgs(commandArgs);
1529
+ return withVerbose(parsed.error ? parsed : { command, ...parsed });
1530
+ }
1531
+
1315
1532
  if (command === "completion") {
1316
1533
  const parsed = parseCompletionArgs(commandArgs);
1317
1534
  return withVerbose(parsed.error ? parsed : { command, ...parsed });
@@ -1471,6 +1688,140 @@ function extractOperationEntries(api) {
1471
1688
  return entries;
1472
1689
  }
1473
1690
 
1691
+ function normalizeDslState(resourceDsl, stateValue) {
1692
+ const stateAliases = resourceDsl && resourceDsl.states && typeof resourceDsl.states === "object"
1693
+ ? resourceDsl.states
1694
+ : {};
1695
+
1696
+ if (stateValue == null) {
1697
+ return stateValue;
1698
+ }
1699
+
1700
+ const raw = String(stateValue);
1701
+ return stateAliases[raw] || raw;
1702
+ }
1703
+
1704
+ function expandResourceDsl(resourceDsl) {
1705
+ const defaults = resourceDsl && resourceDsl.defaults && typeof resourceDsl.defaults === "object"
1706
+ ? resourceDsl.defaults
1707
+ : {};
1708
+ const flowDefaults = defaults.flow && typeof defaults.flow === "object"
1709
+ ? defaults.flow
1710
+ : {};
1711
+ const { id_prefix: flowIdPrefix, ...flowDefaultsForPayload } = flowDefaults;
1712
+ const transitionDefaults = defaults.transition && typeof defaults.transition === "object"
1713
+ ? defaults.transition
1714
+ : {};
1715
+
1716
+ const resourceTransitions = Array.isArray(resourceDsl && resourceDsl.transitions)
1717
+ ? resourceDsl.transitions
1718
+ : [];
1719
+
1720
+ const outgoingByState = new Map();
1721
+ for (const transition of resourceTransitions) {
1722
+ if (!transition || !transition.from || !transition.to) {
1723
+ continue;
1724
+ }
1725
+
1726
+ const fromState = normalizeDslState(resourceDsl, transition.from);
1727
+ const targetState = normalizeDslState(resourceDsl, transition.to);
1728
+
1729
+ if (!outgoingByState.has(fromState)) {
1730
+ outgoingByState.set(fromState, []);
1731
+ }
1732
+
1733
+ outgoingByState.get(fromState).push({
1734
+ ...transitionDefaults,
1735
+ target_state: targetState,
1736
+ trigger_type: transition.trigger_type || transitionDefaults.trigger_type || "synchronous",
1737
+ condition: transition.condition,
1738
+ next_operation_id: transition.next_operation_id,
1739
+ prerequisite_operation_ids: Array.isArray(transition.prerequisite_operation_ids)
1740
+ ? transition.prerequisite_operation_ids
1741
+ : undefined,
1742
+ prerequisite_field_refs: Array.isArray(transition.prerequisite_field_refs)
1743
+ ? transition.prerequisite_field_refs
1744
+ : undefined,
1745
+ propagated_field_refs: Array.isArray(transition.propagated_field_refs)
1746
+ ? transition.propagated_field_refs
1747
+ : undefined,
1748
+ });
1749
+ }
1750
+
1751
+ const operations = Array.isArray(resourceDsl && resourceDsl.operations)
1752
+ ? resourceDsl.operations
1753
+ : [];
1754
+
1755
+ return operations
1756
+ .filter((operationEntry) => operationEntry && operationEntry.operationId)
1757
+ .map((operationEntry) => {
1758
+ const currentState = normalizeDslState(
1759
+ resourceDsl,
1760
+ operationEntry.current_state != null ? operationEntry.current_state : operationEntry.state
1761
+ );
1762
+
1763
+ const explicitFlow =
1764
+ operationEntry["x-openapi-flow"] && typeof operationEntry["x-openapi-flow"] === "object"
1765
+ ? operationEntry["x-openapi-flow"]
1766
+ : null;
1767
+
1768
+ const explicitTransitions = Array.isArray(operationEntry.transitions)
1769
+ ? operationEntry.transitions.map((transition) => ({
1770
+ ...transitionDefaults,
1771
+ ...transition,
1772
+ target_state: normalizeDslState(resourceDsl, transition.target_state || transition.to),
1773
+ trigger_type: transition.trigger_type || transitionDefaults.trigger_type || "synchronous",
1774
+ }))
1775
+ : null;
1776
+
1777
+ const inheritedTransitions = outgoingByState.has(currentState)
1778
+ ? outgoingByState.get(currentState).map((transition) => ({ ...transition }))
1779
+ : [];
1780
+
1781
+ const transitions = explicitTransitions || inheritedTransitions;
1782
+ const defaultIdPrefix = flowIdPrefix ? String(flowIdPrefix) : "";
1783
+ const generatedId = defaultIdPrefix
1784
+ ? `${defaultIdPrefix}-${toKebabCase(operationEntry.operationId)}`
1785
+ : toKebabCase(operationEntry.operationId);
1786
+
1787
+ const flow = {
1788
+ version: "1.0",
1789
+ ...flowDefaultsForPayload,
1790
+ ...explicitFlow,
1791
+ id: operationEntry.id || (explicitFlow && explicitFlow.id) || generatedId,
1792
+ current_state: currentState,
1793
+ description: operationEntry.description || (explicitFlow && explicitFlow.description),
1794
+ idempotency: operationEntry.idempotency || (explicitFlow && explicitFlow.idempotency),
1795
+ transitions,
1796
+ };
1797
+
1798
+ if (!flow.description) {
1799
+ delete flow.description;
1800
+ }
1801
+ if (!flow.idempotency) {
1802
+ delete flow.idempotency;
1803
+ }
1804
+
1805
+ return {
1806
+ operationId: operationEntry.operationId,
1807
+ "x-openapi-flow": flow,
1808
+ };
1809
+ });
1810
+ }
1811
+
1812
+ function expandResourceDslOperations(parsed) {
1813
+ const resourceDslEntries = Array.isArray(parsed && parsed.resources)
1814
+ ? parsed.resources
1815
+ : [];
1816
+
1817
+ const expanded = [];
1818
+ for (const resourceDsl of resourceDslEntries) {
1819
+ expanded.push(...expandResourceDsl(resourceDsl));
1820
+ }
1821
+
1822
+ return expanded;
1823
+ }
1824
+
1474
1825
  function readFlowsFile(flowsPath) {
1475
1826
  if (!fs.existsSync(flowsPath)) {
1476
1827
  return {
@@ -1486,9 +1837,12 @@ function readFlowsFile(flowsPath) {
1486
1837
  return { version: "1.0", operations: [] };
1487
1838
  }
1488
1839
 
1840
+ const directOperations = Array.isArray(parsed.operations) ? parsed.operations : [];
1841
+ const expandedOperations = expandResourceDslOperations(parsed);
1842
+
1489
1843
  return {
1490
1844
  version: parsed.version || "1.0",
1491
- operations: Array.isArray(parsed.operations) ? parsed.operations : [],
1845
+ operations: [...directOperations, ...expandedOperations],
1492
1846
  };
1493
1847
  }
1494
1848
 
@@ -1726,6 +2080,445 @@ function runInit(parsed) {
1726
2080
  return 0;
1727
2081
  }
1728
2082
 
2083
+ function isDirectoryEmpty(directoryPath) {
2084
+ if (!fs.existsSync(directoryPath)) {
2085
+ return true;
2086
+ }
2087
+
2088
+ try {
2089
+ const entries = fs.readdirSync(directoryPath);
2090
+ return entries.length === 0;
2091
+ } catch (_err) {
2092
+ return false;
2093
+ }
2094
+ }
2095
+
2096
+ function buildQuickstartOpenApi() {
2097
+ return {
2098
+ openapi: "3.0.3",
2099
+ info: {
2100
+ title: "Quickstart Orders API",
2101
+ version: "1.0.0",
2102
+ },
2103
+ paths: {
2104
+ "/orders": {
2105
+ post: {
2106
+ operationId: "createOrder",
2107
+ responses: {
2108
+ 201: { description: "Order created" },
2109
+ },
2110
+ },
2111
+ },
2112
+ "/orders/{id}/pay": {
2113
+ post: {
2114
+ operationId: "payOrder",
2115
+ parameters: [
2116
+ {
2117
+ name: "id",
2118
+ in: "path",
2119
+ required: true,
2120
+ schema: { type: "string" },
2121
+ },
2122
+ ],
2123
+ responses: {
2124
+ 200: { description: "Order paid" },
2125
+ },
2126
+ },
2127
+ },
2128
+ "/orders/{id}/ship": {
2129
+ post: {
2130
+ operationId: "shipOrder",
2131
+ parameters: [
2132
+ {
2133
+ name: "id",
2134
+ in: "path",
2135
+ required: true,
2136
+ schema: { type: "string" },
2137
+ },
2138
+ ],
2139
+ responses: {
2140
+ 200: { description: "Order shipped" },
2141
+ },
2142
+ },
2143
+ },
2144
+ "/orders/{id}": {
2145
+ get: {
2146
+ operationId: "getOrder",
2147
+ parameters: [
2148
+ {
2149
+ name: "id",
2150
+ in: "path",
2151
+ required: true,
2152
+ schema: { type: "string" },
2153
+ },
2154
+ ],
2155
+ responses: {
2156
+ 200: { description: "Order state" },
2157
+ 404: { description: "Order not found" },
2158
+ },
2159
+ },
2160
+ },
2161
+ },
2162
+ };
2163
+ }
2164
+
2165
+ function buildQuickstartSidecar() {
2166
+ return {
2167
+ version: "1.0",
2168
+ operations: [
2169
+ {
2170
+ operationId: "createOrder",
2171
+ "x-openapi-flow": {
2172
+ version: "1.0",
2173
+ id: "create-order-flow",
2174
+ current_state: "CREATED",
2175
+ transitions: [
2176
+ {
2177
+ target_state: "PAID",
2178
+ trigger_type: "synchronous",
2179
+ next_operation_id: "payOrder",
2180
+ },
2181
+ ],
2182
+ },
2183
+ },
2184
+ {
2185
+ operationId: "payOrder",
2186
+ "x-openapi-flow": {
2187
+ version: "1.0",
2188
+ id: "pay-order-flow",
2189
+ current_state: "PAID",
2190
+ transitions: [
2191
+ {
2192
+ target_state: "SHIPPED",
2193
+ trigger_type: "synchronous",
2194
+ next_operation_id: "shipOrder",
2195
+ },
2196
+ ],
2197
+ },
2198
+ },
2199
+ {
2200
+ operationId: "shipOrder",
2201
+ "x-openapi-flow": {
2202
+ version: "1.0",
2203
+ id: "ship-order-flow",
2204
+ current_state: "SHIPPED",
2205
+ transitions: [],
2206
+ },
2207
+ },
2208
+ ],
2209
+ };
2210
+ }
2211
+
2212
+ function buildQuickstartServerJs(runtime) {
2213
+ if (runtime === "fastify") {
2214
+ return `"use strict";
2215
+
2216
+ const fastify = require("fastify")({ logger: false });
2217
+ const openapi = require("./openapi.flow.json");
2218
+ const { createFastifyFlowGuard } = require("x-openapi-flow/lib/runtime-guard");
2219
+
2220
+ const PORT = Number(process.env.PORT || 3110);
2221
+ const orderStore = new Map();
2222
+
2223
+ fastify.addHook(
2224
+ "preHandler",
2225
+ createFastifyFlowGuard({
2226
+ openapi,
2227
+ async getCurrentState({ resourceId }) {
2228
+ if (!resourceId) {
2229
+ return null;
2230
+ }
2231
+
2232
+ const order = orderStore.get(resourceId);
2233
+ return order ? order.state : null;
2234
+ },
2235
+ resolveResourceId: ({ params }) => (params && params.id ? String(params.id) : null),
2236
+ allowUnknownOperations: true,
2237
+ })
2238
+ );
2239
+
2240
+ fastify.post(
2241
+ "/orders",
2242
+ {
2243
+ config: {
2244
+ operationId: "createOrder",
2245
+ },
2246
+ },
2247
+ async (_request, reply) => {
2248
+ const id = \`ord_\${Date.now()}\`;
2249
+ const order = { id, state: "CREATED" };
2250
+ orderStore.set(id, order);
2251
+ return reply.code(201).send(order);
2252
+ }
2253
+ );
2254
+
2255
+ fastify.post(
2256
+ "/orders/:id/pay",
2257
+ {
2258
+ config: {
2259
+ operationId: "payOrder",
2260
+ },
2261
+ },
2262
+ async (request, reply) => {
2263
+ const order = orderStore.get(request.params.id);
2264
+ if (!order) {
2265
+ return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Order not found." } });
2266
+ }
2267
+
2268
+ order.state = "PAID";
2269
+ orderStore.set(order.id, order);
2270
+ return reply.code(200).send(order);
2271
+ }
2272
+ );
2273
+
2274
+ fastify.post(
2275
+ "/orders/:id/ship",
2276
+ {
2277
+ config: {
2278
+ operationId: "shipOrder",
2279
+ },
2280
+ },
2281
+ async (request, reply) => {
2282
+ const order = orderStore.get(request.params.id);
2283
+ if (!order) {
2284
+ return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Order not found." } });
2285
+ }
2286
+
2287
+ order.state = "SHIPPED";
2288
+ orderStore.set(order.id, order);
2289
+ return reply.code(200).send(order);
2290
+ }
2291
+ );
2292
+
2293
+ fastify.get(
2294
+ "/orders/:id",
2295
+ {
2296
+ config: {
2297
+ operationId: "getOrder",
2298
+ },
2299
+ },
2300
+ async (request, reply) => {
2301
+ const order = orderStore.get(request.params.id);
2302
+ if (!order) {
2303
+ return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Order not found." } });
2304
+ }
2305
+
2306
+ return reply.code(200).send(order);
2307
+ }
2308
+ );
2309
+
2310
+ fastify.listen({ port: PORT, host: "0.0.0.0" }).then(() => {
2311
+ console.log(\`Quickstart API running on http://localhost:\${PORT}\`);
2312
+ });
2313
+ `;
2314
+ }
2315
+
2316
+ return `"use strict";
2317
+
2318
+ const express = require("express");
2319
+ const openapi = require("./openapi.flow.json");
2320
+ const { createExpressFlowGuard } = require("x-openapi-flow/lib/runtime-guard");
2321
+
2322
+ const app = express();
2323
+ app.use(express.json());
2324
+
2325
+ const PORT = Number(process.env.PORT || 3110);
2326
+ const orderStore = new Map();
2327
+
2328
+ function resolveOrderIdFromPath(req) {
2329
+ const fromParams = req && req.params && req.params.id ? String(req.params.id) : null;
2330
+ if (fromParams) {
2331
+ return fromParams;
2332
+ }
2333
+
2334
+ const rawPath = req && (req.path || (req.originalUrl ? req.originalUrl.split("?")[0] : null));
2335
+ if (!rawPath) {
2336
+ return null;
2337
+ }
2338
+
2339
+ const match = String(rawPath).match(/^\\/orders\\/([^/]+)\\/(pay|ship)$/);
2340
+ return match ? match[1] : null;
2341
+ }
2342
+
2343
+ app.use(
2344
+ createExpressFlowGuard({
2345
+ openapi,
2346
+ async getCurrentState({ resourceId }) {
2347
+ if (!resourceId) {
2348
+ return null;
2349
+ }
2350
+
2351
+ const order = orderStore.get(resourceId);
2352
+ return order ? order.state : null;
2353
+ },
2354
+ resolveResourceId: ({ req }) => resolveOrderIdFromPath(req),
2355
+ allowUnknownOperations: true,
2356
+ })
2357
+ );
2358
+
2359
+ app.post("/orders", (_req, res) => {
2360
+ const id = \`ord_\${Date.now()}\`;
2361
+ const order = { id, state: "CREATED" };
2362
+ orderStore.set(id, order);
2363
+ return res.status(201).json(order);
2364
+ });
2365
+
2366
+ app.post("/orders/:id/pay", (req, res) => {
2367
+ const order = orderStore.get(req.params.id);
2368
+ if (!order) {
2369
+ return res.status(404).json({ error: { code: "NOT_FOUND", message: "Order not found." } });
2370
+ }
2371
+
2372
+ order.state = "PAID";
2373
+ orderStore.set(order.id, order);
2374
+ return res.status(200).json(order);
2375
+ });
2376
+
2377
+ app.post("/orders/:id/ship", (req, res) => {
2378
+ const order = orderStore.get(req.params.id);
2379
+ if (!order) {
2380
+ return res.status(404).json({ error: { code: "NOT_FOUND", message: "Order not found." } });
2381
+ }
2382
+
2383
+ order.state = "SHIPPED";
2384
+ orderStore.set(order.id, order);
2385
+ return res.status(200).json(order);
2386
+ });
2387
+
2388
+ app.get("/orders/:id", (req, res) => {
2389
+ const order = orderStore.get(req.params.id);
2390
+ if (!order) {
2391
+ return res.status(404).json({ error: { code: "NOT_FOUND", message: "Order not found." } });
2392
+ }
2393
+
2394
+ return res.status(200).json(order);
2395
+ });
2396
+
2397
+ app.listen(PORT, () => {
2398
+ console.log(\`Quickstart API running on http://localhost:\${PORT}\`);
2399
+ });
2400
+ `;
2401
+ }
2402
+
2403
+ function buildQuickstartReadme(runtime) {
2404
+ return `# x-openapi-flow Quickstart Project
2405
+
2406
+ This starter was generated by \`x-openapi-flow quickstart\`.
2407
+
2408
+ Runtime: \`${runtime}\`
2409
+
2410
+ ## Fast path (under 5 minutes)
2411
+
2412
+ 1. Install and start:
2413
+
2414
+ \`\`\`bash
2415
+ npm install
2416
+ npm start
2417
+ \`\`\`
2418
+
2419
+ 2. Create an order and try invalid shipping (blocked):
2420
+
2421
+ \`\`\`bash
2422
+ curl -s -X POST http://localhost:3110/orders
2423
+ curl -i -X POST http://localhost:3110/orders/<id>/ship
2424
+ \`\`\`
2425
+
2426
+ Expected: \`409 INVALID_STATE_TRANSITION\`.
2427
+
2428
+ 3. Follow the valid path:
2429
+
2430
+ \`\`\`bash
2431
+ curl -s -X POST http://localhost:3110/orders/<id>/pay
2432
+ curl -s -X POST http://localhost:3110/orders/<id>/ship
2433
+ \`\`\`
2434
+
2435
+ ## About files (keep it simple)
2436
+
2437
+ - \`openapi.flow.json\`: the file used at runtime right now.
2438
+ - \`openapi.json\`: base OpenAPI source.
2439
+ - \`openapi.x.yaml\`: sidecar flow metadata (you can ignore this file at first).
2440
+
2441
+ When you are ready to edit flows manually:
2442
+
2443
+ \`\`\`bash
2444
+ npx x-openapi-flow apply openapi.json --flows openapi.x.yaml --out openapi.flow.json
2445
+ npx x-openapi-flow validate openapi.flow.json --profile strict
2446
+ \`\`\`
2447
+ `;
2448
+ }
2449
+
2450
+ function runQuickstart(parsed) {
2451
+ const targetDir = parsed.targetDir;
2452
+ const runtime = parsed.runtime || "express";
2453
+ const exists = fs.existsSync(targetDir);
2454
+
2455
+ if (exists && !isDirectoryEmpty(targetDir) && !parsed.force) {
2456
+ console.error(`ERROR: Target directory is not empty: ${targetDir}`);
2457
+ console.error("Use --force to overwrite scaffold files in this directory.");
2458
+ return 1;
2459
+ }
2460
+
2461
+ fs.mkdirSync(targetDir, { recursive: true });
2462
+
2463
+ const openapiPath = path.join(targetDir, "openapi.json");
2464
+ const sidecarPath = path.join(targetDir, "openapi.x.yaml");
2465
+ const flowPath = path.join(targetDir, "openapi.flow.json");
2466
+ const packagePath = path.join(targetDir, "package.json");
2467
+ const serverPath = path.join(targetDir, "server.js");
2468
+ const readmePath = path.join(targetDir, "README.md");
2469
+ const gitignorePath = path.join(targetDir, ".gitignore");
2470
+
2471
+ const openapi = buildQuickstartOpenApi();
2472
+ const sidecar = buildQuickstartSidecar();
2473
+ const flowApi = JSON.parse(JSON.stringify(openapi));
2474
+ applyFlowsToOpenApi(flowApi, sidecar);
2475
+
2476
+ fs.writeFileSync(openapiPath, `${JSON.stringify(openapi, null, 2)}\n`, "utf8");
2477
+ fs.writeFileSync(sidecarPath, yaml.dump(sidecar, { noRefs: true, lineWidth: -1 }), "utf8");
2478
+ fs.writeFileSync(flowPath, `${JSON.stringify(flowApi, null, 2)}\n`, "utf8");
2479
+ fs.writeFileSync(
2480
+ packagePath,
2481
+ `${JSON.stringify({
2482
+ name: `x-openapi-flow-quickstart-${runtime}`,
2483
+ private: true,
2484
+ version: "1.0.0",
2485
+ description: `Quickstart scaffold generated by x-openapi-flow (${runtime})`,
2486
+ main: "server.js",
2487
+ scripts: {
2488
+ start: "node server.js",
2489
+ apply: "x-openapi-flow apply openapi.json --flows openapi.x.yaml --out openapi.flow.json",
2490
+ validate: "x-openapi-flow validate openapi.flow.json --profile strict",
2491
+ },
2492
+ dependencies: {
2493
+ ...(runtime === "fastify" ? { fastify: "^5.2.1" } : { express: "^4.21.2" }),
2494
+ "x-openapi-flow": "latest",
2495
+ },
2496
+ }, null, 2)}\n`,
2497
+ "utf8"
2498
+ );
2499
+ fs.writeFileSync(serverPath, buildQuickstartServerJs(runtime), "utf8");
2500
+ fs.writeFileSync(readmePath, buildQuickstartReadme(runtime), "utf8");
2501
+ fs.writeFileSync(gitignorePath, "node_modules\n", "utf8");
2502
+
2503
+ console.log(`Quickstart project created: ${targetDir}`);
2504
+ console.log(`Runtime: ${runtime}`);
2505
+ console.log("Generated files:");
2506
+ console.log("- openapi.json (base spec)");
2507
+ console.log("- openapi.x.yaml (sidecar metadata)");
2508
+ console.log("- openapi.flow.json (runtime-ready spec)");
2509
+ console.log(`- server.js (${runtime} runtime guard demo)`);
2510
+ console.log("- package.json (scripts: start/apply/validate)");
2511
+ console.log("---");
2512
+ console.log("Next steps:");
2513
+ console.log(`cd ${targetDir}`);
2514
+ console.log("npm install");
2515
+ console.log("npm start");
2516
+ console.log("curl -s -X POST http://localhost:3110/orders");
2517
+ console.log("curl -i -X POST http://localhost:3110/orders/<id>/ship");
2518
+ console.log("(Expected: 409 INVALID_STATE_TRANSITION)");
2519
+ return 0;
2520
+ }
2521
+
1729
2522
  function runApply(parsed) {
1730
2523
  let targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
1731
2524
  let flowsPathFromPositional = null;
@@ -1923,11 +2716,13 @@ function runLint(parsed, configData = {}) {
1923
2716
 
1924
2717
  const flows = extractFlows(api);
1925
2718
  const lintConfig = (configData && configData.lint && configData.lint.rules) || {};
2719
+ const semanticEnabled = parsed.semantic === true || lintConfig.semantic === true;
1926
2720
  const ruleConfig = {
1927
2721
  next_operation_id_exists: lintConfig.next_operation_id_exists !== false,
1928
2722
  prerequisite_operation_ids_exist: lintConfig.prerequisite_operation_ids_exist !== false,
1929
2723
  duplicate_transitions: lintConfig.duplicate_transitions !== false,
1930
2724
  terminal_path: lintConfig.terminal_path !== false,
2725
+ semantic_consistency: semanticEnabled,
1931
2726
  };
1932
2727
 
1933
2728
  const operationsById = collectOperationIds(api);
@@ -1935,10 +2730,12 @@ function runLint(parsed, configData = {}) {
1935
2730
  const invalidOperationReferences = detectInvalidOperationReferences(operationsById, flows);
1936
2731
  const duplicateTransitions = detectDuplicateTransitions(flows);
1937
2732
  const terminalCoverage = detectTerminalCoverage(graph);
2733
+ const semanticWarnings = semanticEnabled ? detectSemanticModelingWarnings(flows) : [];
1938
2734
 
1939
2735
  const nextOperationIssues = invalidOperationReferences
1940
2736
  .filter((entry) => entry.type === "next_operation_id")
1941
2737
  .map((entry) => ({
2738
+ code: CODES.LINT_NEXT_OPERATION_ID_EXISTS.code,
1942
2739
  operation_id: entry.operation_id,
1943
2740
  declared_in: entry.declared_in,
1944
2741
  }));
@@ -1946,6 +2743,7 @@ function runLint(parsed, configData = {}) {
1946
2743
  const prerequisiteIssues = invalidOperationReferences
1947
2744
  .filter((entry) => entry.type === "prerequisite_operation_ids")
1948
2745
  .map((entry) => ({
2746
+ code: CODES.LINT_PREREQUISITE_OPERATION_IDS_EXIST.code,
1949
2747
  operation_id: entry.operation_id,
1950
2748
  declared_in: entry.declared_in,
1951
2749
  }));
@@ -1953,18 +2751,22 @@ function runLint(parsed, configData = {}) {
1953
2751
  const issues = {
1954
2752
  next_operation_id_exists: ruleConfig.next_operation_id_exists ? nextOperationIssues : [],
1955
2753
  prerequisite_operation_ids_exist: ruleConfig.prerequisite_operation_ids_exist ? prerequisiteIssues : [],
1956
- duplicate_transitions: ruleConfig.duplicate_transitions ? duplicateTransitions : [],
2754
+ duplicate_transitions: ruleConfig.duplicate_transitions
2755
+ ? duplicateTransitions.map((entry) => ({ code: CODES.LINT_DUPLICATE_TRANSITIONS.code, ...entry }))
2756
+ : [],
1957
2757
  terminal_path: {
1958
2758
  terminal_states: ruleConfig.terminal_path ? terminalCoverage.terminal_states : [],
1959
2759
  non_terminating_states: ruleConfig.terminal_path ? terminalCoverage.non_terminating_states : [],
1960
2760
  },
2761
+ semantic_consistency: ruleConfig.semantic_consistency ? semanticWarnings : [],
1961
2762
  };
1962
2763
 
1963
2764
  const errorCount =
1964
2765
  issues.next_operation_id_exists.length +
1965
2766
  issues.prerequisite_operation_ids_exist.length +
1966
2767
  issues.duplicate_transitions.length +
1967
- issues.terminal_path.non_terminating_states.length;
2768
+ issues.terminal_path.non_terminating_states.length +
2769
+ issues.semantic_consistency.length;
1968
2770
 
1969
2771
  const result = {
1970
2772
  ok: errorCount === 0,
@@ -1979,6 +2781,7 @@ function runLint(parsed, configData = {}) {
1979
2781
  prerequisite_operation_ids_exist: issues.prerequisite_operation_ids_exist.length,
1980
2782
  duplicate_transitions: issues.duplicate_transitions.length,
1981
2783
  terminal_path: issues.terminal_path.non_terminating_states.length,
2784
+ semantic_consistency: issues.semantic_consistency.length,
1982
2785
  })
1983
2786
  .filter(([, count]) => count > 0)
1984
2787
  .map(([rule]) => rule),
@@ -1986,7 +2789,25 @@ function runLint(parsed, configData = {}) {
1986
2789
  };
1987
2790
 
1988
2791
  if (parsed.format === "json") {
1989
- console.log(JSON.stringify(result, null, 2));
2792
+ const jsonResult = {
2793
+ ...result,
2794
+ issues: {
2795
+ ...result.issues,
2796
+ terminal_path: {
2797
+ ...result.issues.terminal_path,
2798
+ non_terminating_states: result.issues.terminal_path.non_terminating_states.map((state) => ({
2799
+ code: CODES.LINT_TERMINAL_PATH.code,
2800
+ state,
2801
+ })),
2802
+ },
2803
+ semantic_consistency: result.issues.semantic_consistency.map((message) => ({
2804
+ code: CODES.LINT_SEMANTIC_CONSISTENCY.code,
2805
+ message,
2806
+ })),
2807
+ },
2808
+ };
2809
+
2810
+ console.log(JSON.stringify(jsonResult, null, 2));
1990
2811
  return result.ok ? 0 : 1;
1991
2812
  }
1992
2813
 
@@ -2037,6 +2858,17 @@ function runLint(parsed, configData = {}) {
2037
2858
  );
2038
2859
  }
2039
2860
 
2861
+ if (ruleConfig.semantic_consistency) {
2862
+ if (issues.semantic_consistency.length === 0) {
2863
+ console.log("✔ semantic_consistency: no semantic ambiguities detected.");
2864
+ } else {
2865
+ console.error(`✘ semantic_consistency: ${issues.semantic_consistency.length} issue(s).`);
2866
+ issues.semantic_consistency.forEach((entry) => {
2867
+ console.error(` - ${entry}`);
2868
+ });
2869
+ }
2870
+ }
2871
+
2040
2872
  if (result.ok) {
2041
2873
  console.log("Lint checks passed ✔");
2042
2874
  } else {
@@ -2183,6 +3015,51 @@ function extractFlowsForGraph(filePath) {
2183
3015
  return sidecarFlows;
2184
3016
  }
2185
3017
 
3018
+ function runQualityReport(parsed) {
3019
+ if (!parsed.filePath || !fs.existsSync(parsed.filePath)) {
3020
+ const missing = parsed.filePath || "(unknown)";
3021
+ console.error(`ERROR: File not found — ${missing}`);
3022
+ return 1;
3023
+ }
3024
+
3025
+ const options = {
3026
+ output: "json",
3027
+ strictQuality: false,
3028
+ semantic: parsed.semantic === true,
3029
+ profile: parsed.profile || "strict",
3030
+ };
3031
+
3032
+ const originalLog = console.log;
3033
+ let result;
3034
+ try {
3035
+ // run(..., { output: "json" }) prints its own JSON payload; suppress it so
3036
+ // quality-report emits only the consolidated report.
3037
+ console.log = () => {};
3038
+ result = run(parsed.filePath, options);
3039
+ } finally {
3040
+ console.log = originalLog;
3041
+ }
3042
+
3043
+ const report = computeQualityReport(result, { semantic: options.semantic });
3044
+
3045
+ const output = JSON.stringify(report, null, 2);
3046
+
3047
+ if (parsed.outputPath) {
3048
+ try {
3049
+ fs.mkdirSync(path.dirname(parsed.outputPath), { recursive: true });
3050
+ fs.writeFileSync(parsed.outputPath, output + "\n", "utf8");
3051
+ console.error(`Quality report written to ${parsed.outputPath}`);
3052
+ } catch (err) {
3053
+ console.error(`ERROR: Could not write output file — ${err.message}`);
3054
+ return 1;
3055
+ }
3056
+ } else {
3057
+ console.log(output);
3058
+ }
3059
+
3060
+ return report.ok ? 0 : 1;
3061
+ }
3062
+
2186
3063
  function runGraph(parsed) {
2187
3064
  try {
2188
3065
  const graphResult = buildMermaidGraph(parsed.filePath);
@@ -2226,7 +3103,6 @@ function inferCurrentState(entry) {
2226
3103
  .filter(Boolean)
2227
3104
  .join(" ")
2228
3105
  .toLowerCase();
2229
-
2230
3106
  const keywordToState = [
2231
3107
  { match: /cancel|void/, state: "CANCELED" },
2232
3108
  { match: /refund/, state: "REFUNDED" },
@@ -2666,6 +3542,42 @@ function runGenerateRedoc(parsed) {
2666
3542
  }
2667
3543
  }
2668
3544
 
3545
+ function runGenerateFlowTests(parsed) {
3546
+ const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
3547
+ if (!targetOpenApiFile) {
3548
+ console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
3549
+ console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
3550
+ return 1;
3551
+ }
3552
+
3553
+ try {
3554
+ const result = generateFlowTests({
3555
+ apiPath: targetOpenApiFile,
3556
+ format: parsed.format,
3557
+ outputPath: parsed.outputPath,
3558
+ withScripts: parsed.withScripts,
3559
+ });
3560
+
3561
+ console.log(`OpenAPI source: ${targetOpenApiFile}`);
3562
+ console.log(`Test format: ${result.format}`);
3563
+ console.log(`Output: ${result.outputPath}`);
3564
+ console.log(`Flow transitions processed: ${result.flowCount}`);
3565
+ if (result.happyPathTests != null) {
3566
+ console.log(`Happy path tests: ${result.happyPathTests}`);
3567
+ }
3568
+ if (result.invalidCaseTests != null) {
3569
+ console.log(`Invalid transition tests: ${result.invalidCaseTests}`);
3570
+ }
3571
+ if (result.withScripts != null) {
3572
+ console.log(`Scripts enabled: ${result.withScripts}`);
3573
+ }
3574
+ return 0;
3575
+ } catch (err) {
3576
+ console.error(`ERROR: Could not generate flow tests — ${err.message}`);
3577
+ return 1;
3578
+ }
3579
+ }
3580
+
2669
3581
  function main() {
2670
3582
  const parsed = parseArgs(process.argv);
2671
3583
  printVerbose(parsed);
@@ -2695,6 +3607,10 @@ function main() {
2695
3607
  process.exit(runInit(parsed));
2696
3608
  }
2697
3609
 
3610
+ if (parsed.command === "quickstart") {
3611
+ process.exit(runQuickstart(parsed));
3612
+ }
3613
+
2698
3614
  if (parsed.command === "doctor") {
2699
3615
  process.exit(runDoctor(parsed));
2700
3616
  }
@@ -2727,6 +3643,10 @@ function main() {
2727
3643
  process.exit(runGenerateRedoc(parsed));
2728
3644
  }
2729
3645
 
3646
+ if (parsed.command === "generate-flow-tests") {
3647
+ process.exit(runGenerateFlowTests(parsed));
3648
+ }
3649
+
2730
3650
  if (parsed.command === "completion") {
2731
3651
  process.exit(runCompletion(parsed));
2732
3652
  }
@@ -2748,6 +3668,10 @@ function main() {
2748
3668
  process.exit(runLint(parsed, config.data));
2749
3669
  }
2750
3670
 
3671
+ if (parsed.command === "quality-report") {
3672
+ process.exit(runQualityReport(parsed));
3673
+ }
3674
+
2751
3675
  const config = loadConfig(parsed.configPath);
2752
3676
  if (config.error) {
2753
3677
  console.error(`ERROR: ${config.error}`);
@@ -2759,6 +3683,9 @@ function main() {
2759
3683
  strictQuality:
2760
3684
  parsed.strictQuality ||
2761
3685
  config.data.strictQuality === true,
3686
+ semantic:
3687
+ parsed.semantic ||
3688
+ config.data.semantic === true,
2762
3689
  profile: parsed.profile || config.data.profile || "strict",
2763
3690
  };
2764
3691