@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/CONTRIBUTING.md +15 -0
  3. package/LICENSE +17 -0
  4. package/README.md +284 -0
  5. package/SECURITY.md +26 -0
  6. package/biome.json +25 -0
  7. package/demo-validate.gif +0 -0
  8. package/dist/cli-findings.d.ts +23 -0
  9. package/dist/cli-findings.d.ts.map +1 -0
  10. package/dist/cli-findings.js +88 -0
  11. package/dist/cli.d.ts +7 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +341 -0
  14. package/dist/edgeConfigCodeGenerator.d.ts +55 -0
  15. package/dist/edgeConfigCodeGenerator.d.ts.map +1 -0
  16. package/dist/edgeConfigCodeGenerator.js +249 -0
  17. package/dist/exportPipeline.d.ts +23 -0
  18. package/dist/exportPipeline.d.ts.map +1 -0
  19. package/dist/exportPipeline.js +65 -0
  20. package/dist/golden-bundle.d.ts +21 -0
  21. package/dist/golden-bundle.d.ts.map +1 -0
  22. package/dist/golden-bundle.js +166 -0
  23. package/dist/graphPredicates.d.ts +10 -0
  24. package/dist/graphPredicates.d.ts.map +1 -0
  25. package/dist/graphPredicates.js +33 -0
  26. package/dist/hostPort.d.ts +12 -0
  27. package/dist/hostPort.d.ts.map +1 -0
  28. package/dist/hostPort.js +39 -0
  29. package/dist/index.d.ts +22 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +21 -0
  32. package/dist/ir-lint.d.ts +11 -0
  33. package/dist/ir-lint.d.ts.map +1 -0
  34. package/dist/ir-lint.js +16 -0
  35. package/dist/ir-normalize.d.ts +48 -0
  36. package/dist/ir-normalize.d.ts.map +1 -0
  37. package/dist/ir-normalize.js +81 -0
  38. package/dist/ir-structural.d.ts +40 -0
  39. package/dist/ir-structural.d.ts.map +1 -0
  40. package/dist/ir-structural.js +267 -0
  41. package/dist/lint-graph.d.ts +40 -0
  42. package/dist/lint-graph.d.ts.map +1 -0
  43. package/dist/lint-graph.js +133 -0
  44. package/dist/lint-rules.d.ts +40 -0
  45. package/dist/lint-rules.d.ts.map +1 -0
  46. package/dist/lint-rules.js +290 -0
  47. package/dist/nodeExpress.d.ts +2 -0
  48. package/dist/nodeExpress.d.ts.map +1 -0
  49. package/dist/nodeExpress.js +528 -0
  50. package/dist/openapi-structural.d.ts +26 -0
  51. package/dist/openapi-structural.d.ts.map +1 -0
  52. package/dist/openapi-structural.js +82 -0
  53. package/dist/openapi-to-ir.d.ts +26 -0
  54. package/dist/openapi-to-ir.d.ts.map +1 -0
  55. package/dist/openapi-to-ir.js +131 -0
  56. package/dist/pythonFastAPI.d.ts +2 -0
  57. package/dist/pythonFastAPI.d.ts.map +1 -0
  58. package/dist/pythonFastAPI.js +664 -0
  59. package/dist/validate-drift.d.ts +54 -0
  60. package/dist/validate-drift.d.ts.map +1 -0
  61. package/dist/validate-drift.js +184 -0
  62. package/dist/yamlToIr.d.ts +14 -0
  63. package/dist/yamlToIr.d.ts.map +1 -0
  64. package/dist/yamlToIr.js +39 -0
  65. package/docs/CONCEPT_ADOPTION_AND_LIMITS.md +47 -0
  66. package/docs/CUSTOM_RULES.md +87 -0
  67. package/docs/ENGINEERING_NOTES.md +42 -0
  68. package/docs/IR_CONTRACT.md +54 -0
  69. package/docs/STRUCTURAL_VS_SEMANTIC_VALIDATION.md +86 -0
  70. package/fixtures/demo-direct-db-layered.json +37 -0
  71. package/fixtures/demo-direct-db-violation.json +22 -0
  72. package/fixtures/ecommerce-with-warnings.json +89 -0
  73. package/fixtures/invalid-cycle.json +15 -0
  74. package/fixtures/invalid-edge-unknown-node.json +14 -0
  75. package/fixtures/minimal-graph.json +14 -0
  76. package/fixtures/minimal-graph.yaml +13 -0
  77. package/fixtures/payment-retry-demo.json +43 -0
  78. package/llms.txt +99 -0
  79. package/package.json +84 -0
  80. package/schemas/archrad-ir-graph-v1.schema.json +67 -0
  81. package/scripts/DEMO_GIF_STORYBOARD.md +100 -0
  82. package/scripts/GIF_RECORDING_STEP_BY_STEP.md +125 -0
  83. package/scripts/README_DEMO_RECORDING.md +314 -0
  84. package/scripts/SOCIAL_POST_DRIFT_AND_INGESTION.md +17 -0
  85. package/scripts/golden-path-demo.ps1 +25 -0
  86. package/scripts/golden-path-demo.sh +23 -0
  87. package/scripts/invoke-drift-check.ps1 +16 -0
  88. package/scripts/record-demo-drift.tape +50 -0
  89. package/scripts/record-demo-payment-retry.tape +36 -0
  90. package/scripts/record-demo-validate.tape +34 -0
  91. package/scripts/record-demo.tape +33 -0
  92. package/scripts/run-demo-drift-sequence.ps1 +45 -0
  93. package/scripts/run-demo-drift-sequence.sh +41 -0
@@ -0,0 +1,664 @@
1
+ // Deterministic Python FastAPI exporter
2
+ // Produces a map of filename -> content given an IR (plan graph) and options.
3
+ import { getEdgeConfig, generateRetryCode, generateCircuitBreakerCode } from './edgeConfigCodeGenerator.js';
4
+ function safeId(id) {
5
+ return String(id || '').replace(/[^A-Za-z0-9_\-]/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'node';
6
+ }
7
+ function handlerNameFor(n) {
8
+ if (n && n.config && n.config.name)
9
+ return String(n.config.name).replace(/[^A-Za-z0-9_]/g, '_');
10
+ const id = safeId(n && n.id ? n.id : n && n.name ? n.name : 'handler');
11
+ return `handler_${id.replace(/-/g, '_')}`;
12
+ }
13
+ function pyTypeFromSchema(def) {
14
+ const dtype = def?.type || def?.dataType || (def?.properties ? 'object' : def?.items ? 'array' : 'string');
15
+ if (dtype === 'number' || dtype === 'integer')
16
+ return 'float';
17
+ if (dtype === 'boolean')
18
+ return 'bool';
19
+ if (dtype === 'array')
20
+ return 'List[Any]';
21
+ if (dtype === 'object')
22
+ return 'Dict[str, Any]';
23
+ return 'str';
24
+ }
25
+ function buildPydanticModel(className, schema) {
26
+ if (!schema || typeof schema !== 'object') {
27
+ return `class ${className}(BaseModel):\n status: str = "ok"\n message: str | None = None\n data: Dict[str, Any] | None = None\n`;
28
+ }
29
+ const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : schema;
30
+ const lines = [`class ${className}(BaseModel):`];
31
+ lines.push(' status: str = "ok"');
32
+ lines.push(' message: str | None = None');
33
+ lines.push(' data: Dict[str, Any] | None = None');
34
+ if (props && typeof props === 'object') {
35
+ const keys = Object.keys(props);
36
+ keys.forEach((k) => {
37
+ const ident = k.replace(/[^A-Za-z0-9_]/g, '_') || 'field';
38
+ const t = pyTypeFromSchema(props[k]);
39
+ lines.push(` ${ident}: ${t} | None = None`);
40
+ });
41
+ }
42
+ lines.push('');
43
+ return lines.join('\n');
44
+ }
45
+ function renderSchema(schema, indent = ' ') {
46
+ if (!schema || typeof schema !== 'object') {
47
+ return [`${indent}type: object`];
48
+ }
49
+ const lines = [];
50
+ const t = schema.type ||
51
+ schema.dataType ||
52
+ (schema.properties ? 'object' : Array.isArray(schema) || schema.items ? 'array' : 'object');
53
+ lines.push(`${indent}type: ${t === 'integer' ? 'number' : t}`);
54
+ if (t === 'array' && schema.items) {
55
+ lines.push(`${indent}items:`);
56
+ lines.push(...renderSchema(schema.items, `${indent} `));
57
+ }
58
+ if (t === 'object' && schema.properties && typeof schema.properties === 'object') {
59
+ lines.push(`${indent}properties:`);
60
+ Object.entries(schema.properties).forEach(([k, v]) => {
61
+ lines.push(`${indent} ${k}:`);
62
+ lines.push(...renderSchema(v, `${indent} `));
63
+ });
64
+ }
65
+ if (schema.format) {
66
+ lines.push(`${indent}format: ${schema.format}`);
67
+ }
68
+ return lines;
69
+ }
70
+ function buildOpenApiSpec(serviceName, endpoints, nonHttpNodes) {
71
+ const schemaEntries = [];
72
+ const seenSchemas = new Set();
73
+ const safeName = (route, method, kind) => `${method.toLowerCase()}_${route.replace(/[^A-Za-z0-9]+/g, '_') || 'root'}_${kind}`;
74
+ const addSchema = (name, schema) => {
75
+ const key = JSON.stringify(schema || {});
76
+ if (!schema || typeof schema !== 'object')
77
+ return null;
78
+ const compositeKey = `${name}:${key}`;
79
+ if (seenSchemas.has(compositeKey))
80
+ return name;
81
+ seenSchemas.add(compositeKey);
82
+ schemaEntries.push({ name, schema });
83
+ return name;
84
+ };
85
+ const lines = [];
86
+ lines.push('openapi: 3.0.0');
87
+ lines.push('info:');
88
+ lines.push(` title: ${serviceName}`);
89
+ lines.push(' version: 0.0.1');
90
+ lines.push('paths:');
91
+ if (!endpoints.length) {
92
+ lines.push(' /:');
93
+ lines.push(' get:');
94
+ lines.push(' responses:');
95
+ lines.push(' "200":');
96
+ lines.push(' description: OK');
97
+ }
98
+ else {
99
+ endpoints.forEach((ep) => {
100
+ const lower = ep.method.toLowerCase();
101
+ const success = String(ep.success || 200);
102
+ lines.push(` ${ep.route}:`);
103
+ lines.push(` ${lower}:`);
104
+ const reqRef = ep.requestSchema ? addSchema(safeName(ep.route, ep.method, 'request'), ep.requestSchema) : null;
105
+ const resRef = ep.responseSchema ? addSchema(safeName(ep.route, ep.method, 'response'), ep.responseSchema) : null;
106
+ if (ep.hasBody && success !== '204') {
107
+ lines.push(' requestBody:');
108
+ lines.push(' content:');
109
+ lines.push(' application/json:');
110
+ lines.push(' schema:');
111
+ if (reqRef) {
112
+ lines.push(` $ref: "#/components/schemas/${reqRef}"`);
113
+ }
114
+ else {
115
+ lines.push(...renderSchema(ep.requestSchema || {}, ' '));
116
+ }
117
+ }
118
+ lines.push(' responses:');
119
+ lines.push(` "${success}":`);
120
+ lines.push(` description: ${success === '201' ? 'Created' : success === '202' ? 'Accepted' : success === '204' ? 'No Content' : 'OK'}`);
121
+ if (success !== '204') {
122
+ lines.push(' content:');
123
+ lines.push(' application/json:');
124
+ lines.push(' schema:');
125
+ if (resRef) {
126
+ lines.push(` $ref: "#/components/schemas/${resRef}"`);
127
+ }
128
+ else if (ep.responseSchema) {
129
+ lines.push(...renderSchema(ep.responseSchema, ' '));
130
+ }
131
+ else {
132
+ lines.push(' $ref: "#/components/schemas/Response"');
133
+ }
134
+ }
135
+ lines.push(' "400":');
136
+ lines.push(' description: Bad Request');
137
+ lines.push(' "401":');
138
+ lines.push(' description: Unauthorized');
139
+ lines.push(' "500":');
140
+ lines.push(' description: Server Error');
141
+ });
142
+ }
143
+ lines.push('components:');
144
+ lines.push(' schemas:');
145
+ lines.push(' Response:');
146
+ lines.push(' type: object');
147
+ lines.push(' properties:');
148
+ lines.push(' status:');
149
+ lines.push(' type: string');
150
+ lines.push(' message:');
151
+ lines.push(' type: string');
152
+ lines.push(' data:');
153
+ lines.push(' type: object');
154
+ schemaEntries.forEach((entry) => {
155
+ lines.push(` ${entry.name}:`);
156
+ lines.push(...renderSchema(entry.schema, ' '));
157
+ });
158
+ if (nonHttpNodes && nonHttpNodes.length) {
159
+ lines.push('x-nonHttpNodes:');
160
+ nonHttpNodes.forEach((n) => {
161
+ const componentName = addSchema(`nonhttp_${n.type}_${n.id}`, n.schema || {});
162
+ lines.push(` - id: ${n.id}`);
163
+ lines.push(` type: ${n.type}`);
164
+ lines.push(` name: ${JSON.stringify(n.name)}`);
165
+ if (n.config && Object.keys(n.config).length) {
166
+ lines.push(' configKeys:');
167
+ Object.keys(n.config).slice(0, 8).forEach((k) => lines.push(` - ${k}`));
168
+ }
169
+ if (componentName) {
170
+ lines.push(` schemaRef: "#/components/schemas/${componentName}"`);
171
+ }
172
+ });
173
+ }
174
+ return lines.join('\n');
175
+ }
176
+ /**
177
+ * Generate code for inner nodes (support nodes) that are embedded within a key node
178
+ */
179
+ function generateInnerNodeCode(innerNodes) {
180
+ const code = [];
181
+ for (const innerNode of innerNodes) {
182
+ if (!innerNode || !innerNode.type)
183
+ continue;
184
+ const innerType = String(innerNode.type || innerNode.kind || '').toLowerCase();
185
+ const innerId = safeId(innerNode.id || innerNode.name);
186
+ const innerCfg = (innerNode && innerNode.config) || {};
187
+ // Generate code based on inner node type
188
+ if (innerType === 'transform' && innerCfg.transform === 'authenticate') {
189
+ code.push(` # Inner node: Authentication (${innerId})`);
190
+ code.push(` auth_result = authenticate_request(request)`);
191
+ code.push(` if not auth_result.get('valid', False):`);
192
+ code.push(` return {"error": "Unauthorized", "status": 401}`);
193
+ code.push('');
194
+ }
195
+ else if (innerType === 'transform' && innerCfg.transform === 'validate') {
196
+ code.push(` # Inner node: Validation (${innerId})`);
197
+ code.push(` validation_result = validate_schema(payload)`);
198
+ code.push(` if not validation_result.get('valid', False):`);
199
+ code.push(` return {"error": "Validation failed", "status": 400, "details": validation_result.get('errors', [])}`);
200
+ code.push('');
201
+ }
202
+ else if (innerType === 'transform' && innerCfg.transform === 'parse') {
203
+ code.push(` # Inner node: Parse (${innerId})`);
204
+ code.push(` parsed_data = parse_payload(payload)`);
205
+ code.push('');
206
+ }
207
+ else if (innerType === 'retry' || innerCfg.retryPolicy) {
208
+ code.push(` # Inner node: Retry Logic (${innerId})`);
209
+ code.push(` max_attempts = ${innerCfg.maxAttempts || innerCfg.max_attempts || 3}`);
210
+ code.push(` for attempt in range(max_attempts):`);
211
+ code.push(` try:`);
212
+ code.push(` # Retryable operation`);
213
+ code.push(` break`);
214
+ code.push(` except Exception as e:`);
215
+ code.push(` if attempt == max_attempts - 1:`);
216
+ code.push(` raise`);
217
+ code.push(` await asyncio.sleep(2 ** attempt) # Exponential backoff`);
218
+ code.push('');
219
+ }
220
+ else if (innerType === 'error-handler' || innerCfg.errorHandling) {
221
+ code.push(` # Inner node: Error Handling (${innerId})`);
222
+ code.push(` try:`);
223
+ code.push(` # Error-handled operation`);
224
+ code.push(` pass`);
225
+ code.push(` except Exception as e:`);
226
+ code.push(` logger.error(f"Error in ${innerId}: {str(e)}")`);
227
+ code.push(` return {"error": str(e), "status": 500}`);
228
+ code.push('');
229
+ }
230
+ }
231
+ return code;
232
+ }
233
+ export default async function generatePythonFastAPIFiles(actualIR, opts = {}) {
234
+ const files = {};
235
+ const graph = actualIR && actualIR.graph ? actualIR.graph : actualIR || {};
236
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
237
+ const edges = Array.isArray(graph.edges) ? graph.edges : [];
238
+ const handlers = [];
239
+ const routes = [];
240
+ const models = [];
241
+ const endpoints = [];
242
+ const nonHttpNodes = [];
243
+ // Track edge config utilities (retry, circuit breaker) to include once
244
+ const edgeUtilityCode = new Set();
245
+ for (const n of nodes) {
246
+ // expose cloudFunction and http nodes as FastAPI endpoints
247
+ if (!n || !n.type)
248
+ continue;
249
+ if (n.type === 'cloudFunction' || n.type === 'http' || n.type === 'httpRequest') {
250
+ const id = safeId(n.id || n.name);
251
+ const nodeConfig = n?.config || {};
252
+ const dataConfig = n?.data?.config || {};
253
+ const cfg = { ...nodeConfig, ...dataConfig };
254
+ // Extract businessLogic for implementation guidance
255
+ const businessLogic = cfg.businessLogic || n?.businessLogic || n?.description || null;
256
+ const method = String(cfg.method || 'post').toLowerCase();
257
+ const route = cfg.route || cfg.url ? String(cfg.route || cfg.url) : `/${id}`;
258
+ const isAsync = cfg.async === true || cfg.asyncProcessing === true || cfg.accepted === true;
259
+ const successCode = method === 'post' ? (isAsync ? 202 : 201) : method === 'delete' ? 204 : 200;
260
+ const hname = handlerNameFor(n);
261
+ const reqModelName = `Payload${id.replace(/[^A-Za-z0-9]/g, '') || 'Request'}`;
262
+ const respModelName = `Response${id.replace(/[^A-Za-z0-9]/g, '') || 'Response'}`;
263
+ const requestSchema = cfg.schema || cfg.fields;
264
+ const responseSchema = cfg.responseSchema || cfg.response_schema || cfg.response;
265
+ models.push(buildPydanticModel(reqModelName, requestSchema));
266
+ models.push(buildPydanticModel(respModelName, responseSchema));
267
+ endpoints.push({ route, method, hasBody: method !== 'get', success: successCode, responseSchema, requestSchema });
268
+ // Get inner nodes if they exist (support nodes embedded in this key node)
269
+ const innerNodes = n.innerNodes || [];
270
+ // Get edge configurations for incoming edges to this node
271
+ const nodeId = String(n.id || '');
272
+ const incomingEdges = edges.filter((e) => {
273
+ const targetId = String(e.to || e.target || '');
274
+ return targetId === nodeId;
275
+ });
276
+ // Collect edge configs and apply them
277
+ let edgeRetryConfig = null;
278
+ let edgeCircuitBreakerConfig = null;
279
+ let edgeTimeout = null;
280
+ for (const edge of incomingEdges) {
281
+ const sourceId = String(edge.from || edge.source || '');
282
+ const edgeConfig = getEdgeConfig(edges, sourceId, nodeId);
283
+ if (edgeConfig) {
284
+ // Apply edge timeout (override node timeout if edge has one)
285
+ if (edgeConfig.config?.timeout) {
286
+ edgeTimeout = edgeConfig.config.timeout;
287
+ }
288
+ // Collect retry config from edge (prefer edge over node)
289
+ if (edgeConfig.config?.retry?.maxAttempts && edgeConfig.config.retry.maxAttempts > 0) {
290
+ edgeRetryConfig = edgeConfig;
291
+ const retryCode = generateRetryCode(edgeConfig, 'python');
292
+ if (retryCode) {
293
+ edgeUtilityCode.add(retryCode);
294
+ }
295
+ }
296
+ // Collect circuit breaker config from edge
297
+ if (edgeConfig.config?.circuitBreaker?.enabled) {
298
+ edgeCircuitBreakerConfig = edgeConfig;
299
+ const cbCode = generateCircuitBreakerCode(edgeConfig, 'python');
300
+ if (cbCode) {
301
+ edgeUtilityCode.add(cbCode);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ // Build handler code with inner nodes
307
+ const handlerLines = [];
308
+ handlerLines.push(`def ${hname}(payload: ${reqModelName} | None = None):`);
309
+ // Include businessLogic in docstring if available
310
+ const docstring = businessLogic
311
+ ? ` """Handler for node ${String(n.id || '')}${innerNodes.length > 0 ? ` (with ${innerNodes.length} inner node(s))` : ''}\n \n Business Logic: ${businessLogic}\n """`
312
+ : ` """Handler for node ${String(n.id || '')}${innerNodes.length > 0 ? ` (with ${innerNodes.length} inner node(s))` : ''}"""`;
313
+ handlerLines.push(docstring);
314
+ if (businessLogic) {
315
+ handlerLines.push(` # Business Logic: ${businessLogic}`);
316
+ }
317
+ handlerLines.push(' # Rate limit / quota stub');
318
+ handlerLines.push(' # TODO: implement per-endpoint rate limiting');
319
+ handlerLines.push(' # Policy enforcement (fail open on error)');
320
+ handlerLines.push(' import asyncio');
321
+ handlerLines.push(' try:');
322
+ handlerLines.push(' ok = asyncio.get_event_loop().run_until_complete(enforce_policy({"nodeId": "' + String(n.id || '') + '", "route": "' + route + '", "method": "' + method + '"}))');
323
+ handlerLines.push(' if not ok:');
324
+ handlerLines.push(' return Response(status_code=403, content="policy_blocked")');
325
+ handlerLines.push(' except Exception:');
326
+ handlerLines.push(' logging.warning("Policy check failed; continuing");');
327
+ // Add inner node code first (authentication, validation, etc.)
328
+ if (innerNodes.length > 0) {
329
+ const innerCode = generateInnerNodeCode(innerNodes);
330
+ handlerLines.push(...innerCode);
331
+ }
332
+ // Add main business logic driven by config
333
+ handlerLines.push(' import asyncio');
334
+ handlerLines.push(' import time');
335
+ handlerLines.push(` config = ${JSON.stringify(cfg)}`);
336
+ handlerLines.push(' request_id = str(getattr(payload, "requestId", None) or "")');
337
+ handlerLines.push(' filters = getattr(payload, "filters", {}) if payload else {}');
338
+ handlerLines.push(' page = int(filters.get("page", filters.get("offset", 1)) or 1)');
339
+ handlerLines.push(' page_size = int(filters.get("pageSize", filters.get("limit", 20)) or 20)');
340
+ handlerLines.push(' status = filters.get("status")');
341
+ handlerLines.push(' date_from = filters.get("from") or filters.get("startDate")');
342
+ handlerLines.push(' date_to = filters.get("to") or filters.get("endDate")');
343
+ // Use edge timeout if available, otherwise fall back to node config
344
+ const effectiveTimeout = edgeTimeout ?? cfg.timeoutMs ?? 2000;
345
+ handlerLines.push(` timeout_ms = int(${effectiveTimeout})`);
346
+ // Use edge retry config if available, otherwise fall back to node config
347
+ const effectiveRetryPolicy = edgeRetryConfig?.config?.retry || cfg.retryPolicy || { "maxAttempts": 2, "backoffMs": 500 };
348
+ handlerLines.push(` retry_policy = ${JSON.stringify(effectiveRetryPolicy)}`);
349
+ handlerLines.push(' max_attempts = int(retry_policy.get("maxAttempts", 1))');
350
+ handlerLines.push(' backoff_ms = int(retry_policy.get("backoffMs", 200))');
351
+ const retryStrategy = edgeRetryConfig?.config?.retry?.strategy || 'exponential';
352
+ handlerLines.push(` operation = ${JSON.stringify(cfg.operation || 'read')}`);
353
+ handlerLines.push(` primary_key = ${JSON.stringify(cfg.primaryKey || 'id')}`);
354
+ handlerLines.push(` table = ${JSON.stringify(cfg.table || 'records')}`);
355
+ handlerLines.push(` base_query = ${JSON.stringify(cfg.query || '')}`);
356
+ handlerLines.push('');
357
+ handlerLines.push(' async def run_operation():');
358
+ // Use businessLogic to generate more specific implementation
359
+ if (businessLogic) {
360
+ handlerLines.push(` # Business Logic: ${businessLogic}`);
361
+ // Generate implementation based on businessLogic keywords
362
+ if (businessLogic.toLowerCase().includes('validate') || businessLogic.toLowerCase().includes('check')) {
363
+ handlerLines.push(' # Validation logic based on business requirements');
364
+ handlerLines.push(' if payload:');
365
+ handlerLines.push(' payload_dict = payload.dict() if hasattr(payload, "dict") else payload');
366
+ handlerLines.push(' # Add validation checks based on business rules');
367
+ }
368
+ if (businessLogic.toLowerCase().includes('fetch') || businessLogic.toLowerCase().includes('get') || businessLogic.toLowerCase().includes('retrieve')) {
369
+ handlerLines.push(' # Fetch/retrieve operation based on business logic');
370
+ }
371
+ if (businessLogic.toLowerCase().includes('create') || businessLogic.toLowerCase().includes('insert')) {
372
+ handlerLines.push(' # Create operation with business validation');
373
+ }
374
+ if (businessLogic.toLowerCase().includes('calculate') || businessLogic.toLowerCase().includes('compute')) {
375
+ handlerLines.push(' # Calculation logic based on business rules');
376
+ }
377
+ }
378
+ handlerLines.push(' # Database operation honoring config/query/engine');
379
+ handlerLines.push(' if operation == "create":');
380
+ handlerLines.push(' body = payload.dict() if payload else {};');
381
+ handlerLines.push(' new_id = body.get(primary_key) or f"new-{int(time.time()*1000)}"');
382
+ handlerLines.push(' created = {**body, primary_key: new_id, "createdAt": datetime.utcnow().isoformat() + "Z", "table": table, "query": base_query}');
383
+ handlerLines.push(' return {"created": created}');
384
+ handlerLines.push(' if operation == "update":');
385
+ handlerLines.push(' body = payload.dict() if payload else {};');
386
+ handlerLines.push(' the_id = body.get(primary_key) or filters.get(primary_key)');
387
+ handlerLines.push(' if not the_id:');
388
+ handlerLines.push(' raise HTTPException(status_code=400, detail="missing primary key")');
389
+ handlerLines.push(' updated = {**body, primary_key: the_id, "updatedAt": datetime.utcnow().isoformat() + "Z", "table": table, "query": base_query}');
390
+ handlerLines.push(' return {"updated": updated}');
391
+ handlerLines.push(' if operation == "delete":');
392
+ handlerLines.push(' the_id = filters.get(primary_key) or ((payload.dict() if payload else {}).get(primary_key) if payload else None)');
393
+ handlerLines.push(' if not the_id:');
394
+ handlerLines.push(' raise HTTPException(status_code=400, detail="missing primary key")');
395
+ handlerLines.push(' return {"deleted": True, "id": the_id, "table": table, "query": base_query}');
396
+ handlerLines.push(' rows = [');
397
+ handlerLines.push(' {');
398
+ handlerLines.push(' primary_key: f"ORD-{page}-{i+1}",');
399
+ handlerLines.push(' "status": status or "pending",');
400
+ handlerLines.push(' "total": 100 + i,');
401
+ handlerLines.push(' "createdAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),');
402
+ handlerLines.push(' "table": table,');
403
+ handlerLines.push(' "query": base_query or None');
404
+ handlerLines.push(' } for i in range(min(page_size, 5))');
405
+ handlerLines.push(' ]');
406
+ handlerLines.push(' return rows');
407
+ handlerLines.push('');
408
+ handlerLines.push(' data = None');
409
+ // Use circuit breaker if configured on edge
410
+ if (edgeCircuitBreakerConfig) {
411
+ handlerLines.push(' # Circuit breaker protection (from edge config)');
412
+ handlerLines.push(` circuit_breaker = CircuitBreaker(${edgeCircuitBreakerConfig.config?.circuitBreaker?.failureThreshold || 5}, ${(edgeCircuitBreakerConfig.config?.circuitBreaker?.resetTimeoutMs || 60000) / 1000})`);
413
+ handlerLines.push(' try:');
414
+ handlerLines.push(' # Wrap async operation for circuit breaker');
415
+ handlerLines.push(' async def protected_operation():');
416
+ handlerLines.push(' return await asyncio.wait_for(run_operation(), timeout_ms / 1000)');
417
+ handlerLines.push(' # Circuit breaker expects sync function, so we run async in executor');
418
+ handlerLines.push(' loop = asyncio.get_event_loop()');
419
+ handlerLines.push(' data = await loop.run_in_executor(None, lambda: circuit_breaker.call(lambda: loop.run_until_complete(protected_operation())))');
420
+ handlerLines.push(' except Exception as err:');
421
+ handlerLines.push(' logger.error("circuit breaker open or operation failed", exc_info=True)');
422
+ handlerLines.push(' return Response(status_code=500, content="upstream_failed")');
423
+ }
424
+ else {
425
+ // Use retry logic (from edge or node config)
426
+ const useEdgeRetry = edgeRetryConfig && retryStrategy === 'exponential';
427
+ if (useEdgeRetry) {
428
+ handlerLines.push(' # Retry with exponential backoff (from edge config)');
429
+ handlerLines.push(' # Note: Edge retry utility is sync, so we adapt for async');
430
+ handlerLines.push(' for attempt in range(max_attempts):');
431
+ handlerLines.push(' try:');
432
+ handlerLines.push(' data = await asyncio.wait_for(run_operation(), timeout_ms / 1000)');
433
+ handlerLines.push(' break');
434
+ handlerLines.push(' except Exception as err:');
435
+ handlerLines.push(' if attempt == max_attempts - 1:');
436
+ handlerLines.push(' logger.error("upstream_failed", exc_info=True)');
437
+ handlerLines.push(' return Response(status_code=500, content="upstream_failed")');
438
+ handlerLines.push(' # Exponential backoff: base_delay * (2 ** attempt)');
439
+ handlerLines.push(' delay = (backoff_ms / 1000) * (2 ** attempt)');
440
+ handlerLines.push(' await asyncio.sleep(delay)');
441
+ }
442
+ else {
443
+ // Fallback to standard retry loop
444
+ handlerLines.push(' for attempt in range(1, max_attempts + 1):');
445
+ handlerLines.push(' try:');
446
+ handlerLines.push(' # In real code, apply timeout using asyncio.wait_for');
447
+ handlerLines.push(' data = await asyncio.wait_for(run_operation(), timeout_ms / 1000);');
448
+ handlerLines.push(' break');
449
+ handlerLines.push(' except Exception as err:');
450
+ handlerLines.push(' if attempt == max_attempts:');
451
+ handlerLines.push(' logger.error("upstream_failed", exc_info=True)');
452
+ handlerLines.push(' return Response(status_code=500, content="upstream_failed")');
453
+ handlerLines.push(' await asyncio.sleep(backoff_ms / 1000)');
454
+ }
455
+ }
456
+ handlerLines.push('');
457
+ handlerLines.push(' logger.info({');
458
+ handlerLines.push(' "event": "audit",');
459
+ handlerLines.push(' "route": "' + route + '",');
460
+ handlerLines.push(' "status": "success",');
461
+ handlerLines.push(' "filters": {');
462
+ handlerLines.push(' "status": status, "date_from": date_from, "date_to": date_to, "page": page, "page_size": page_size');
463
+ handlerLines.push(' }');
464
+ handlerLines.push(' })');
465
+ if (successCode === 204) {
466
+ handlerLines.push(' return Response(status_code=204)');
467
+ }
468
+ else {
469
+ handlerLines.push(` return ${respModelName}(status="ok", message="Handled ${String(n.name || n.id || '')}", data={"items": data, "page": page, "pageSize": page_size})`);
470
+ }
471
+ handlerLines.push('');
472
+ handlers.push(handlerLines.join('\n'));
473
+ // route wrapper
474
+ const routeLines = [];
475
+ const statusCode = successCode;
476
+ const responseModelPart = statusCode === 204 ? 'response_model=None' : `response_model=${respModelName}`;
477
+ const methodDecorator = method === 'get'
478
+ ? `@app.get("${route}", ${responseModelPart}, status_code=${statusCode})`
479
+ : `@app.post("${route}", ${responseModelPart}, status_code=${statusCode})`;
480
+ routeLines.push(methodDecorator);
481
+ routeLines.push(`async def ${hname}_endpoint(request):`);
482
+ routeLines.push(' try:');
483
+ routeLines.push(' payload_dict = await request.json()');
484
+ routeLines.push(' except Exception:');
485
+ routeLines.push(' payload_dict = {}');
486
+ routeLines.push(` payload = ${reqModelName}(**payload_dict) if payload_dict else None`);
487
+ routeLines.push(` return ${hname}(payload)`);
488
+ routeLines.push('');
489
+ routes.push(routeLines.join('\n'));
490
+ }
491
+ }
492
+ // Collect non-http nodes
493
+ nodes.forEach((n) => {
494
+ if (!n || !n.type)
495
+ return;
496
+ const t = String(n.type || '').toLowerCase();
497
+ if (t.includes('http'))
498
+ return;
499
+ const cfg = n.config || n.data?.config || {};
500
+ nonHttpNodes.push({
501
+ id: String(n.id || ''),
502
+ type: String(n.type || ''),
503
+ name: String(n.name || n.id || ''),
504
+ schema: cfg.schema || cfg.fields,
505
+ config: cfg,
506
+ });
507
+ });
508
+ // Helper functions for inner nodes
509
+ const helperFunctions = [];
510
+ helperFunctions.push('# Helper functions for inner nodes (support nodes)');
511
+ helperFunctions.push('import asyncio');
512
+ helperFunctions.push('import logging');
513
+ helperFunctions.push('');
514
+ helperFunctions.push('logger = logging.getLogger(__name__)');
515
+ helperFunctions.push('');
516
+ helperFunctions.push('def authenticate_request(request):');
517
+ helperFunctions.push(' """Authentication helper for inner nodes"""');
518
+ helperFunctions.push(' # Implement authentication based on request headers');
519
+ helperFunctions.push(' auth_header = request.headers.get("Authorization", "") if hasattr(request, "headers") else ""');
520
+ helperFunctions.push(' if auth_header.startswith("Bearer "):');
521
+ helperFunctions.push(' token = auth_header[7:]');
522
+ helperFunctions.push(' # Validate JWT token (implement actual validation)');
523
+ helperFunctions.push(' # For now, return valid if token exists');
524
+ helperFunctions.push(' return {"valid": bool(token), "user": {"id": "user-123", "token": token[:10] + "..."} if token else None}');
525
+ helperFunctions.push(' return {"valid": False, "user": None, "error": "Missing or invalid authorization"}');
526
+ helperFunctions.push('');
527
+ helperFunctions.push('def validate_schema(payload: dict):');
528
+ helperFunctions.push(' """Schema validation helper for inner nodes"""');
529
+ helperFunctions.push(' # Implement schema validation based on payload structure');
530
+ helperFunctions.push(' errors = []');
531
+ helperFunctions.push(' if not payload:');
532
+ helperFunctions.push(' return {"valid": False, "errors": ["Payload is required"]}');
533
+ helperFunctions.push(' # Add field-specific validation based on schema');
534
+ helperFunctions.push(' # Example: check required fields, types, ranges, etc.');
535
+ helperFunctions.push(' return {"valid": len(errors) == 0, "errors": errors}');
536
+ helperFunctions.push('');
537
+ helperFunctions.push('def parse_payload(payload: dict):');
538
+ helperFunctions.push(' """Payload parsing helper for inner nodes"""');
539
+ helperFunctions.push(' # Parse and normalize payload data');
540
+ helperFunctions.push(' if isinstance(payload, str):');
541
+ helperFunctions.push(' import json');
542
+ helperFunctions.push(' try:');
543
+ helperFunctions.push(' return json.loads(payload)');
544
+ helperFunctions.push(' except json.JSONDecodeError:');
545
+ helperFunctions.push(' return {"error": "Invalid JSON", "raw": payload}');
546
+ helperFunctions.push(' # Normalize dict keys, handle nested structures');
547
+ helperFunctions.push(' if isinstance(payload, dict):');
548
+ helperFunctions.push(' return {k: v for k, v in payload.items() if v is not None}');
549
+ helperFunctions.push(' return payload');
550
+ helperFunctions.push('');
551
+ // Add edge config utilities (retry, circuit breaker) if any were generated
552
+ if (edgeUtilityCode.size > 0) {
553
+ helperFunctions.push('# Edge configuration utilities (retry, circuit breaker)');
554
+ edgeUtilityCode.forEach(code => helperFunctions.push(code));
555
+ }
556
+ // main app file
557
+ const appLines = [];
558
+ appLines.push('from fastapi import FastAPI, Request, Response');
559
+ appLines.push('from typing import Dict, Any, List');
560
+ appLines.push('from pydantic import BaseModel');
561
+ appLines.push('import time');
562
+ appLines.push('import uuid');
563
+ appLines.push('import logging');
564
+ appLines.push('import httpx');
565
+ appLines.push('from fastapi.middleware.cors import CORSMiddleware');
566
+ appLines.push('');
567
+ appLines.push("app = FastAPI(title=\"Generated FastAPI App\")");
568
+ appLines.push('');
569
+ appLines.push("# CORS tightened via ALLOWED_ORIGINS env; defaults to none");
570
+ appLines.push("import os");
571
+ appLines.push("allowed = [o for o in os.getenv('ALLOWED_ORIGINS', '').split(',') if o]");
572
+ appLines.push("if allowed:");
573
+ appLines.push(" app.add_middleware(CORSMiddleware, allow_origins=allowed, allow_credentials=True, allow_methods=['*'], allow_headers=['*'])");
574
+ appLines.push('');
575
+ appLines.push('# Middlewares');
576
+ appLines.push('@app.middleware("http")');
577
+ appLines.push('async def runtime_middleware(request: Request, call_next):');
578
+ appLines.push(' rid = request.headers.get("x-request-id", str(uuid.uuid4()))');
579
+ appLines.push(' start = time.time()');
580
+ appLines.push(' request.state.request_id = rid');
581
+ appLines.push(' try:');
582
+ appLines.push(' response = await call_next(request)');
583
+ appLines.push(' except Exception as exc:');
584
+ appLines.push(' logging.exception("Request failed");');
585
+ appLines.push(' return Response(status_code=500, content="internal error")');
586
+ appLines.push(' duration = int((time.time() - start) * 1000)');
587
+ appLines.push(' response.headers["x-request-id"] = rid');
588
+ appLines.push(' response.headers["x-duration-ms"] = str(duration)');
589
+ appLines.push(' return response');
590
+ appLines.push('');
591
+ appLines.push('async def enforce_policy(payload: dict) -> bool:');
592
+ appLines.push(' url = ""');
593
+ appLines.push(' api_key = ""');
594
+ appLines.push(' if not url:');
595
+ appLines.push(' return True');
596
+ appLines.push(' try:');
597
+ appLines.push(' async with httpx.AsyncClient() as client:');
598
+ appLines.push(' headers = {"x-api-key": api_key} if api_key else {}');
599
+ appLines.push(' resp = await client.post(url, json=payload, headers=headers)');
600
+ appLines.push(' return resp.status_code < 300');
601
+ appLines.push(' except Exception:');
602
+ appLines.push(' logging.warning("Policy enforcement failed", exc_info=True)');
603
+ appLines.push(' return True # fail open');
604
+ appLines.push('');
605
+ // models
606
+ appLines.push('# Models derived from node schemas');
607
+ appLines.push(models.join('\n'));
608
+ // include helper functions
609
+ appLines.push(helperFunctions.join('\n'));
610
+ // include handlers
611
+ appLines.push('# Handlers');
612
+ appLines.push(handlers.join('\n'));
613
+ appLines.push('');
614
+ appLines.push('# Routes');
615
+ appLines.push(routes.join('\n'));
616
+ appLines.push('');
617
+ appLines.push('@app.get("/healthz")');
618
+ appLines.push('async def healthz():');
619
+ appLines.push(' return {"ok": True}');
620
+ appLines.push('');
621
+ appLines.push('@app.get("/ready")');
622
+ appLines.push('async def ready():');
623
+ appLines.push(' return {"ok": True}');
624
+ appLines.push('');
625
+ appLines.push("if __name__ == '__main__':");
626
+ appLines.push(" import uvicorn");
627
+ appLines.push(" uvicorn.run(app, host='0.0.0.0', port=8080)");
628
+ files['app/main.py'] = appLines.join('\n');
629
+ // requirements
630
+ files['requirements.txt'] = 'fastapi\nuvicorn\nhttpx\npydantic\n';
631
+ // pyproject.toml — single project root (like .sln for C#, package.json for Node)
632
+ const projectName = String((opts && opts.projectName) || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-fastapi').replace(/[^A-Za-z0-9_-]/g, '-');
633
+ files['pyproject.toml'] = `[build-system]
634
+ requires = ["setuptools>=61.0"]
635
+ build-backend = "setuptools.build_meta"
636
+
637
+ [project]
638
+ name = "${projectName}"
639
+ version = "0.1.0"
640
+ description = "Generated FastAPI project"
641
+ requires-python = ">=3.10"
642
+ dependencies = [
643
+ "fastapi>=0.104.0",
644
+ "uvicorn>=0.24.0",
645
+ "httpx>=0.25.0",
646
+ "pydantic>=2.0.0",
647
+ ]
648
+ `;
649
+ files['tests/contract.test.js'] = [
650
+ "const fs = require('fs');",
651
+ "const path = require('path');",
652
+ "describe('contract', () => {",
653
+ " it('should have an openapi file', () => {",
654
+ " const spec = fs.readFileSync(path.join(__dirname, '../openapi.yaml'), 'utf-8');",
655
+ " expect(spec).toBeTruthy();",
656
+ " });",
657
+ "});",
658
+ ].join('\n');
659
+ files['openapi.yaml'] = buildOpenApiSpec(opts.projectName || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-fastapi', endpoints, nonHttpNodes);
660
+ // README
661
+ const projectDisplayName = String((opts && opts.projectName) || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-fastapi');
662
+ files['README.md'] = `# ${projectDisplayName}\n\nGenerated FastAPI project.\n\nRun:\n\n pip install -r requirements.txt\n python app/main.py\n`;
663
+ return files;
664
+ }