x-openapi-flow 1.4.3 → 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.
- package/README.md +188 -1
- package/adapters/flow-output-adapters.js +2 -0
- package/adapters/tests/flow-test-adapter.js +254 -0
- package/bin/x-openapi-flow.js +942 -15
- package/lib/error-codes.js +289 -0
- package/lib/openapi-state-machine-adapter.js +151 -0
- package/lib/runtime-guard/core.js +176 -0
- package/lib/runtime-guard/errors.js +85 -0
- package/lib/runtime-guard/express.js +70 -0
- package/lib/runtime-guard/fastify.js +65 -0
- package/lib/runtime-guard/index.js +15 -0
- package/lib/runtime-guard/model.js +85 -0
- package/lib/runtime-guard.js +3 -0
- package/lib/state-machine-engine.js +208 -0
- package/lib/validator.js +378 -1
- package/package.json +4 -2
package/lib/validator.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const yaml = require("js-yaml");
|
|
6
6
|
const Ajv = require("ajv");
|
|
7
|
+
const { CODES, buildIssue } = require("./error-codes");
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// Paths
|
|
@@ -150,6 +151,7 @@ function defaultResult(pathValue, ok = true) {
|
|
|
150
151
|
non_terminating_states: [],
|
|
151
152
|
invalid_operation_references: [],
|
|
152
153
|
invalid_field_references: [],
|
|
154
|
+
semantic_warnings: [],
|
|
153
155
|
warnings: [],
|
|
154
156
|
},
|
|
155
157
|
};
|
|
@@ -701,6 +703,88 @@ function detectTerminalCoverage(graph) {
|
|
|
701
703
|
};
|
|
702
704
|
}
|
|
703
705
|
|
|
706
|
+
function detectStateNamingStyle(state) {
|
|
707
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(state)) {
|
|
708
|
+
return "upper_snake";
|
|
709
|
+
}
|
|
710
|
+
if (/^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/.test(state)) {
|
|
711
|
+
return "kebab_case";
|
|
712
|
+
}
|
|
713
|
+
if (/^[a-z][a-zA-Z0-9]*$/.test(state)) {
|
|
714
|
+
return "camelCase";
|
|
715
|
+
}
|
|
716
|
+
if (/^[A-Z][a-zA-Z0-9]*$/.test(state)) {
|
|
717
|
+
return "PascalCase";
|
|
718
|
+
}
|
|
719
|
+
return "other";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function canonicalizeStateName(state) {
|
|
723
|
+
return String(state || "")
|
|
724
|
+
.toLowerCase()
|
|
725
|
+
.replace(/[_\-\s]+/g, "");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function detectSemanticModelingWarnings(flows) {
|
|
729
|
+
const warnings = [];
|
|
730
|
+
const states = [...new Set(flows.map(({ flow }) => String(flow.current_state)).filter(Boolean))];
|
|
731
|
+
|
|
732
|
+
if (states.length > 0) {
|
|
733
|
+
const styleSet = new Set(states.map((state) => detectStateNamingStyle(state)));
|
|
734
|
+
if (styleSet.size > 1) {
|
|
735
|
+
warnings.push(
|
|
736
|
+
`Semantic: inconsistent state naming styles detected (${[...styleSet].sort().join(", ")}).`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const byCanonical = new Map();
|
|
741
|
+
for (const state of states) {
|
|
742
|
+
const key = canonicalizeStateName(state);
|
|
743
|
+
if (!byCanonical.has(key)) {
|
|
744
|
+
byCanonical.set(key, new Set());
|
|
745
|
+
}
|
|
746
|
+
byCanonical.get(key).add(state);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
for (const variants of byCanonical.values()) {
|
|
750
|
+
if (variants.size > 1) {
|
|
751
|
+
warnings.push(
|
|
752
|
+
`Semantic: ambiguous state variants found (${[...variants].sort().join(", ")}).`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const transitionSignature = new Map();
|
|
759
|
+
for (const { flow } of flows) {
|
|
760
|
+
const from = String(flow.current_state || "");
|
|
761
|
+
const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
|
|
762
|
+
|
|
763
|
+
for (const transition of transitions) {
|
|
764
|
+
if (!transition || !transition.next_operation_id) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const key = `${from}::${transition.next_operation_id}`;
|
|
769
|
+
if (!transitionSignature.has(key)) {
|
|
770
|
+
transitionSignature.set(key, new Set());
|
|
771
|
+
}
|
|
772
|
+
transitionSignature.get(key).add(String(transition.target_state || ""));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
for (const [signature, targetStates] of transitionSignature.entries()) {
|
|
777
|
+
if (targetStates.size > 1) {
|
|
778
|
+
const [signatureFrom, signatureOperation] = signature.split("::");
|
|
779
|
+
warnings.push(
|
|
780
|
+
`Semantic: ambiguous transition mapping from state '${signatureFrom}' to next_operation_id '${signatureOperation}' with multiple target states (${[...targetStates].sort().join(", ")}).`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return [...new Set(warnings)];
|
|
786
|
+
}
|
|
787
|
+
|
|
704
788
|
// ---------------------------------------------------------------------------
|
|
705
789
|
// Main runner
|
|
706
790
|
// ---------------------------------------------------------------------------
|
|
@@ -708,12 +792,13 @@ function detectTerminalCoverage(graph) {
|
|
|
708
792
|
/**
|
|
709
793
|
* Run all validations against an OAS file and print results.
|
|
710
794
|
* @param {string} [apiPath] - Path to the OAS YAML file (defaults to payment-api.yaml).
|
|
711
|
-
* @param {{ output?: "pretty" | "json", strictQuality?: boolean, profile?: "core" | "relaxed" | "strict" }} [options]
|
|
795
|
+
* @param {{ output?: "pretty" | "json", strictQuality?: boolean, profile?: "core" | "relaxed" | "strict", semantic?: boolean }} [options]
|
|
712
796
|
* @returns {{ ok: boolean, path: string, flowCount: number, schemaFailures: object[], orphans: object[], graphChecks: object }}
|
|
713
797
|
*/
|
|
714
798
|
function run(apiPath, options = {}) {
|
|
715
799
|
const output = options.output || "pretty";
|
|
716
800
|
const strictQuality = options.strictQuality === true;
|
|
801
|
+
const semantic = options.semantic === true;
|
|
717
802
|
const profile = options.profile || "strict";
|
|
718
803
|
const profiles = {
|
|
719
804
|
core: { runAdvanced: false, failAdvanced: false, runQuality: false },
|
|
@@ -861,6 +946,7 @@ function run(apiPath, options = {}) {
|
|
|
861
946
|
}
|
|
862
947
|
|
|
863
948
|
const qualityWarnings = [];
|
|
949
|
+
const semanticWarnings = semantic ? detectSemanticModelingWarnings(flows) : [];
|
|
864
950
|
|
|
865
951
|
if (profileConfig.runQuality && multipleInitialStates.length > 0) {
|
|
866
952
|
qualityWarnings.push(
|
|
@@ -895,6 +981,10 @@ function run(apiPath, options = {}) {
|
|
|
895
981
|
);
|
|
896
982
|
}
|
|
897
983
|
|
|
984
|
+
if (profileConfig.runQuality && semanticWarnings.length > 0) {
|
|
985
|
+
qualityWarnings.push(...semanticWarnings);
|
|
986
|
+
}
|
|
987
|
+
|
|
898
988
|
if (strictQuality && qualityWarnings.length > 0) {
|
|
899
989
|
hasErrors = true;
|
|
900
990
|
}
|
|
@@ -985,10 +1075,14 @@ function run(apiPath, options = {}) {
|
|
|
985
1075
|
non_terminating_states: terminalCoverage.non_terminating_states,
|
|
986
1076
|
invalid_operation_references: invalidOperationReferences,
|
|
987
1077
|
invalid_field_references: invalidFieldReferences,
|
|
1078
|
+
semantic_warnings: semanticWarnings,
|
|
988
1079
|
warnings: qualityWarnings,
|
|
989
1080
|
},
|
|
990
1081
|
};
|
|
991
1082
|
|
|
1083
|
+
// Attach structured issues list to JSON output
|
|
1084
|
+
result.issues = buildStructuredIssues(result);
|
|
1085
|
+
|
|
992
1086
|
if (output === "json") {
|
|
993
1087
|
console.log(JSON.stringify(result, null, 2));
|
|
994
1088
|
} else {
|
|
@@ -1003,6 +1097,286 @@ function run(apiPath, options = {}) {
|
|
|
1003
1097
|
return result;
|
|
1004
1098
|
}
|
|
1005
1099
|
|
|
1100
|
+
// ---------------------------------------------------------------------------
|
|
1101
|
+
// Structured issue builder (maps raw results → standard issue objects)
|
|
1102
|
+
// ---------------------------------------------------------------------------
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Convert raw validation result into a flat array of structured issue objects,
|
|
1106
|
+
* each with a stable XFLOW error code.
|
|
1107
|
+
*/
|
|
1108
|
+
function buildStructuredIssues(result) {
|
|
1109
|
+
const issues = [];
|
|
1110
|
+
|
|
1111
|
+
// Schema failures
|
|
1112
|
+
for (const failure of (result.schemaFailures || [])) {
|
|
1113
|
+
for (const err of (failure.errors || [])) {
|
|
1114
|
+
const isAdditional = err.keyword === "additionalProperties" && err.params && err.params.additionalProperty;
|
|
1115
|
+
const isMissing = err.keyword === "required" && err.params && err.params.missingProperty;
|
|
1116
|
+
const isEnum = err.keyword === "enum";
|
|
1117
|
+
|
|
1118
|
+
let def;
|
|
1119
|
+
if (isAdditional) {
|
|
1120
|
+
def = CODES.SCHEMA_ADDITIONAL_PROPERTY;
|
|
1121
|
+
} else if (isMissing) {
|
|
1122
|
+
def = CODES.SCHEMA_MISSING_REQUIRED;
|
|
1123
|
+
} else if (isEnum) {
|
|
1124
|
+
def = CODES.SCHEMA_INVALID_ENUM;
|
|
1125
|
+
} else {
|
|
1126
|
+
def = CODES.SCHEMA_VALIDATION_FAILED;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const msg = isMissing
|
|
1130
|
+
? `Missing required property '${err.params.missingProperty}'.`
|
|
1131
|
+
: isAdditional
|
|
1132
|
+
? `Property '${err.params.additionalProperty}' is not allowed.`
|
|
1133
|
+
: err.message || def.title;
|
|
1134
|
+
|
|
1135
|
+
const suggestion = isMissing && err.params.missingProperty === "version"
|
|
1136
|
+
? "Add `version: \"1.0\"` to the x-openapi-flow object."
|
|
1137
|
+
: isMissing && err.params.missingProperty === "current_state"
|
|
1138
|
+
? "Add `current_state` to describe the operation state."
|
|
1139
|
+
: isAdditional
|
|
1140
|
+
? `Remove unsupported property \`${err.params.additionalProperty}\` from x-openapi-flow payload.`
|
|
1141
|
+
: undefined;
|
|
1142
|
+
|
|
1143
|
+
issues.push(buildIssue(def, msg, { location: failure.endpoint, suggestion }));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Orphan states
|
|
1148
|
+
for (const orphan of (result.orphans || [])) {
|
|
1149
|
+
issues.push(buildIssue(
|
|
1150
|
+
CODES.GRAPH_ORPHAN_STATES,
|
|
1151
|
+
`Orphan state: '${orphan}'.`,
|
|
1152
|
+
{
|
|
1153
|
+
location: orphan,
|
|
1154
|
+
suggestion: "Connect this state to the flow graph using transitions.",
|
|
1155
|
+
}
|
|
1156
|
+
));
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const gc = result.graphChecks || {};
|
|
1160
|
+
|
|
1161
|
+
// Graph: no initial state
|
|
1162
|
+
if (Array.isArray(gc.initial_states) && gc.initial_states.length === 0 && (result.flowCount || 0) > 0) {
|
|
1163
|
+
issues.push(buildIssue(
|
|
1164
|
+
CODES.GRAPH_NO_INITIAL_STATE,
|
|
1165
|
+
"No initial state detected (every state has incoming transitions).",
|
|
1166
|
+
{ suggestion: "Ensure at least one state has no incoming transitions (flow entry point)." }
|
|
1167
|
+
));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Graph: no terminal state
|
|
1171
|
+
if (Array.isArray(gc.terminal_states) && gc.terminal_states.length === 0 && (result.flowCount || 0) > 0) {
|
|
1172
|
+
issues.push(buildIssue(
|
|
1173
|
+
CODES.GRAPH_NO_TERMINAL_STATE,
|
|
1174
|
+
"No terminal state detected (every state has outgoing transitions).",
|
|
1175
|
+
{ suggestion: "Ensure at least one state has no outgoing transitions (flow end point)." }
|
|
1176
|
+
));
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Graph: unreachable
|
|
1180
|
+
for (const state of (gc.unreachable_states || [])) {
|
|
1181
|
+
issues.push(buildIssue(
|
|
1182
|
+
CODES.GRAPH_UNREACHABLE_STATES,
|
|
1183
|
+
`State '${state}' is unreachable from any initial state.`,
|
|
1184
|
+
{ location: state, suggestion: `Add a transition leading to '${state}' or remove it.` }
|
|
1185
|
+
));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Graph: cycle
|
|
1189
|
+
if (gc.cycle && gc.cycle.has_cycle) {
|
|
1190
|
+
const cyclePath = Array.isArray(gc.cycle.cycle_path) ? gc.cycle.cycle_path.join(" → ") : "";
|
|
1191
|
+
issues.push(buildIssue(
|
|
1192
|
+
CODES.GRAPH_CYCLE_DETECTED,
|
|
1193
|
+
`Cycle detected in flow graph: ${cyclePath}.`,
|
|
1194
|
+
{
|
|
1195
|
+
suggestion: "Remove the back-edge creating the cycle or use profile 'relaxed' to allow cycles.",
|
|
1196
|
+
details: { cycle_path: gc.cycle.cycle_path },
|
|
1197
|
+
}
|
|
1198
|
+
));
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const qc = result.qualityChecks || {};
|
|
1202
|
+
|
|
1203
|
+
// Quality: multiple initial states
|
|
1204
|
+
for (const state of (qc.multiple_initial_states || [])) {
|
|
1205
|
+
issues.push(buildIssue(
|
|
1206
|
+
CODES.QUALITY_MULTIPLE_INITIAL_STATES,
|
|
1207
|
+
`State '${state}' is an additional initial state — the flow has multiple entry points.`,
|
|
1208
|
+
{ location: state, suggestion: "Consolidate into a single initial state if possible." }
|
|
1209
|
+
));
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Quality: duplicate transitions
|
|
1213
|
+
for (const dup of (qc.duplicate_transitions || [])) {
|
|
1214
|
+
issues.push(buildIssue(
|
|
1215
|
+
CODES.QUALITY_DUPLICATE_TRANSITIONS,
|
|
1216
|
+
`Duplicate transition from '${dup.from}' to '${dup.to}' (count: ${dup.count}).`,
|
|
1217
|
+
{ location: `${dup.from} → ${dup.to}`, suggestion: "Remove redundant transition entries." }
|
|
1218
|
+
));
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Quality: non-terminating states
|
|
1222
|
+
for (const state of (qc.non_terminating_states || [])) {
|
|
1223
|
+
issues.push(buildIssue(
|
|
1224
|
+
CODES.QUALITY_NON_TERMINATING_STATES,
|
|
1225
|
+
`State '${state}' has no path to a terminal state.`,
|
|
1226
|
+
{ location: state, suggestion: "Add a transition from this state to a terminal state." }
|
|
1227
|
+
));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Quality: invalid operation references
|
|
1231
|
+
for (const ref of (qc.invalid_operation_references || [])) {
|
|
1232
|
+
issues.push(buildIssue(
|
|
1233
|
+
CODES.QUALITY_INVALID_OPERATION_REF,
|
|
1234
|
+
`Transition in '${ref.declared_in}' references unknown operationId '${ref.operation_id}' (type: ${ref.type}).`,
|
|
1235
|
+
{
|
|
1236
|
+
location: ref.declared_in,
|
|
1237
|
+
suggestion: `Check that '${ref.operation_id}' is defined in the OpenAPI spec with an operationId.`,
|
|
1238
|
+
details: { operation_id: ref.operation_id, ref_type: ref.type },
|
|
1239
|
+
}
|
|
1240
|
+
));
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Quality: invalid field references
|
|
1244
|
+
for (const ref of (qc.invalid_field_refs || qc.invalid_field_references || [])) {
|
|
1245
|
+
if (!ref) continue;
|
|
1246
|
+
issues.push(buildIssue(
|
|
1247
|
+
CODES.QUALITY_INVALID_FIELD_REF,
|
|
1248
|
+
typeof ref === "string"
|
|
1249
|
+
? `Invalid field reference: ${ref}`
|
|
1250
|
+
: `Invalid field reference '${ref.ref || ref.message}' in '${ref.endpoint || ref.declared_in || "unknown"}'.`,
|
|
1251
|
+
{ suggestion: "Verify the field path and operationId in the field reference." }
|
|
1252
|
+
));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Semantic warnings
|
|
1256
|
+
for (const warning of (qc.semantic_warnings || [])) {
|
|
1257
|
+
const isAmbiguous = warning.includes("ambiguous state variants") || warning.includes("ambiguous transition");
|
|
1258
|
+
const def = isAmbiguous ? CODES.QUALITY_SEMANTIC_AMBIGUOUS_VARIANTS : CODES.QUALITY_SEMANTIC_INCONSISTENT_NAMING;
|
|
1259
|
+
issues.push(buildIssue(
|
|
1260
|
+
def,
|
|
1261
|
+
warning,
|
|
1262
|
+
{ suggestion: "Adopt a single consistent naming convention for all state names (e.g. UPPER_SNAKE_CASE)." }
|
|
1263
|
+
));
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return issues;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ---------------------------------------------------------------------------
|
|
1270
|
+
// Quality report
|
|
1271
|
+
// ---------------------------------------------------------------------------
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Compute a quality score and structured report from a validation result.
|
|
1275
|
+
*
|
|
1276
|
+
* @param {object} result - Result returned by `run()`.
|
|
1277
|
+
* @param {{ semantic?: boolean }} [options]
|
|
1278
|
+
* @returns {object} Quality report with score, grade, issues, suggestions, breakdown.
|
|
1279
|
+
*/
|
|
1280
|
+
function computeQualityReport(result, options = {}) {
|
|
1281
|
+
const semantic = options.semantic === true;
|
|
1282
|
+
const issues = result.issues && result.issues.length > 0 ? result.issues : buildStructuredIssues(result);
|
|
1283
|
+
const flowCount = result.flowCount || 0;
|
|
1284
|
+
|
|
1285
|
+
// ── Schema score (weight 40 pts) ────────────────────────────────────────
|
|
1286
|
+
const schemaErrors = (result.schemaFailures || []).length;
|
|
1287
|
+
const schemaScore = flowCount === 0 ? 100 : Math.max(0, Math.round((1 - schemaErrors / flowCount) * 100));
|
|
1288
|
+
|
|
1289
|
+
// ── Graph score (weight 30 pts) ──────────────────────────────────────────
|
|
1290
|
+
const gc = result.graphChecks || {};
|
|
1291
|
+
const graphChecksTotal = 4;
|
|
1292
|
+
let graphPassed = 0;
|
|
1293
|
+
if (!Array.isArray(gc.initial_states) || gc.initial_states.length > 0 || flowCount === 0) graphPassed += 1;
|
|
1294
|
+
if (!Array.isArray(gc.terminal_states) || gc.terminal_states.length > 0 || flowCount === 0) graphPassed += 1;
|
|
1295
|
+
if (!Array.isArray(gc.unreachable_states) || gc.unreachable_states.length === 0) graphPassed += 1;
|
|
1296
|
+
if (!gc.cycle || !gc.cycle.has_cycle) graphPassed += 1;
|
|
1297
|
+
const graphScore = Math.round((graphPassed / graphChecksTotal) * 100);
|
|
1298
|
+
|
|
1299
|
+
// ── Quality score (weight 20 pts) ────────────────────────────────────────
|
|
1300
|
+
const qc = result.qualityChecks || {};
|
|
1301
|
+
const qualityIssueCount =
|
|
1302
|
+
(qc.multiple_initial_states || []).length +
|
|
1303
|
+
(qc.duplicate_transitions || []).length +
|
|
1304
|
+
(qc.non_terminating_states || []).length +
|
|
1305
|
+
(qc.invalid_operation_references || []).length +
|
|
1306
|
+
(qc.invalid_field_references || []).length;
|
|
1307
|
+
const qualityScore = flowCount === 0 ? 100 : Math.max(0, Math.round((1 - Math.min(1, qualityIssueCount / Math.max(1, flowCount))) * 100));
|
|
1308
|
+
|
|
1309
|
+
// ── Semantic score (weight 10 pts) ───────────────────────────────────────
|
|
1310
|
+
const semanticIssueCount = semantic ? (qc.semantic_warnings || []).length : 0;
|
|
1311
|
+
const semanticScore = flowCount === 0 ? 100 : Math.max(0, Math.round((1 - Math.min(1, semanticIssueCount / Math.max(1, flowCount))) * 100));
|
|
1312
|
+
|
|
1313
|
+
// ── Overall weighted score ───────────────────────────────────────────────
|
|
1314
|
+
const semanticWeight = semantic ? 0.10 : 0;
|
|
1315
|
+
const qualityWeight = semantic ? 0.20 : 0.25;
|
|
1316
|
+
const graphWeight = semantic ? 0.30 : 0.35;
|
|
1317
|
+
const schemaWeight = semantic ? 0.40 : 0.40;
|
|
1318
|
+
|
|
1319
|
+
const weightedScore =
|
|
1320
|
+
schemaScore * schemaWeight +
|
|
1321
|
+
graphScore * graphWeight +
|
|
1322
|
+
qualityScore * qualityWeight +
|
|
1323
|
+
(semantic ? semanticScore * semanticWeight : 0);
|
|
1324
|
+
|
|
1325
|
+
const score = Math.round(weightedScore);
|
|
1326
|
+
|
|
1327
|
+
let grade;
|
|
1328
|
+
if (score >= 90) grade = "A";
|
|
1329
|
+
else if (score >= 80) grade = "B";
|
|
1330
|
+
else if (score >= 70) grade = "C";
|
|
1331
|
+
else if (score >= 60) grade = "D";
|
|
1332
|
+
else grade = "F";
|
|
1333
|
+
|
|
1334
|
+
const suggestions = [...new Set(
|
|
1335
|
+
issues.map((issue) => issue.suggestion).filter(Boolean)
|
|
1336
|
+
)];
|
|
1337
|
+
|
|
1338
|
+
const breakdown = {
|
|
1339
|
+
schema: {
|
|
1340
|
+
score: schemaScore,
|
|
1341
|
+
weight: Math.round(schemaWeight * 100),
|
|
1342
|
+
failed: schemaErrors,
|
|
1343
|
+
passed: flowCount - schemaErrors,
|
|
1344
|
+
},
|
|
1345
|
+
graph: {
|
|
1346
|
+
score: graphScore,
|
|
1347
|
+
weight: Math.round(graphWeight * 100),
|
|
1348
|
+
checks_passed: graphPassed,
|
|
1349
|
+
checks_total: graphChecksTotal,
|
|
1350
|
+
},
|
|
1351
|
+
quality: {
|
|
1352
|
+
score: qualityScore,
|
|
1353
|
+
weight: Math.round(qualityWeight * 100),
|
|
1354
|
+
issues: qualityIssueCount,
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
if (semantic) {
|
|
1359
|
+
breakdown.semantic = {
|
|
1360
|
+
score: semanticScore,
|
|
1361
|
+
weight: Math.round(semanticWeight * 100),
|
|
1362
|
+
issues: semanticIssueCount,
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
return {
|
|
1367
|
+
generated_at: new Date().toISOString(),
|
|
1368
|
+
path: result.path,
|
|
1369
|
+
profile: result.profile,
|
|
1370
|
+
score,
|
|
1371
|
+
grade,
|
|
1372
|
+
flow_count: flowCount,
|
|
1373
|
+
ok: result.ok,
|
|
1374
|
+
issues: issues.filter((issue) => semantic || issue.category !== "quality" || !issue.code.startsWith("XFLOW_W20")),
|
|
1375
|
+
suggestions,
|
|
1376
|
+
breakdown,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1006
1380
|
// Allow the module to be required by tests without side-effects.
|
|
1007
1381
|
if (require.main === module) {
|
|
1008
1382
|
const result = run(process.argv[2]);
|
|
@@ -1018,5 +1392,8 @@ module.exports = {
|
|
|
1018
1392
|
detectDuplicateTransitions,
|
|
1019
1393
|
detectInvalidOperationReferences,
|
|
1020
1394
|
detectTerminalCoverage,
|
|
1395
|
+
detectSemanticModelingWarnings,
|
|
1396
|
+
buildStructuredIssues,
|
|
1397
|
+
computeQualityReport,
|
|
1021
1398
|
run,
|
|
1022
1399
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x-openapi-flow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "OpenAPI extension for resource workflow and lifecycle management",
|
|
5
5
|
"main": "lib/validator.js",
|
|
6
6
|
"repository": {
|
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"sync:readme": "node ../scripts/sync-package-readme.js",
|
|
35
35
|
"prepack": "npm run sync:readme",
|
|
36
|
-
"test": "npm run test:cli && npm run test:ui && npm run test:integration && npm run test:smoke",
|
|
36
|
+
"test": "npm run test:cli && npm run test:ui && npm run test:integration && npm run test:runtime && npm run test:engine && npm run test:smoke",
|
|
37
37
|
"test:cli": "node --test tests/cli/cli.test.js",
|
|
38
38
|
"test:ui": "node --test tests/plugins/*.test.js",
|
|
39
39
|
"test:integration": "node --test tests/integration/*.test.js",
|
|
40
|
+
"test:runtime": "node --test tests/runtime/*.test.js",
|
|
41
|
+
"test:engine": "node --test tests/runtime/state-machine-engine.test.js tests/runtime/openapi-state-machine-adapter.test.js",
|
|
40
42
|
"test:examples": "node --test tests/integration/examples.test.js",
|
|
41
43
|
"test:smoke": "node bin/x-openapi-flow.js validate examples/payment-api.yaml --profile strict"
|
|
42
44
|
},
|