@archrad/deterministic 0.1.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/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +15 -0
- package/LICENSE +17 -0
- package/README.md +284 -0
- package/SECURITY.md +26 -0
- package/biome.json +25 -0
- package/demo-validate.gif +0 -0
- package/dist/cli-findings.d.ts +23 -0
- package/dist/cli-findings.d.ts.map +1 -0
- package/dist/cli-findings.js +88 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +341 -0
- package/dist/edgeConfigCodeGenerator.d.ts +55 -0
- package/dist/edgeConfigCodeGenerator.d.ts.map +1 -0
- package/dist/edgeConfigCodeGenerator.js +249 -0
- package/dist/exportPipeline.d.ts +23 -0
- package/dist/exportPipeline.d.ts.map +1 -0
- package/dist/exportPipeline.js +65 -0
- package/dist/golden-bundle.d.ts +21 -0
- package/dist/golden-bundle.d.ts.map +1 -0
- package/dist/golden-bundle.js +166 -0
- package/dist/graphPredicates.d.ts +10 -0
- package/dist/graphPredicates.d.ts.map +1 -0
- package/dist/graphPredicates.js +33 -0
- package/dist/hostPort.d.ts +12 -0
- package/dist/hostPort.d.ts.map +1 -0
- package/dist/hostPort.js +39 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/ir-lint.d.ts +11 -0
- package/dist/ir-lint.d.ts.map +1 -0
- package/dist/ir-lint.js +16 -0
- package/dist/ir-normalize.d.ts +48 -0
- package/dist/ir-normalize.d.ts.map +1 -0
- package/dist/ir-normalize.js +81 -0
- package/dist/ir-structural.d.ts +40 -0
- package/dist/ir-structural.d.ts.map +1 -0
- package/dist/ir-structural.js +267 -0
- package/dist/lint-graph.d.ts +40 -0
- package/dist/lint-graph.d.ts.map +1 -0
- package/dist/lint-graph.js +133 -0
- package/dist/lint-rules.d.ts +40 -0
- package/dist/lint-rules.d.ts.map +1 -0
- package/dist/lint-rules.js +290 -0
- package/dist/nodeExpress.d.ts +2 -0
- package/dist/nodeExpress.d.ts.map +1 -0
- package/dist/nodeExpress.js +528 -0
- package/dist/openapi-structural.d.ts +26 -0
- package/dist/openapi-structural.d.ts.map +1 -0
- package/dist/openapi-structural.js +82 -0
- package/dist/openapi-to-ir.d.ts +26 -0
- package/dist/openapi-to-ir.d.ts.map +1 -0
- package/dist/openapi-to-ir.js +131 -0
- package/dist/pythonFastAPI.d.ts +2 -0
- package/dist/pythonFastAPI.d.ts.map +1 -0
- package/dist/pythonFastAPI.js +664 -0
- package/dist/validate-drift.d.ts +54 -0
- package/dist/validate-drift.d.ts.map +1 -0
- package/dist/validate-drift.js +184 -0
- package/dist/yamlToIr.d.ts +14 -0
- package/dist/yamlToIr.d.ts.map +1 -0
- package/dist/yamlToIr.js +39 -0
- package/docs/CONCEPT_ADOPTION_AND_LIMITS.md +47 -0
- package/docs/CUSTOM_RULES.md +87 -0
- package/docs/ENGINEERING_NOTES.md +42 -0
- package/docs/IR_CONTRACT.md +54 -0
- package/docs/STRUCTURAL_VS_SEMANTIC_VALIDATION.md +86 -0
- package/fixtures/demo-direct-db-layered.json +37 -0
- package/fixtures/demo-direct-db-violation.json +22 -0
- package/fixtures/ecommerce-with-warnings.json +89 -0
- package/fixtures/invalid-cycle.json +15 -0
- package/fixtures/invalid-edge-unknown-node.json +14 -0
- package/fixtures/minimal-graph.json +14 -0
- package/fixtures/minimal-graph.yaml +13 -0
- package/fixtures/payment-retry-demo.json +43 -0
- package/llms.txt +99 -0
- package/package.json +84 -0
- package/schemas/archrad-ir-graph-v1.schema.json +67 -0
- package/scripts/DEMO_GIF_STORYBOARD.md +100 -0
- package/scripts/GIF_RECORDING_STEP_BY_STEP.md +125 -0
- package/scripts/README_DEMO_RECORDING.md +314 -0
- package/scripts/SOCIAL_POST_DRIFT_AND_INGESTION.md +17 -0
- package/scripts/golden-path-demo.ps1 +25 -0
- package/scripts/golden-path-demo.sh +23 -0
- package/scripts/invoke-drift-check.ps1 +16 -0
- package/scripts/record-demo-drift.tape +50 -0
- package/scripts/record-demo-payment-retry.tape +36 -0
- package/scripts/record-demo-validate.tape +34 -0
- package/scripts/record-demo.tape +33 -0
- package/scripts/run-demo-drift-sequence.ps1 +45 -0
- package/scripts/run-demo-drift-sequence.sh +41 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// Deterministic Node Express exporter (skeleton)
|
|
2
|
+
// Exports a map of filename -> content for a generated Express app.
|
|
3
|
+
import { getEdgeConfig, generateRetryCode, generateCircuitBreakerCode } from './edgeConfigCodeGenerator.js';
|
|
4
|
+
export default async function generateNodeExpressFiles(actualIR, opts = {}) {
|
|
5
|
+
const files = {};
|
|
6
|
+
const graph = (actualIR && actualIR.graph) ? actualIR.graph : (actualIR || {});
|
|
7
|
+
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
8
|
+
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
9
|
+
const handlers = [];
|
|
10
|
+
const routes = [];
|
|
11
|
+
const endpoints = [];
|
|
12
|
+
const nonHttpNodes = [];
|
|
13
|
+
// Track edge config utilities (retry, circuit breaker) to include once
|
|
14
|
+
const edgeUtilityCode = new Set();
|
|
15
|
+
function safeId(id) { return String(id || '').replace(/[^A-Za-z0-9_\-]/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'node'; }
|
|
16
|
+
function handlerName(n) { return `handler_${safeId(n && (n.id || n.name))}`.replace(/-/g, '_'); }
|
|
17
|
+
/**
|
|
18
|
+
* Generate code for inner nodes (support nodes) that are embedded within a key node
|
|
19
|
+
*/
|
|
20
|
+
function generateInnerNodeCode(innerNodes) {
|
|
21
|
+
const code = [];
|
|
22
|
+
for (const innerNode of innerNodes) {
|
|
23
|
+
if (!innerNode || !innerNode.type)
|
|
24
|
+
continue;
|
|
25
|
+
const innerType = String(innerNode.type || innerNode.kind || '').toLowerCase();
|
|
26
|
+
const innerId = safeId(innerNode.id || innerNode.name);
|
|
27
|
+
const innerCfg = (innerNode && innerNode.config) || {};
|
|
28
|
+
// Generate code based on inner node type
|
|
29
|
+
if (innerType === 'transform' && innerCfg.transform === 'authenticate') {
|
|
30
|
+
code.push(` // Inner node: Authentication (${innerId})`);
|
|
31
|
+
code.push(` const authResult = authenticateRequest(req);`);
|
|
32
|
+
code.push(` if (!authResult.valid) {`);
|
|
33
|
+
code.push(` return res.status(401).json({ error: 'Unauthorized' });`);
|
|
34
|
+
code.push(` }`);
|
|
35
|
+
code.push('');
|
|
36
|
+
}
|
|
37
|
+
else if (innerType === 'transform' && innerCfg.transform === 'validate') {
|
|
38
|
+
code.push(` // Inner node: Validation (${innerId})`);
|
|
39
|
+
code.push(` const validationResult = validateSchema(req.body);`);
|
|
40
|
+
code.push(` if (!validationResult.valid) {`);
|
|
41
|
+
code.push(` return res.status(400).json({ error: 'Validation failed', details: validationResult.errors });`);
|
|
42
|
+
code.push(` }`);
|
|
43
|
+
code.push('');
|
|
44
|
+
}
|
|
45
|
+
else if (innerType === 'transform' && innerCfg.transform === 'parse') {
|
|
46
|
+
code.push(` // Inner node: Parse (${innerId})`);
|
|
47
|
+
code.push(` const parsedData = parsePayload(req.body);`);
|
|
48
|
+
code.push('');
|
|
49
|
+
}
|
|
50
|
+
else if (innerType === 'retry' || innerCfg.retryPolicy) {
|
|
51
|
+
code.push(` // Inner node: Retry Logic (${innerId})`);
|
|
52
|
+
code.push(` const maxAttempts = ${innerCfg.maxAttempts || innerCfg.max_attempts || 3};`);
|
|
53
|
+
code.push(` for (let attempt = 0; attempt < maxAttempts; attempt++) {`);
|
|
54
|
+
code.push(` try {`);
|
|
55
|
+
code.push(` // Retryable operation`);
|
|
56
|
+
code.push(` break;`);
|
|
57
|
+
code.push(` } catch (error) {`);
|
|
58
|
+
code.push(` if (attempt === maxAttempts - 1) throw error;`);
|
|
59
|
+
code.push(` await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000)); // Exponential backoff`);
|
|
60
|
+
code.push(` }`);
|
|
61
|
+
code.push(` }`);
|
|
62
|
+
code.push('');
|
|
63
|
+
}
|
|
64
|
+
else if (innerType === 'error-handler' || innerCfg.errorHandling) {
|
|
65
|
+
code.push(` // Inner node: Error Handling (${innerId})`);
|
|
66
|
+
code.push(` try {`);
|
|
67
|
+
code.push(` // Error-handled operation`);
|
|
68
|
+
code.push(` } catch (error) {`);
|
|
69
|
+
code.push(` console.error(\`Error in ${innerId}:\`, error);`);
|
|
70
|
+
code.push(` return res.status(500).json({ error: error.message });`);
|
|
71
|
+
code.push(` }`);
|
|
72
|
+
code.push('');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return code;
|
|
76
|
+
}
|
|
77
|
+
for (const n of nodes) {
|
|
78
|
+
if (!n || !n.type)
|
|
79
|
+
continue;
|
|
80
|
+
if (n.type === 'http' || n.type === 'cloudFunction' || n.type === 'httpRequest') {
|
|
81
|
+
const id = safeId(n.id || n.name);
|
|
82
|
+
const method = (n.config && n.config.method) ? String(n.config.method).toLowerCase() : 'post';
|
|
83
|
+
const route = (n.config && (n.config.route || n.config.url)) ? String(n.config.route || n.config.url) : `/${id}`;
|
|
84
|
+
const cfg = { ...(n && n.config), ...(n && n.data?.config) };
|
|
85
|
+
// Extract businessLogic for implementation guidance
|
|
86
|
+
const businessLogic = cfg.businessLogic || n?.businessLogic || n?.description || null;
|
|
87
|
+
const isAsync = cfg.async === true || cfg.asyncProcessing === true || cfg.accepted === true;
|
|
88
|
+
const requestSchema = cfg.schema || cfg.fields;
|
|
89
|
+
const responseSchema = cfg.responseSchema || cfg.response_schema || cfg.response;
|
|
90
|
+
const successCode = method === 'post' ? (isAsync ? 202 : 201) : method === 'delete' ? 204 : 200;
|
|
91
|
+
const h = handlerName(n);
|
|
92
|
+
endpoints.push({ route, method, hasBody: method !== 'get', success: successCode, responseSchema, requestSchema });
|
|
93
|
+
// Get inner nodes if they exist (support nodes embedded in this key node)
|
|
94
|
+
const innerNodes = n.innerNodes || [];
|
|
95
|
+
// Get edge configurations for incoming edges to this node
|
|
96
|
+
const nodeId = String(n.id || '');
|
|
97
|
+
const incomingEdges = edges.filter((e) => {
|
|
98
|
+
const targetId = String(e.to || e.target || '');
|
|
99
|
+
return targetId === nodeId;
|
|
100
|
+
});
|
|
101
|
+
// Collect edge configs and apply them
|
|
102
|
+
let edgeRetryConfig = null;
|
|
103
|
+
let edgeCircuitBreakerConfig = null;
|
|
104
|
+
let edgeTimeout = null;
|
|
105
|
+
for (const edge of incomingEdges) {
|
|
106
|
+
const sourceId = String(edge.from || edge.source || '');
|
|
107
|
+
const edgeConfig = getEdgeConfig(edges, sourceId, nodeId);
|
|
108
|
+
if (edgeConfig) {
|
|
109
|
+
// Apply edge timeout (override node timeout if edge has one)
|
|
110
|
+
if (edgeConfig.config?.timeout) {
|
|
111
|
+
edgeTimeout = edgeConfig.config.timeout;
|
|
112
|
+
}
|
|
113
|
+
// Collect retry config from edge (prefer edge over node)
|
|
114
|
+
if (edgeConfig.config?.retry?.maxAttempts && edgeConfig.config.retry.maxAttempts > 0) {
|
|
115
|
+
edgeRetryConfig = edgeConfig;
|
|
116
|
+
const retryCode = generateRetryCode(edgeConfig, 'nodejs');
|
|
117
|
+
if (retryCode) {
|
|
118
|
+
edgeUtilityCode.add(retryCode);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Collect circuit breaker config from edge
|
|
122
|
+
if (edgeConfig.config?.circuitBreaker?.enabled) {
|
|
123
|
+
edgeCircuitBreakerConfig = edgeConfig;
|
|
124
|
+
const cbCode = generateCircuitBreakerCode(edgeConfig, 'nodejs');
|
|
125
|
+
if (cbCode) {
|
|
126
|
+
edgeUtilityCode.add(cbCode);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Build handler code with inner nodes
|
|
132
|
+
const handlerLines = [];
|
|
133
|
+
handlerLines.push(`async function ${h}(req, res) {`);
|
|
134
|
+
// Include businessLogic in comment if available
|
|
135
|
+
if (businessLogic) {
|
|
136
|
+
handlerLines.push(` // Business Logic: ${businessLogic}`);
|
|
137
|
+
}
|
|
138
|
+
handlerLines.push(` // Handler for node ${String(n.id || '')}${innerNodes.length > 0 ? ` (with ${innerNodes.length} inner node(s))` : ''}`);
|
|
139
|
+
// Add inner node code first (authentication, validation, etc.)
|
|
140
|
+
if (innerNodes.length > 0) {
|
|
141
|
+
const innerCode = generateInnerNodeCode(innerNodes);
|
|
142
|
+
handlerLines.push(...innerCode);
|
|
143
|
+
}
|
|
144
|
+
// Validation with AJV if schema exists
|
|
145
|
+
if (requestSchema && typeof requestSchema === 'object') {
|
|
146
|
+
const schemaName = `${h}_schema`;
|
|
147
|
+
handlerLines.unshift(`const ${schemaName} = ${JSON.stringify(requestSchema)};`);
|
|
148
|
+
handlerLines.splice(handlerLines.findIndex((l) => l.startsWith(' // Add main business logic')), 0, ` const validate = ajv.compile(${schemaName});`, ` const valid = validate(req.body || {});`, ` if (!valid) { return res.status(400).json({ error: 'validation_failed', details: validate.errors }); }`);
|
|
149
|
+
}
|
|
150
|
+
// Rate limit stub
|
|
151
|
+
handlerLines.push(` // TODO: rate limit / quota hook here`);
|
|
152
|
+
// Add main business logic (use config to drive behavior)
|
|
153
|
+
handlerLines.push(` const config = ${JSON.stringify(cfg)};`);
|
|
154
|
+
handlerLines.push(` const requestId = req.requestId || req.headers['x-request-id'] || uuidv4();`);
|
|
155
|
+
handlerLines.push(` const filters = req.query || {};`);
|
|
156
|
+
handlerLines.push(` const page = Number(filters.page || filters.offset || 1);`);
|
|
157
|
+
handlerLines.push(` const pageSize = Number(filters.pageSize || filters.limit || 20);`);
|
|
158
|
+
handlerLines.push(` const status = filters.status || undefined;`);
|
|
159
|
+
handlerLines.push(` const dateFrom = filters.from || filters.startDate || undefined;`);
|
|
160
|
+
handlerLines.push(` const dateTo = filters.to || filters.endDate || undefined;`);
|
|
161
|
+
// Use edge timeout if available, otherwise fall back to node config
|
|
162
|
+
const effectiveTimeout = edgeTimeout ?? cfg.timeoutMs ?? 2000;
|
|
163
|
+
handlerLines.push(` const timeoutMs = Number(${effectiveTimeout});`);
|
|
164
|
+
// Use edge retry config if available, otherwise fall back to node config
|
|
165
|
+
const effectiveRetryPolicy = edgeRetryConfig?.config?.retry || cfg.retryPolicy || { maxAttempts: 2, backoffMs: 500 };
|
|
166
|
+
handlerLines.push(` const retryPolicy = ${JSON.stringify(effectiveRetryPolicy)};`);
|
|
167
|
+
handlerLines.push(` const maxAttempts = Number(retryPolicy.maxAttempts || 1);`);
|
|
168
|
+
handlerLines.push(` const backoffMs = Number(retryPolicy.backoffMs || 200);`);
|
|
169
|
+
const retryStrategy = edgeRetryConfig?.config?.retry?.strategy || 'exponential';
|
|
170
|
+
handlerLines.push(` const operation = ${JSON.stringify(cfg.operation || 'read')};`);
|
|
171
|
+
handlerLines.push(` const primaryKey = ${JSON.stringify(cfg.primaryKey || 'id')};`);
|
|
172
|
+
handlerLines.push(` const table = ${JSON.stringify(cfg.table || 'records')};`);
|
|
173
|
+
handlerLines.push(` const baseQuery = ${JSON.stringify(cfg.query || '')};`);
|
|
174
|
+
handlerLines.push(``);
|
|
175
|
+
if (businessLogic) {
|
|
176
|
+
handlerLines.push(` // Business Logic: ${businessLogic}`);
|
|
177
|
+
}
|
|
178
|
+
handlerLines.push(` // Simulated downstream/data access using retry + timeout`);
|
|
179
|
+
handlerLines.push(` async function runOperation() {`);
|
|
180
|
+
handlerLines.push(` // In real code, call DB/repo with filters/pagination and timeout using baseQuery/table/engine`);
|
|
181
|
+
handlerLines.push(` if (operation === 'create') {`);
|
|
182
|
+
handlerLines.push(` const body = req.body || {};`);
|
|
183
|
+
handlerLines.push(` const id = body[primaryKey] || \`new-\${Date.now()}\`;`);
|
|
184
|
+
handlerLines.push(` return { created: { ...body, [primaryKey]: id, createdAt: new Date().toISOString(), table, query: baseQuery } };`);
|
|
185
|
+
handlerLines.push(` }`);
|
|
186
|
+
handlerLines.push(` if (operation === 'update') {`);
|
|
187
|
+
handlerLines.push(` const body = req.body || {};`);
|
|
188
|
+
handlerLines.push(` const id = body[primaryKey] || filters[primaryKey];`);
|
|
189
|
+
handlerLines.push(` if (!id) throw Object.assign(new Error('missing primary key'), { statusCode: 400 });`);
|
|
190
|
+
handlerLines.push(` return { updated: { ...body, [primaryKey]: id, updatedAt: new Date().toISOString(), table, query: baseQuery } };`);
|
|
191
|
+
handlerLines.push(` }`);
|
|
192
|
+
handlerLines.push(` if (operation === 'delete') {`);
|
|
193
|
+
handlerLines.push(` const id = filters[primaryKey] || (req.body || {})[primaryKey];`);
|
|
194
|
+
handlerLines.push(` if (!id) throw Object.assign(new Error('missing primary key'), { statusCode: 400 });`);
|
|
195
|
+
handlerLines.push(` return { deleted: true, id, table, query: baseQuery };`);
|
|
196
|
+
handlerLines.push(` }`);
|
|
197
|
+
handlerLines.push(` // READ path with filters/pagination`);
|
|
198
|
+
handlerLines.push(` const sample = Array.from({ length: Math.min(pageSize, 5) }).map((_, i) => ({`);
|
|
199
|
+
handlerLines.push(` [primaryKey]: \`ORD-\${page}-\${i+1}\`,`);
|
|
200
|
+
handlerLines.push(` status: status || 'pending',`);
|
|
201
|
+
handlerLines.push(` total: 100 + i,`);
|
|
202
|
+
handlerLines.push(` createdAt: new Date().toISOString(),`);
|
|
203
|
+
handlerLines.push(` table,`);
|
|
204
|
+
handlerLines.push(` query: baseQuery || undefined,`);
|
|
205
|
+
handlerLines.push(` }));`);
|
|
206
|
+
handlerLines.push(` return sample;`);
|
|
207
|
+
handlerLines.push(` }`);
|
|
208
|
+
handlerLines.push(``);
|
|
209
|
+
handlerLines.push(` let data;`);
|
|
210
|
+
// Use circuit breaker if configured on edge
|
|
211
|
+
if (edgeCircuitBreakerConfig) {
|
|
212
|
+
handlerLines.push(` // Circuit breaker protection (from edge config)`);
|
|
213
|
+
handlerLines.push(` const circuitBreaker = new CircuitBreaker(${edgeCircuitBreakerConfig.config?.circuitBreaker?.failureThreshold || 5}, ${edgeCircuitBreakerConfig.config?.circuitBreaker?.resetTimeoutMs || 60000});`);
|
|
214
|
+
handlerLines.push(` try {`);
|
|
215
|
+
handlerLines.push(` data = await circuitBreaker.call(async () => {`);
|
|
216
|
+
handlerLines.push(` const controller = new AbortController();`);
|
|
217
|
+
handlerLines.push(` const to = setTimeout(() => controller.abort(), timeoutMs);`);
|
|
218
|
+
handlerLines.push(` const result = await runOperation();`);
|
|
219
|
+
handlerLines.push(` clearTimeout(to);`);
|
|
220
|
+
handlerLines.push(` return result;`);
|
|
221
|
+
handlerLines.push(` });`);
|
|
222
|
+
handlerLines.push(` } catch (err) {`);
|
|
223
|
+
handlerLines.push(` console.error('[handler:${h}] circuit breaker open or operation failed', err);`);
|
|
224
|
+
handlerLines.push(` const sc = err?.statusCode || 500;`);
|
|
225
|
+
handlerLines.push(` return res.status(sc).json({ error: 'upstream_failed', message: err?.message, requestId });`);
|
|
226
|
+
handlerLines.push(` }`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Use retry logic (from edge or node config)
|
|
230
|
+
const useEdgeRetry = edgeRetryConfig && retryStrategy === 'exponential';
|
|
231
|
+
if (useEdgeRetry) {
|
|
232
|
+
handlerLines.push(` // Retry with exponential backoff (from edge config)`);
|
|
233
|
+
handlerLines.push(` try {`);
|
|
234
|
+
handlerLines.push(` data = await retryWithExponentialBackoff(async () => {`);
|
|
235
|
+
handlerLines.push(` const controller = new AbortController();`);
|
|
236
|
+
handlerLines.push(` const to = setTimeout(() => controller.abort(), timeoutMs);`);
|
|
237
|
+
handlerLines.push(` const result = await runOperation();`);
|
|
238
|
+
handlerLines.push(` clearTimeout(to);`);
|
|
239
|
+
handlerLines.push(` return result;`);
|
|
240
|
+
handlerLines.push(` }, maxAttempts, backoffMs);`);
|
|
241
|
+
handlerLines.push(` } catch (err) {`);
|
|
242
|
+
handlerLines.push(` console.error('[handler:${h}] failed after retries', err);`);
|
|
243
|
+
handlerLines.push(` const sc = err?.statusCode || 500;`);
|
|
244
|
+
handlerLines.push(` return res.status(sc).json({ error: 'upstream_failed', message: err?.message, requestId });`);
|
|
245
|
+
handlerLines.push(` }`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Fallback to standard retry loop
|
|
249
|
+
handlerLines.push(` for (let attempt = 1; attempt <= maxAttempts; attempt++) {`);
|
|
250
|
+
handlerLines.push(` try {`);
|
|
251
|
+
handlerLines.push(` const controller = new AbortController();`);
|
|
252
|
+
handlerLines.push(` const to = setTimeout(() => controller.abort(), timeoutMs);`);
|
|
253
|
+
handlerLines.push(` data = await runOperation();`);
|
|
254
|
+
handlerLines.push(` clearTimeout(to);`);
|
|
255
|
+
handlerLines.push(` break;`);
|
|
256
|
+
handlerLines.push(` } catch (err) {`);
|
|
257
|
+
handlerLines.push(` if (attempt === maxAttempts) {`);
|
|
258
|
+
handlerLines.push(` console.error('[handler:${h}] failed after retries', err);`);
|
|
259
|
+
handlerLines.push(` const sc = err?.statusCode || 500;`);
|
|
260
|
+
handlerLines.push(` return res.status(sc).json({ error: 'upstream_failed', message: err?.message, requestId });`);
|
|
261
|
+
handlerLines.push(` }`);
|
|
262
|
+
handlerLines.push(` await new Promise(r => setTimeout(r, backoffMs));`);
|
|
263
|
+
handlerLines.push(` }`);
|
|
264
|
+
handlerLines.push(` }`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
handlerLines.push(``);
|
|
268
|
+
handlerLines.push(` // Audit log (placeholder)`);
|
|
269
|
+
handlerLines.push(` console.log('[audit]', { requestId, route: '${route}', status: 'success', filters: { status, dateFrom, dateTo, page, pageSize } });`);
|
|
270
|
+
if (successCode === 204) {
|
|
271
|
+
handlerLines.push(` return res.status(204).end();`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
handlerLines.push(` return res.status(${successCode}).json({ status: 'ok', requestId, data });`);
|
|
275
|
+
}
|
|
276
|
+
handlerLines.push(`}`);
|
|
277
|
+
handlers.push(handlerLines.join('\n'));
|
|
278
|
+
routes.push(`app.${method}('${route}', ${h});`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Helper functions for inner nodes
|
|
282
|
+
const helperFunctions = [];
|
|
283
|
+
helperFunctions.push('// Helper functions for inner nodes (support nodes)');
|
|
284
|
+
helperFunctions.push('function authenticateRequest(req) {');
|
|
285
|
+
helperFunctions.push(' // TODO: Implement authentication logic');
|
|
286
|
+
helperFunctions.push(' return { valid: true, user: null };');
|
|
287
|
+
helperFunctions.push('}');
|
|
288
|
+
helperFunctions.push('');
|
|
289
|
+
helperFunctions.push('function validateSchema(payload) {');
|
|
290
|
+
helperFunctions.push(' // TODO: Implement schema validation');
|
|
291
|
+
helperFunctions.push(' return { valid: true, errors: [] };');
|
|
292
|
+
helperFunctions.push('}');
|
|
293
|
+
helperFunctions.push('');
|
|
294
|
+
helperFunctions.push('function parsePayload(payload) {');
|
|
295
|
+
helperFunctions.push(' // TODO: Implement payload parsing');
|
|
296
|
+
helperFunctions.push(' return payload;');
|
|
297
|
+
helperFunctions.push('}');
|
|
298
|
+
helperFunctions.push('');
|
|
299
|
+
// Add edge config utilities (retry, circuit breaker) if any were generated
|
|
300
|
+
if (edgeUtilityCode.size > 0) {
|
|
301
|
+
helperFunctions.push('// Edge configuration utilities (retry, circuit breaker)');
|
|
302
|
+
edgeUtilityCode.forEach(code => helperFunctions.push(code));
|
|
303
|
+
}
|
|
304
|
+
files['app/index.js'] = [
|
|
305
|
+
"const express = require('express');",
|
|
306
|
+
"const bodyParser = require('body-parser');",
|
|
307
|
+
"const cors = require('cors');",
|
|
308
|
+
"const { v4: uuidv4 } = require('uuid');",
|
|
309
|
+
"const Ajv = require('ajv');",
|
|
310
|
+
"const ajv = new Ajv({ allErrors: true, coerceTypes: true });",
|
|
311
|
+
"const app = express();",
|
|
312
|
+
"app.use(bodyParser.json());",
|
|
313
|
+
"",
|
|
314
|
+
"// CORS tightened via ALLOWED_ORIGINS env (comma-separated); defaults to none",
|
|
315
|
+
"const allowed = (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean);",
|
|
316
|
+
"app.use(cors({ origin: allowed.length ? allowed : false }));",
|
|
317
|
+
"",
|
|
318
|
+
"// Runtime kit: request id + timing + basic error handler",
|
|
319
|
+
"app.use((req, res, next) => {",
|
|
320
|
+
" req.requestId = req.headers['x-request-id'] || uuidv4();",
|
|
321
|
+
" const start = Date.now();",
|
|
322
|
+
" res.setHeader('x-request-id', req.requestId);",
|
|
323
|
+
" res.on('finish', () => {",
|
|
324
|
+
" res.setHeader('x-duration-ms', Date.now() - start);",
|
|
325
|
+
" });",
|
|
326
|
+
" next();",
|
|
327
|
+
"});",
|
|
328
|
+
"",
|
|
329
|
+
"// Helper functions for inner nodes",
|
|
330
|
+
helperFunctions.join('\n'),
|
|
331
|
+
"// Handlers",
|
|
332
|
+
handlers.join('\n\n'),
|
|
333
|
+
"",
|
|
334
|
+
"// Routes",
|
|
335
|
+
routes.join('\n'),
|
|
336
|
+
"",
|
|
337
|
+
"// Health/ready",
|
|
338
|
+
"app.get('/healthz', (req, res) => res.json({ ok: true }));",
|
|
339
|
+
"app.get('/ready', (req, res) => res.json({ ok: true }));",
|
|
340
|
+
"",
|
|
341
|
+
"// Error handler",
|
|
342
|
+
"app.use((err, req, res, next) => {",
|
|
343
|
+
" console.error('Unhandled error', err);",
|
|
344
|
+
" res.status(500).json({ error: 'internal_error', requestId: req.requestId });",
|
|
345
|
+
"});",
|
|
346
|
+
"",
|
|
347
|
+
"const port = Number(process.env.PORT) || 8080;",
|
|
348
|
+
"app.listen(port, '0.0.0.0', () => console.log(`Server listening on ${port}`));"
|
|
349
|
+
].join('\n');
|
|
350
|
+
files['package.json'] = JSON.stringify({
|
|
351
|
+
name: (opts.projectName || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-express'),
|
|
352
|
+
version: '0.1.0',
|
|
353
|
+
main: 'app/index.js',
|
|
354
|
+
scripts: { start: 'node app/index.js' },
|
|
355
|
+
dependencies: {
|
|
356
|
+
express: '^4.18.0',
|
|
357
|
+
'body-parser': '^1.20.0',
|
|
358
|
+
uuid: '^9.0.1',
|
|
359
|
+
cors: '^2.8.5',
|
|
360
|
+
ajv: '^8.12.0',
|
|
361
|
+
},
|
|
362
|
+
}, null, 2);
|
|
363
|
+
files['tests/contract.test.js'] = [
|
|
364
|
+
"const fs = require('fs');",
|
|
365
|
+
"const path = require('path');",
|
|
366
|
+
"describe('contract', () => {",
|
|
367
|
+
" it('should have an openapi file', () => {",
|
|
368
|
+
" const spec = fs.readFileSync(path.join(__dirname, '../openapi.yaml'), 'utf-8');",
|
|
369
|
+
" expect(spec).toBeTruthy();",
|
|
370
|
+
" });",
|
|
371
|
+
"});",
|
|
372
|
+
].join('\n');
|
|
373
|
+
// Non-http nodes
|
|
374
|
+
nodes.forEach((raw) => {
|
|
375
|
+
if (!raw || typeof raw !== 'object')
|
|
376
|
+
return;
|
|
377
|
+
const n = raw;
|
|
378
|
+
if (!n.type)
|
|
379
|
+
return;
|
|
380
|
+
const t = String(n.type || '').toLowerCase();
|
|
381
|
+
if (t.includes('http'))
|
|
382
|
+
return;
|
|
383
|
+
const cfg = n.config && typeof n.config === 'object' && !Array.isArray(n.config)
|
|
384
|
+
? n.config
|
|
385
|
+
: {};
|
|
386
|
+
nonHttpNodes.push({
|
|
387
|
+
id: String(n.id || ''),
|
|
388
|
+
type: String(n.type || ''),
|
|
389
|
+
name: String(n.name || n.id || ''),
|
|
390
|
+
schema: cfg.schema || cfg.fields,
|
|
391
|
+
config: cfg,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
files['openapi.yaml'] = buildOpenApiSpec(opts.projectName || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-express', endpoints, nonHttpNodes);
|
|
395
|
+
files['README.md'] = `# ${opts.projectName || 'generated-express'}\n\nGenerated Express app.\n\nRun:\n\n npm install\n npm start\n`;
|
|
396
|
+
return files;
|
|
397
|
+
}
|
|
398
|
+
function renderSchema(schema, indent = ' ') {
|
|
399
|
+
if (!schema || typeof schema !== 'object') {
|
|
400
|
+
return [`${indent}type: object`];
|
|
401
|
+
}
|
|
402
|
+
const lines = [];
|
|
403
|
+
const t = schema.type ||
|
|
404
|
+
schema.dataType ||
|
|
405
|
+
(schema.properties ? 'object' : Array.isArray(schema) || schema.items ? 'array' : 'object');
|
|
406
|
+
lines.push(`${indent}type: ${t === 'integer' ? 'number' : t}`);
|
|
407
|
+
if (t === 'array' && schema.items) {
|
|
408
|
+
lines.push(`${indent}items:`);
|
|
409
|
+
lines.push(...renderSchema(schema.items, `${indent} `));
|
|
410
|
+
}
|
|
411
|
+
if (t === 'object' && schema.properties && typeof schema.properties === 'object') {
|
|
412
|
+
lines.push(`${indent}properties:`);
|
|
413
|
+
Object.entries(schema.properties).forEach(([k, v]) => {
|
|
414
|
+
lines.push(`${indent} ${k}:`);
|
|
415
|
+
lines.push(...renderSchema(v, `${indent} `));
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (schema.format) {
|
|
419
|
+
lines.push(`${indent}format: ${schema.format}`);
|
|
420
|
+
}
|
|
421
|
+
return lines;
|
|
422
|
+
}
|
|
423
|
+
function buildOpenApiSpec(serviceName, endpoints, nonHttpNodes) {
|
|
424
|
+
const schemaEntries = [];
|
|
425
|
+
const seenSchemas = new Set();
|
|
426
|
+
const safeName = (route, method, kind) => `${method.toLowerCase()}_${route.replace(/[^A-Za-z0-9]+/g, '_') || 'root'}_${kind}`;
|
|
427
|
+
const addSchema = (name, schema) => {
|
|
428
|
+
const key = JSON.stringify(schema || {});
|
|
429
|
+
if (!schema || typeof schema !== 'object')
|
|
430
|
+
return null;
|
|
431
|
+
const compositeKey = `${name}:${key}`;
|
|
432
|
+
if (seenSchemas.has(compositeKey))
|
|
433
|
+
return name;
|
|
434
|
+
seenSchemas.add(compositeKey);
|
|
435
|
+
schemaEntries.push({ name, schema });
|
|
436
|
+
return name;
|
|
437
|
+
};
|
|
438
|
+
const lines = [];
|
|
439
|
+
lines.push('openapi: 3.0.0');
|
|
440
|
+
lines.push('info:');
|
|
441
|
+
lines.push(` title: ${serviceName}`);
|
|
442
|
+
lines.push(' version: 0.0.1');
|
|
443
|
+
lines.push('paths:');
|
|
444
|
+
if (!endpoints.length) {
|
|
445
|
+
lines.push(' /:');
|
|
446
|
+
lines.push(' get:');
|
|
447
|
+
lines.push(' responses:');
|
|
448
|
+
lines.push(' "200":');
|
|
449
|
+
lines.push(' description: OK');
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
endpoints.forEach((ep) => {
|
|
453
|
+
const lower = ep.method.toLowerCase();
|
|
454
|
+
const success = String(ep.success || 200);
|
|
455
|
+
lines.push(` ${ep.route}:`);
|
|
456
|
+
lines.push(` ${lower}:`);
|
|
457
|
+
const reqRef = ep.requestSchema ? addSchema(safeName(ep.route, ep.method, 'request'), ep.requestSchema) : null;
|
|
458
|
+
const resRef = ep.responseSchema ? addSchema(safeName(ep.route, ep.method, 'response'), ep.responseSchema) : null;
|
|
459
|
+
if (ep.hasBody && success !== '204') {
|
|
460
|
+
lines.push(' requestBody:');
|
|
461
|
+
lines.push(' content:');
|
|
462
|
+
lines.push(' application/json:');
|
|
463
|
+
lines.push(' schema:');
|
|
464
|
+
if (reqRef) {
|
|
465
|
+
lines.push(` $ref: "#/components/schemas/${reqRef}"`);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
lines.push(...renderSchema(ep.requestSchema || {}, ' '));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
lines.push(' responses:');
|
|
472
|
+
lines.push(` "${success}":`);
|
|
473
|
+
lines.push(` description: ${success === '201' ? 'Created' : success === '202' ? 'Accepted' : success === '204' ? 'No Content' : 'OK'}`);
|
|
474
|
+
if (success !== '204') {
|
|
475
|
+
lines.push(' content:');
|
|
476
|
+
lines.push(' application/json:');
|
|
477
|
+
lines.push(' schema:');
|
|
478
|
+
if (resRef) {
|
|
479
|
+
lines.push(` $ref: "#/components/schemas/${resRef}"`);
|
|
480
|
+
}
|
|
481
|
+
else if (ep.responseSchema) {
|
|
482
|
+
lines.push(...renderSchema(ep.responseSchema, ' '));
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
lines.push(' $ref: "#/components/schemas/Response"');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
lines.push(' "400":');
|
|
489
|
+
lines.push(' description: Bad Request');
|
|
490
|
+
lines.push(' "401":');
|
|
491
|
+
lines.push(' description: Unauthorized');
|
|
492
|
+
lines.push(' "500":');
|
|
493
|
+
lines.push(' description: Server Error');
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
lines.push('components:');
|
|
497
|
+
lines.push(' schemas:');
|
|
498
|
+
lines.push(' Response:');
|
|
499
|
+
lines.push(' type: object');
|
|
500
|
+
lines.push(' properties:');
|
|
501
|
+
lines.push(' status:');
|
|
502
|
+
lines.push(' type: string');
|
|
503
|
+
lines.push(' message:');
|
|
504
|
+
lines.push(' type: string');
|
|
505
|
+
lines.push(' data:');
|
|
506
|
+
lines.push(' type: object');
|
|
507
|
+
schemaEntries.forEach((entry) => {
|
|
508
|
+
lines.push(` ${entry.name}:`);
|
|
509
|
+
lines.push(...renderSchema(entry.schema, ' '));
|
|
510
|
+
});
|
|
511
|
+
if (nonHttpNodes && nonHttpNodes.length) {
|
|
512
|
+
lines.push('x-nonHttpNodes:');
|
|
513
|
+
nonHttpNodes.forEach((n) => {
|
|
514
|
+
const componentName = addSchema(`nonhttp_${n.type}_${n.id}`, n.schema || {});
|
|
515
|
+
lines.push(` - id: ${n.id}`);
|
|
516
|
+
lines.push(` type: ${n.type}`);
|
|
517
|
+
lines.push(` name: ${JSON.stringify(n.name)}`);
|
|
518
|
+
if (n.config && Object.keys(n.config).length) {
|
|
519
|
+
lines.push(' configKeys:');
|
|
520
|
+
Object.keys(n.config).slice(0, 8).forEach((k) => lines.push(` - ${k}`));
|
|
521
|
+
}
|
|
522
|
+
if (componentName) {
|
|
523
|
+
lines.push(` schemaRef: "#/components/schemas/${componentName}"`);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return lines.join('\n');
|
|
528
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.x **document shape**: parse JSON/YAML and check required top-level fields
|
|
3
|
+
* (`openapi` version, `paths`, `info.title`, `info.version`). Not Spectral-style API lint
|
|
4
|
+
* (no `security`, `operationId`, or style rules). No LLM, no network.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findOpenApiInBundle(files: Record<string, string>): {
|
|
7
|
+
path: string;
|
|
8
|
+
content: string;
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function parseOpenApiString(content: string): {
|
|
11
|
+
doc: Record<string, unknown>;
|
|
12
|
+
format: 'yaml' | 'json';
|
|
13
|
+
} | null;
|
|
14
|
+
/** Minimal **document shape** validation (parseable OpenAPI 3.x with paths + info). */
|
|
15
|
+
export declare function validateOpenApiStructural(doc: unknown): {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
errors: string[];
|
|
18
|
+
};
|
|
19
|
+
export declare function serializeOpenApiDoc(doc: Record<string, unknown>, format: 'yaml' | 'json'): string;
|
|
20
|
+
/** Validate generated OpenAPI file in bundle (document shape only; no mutation or LLM repair). */
|
|
21
|
+
export declare function validateOpenApiInBundleStructural(files: Record<string, string>): {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
path: string | null;
|
|
24
|
+
errors: string[];
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=openapi-structural.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-structural.d.ts","sourceRoot":"","sources":["../src/openapi-structural.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO3G;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,IAAI,CAepH;AAED,uFAAuF;AACvF,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAqBzF;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAGjG;AAED,kGAAkG;AAClG,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IAChF,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAWA"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.x **document shape**: parse JSON/YAML and check required top-level fields
|
|
3
|
+
* (`openapi` version, `paths`, `info.title`, `info.version`). Not Spectral-style API lint
|
|
4
|
+
* (no `security`, `operationId`, or style rules). No LLM, no network.
|
|
5
|
+
*/
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
export function findOpenApiInBundle(files) {
|
|
8
|
+
const keys = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'docs/openapi.yaml'];
|
|
9
|
+
for (const k of keys) {
|
|
10
|
+
const c = files[k];
|
|
11
|
+
if (c && typeof c === 'string' && c.trim())
|
|
12
|
+
return { path: k, content: c };
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
export function parseOpenApiString(content) {
|
|
17
|
+
const trimmed = content.trim();
|
|
18
|
+
try {
|
|
19
|
+
const doc = JSON.parse(trimmed);
|
|
20
|
+
if (doc && typeof doc === 'object')
|
|
21
|
+
return { doc, format: 'json' };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* try yaml */
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const doc = yaml.load(trimmed);
|
|
28
|
+
if (doc && typeof doc === 'object')
|
|
29
|
+
return { doc, format: 'yaml' };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** Minimal **document shape** validation (parseable OpenAPI 3.x with paths + info). */
|
|
37
|
+
export function validateOpenApiStructural(doc) {
|
|
38
|
+
const errors = [];
|
|
39
|
+
if (!doc || typeof doc !== 'object') {
|
|
40
|
+
errors.push('Document is empty or not an object');
|
|
41
|
+
return { ok: false, errors };
|
|
42
|
+
}
|
|
43
|
+
const o = doc;
|
|
44
|
+
const ver = o.openapi ?? o.swagger;
|
|
45
|
+
if (ver === undefined || ver === null)
|
|
46
|
+
errors.push('Missing openapi or swagger version field');
|
|
47
|
+
else {
|
|
48
|
+
const s = String(ver);
|
|
49
|
+
if (o.openapi != null && !s.startsWith('3'))
|
|
50
|
+
errors.push(`Expected OpenAPI 3.x, got: ${s}`);
|
|
51
|
+
}
|
|
52
|
+
if (!o.paths || typeof o.paths !== 'object')
|
|
53
|
+
errors.push('Missing or invalid paths object');
|
|
54
|
+
if (!o.info || typeof o.info !== 'object')
|
|
55
|
+
errors.push('Missing info object');
|
|
56
|
+
else {
|
|
57
|
+
const info = o.info;
|
|
58
|
+
if (info.title === undefined || info.title === '')
|
|
59
|
+
errors.push('Missing info.title');
|
|
60
|
+
if (info.version === undefined || info.version === '')
|
|
61
|
+
errors.push('Missing info.version');
|
|
62
|
+
}
|
|
63
|
+
return { ok: errors.length === 0, errors };
|
|
64
|
+
}
|
|
65
|
+
export function serializeOpenApiDoc(doc, format) {
|
|
66
|
+
if (format === 'json')
|
|
67
|
+
return JSON.stringify(doc, null, 2);
|
|
68
|
+
return yaml.dump(doc, { lineWidth: 120, noRefs: true, skipInvalid: true });
|
|
69
|
+
}
|
|
70
|
+
/** Validate generated OpenAPI file in bundle (document shape only; no mutation or LLM repair). */
|
|
71
|
+
export function validateOpenApiInBundleStructural(files) {
|
|
72
|
+
const found = findOpenApiInBundle(files);
|
|
73
|
+
if (!found) {
|
|
74
|
+
return { ok: true, path: null, errors: [] };
|
|
75
|
+
}
|
|
76
|
+
const parsed = parseOpenApiString(found.content);
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
return { ok: false, path: found.path, errors: ['Could not parse as JSON or YAML'] };
|
|
79
|
+
}
|
|
80
|
+
const v = validateOpenApiStructural(parsed.doc);
|
|
81
|
+
return { ok: v.ok, path: found.path, errors: v.errors };
|
|
82
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.x → canonical blueprint IR (structural HTTP surface only).
|
|
3
|
+
* OSS + product share this: CLI `archrad ingest openapi`, Cloud merge-into-graph, CI regenerate.
|
|
4
|
+
* This is not semantic architecture truth — only operations under `paths` become `http` nodes.
|
|
5
|
+
*/
|
|
6
|
+
export declare class OpenApiIngestError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
export type OpenApiHttpNode = {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
name: string;
|
|
14
|
+
config: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* List HTTP operations as IR `http` nodes (ids unique within this batch; caller dedupes against a graph).
|
|
18
|
+
*/
|
|
19
|
+
export declare function openApiDocumentToHttpNodes(doc: Record<string, unknown>): OpenApiHttpNode[];
|
|
20
|
+
/**
|
|
21
|
+
* Full canonical IR wrapper: `{ graph: { metadata, nodes, edges }, metadata? }` — same shape as `yaml-to-ir` / `minimal-graph.json`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function openApiDocumentToCanonicalIr(doc: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
+
export declare function openApiStringToCanonicalIr(content: string): Record<string, unknown>;
|
|
25
|
+
export declare function openApiUnknownToCanonicalIr(input: unknown): Record<string, unknown>;
|
|
26
|
+
//# sourceMappingURL=openapi-to-ir.d.ts.map
|