x-openapi-flow 1.1.3 → 1.2.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 CHANGED
@@ -8,6 +8,21 @@ CLI and specification for validating the `x-openapi-flow` extension field in Ope
8
8
  npm install x-openapi-flow
9
9
  ```
10
10
 
11
+ Optional mirror on GitHub Packages (default usage remains unscoped on npm):
12
+
13
+ ```bash
14
+ npm config set @tiago-marques:registry https://npm.pkg.github.com
15
+ npm install @tiago-marques/x-openapi-flow
16
+ ```
17
+
18
+ If authentication is required, include this in your `.npmrc`:
19
+
20
+ ```ini
21
+ //npm.pkg.github.com/:_authToken=${GH_PACKAGES_TOKEN}
22
+ ```
23
+
24
+ Use a GitHub PAT with `read:packages` (install) and `write:packages` (publish).
25
+
11
26
  ## Quick Usage
12
27
 
13
28
  ```bash
@@ -60,6 +75,13 @@ Create `x-openapi-flow.config.json` in your project directory:
60
75
 
61
76
  - `next_operation_id`: operationId usually called for the next state transition
62
77
  - `prerequisite_operation_ids`: operationIds expected before a transition
78
+ - `prerequisite_field_refs`: required field refs before transition
79
+ - `propagated_field_refs`: field refs used by downstream flows
80
+
81
+ Field reference format:
82
+
83
+ - `operationId:request.body.field`
84
+ - `operationId:response.<status>.body.field`
63
85
 
64
86
  ## Swagger UI
65
87
 
@@ -67,6 +89,8 @@ Create `x-openapi-flow.config.json` in your project directory:
67
89
  - For UI interpretation of `x-openapi-flow`, use `showExtensions: true` plus the example plugin at `examples/swagger-ui/x-openapi-flow-plugin.js`.
68
90
  - A ready HTML example is available at `examples/swagger-ui/index.html`.
69
91
 
92
+ ![Swagger UI integration result](../docs/assets/swagger-ui-integration-result-v2.svg)
93
+
70
94
  ## Graph Output Example
71
95
 
72
96
  `x-openapi-flow graph` includes transition guidance labels in Mermaid output when present (`next_operation_id`, `prerequisite_operation_ids`).
@@ -22,6 +22,12 @@ paths:
22
22
  - target_state: CAPTURED
23
23
  condition: Merchant explicitly requests capture.
24
24
  trigger_type: synchronous
25
+ next_operation_id: capturePayment
26
+ prerequisite_field_refs:
27
+ - createPayment:response.201.body.id
28
+ propagated_field_refs:
29
+ - createPayment:request.body.amount
30
+ - createPayment:request.body.currency
25
31
  requestBody:
26
32
  required: true
27
33
  content:
@@ -15,6 +15,9 @@
15
15
 
16
16
  <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
17
17
  <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
18
+ <script>
19
+ window.XOpenApiFlowGraphImageUrl = "https://raw.githubusercontent.com/tiago-marques/x-openapi-flow/main/docs/assets/graph-order-guided.svg";
20
+ </script>
18
21
  <script src="./x-openapi-flow-plugin.js"></script>
19
22
  <script>
20
23
  window.ui = SwaggerUIBundle({
@@ -1,46 +1,262 @@
1
1
  window.XOpenApiFlowPlugin = function () {
2
+ const h = React.createElement;
3
+
4
+ function toPlain(value) {
5
+ if (!value) return value;
6
+ return value.toJS ? value.toJS() : value;
7
+ }
8
+
9
+ function text(value) {
10
+ if (value === null || value === undefined || value === "") return "-";
11
+ if (Array.isArray(value)) return value.length ? value.join(", ") : "-";
12
+ return String(value);
13
+ }
14
+
15
+ function transitionsList(currentState, transitions) {
16
+ if (!Array.isArray(transitions) || transitions.length === 0) {
17
+ return h("div", { style: { opacity: 0.85, fontStyle: "italic" } }, "No transitions (terminal state)");
18
+ }
19
+
20
+ return h(
21
+ "ul",
22
+ { style: { margin: "6px 0 0 18px", padding: 0 } },
23
+ transitions.map((transition, index) =>
24
+ h(
25
+ "li",
26
+ { key: `${currentState}-${index}`, style: { marginBottom: "4px", lineHeight: 1.45 } },
27
+ h("strong", null, text(transition.trigger_type)),
28
+ " → ",
29
+ h("strong", null, text(transition.target_state)),
30
+ transition.condition ? ` — ${text(transition.condition)}` : "",
31
+ transition.next_operation_id ? ` (next: ${text(transition.next_operation_id)})` : ""
32
+ )
33
+ )
34
+ );
35
+ }
36
+
37
+ function miniGraph(currentState, transitions) {
38
+ if (!Array.isArray(transitions) || transitions.length === 0) {
39
+ return [h("div", { key: "terminal", style: { fontFamily: "monospace" } }, `${text(currentState)} [terminal]`)];
40
+ }
41
+
42
+ return transitions.map((transition, index) =>
43
+ h(
44
+ "div",
45
+ { key: `edge-${index}`, style: { fontFamily: "monospace", lineHeight: 1.45 } },
46
+ `${text(currentState)} --> ${text(transition.target_state)} [${text(transition.trigger_type)}]`
47
+ )
48
+ );
49
+ }
50
+
2
51
  return {
3
52
  wrapComponents: {
4
- OperationSummary: (Original, system) => (props) => {
53
+ OperationSummary: (Original) => (props) => {
5
54
  const operation = props.operation;
6
55
  const flow = operation && operation.get && operation.get("x-openapi-flow");
7
56
 
8
57
  if (!flow) {
9
- return React.createElement(Original, props);
58
+ return h(Original, props);
10
59
  }
11
60
 
12
- const flowObject = flow && flow.toJS ? flow.toJS() : flow;
13
- const currentState = flowObject.current_state || "-";
14
- const version = flowObject.version || "-";
61
+ const flowObject = toPlain(flow) || {};
62
+ const currentState = flowObject.current_state;
63
+ const transitions = Array.isArray(flowObject.transitions) ? flowObject.transitions : [];
64
+ const graphImageUrl = flowObject.graph_image_url || window.XOpenApiFlowGraphImageUrl;
15
65
 
16
- return React.createElement(
66
+ const metadataGrid = h(
67
+ "div",
68
+ {
69
+ style: {
70
+ display: "grid",
71
+ gridTemplateColumns: "140px 1fr",
72
+ gap: "4px 10px",
73
+ fontSize: "12px",
74
+ marginTop: "6px",
75
+ },
76
+ },
77
+ h("div", { style: { opacity: 0.85 } }, "version"),
78
+ h("div", null, text(flowObject.version)),
79
+ h("div", { style: { opacity: 0.85 } }, "id"),
80
+ h("div", null, text(flowObject.id)),
81
+ h("div", { style: { opacity: 0.85 } }, "current_state"),
82
+ h("div", null, text(currentState))
83
+ );
84
+
85
+ const graphImageNode = graphImageUrl
86
+ ? h(
87
+ "div",
88
+ { style: { marginTop: "10px" } },
89
+ h("div", { style: { fontWeight: 700, marginBottom: "6px" } }, "Flow graph image"),
90
+ h("img", {
91
+ src: graphImageUrl,
92
+ alt: "x-openapi-flow graph",
93
+ style: {
94
+ width: "100%",
95
+ maxWidth: "560px",
96
+ border: "1px solid rgba(255,255,255,0.3)",
97
+ borderRadius: "6px",
98
+ },
99
+ })
100
+ )
101
+ : null;
102
+
103
+ return h(
17
104
  "div",
18
105
  null,
19
- React.createElement(Original, props),
20
- React.createElement(
106
+ h(Original, props),
107
+ h(
21
108
  "div",
22
109
  {
23
110
  style: {
24
111
  marginTop: "8px",
25
- padding: "8px 10px",
26
- border: "1px solid #d9d9d9",
27
- borderRadius: "6px",
28
- background: "#fafafa",
112
+ padding: "10px",
113
+ border: "1px solid rgba(255,255,255,0.28)",
114
+ borderRadius: "8px",
115
+ background: "rgba(0,0,0,0.12)",
29
116
  fontSize: "12px",
30
117
  },
31
118
  },
32
- React.createElement("strong", null, "x-openapi-flow"),
33
- React.createElement(
119
+ h("div", { style: { fontWeight: 700 } }, "x-openapi-flow"),
120
+ metadataGrid,
121
+ h("div", { style: { marginTop: "10px", fontWeight: 700 } }, "Transitions"),
122
+ transitionsList(currentState, transitions),
123
+ h("div", { style: { marginTop: "10px", fontWeight: 700 } }, "Flow graph (operation-level)"),
124
+ h(
34
125
  "div",
35
- { style: { marginTop: "4px" } },
36
- "version: ",
37
- version,
38
- " | current_state: ",
39
- currentState
40
- )
126
+ {
127
+ style: {
128
+ marginTop: "6px",
129
+ border: "1px dashed rgba(255,255,255,0.32)",
130
+ borderRadius: "6px",
131
+ padding: "8px",
132
+ },
133
+ },
134
+ ...miniGraph(currentState, transitions)
135
+ ),
136
+ graphImageNode
41
137
  )
42
138
  );
43
139
  },
44
140
  },
45
141
  };
46
142
  };
143
+
144
+ (function () {
145
+ const styleId = 'x-openapi-flow-ui-style';
146
+
147
+ function injectStyles() {
148
+ if (document.getElementById(styleId)) return;
149
+
150
+ const style = document.createElement('style');
151
+ style.id = styleId;
152
+ style.textContent = `
153
+ .xof-card { border: 1px solid rgba(255,255,255,0.28); border-radius: 8px; padding: 10px; background: rgba(0,0,0,0.12); }
154
+ .xof-title { font-weight: 700; margin-bottom: 8px; }
155
+ .xof-meta { display: grid; grid-template-columns: 140px 1fr; gap: 4px 10px; font-size: 12px; margin-bottom: 10px; }
156
+ .xof-meta-label { opacity: 0.85; }
157
+ .xof-list { margin: 0; padding-left: 18px; }
158
+ .xof-list li { margin: 4px 0; }
159
+ .xof-graph { margin-top: 10px; padding: 8px; border: 1px dashed rgba(255,255,255,0.32); border-radius: 6px; }
160
+ .xof-graph-title { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
161
+ .xof-edge { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; }
162
+ .xof-empty { opacity: 0.85; font-style: italic; }
163
+ `;
164
+
165
+ document.head.appendChild(style);
166
+ }
167
+
168
+ function text(value) {
169
+ if (value === null || value === undefined || value === '') return '-';
170
+ if (Array.isArray(value)) return value.length ? value.join(', ') : '-';
171
+ return String(value);
172
+ }
173
+
174
+ function renderTransitions(currentState, transitions) {
175
+ if (!Array.isArray(transitions) || transitions.length === 0) {
176
+ return '<div class="xof-empty">No transitions (terminal state)</div>';
177
+ }
178
+
179
+ return `<ul class="xof-list">${transitions
180
+ .map((transition) => {
181
+ const condition = transition.condition ? ` — ${text(transition.condition)}` : '';
182
+ const nextOperation = transition.next_operation_id ? ` (next: ${text(transition.next_operation_id)})` : '';
183
+ return `<li><strong>${text(transition.trigger_type)}</strong> → <strong>${text(transition.target_state)}</strong>${condition}${nextOperation}</li>`;
184
+ })
185
+ .join('')}</ul>`;
186
+ }
187
+
188
+ function renderGraph(currentState, transitions) {
189
+ if (!Array.isArray(transitions) || transitions.length === 0) {
190
+ return `<div class="xof-edge">${text(currentState)} [terminal]</div>`;
191
+ }
192
+
193
+ return transitions
194
+ .map((transition) => `<div class="xof-edge">${text(currentState)} --> ${text(transition.target_state)} [${text(transition.trigger_type)}]</div>`)
195
+ .join('');
196
+ }
197
+
198
+ function renderCard(flow) {
199
+ const transitions = Array.isArray(flow.transitions) ? flow.transitions : [];
200
+ return `
201
+ <div class="xof-card">
202
+ <div class="xof-title">x-openapi-flow</div>
203
+ <div class="xof-meta">
204
+ <div class="xof-meta-label">version</div><div>${text(flow.version)}</div>
205
+ <div class="xof-meta-label">id</div><div>${text(flow.id)}</div>
206
+ <div class="xof-meta-label">current_state</div><div>${text(flow.current_state)}</div>
207
+ </div>
208
+ <div><strong>Transitions</strong></div>
209
+ ${renderTransitions(flow.current_state, transitions)}
210
+ <div class="xof-graph">
211
+ <div class="xof-graph-title">Flow graph (operation-level)</div>
212
+ ${renderGraph(flow.current_state, transitions)}
213
+ </div>
214
+ </div>
215
+ `;
216
+ }
217
+
218
+ function findXOpenApiFlowValueCell(opblock) {
219
+ const rows = opblock.querySelectorAll('tr');
220
+ for (const row of rows) {
221
+ const cells = row.querySelectorAll('td');
222
+ if (cells.length < 2) continue;
223
+ if (cells[0].innerText.trim() === 'x-openapi-flow') {
224
+ return cells[1];
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+
230
+ function enhanceOperation(opblock) {
231
+ const valueCell = findXOpenApiFlowValueCell(opblock);
232
+ if (!valueCell || valueCell.dataset.xofEnhanced === '1') return;
233
+
234
+ const raw = valueCell.innerText.trim();
235
+ if (!raw) return;
236
+
237
+ let flow;
238
+ try {
239
+ flow = JSON.parse(raw);
240
+ } catch (_error) {
241
+ return;
242
+ }
243
+
244
+ valueCell.innerHTML = renderCard(flow);
245
+ valueCell.dataset.xofEnhanced = '1';
246
+ }
247
+
248
+ function enhanceAll() {
249
+ injectStyles();
250
+ const opblocks = document.querySelectorAll('.opblock');
251
+ opblocks.forEach((opblock) => enhanceOperation(opblock));
252
+ }
253
+
254
+ const observer = new MutationObserver(() => {
255
+ enhanceAll();
256
+ });
257
+
258
+ window.addEventListener('load', () => {
259
+ enhanceAll();
260
+ observer.observe(document.body, { childList: true, subtree: true });
261
+ });
262
+ })();
package/lib/validator.js CHANGED
@@ -149,20 +149,42 @@ function defaultResult(pathValue, ok = true) {
149
149
  duplicate_transitions: [],
150
150
  non_terminating_states: [],
151
151
  invalid_operation_references: [],
152
+ invalid_field_references: [],
152
153
  warnings: [],
153
154
  },
154
155
  };
155
156
  }
156
157
 
158
+ function getOperationsById(api) {
159
+ const operationsById = new Map();
160
+ const paths = (api && api.paths) || {};
161
+ const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
162
+
163
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
164
+ for (const method of methods) {
165
+ const operation = pathItem[method];
166
+ if (!operation || !operation.operationId) {
167
+ continue;
168
+ }
169
+
170
+ operationsById.set(operation.operationId, {
171
+ operation,
172
+ endpoint: `${method.toUpperCase()} ${pathKey}`,
173
+ });
174
+ }
175
+ }
176
+
177
+ return operationsById;
178
+ }
179
+
157
180
  /**
158
181
  * Detect invalid operationId references declared in transitions.
182
+ * @param {Map<string, { operation: object, endpoint: string }>} operationsById
159
183
  * @param {{ endpoint: string, operation_id?: string, flow: object }[]} flows
160
184
  * @returns {{ type: string, operation_id: string, declared_in: string }[]}
161
185
  */
162
- function detectInvalidOperationReferences(flows) {
163
- const knownOperationIds = new Set(
164
- flows.map(({ operation_id }) => operation_id).filter(Boolean)
165
- );
186
+ function detectInvalidOperationReferences(operationsById, flows) {
187
+ const knownOperationIds = new Set(operationsById.keys());
166
188
 
167
189
  const invalidReferences = [];
168
190
 
@@ -197,6 +219,196 @@ function detectInvalidOperationReferences(flows) {
197
219
  return invalidReferences;
198
220
  }
199
221
 
222
+ function parseFieldReference(refValue) {
223
+ if (typeof refValue !== "string") {
224
+ return null;
225
+ }
226
+
227
+ const match = refValue.match(/^([^:]+):(request\.body|response\.(\d{3}|default)\.body)\.(.+)$/);
228
+ if (!match) {
229
+ return null;
230
+ }
231
+
232
+ const operationId = match[1];
233
+ const scope = match[2];
234
+ const responseCode = match[3];
235
+ const fieldPath = match[4];
236
+
237
+ return {
238
+ operation_id: operationId,
239
+ scope,
240
+ response_code: responseCode,
241
+ field_path: fieldPath,
242
+ };
243
+ }
244
+
245
+ function resolveSchema(api, schema, depth = 0) {
246
+ if (!schema || typeof schema !== "object") {
247
+ return null;
248
+ }
249
+
250
+ if (depth > 10) {
251
+ return null;
252
+ }
253
+
254
+ if (schema.$ref && typeof schema.$ref === "string") {
255
+ const ref = schema.$ref;
256
+ if (!ref.startsWith("#/")) {
257
+ return null;
258
+ }
259
+
260
+ const tokens = ref.slice(2).split("/");
261
+ let target = api;
262
+ for (const token of tokens) {
263
+ if (!target || typeof target !== "object") {
264
+ return null;
265
+ }
266
+ target = target[token];
267
+ }
268
+
269
+ return resolveSchema(api, target, depth + 1);
270
+ }
271
+
272
+ return schema;
273
+ }
274
+
275
+ function hasFieldPath(api, schema, pathTokens) {
276
+ const resolved = resolveSchema(api, schema);
277
+ if (!resolved) {
278
+ return false;
279
+ }
280
+
281
+ if (pathTokens.length === 0) {
282
+ return true;
283
+ }
284
+
285
+ const [currentToken, ...rest] = pathTokens;
286
+
287
+ if (Array.isArray(resolved.anyOf)) {
288
+ return resolved.anyOf.some((item) => hasFieldPath(api, item, pathTokens));
289
+ }
290
+ if (Array.isArray(resolved.oneOf)) {
291
+ return resolved.oneOf.some((item) => hasFieldPath(api, item, pathTokens));
292
+ }
293
+ if (Array.isArray(resolved.allOf)) {
294
+ return resolved.allOf.some((item) => hasFieldPath(api, item, pathTokens));
295
+ }
296
+
297
+ if (resolved.type === "array" && resolved.items) {
298
+ return hasFieldPath(api, resolved.items, pathTokens);
299
+ }
300
+
301
+ if (resolved.properties && typeof resolved.properties === "object") {
302
+ if (!(currentToken in resolved.properties)) {
303
+ return false;
304
+ }
305
+ return hasFieldPath(api, resolved.properties[currentToken], rest);
306
+ }
307
+
308
+ if (resolved.additionalProperties && typeof resolved.additionalProperties === "object") {
309
+ return hasFieldPath(api, resolved.additionalProperties, rest);
310
+ }
311
+
312
+ return false;
313
+ }
314
+
315
+ function resolveFieldReferenceSchema(api, operationsById, parsedRef) {
316
+ const operationInfo = operationsById.get(parsedRef.operation_id);
317
+ if (!operationInfo) {
318
+ return { error: "operation_not_found" };
319
+ }
320
+
321
+ const operation = operationInfo.operation;
322
+ if (parsedRef.scope === "request.body") {
323
+ const requestSchema = operation.requestBody
324
+ && operation.requestBody.content
325
+ && operation.requestBody.content["application/json"]
326
+ && operation.requestBody.content["application/json"].schema;
327
+
328
+ if (!requestSchema) {
329
+ return { error: "request_schema_not_found" };
330
+ }
331
+
332
+ return { schema: requestSchema };
333
+ }
334
+
335
+ const responseCode = parsedRef.response_code;
336
+ const responseSchema = operation.responses
337
+ && operation.responses[responseCode]
338
+ && operation.responses[responseCode].content
339
+ && operation.responses[responseCode].content["application/json"]
340
+ && operation.responses[responseCode].content["application/json"].schema;
341
+
342
+ if (!responseSchema) {
343
+ return { error: "response_schema_not_found" };
344
+ }
345
+
346
+ return { schema: responseSchema };
347
+ }
348
+
349
+ function detectInvalidFieldReferences(api, operationsById, flows) {
350
+ const invalidFieldReferences = [];
351
+
352
+ for (const { endpoint, flow } of flows) {
353
+ const transitions = flow.transitions || [];
354
+
355
+ for (const transition of transitions) {
356
+ const referenceGroups = [
357
+ {
358
+ type: "prerequisite_field_refs",
359
+ refs: Array.isArray(transition.prerequisite_field_refs)
360
+ ? transition.prerequisite_field_refs
361
+ : [],
362
+ },
363
+ {
364
+ type: "propagated_field_refs",
365
+ refs: Array.isArray(transition.propagated_field_refs)
366
+ ? transition.propagated_field_refs
367
+ : [],
368
+ },
369
+ ];
370
+
371
+ for (const group of referenceGroups) {
372
+ for (const refValue of group.refs) {
373
+ const parsedRef = parseFieldReference(refValue);
374
+ if (!parsedRef) {
375
+ invalidFieldReferences.push({
376
+ type: group.type,
377
+ reference: refValue,
378
+ reason: "invalid_format",
379
+ declared_in: endpoint,
380
+ });
381
+ continue;
382
+ }
383
+
384
+ const resolvedSchema = resolveFieldReferenceSchema(api, operationsById, parsedRef);
385
+ if (resolvedSchema.error) {
386
+ invalidFieldReferences.push({
387
+ type: group.type,
388
+ reference: refValue,
389
+ reason: resolvedSchema.error,
390
+ declared_in: endpoint,
391
+ });
392
+ continue;
393
+ }
394
+
395
+ const pathTokens = parsedRef.field_path.split(".").filter(Boolean);
396
+ if (!hasFieldPath(api, resolvedSchema.schema, pathTokens)) {
397
+ invalidFieldReferences.push({
398
+ type: group.type,
399
+ reference: refValue,
400
+ reason: "field_not_found",
401
+ declared_in: endpoint,
402
+ });
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+ return invalidFieldReferences;
410
+ }
411
+
200
412
  /**
201
413
  * Verify that every target_state referenced in transitions corresponds to a
202
414
  * current_state defined in at least one endpoint of the same API.
@@ -588,6 +800,7 @@ function run(apiPath, options = {}) {
588
800
  }
589
801
 
590
802
  // 5. Advanced graph checks
803
+ const operationsById = getOperationsById(api);
591
804
  const graph = buildStateGraph(flows);
592
805
  const initialStates = [...graph.nodes].filter(
593
806
  (state) => graph.indegree.get(state) === 0
@@ -599,7 +812,8 @@ function run(apiPath, options = {}) {
599
812
  const cycle = detectCycle(graph);
600
813
  const duplicateTransitions = detectDuplicateTransitions(flows);
601
814
  const terminalCoverage = detectTerminalCoverage(graph);
602
- const invalidOperationReferences = detectInvalidOperationReferences(flows);
815
+ const invalidOperationReferences = detectInvalidOperationReferences(operationsById, flows);
816
+ const invalidFieldReferences = detectInvalidFieldReferences(api, operationsById, flows);
603
817
  const multipleInitialStates = initialStates.length > 1 ? initialStates : [];
604
818
 
605
819
  if (profileConfig.runAdvanced) {
@@ -645,6 +859,12 @@ function run(apiPath, options = {}) {
645
859
  );
646
860
  }
647
861
 
862
+ if (profileConfig.runQuality && invalidFieldReferences.length > 0) {
863
+ qualityWarnings.push(
864
+ `Transition field references not found/invalid: ${invalidFieldReferences.length}`
865
+ );
866
+ }
867
+
648
868
  if (strictQuality && qualityWarnings.length > 0) {
649
869
  hasErrors = true;
650
870
  }
@@ -734,6 +954,7 @@ function run(apiPath, options = {}) {
734
954
  duplicate_transitions: duplicateTransitions,
735
955
  non_terminating_states: terminalCoverage.non_terminating_states,
736
956
  invalid_operation_references: invalidOperationReferences,
957
+ invalid_field_references: invalidFieldReferences,
737
958
  warnings: qualityWarnings,
738
959
  },
739
960
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-openapi-flow",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "OpenAPI extension for resource workflow and lifecycle management",
5
5
  "main": "lib/validator.js",
6
6
  "repository": {
@@ -69,6 +69,20 @@
69
69
  "items": {
70
70
  "type": "string"
71
71
  }
72
+ },
73
+ "prerequisite_field_refs": {
74
+ "type": "array",
75
+ "description": "Field references required before this transition. Format: operationId:request.body.field or operationId:response.<status>.body.field",
76
+ "items": {
77
+ "type": "string"
78
+ }
79
+ },
80
+ "propagated_field_refs": {
81
+ "type": "array",
82
+ "description": "Field references produced/propagated for downstream flows. Format: operationId:request.body.field or operationId:response.<status>.body.field",
83
+ "items": {
84
+ "type": "string"
85
+ }
72
86
  }
73
87
  },
74
88
  "additionalProperties": false