tova 0.2.9 → 0.3.1

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.
@@ -156,7 +156,7 @@ export class ServerCodegen extends BaseCodegen {
156
156
  const modelDecls = [];
157
157
  const aiConfigs = []; // { name: string|null, config: object }
158
158
 
159
- const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = []) => {
159
+ const collectFromBody = (stmts, groupPrefix = null, groupMiddlewares = [], groupVersion = null) => {
160
160
  for (const stmt of stmts) {
161
161
  if (stmt.type === 'RouteDeclaration') {
162
162
  const route = stmt;
@@ -165,6 +165,7 @@ export class ServerCodegen extends BaseCodegen {
165
165
  ...route,
166
166
  path: groupPrefix + route.path,
167
167
  _groupMiddlewares: groupMiddlewares.length > 0 ? [...groupMiddlewares] : undefined,
168
+ _version: groupVersion || undefined,
168
169
  };
169
170
  routes.push(prefixedRoute);
170
171
  } else {
@@ -196,7 +197,8 @@ export class ServerCodegen extends BaseCodegen {
196
197
  } else if (stmt.type === 'RouteGroupDeclaration') {
197
198
  const prefix = groupPrefix ? groupPrefix + stmt.prefix : stmt.prefix;
198
199
  const grpMw = [...groupMiddlewares]; // inherit parent group middlewares
199
- collectFromBody(stmt.body, prefix, grpMw);
200
+ const ver = stmt.version || groupVersion; // inherit or override version
201
+ collectFromBody(stmt.body, prefix, grpMw, ver);
200
202
  } else if (stmt.type === 'RateLimitDeclaration') {
201
203
  rateLimitConfig = stmt.config;
202
204
  } else if (stmt.type === 'LifecycleHookDeclaration') {
@@ -240,8 +242,8 @@ export class ServerCodegen extends BaseCodegen {
240
242
 
241
243
  // Collect type declarations from shared blocks for model/ORM generation
242
244
  const sharedTypes = new Map(); // typeName -> { fields: [{ name, type }] }
243
- for (const sb of sharedBlocks) {
244
- for (const stmt of sb.body) {
245
+ const _collectTypes = (stmts) => {
246
+ for (const stmt of stmts) {
245
247
  if (stmt.type === 'TypeDeclaration' && stmt.variants) {
246
248
  const fields = [];
247
249
  for (const v of stmt.variants) {
@@ -254,6 +256,13 @@ export class ServerCodegen extends BaseCodegen {
254
256
  }
255
257
  }
256
258
  }
259
+ };
260
+ for (const sb of sharedBlocks) {
261
+ _collectTypes(sb.body);
262
+ }
263
+ // Also collect types from server blocks (for body: Type validation)
264
+ for (const block of serverBlocks) {
265
+ _collectTypes(block.body);
257
266
  }
258
267
 
259
268
  // Separate group-only middlewares from global middlewares
@@ -474,6 +483,7 @@ export class ServerCodegen extends BaseCodegen {
474
483
  lines.push(this._getAiRuntime());
475
484
  lines.push('');
476
485
 
486
+ let hasDefaultAi = false;
477
487
  for (const aiConf of aiConfigs) {
478
488
  const configParts = [];
479
489
  for (const [key, valueNode] of Object.entries(aiConf.config)) {
@@ -487,8 +497,13 @@ export class ServerCodegen extends BaseCodegen {
487
497
  } else {
488
498
  // Default provider: ai { ... } → const ai = __createAI({...})
489
499
  lines.push(`const ai = __createAI(${configStr});`);
500
+ hasDefaultAi = true;
490
501
  }
491
502
  }
503
+ // If no default ai config, create a default for one-off calls
504
+ if (!hasDefaultAi) {
505
+ lines.push('const ai = __createAI({});');
506
+ }
492
507
  lines.push('');
493
508
  }
494
509
 
@@ -782,12 +797,12 @@ export class ServerCodegen extends BaseCodegen {
782
797
  // ════════════════════════════════════════════════════════════
783
798
  lines.push('// ── Router ──');
784
799
  lines.push('const __routes = [];');
785
- lines.push('function __addRoute(method, path, handler) {');
800
+ lines.push('function __addRoute(method, path, handler, version) {');
786
801
  lines.push(' let pattern = path');
787
802
  lines.push(' .replace(/\\*([a-zA-Z_][a-zA-Z0-9_]*)/g, "(?<$1>.+)")');
788
803
  lines.push(' .replace(/\\*$/g, "(.*)")');
789
804
  lines.push(' .replace(/:([^/]+)/g, "(?<$1>[^/]+)");');
790
- lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path });');
805
+ lines.push(' __routes.push({ method, regex: new RegExp(`^${pattern}$`), handler, _path: path, _version: version || null });');
791
806
  lines.push('}');
792
807
  lines.push('');
793
808
 
@@ -1693,6 +1708,10 @@ export class ServerCodegen extends BaseCodegen {
1693
1708
  const validateDec = decorators.find(d => d.name === 'validate');
1694
1709
  const uploadDec = decorators.find(d => d.name === 'upload');
1695
1710
 
1711
+ // T9-5: Version info for route
1712
+ const routeVersion = route._version;
1713
+ const versionArg = routeVersion ? `, ${JSON.stringify(String(routeVersion.version || ''))}` : '';
1714
+
1696
1715
  lines.push(`__addRoute(${JSON.stringify(method)}, ${JSON.stringify(path)}, async (req, params) => {`);
1697
1716
 
1698
1717
  // Auth decorator check
@@ -1735,6 +1754,68 @@ export class ServerCodegen extends BaseCodegen {
1735
1754
  lines.push(` if (__validationErrors.length > 0) return Response.json({ error: "Validation failed", details: __validationErrors }, { status: 400 });`);
1736
1755
  }
1737
1756
 
1757
+ // T9-1: Route-level body type validation — route POST "/api/users" body: User => handler
1758
+ if (route.bodyType && ['POST', 'PUT', 'PATCH'].includes(method)) {
1759
+ const typeName = route.bodyType.name || (route.bodyType.elementType && route.bodyType.elementType.name);
1760
+ if (typeName && sharedTypes.has(typeName)) {
1761
+ const typeInfo = sharedTypes.get(typeName);
1762
+ if (!uploadDec && !validateDec) {
1763
+ lines.push(` const __body = (await __parseBody(req)) || {};`);
1764
+ }
1765
+ const isArray = route.bodyType.type === 'ArrayTypeAnnotation';
1766
+ if (isArray) {
1767
+ lines.push(` if (!Array.isArray(__body)) return Response.json({ error: "Request body must be an array of ${typeName}" }, { status: 400 });`);
1768
+ lines.push(` const __bodyTypeErrors = [];`);
1769
+ lines.push(` for (let __i = 0; __i < __body.length; __i++) {`);
1770
+ lines.push(` const __item = __body[__i];`);
1771
+ for (const f of typeInfo.fields) {
1772
+ if (f.name === 'id') continue;
1773
+ switch (f.type) {
1774
+ case 'String':
1775
+ lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "string") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a string\`);`);
1776
+ break;
1777
+ case 'Int':
1778
+ lines.push(` if (__item.${f.name} !== undefined && !Number.isInteger(__item.${f.name})) __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be an integer\`);`);
1779
+ break;
1780
+ case 'Float':
1781
+ lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "number") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a number\`);`);
1782
+ break;
1783
+ case 'Bool':
1784
+ lines.push(` if (__item.${f.name} !== undefined && typeof __item.${f.name} !== "boolean") __bodyTypeErrors.push(\`[${f.name}] at index \${__i} must be a boolean\`);`);
1785
+ break;
1786
+ }
1787
+ }
1788
+ lines.push(` }`);
1789
+ lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for ${typeName}[]", details: __bodyTypeErrors }, { status: 400 });`);
1790
+ } else {
1791
+ lines.push(` const __bodyTypeErrors = [];`);
1792
+ for (const f of typeInfo.fields) {
1793
+ if (f.name === 'id') continue;
1794
+ switch (f.type) {
1795
+ case 'String':
1796
+ lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "string") __bodyTypeErrors.push("${f.name} must be a string");`);
1797
+ break;
1798
+ case 'Int':
1799
+ lines.push(` if (__body.${f.name} !== undefined && !Number.isInteger(__body.${f.name})) __bodyTypeErrors.push("${f.name} must be an integer");`);
1800
+ break;
1801
+ case 'Float':
1802
+ lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "number") __bodyTypeErrors.push("${f.name} must be a number");`);
1803
+ break;
1804
+ case 'Bool':
1805
+ lines.push(` if (__body.${f.name} !== undefined && typeof __body.${f.name} !== "boolean") __bodyTypeErrors.push("${f.name} must be a boolean");`);
1806
+ break;
1807
+ }
1808
+ }
1809
+ // Check for required fields (non-id fields without defaults)
1810
+ for (const f of typeInfo.fields) {
1811
+ if (f.name === 'id') continue;
1812
+ lines.push(` if (__body.${f.name} === undefined || __body.${f.name} === null) __bodyTypeErrors.push("${f.name} is required");`);
1813
+ }
1814
+ lines.push(` if (__bodyTypeErrors.length > 0) return Response.json({ error: "Validation failed for type ${typeName}", details: __bodyTypeErrors }, { status: 400 });`);
1815
+ }
1816
+ }
1817
+ }
1818
+
1738
1819
  // Type-safe body deserialization: if a param has a shared type annotation, auto-validate
1739
1820
  if (handlerDecl && ['POST', 'PUT', 'PATCH'].includes(method)) {
1740
1821
  for (const p of handlerDecl.params) {
@@ -1851,9 +1932,65 @@ export class ServerCodegen extends BaseCodegen {
1851
1932
  this._emitHandlerCall(lines, `${handler}(req, params)`, timeoutMs);
1852
1933
  }
1853
1934
 
1854
- lines.push(` if (__result instanceof Response) return __result;`);
1855
- lines.push(` return Response.json(__result);`);
1856
- lines.push(`});`);
1935
+ // T9-3: Detect generator-based streaming — if handler uses yield, wrap in streaming response
1936
+ const isGeneratorHandler = route.handler.type === 'Identifier'
1937
+ ? (handlerDecl && this._containsYield(handlerDecl.body))
1938
+ : (route.handler.type === 'FunctionDeclaration' || route.handler.type === 'LambdaExpression')
1939
+ ? this._containsYield(route.handler.body || route.handler)
1940
+ : false;
1941
+
1942
+ if (isGeneratorHandler) {
1943
+ lines.push(` if (__result && typeof __result[Symbol.asyncIterator] === "function") {`);
1944
+ lines.push(` const __encoder = new TextEncoder();`);
1945
+ lines.push(` const __stream = new ReadableStream({`);
1946
+ lines.push(` async start(controller) {`);
1947
+ lines.push(` try {`);
1948
+ lines.push(` for await (const __chunk of __result) {`);
1949
+ lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
1950
+ lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
1951
+ lines.push(` }`);
1952
+ lines.push(` } finally { controller.close(); }`);
1953
+ lines.push(` }`);
1954
+ lines.push(` });`);
1955
+ lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
1956
+ lines.push(` }`);
1957
+ lines.push(` if (__result && typeof __result[Symbol.iterator] === "function" && typeof __result !== "string") {`);
1958
+ lines.push(` const __encoder = new TextEncoder();`);
1959
+ lines.push(` const __chunks = [...__result];`);
1960
+ lines.push(` const __stream = new ReadableStream({`);
1961
+ lines.push(` start(controller) {`);
1962
+ lines.push(` for (const __chunk of __chunks) {`);
1963
+ lines.push(` const __data = typeof __chunk === "string" ? __chunk : JSON.stringify(__chunk);`);
1964
+ lines.push(` controller.enqueue(__encoder.encode(\`data: \${__data}\\n\\n\`));`);
1965
+ lines.push(` }`);
1966
+ lines.push(` controller.close();`);
1967
+ lines.push(` }`);
1968
+ lines.push(` });`);
1969
+ lines.push(` return new Response(__stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" } });`);
1970
+ lines.push(` }`);
1971
+ }
1972
+ // T9-5: API versioning — add version headers to responses
1973
+ if (routeVersion) {
1974
+ const ver = JSON.stringify(String(routeVersion.version || ''));
1975
+ lines.push(` const __addVersionHeaders = (res) => {`);
1976
+ lines.push(` const h = new Headers(res.headers);`);
1977
+ lines.push(` h.set("API-Version", ${ver});`);
1978
+ if (routeVersion.deprecated) {
1979
+ lines.push(` h.set("Deprecation", "true");`);
1980
+ if (routeVersion.sunset) {
1981
+ lines.push(` h.set("Sunset", ${JSON.stringify(String(routeVersion.sunset))});`);
1982
+ }
1983
+ lines.push(` h.set("Link", '</api/v' + (parseInt(${ver}) + 1) + req.url.replace(/\\/api\\/v\\d+/, "") + '>; rel="successor-version"');`);
1984
+ }
1985
+ lines.push(` return new Response(res.body, { status: res.status, headers: h });`);
1986
+ lines.push(` };`);
1987
+ lines.push(` if (__result instanceof Response) return __addVersionHeaders(__result);`);
1988
+ lines.push(` return __addVersionHeaders(Response.json(__result));`);
1989
+ } else {
1990
+ lines.push(` if (__result instanceof Response) return __result;`);
1991
+ lines.push(` return Response.json(__result);`);
1992
+ }
1993
+ lines.push(`}${versionArg});`);
1857
1994
  lines.push('');
1858
1995
  }
1859
1996
  }
@@ -1922,46 +2059,66 @@ export class ServerCodegen extends BaseCodegen {
1922
2059
  lines.push(' ],');
1923
2060
  }
1924
2061
 
1925
- // Request body schema for POST/PUT/PATCH
1926
- if (['post', 'put', 'patch'].includes(method) && handlerDecl) {
1927
- const bodyParams = handlerDecl.params.filter(p => p.name !== 'req' && !pathParams.includes(p.name));
1928
- if (bodyParams.length > 0) {
1929
- lines.push(' requestBody: {');
1930
- lines.push(' content: { "application/json": { schema: { type: "object", properties: {');
1931
- for (const bp of bodyParams) {
1932
- const ta = bp.typeAnnotation;
1933
- if (ta && ta.name && sharedTypes.has(ta.name)) {
1934
- lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
1935
- } else if (ta) {
1936
- let jsonType;
1937
- switch (ta.name) {
1938
- case 'Int': jsonType = '"integer"'; break;
1939
- case 'Float': jsonType = '"number"'; break;
1940
- case 'Bool': jsonType = '"boolean"'; break;
1941
- default: jsonType = '"string"'; break;
1942
- }
1943
- lines.push(` ${bp.name}: { type: ${jsonType} },`);
2062
+ // Request body schema for POST/PUT/PATCH — prefer route-level bodyType, fall back to handler params
2063
+ if (['post', 'put', 'patch'].includes(method)) {
2064
+ if (route.bodyType) {
2065
+ // T9-1: Route-level body type annotation
2066
+ const bt = route.bodyType;
2067
+ if (bt.type === 'ArrayTypeAnnotation' && bt.elementType) {
2068
+ const elName = bt.elementType.name;
2069
+ if (sharedTypes.has(elName)) {
2070
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } },`);
1944
2071
  } else {
1945
- lines.push(` ${bp.name}: { type: "string" },`);
2072
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } },`);
1946
2073
  }
2074
+ } else if (bt.type === 'TypeAnnotation' && sharedTypes.has(bt.name)) {
2075
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: { "$ref": "#/components/schemas/${bt.name}" } } } },`);
2076
+ } else if (bt.type === 'TypeAnnotation') {
2077
+ lines.push(` requestBody: { required: true, content: { "application/json": { schema: ${tovaTypeToJsonSchema(bt.name)} } } },`);
2078
+ }
2079
+ } else if (handlerDecl) {
2080
+ const bodyParams = handlerDecl.params.filter(p => p.name !== 'req' && !pathParams.includes(p.name));
2081
+ if (bodyParams.length > 0) {
2082
+ lines.push(' requestBody: {');
2083
+ lines.push(' content: { "application/json": { schema: { type: "object", properties: {');
2084
+ for (const bp of bodyParams) {
2085
+ const ta = bp.typeAnnotation;
2086
+ if (ta && ta.name && sharedTypes.has(ta.name)) {
2087
+ lines.push(` ${bp.name}: { "$ref": "#/components/schemas/${ta.name}" },`);
2088
+ } else if (ta) {
2089
+ let jsonType;
2090
+ switch (ta.name) {
2091
+ case 'Int': jsonType = '"integer"'; break;
2092
+ case 'Float': jsonType = '"number"'; break;
2093
+ case 'Bool': jsonType = '"boolean"'; break;
2094
+ default: jsonType = '"string"'; break;
2095
+ }
2096
+ lines.push(` ${bp.name}: { type: ${jsonType} },`);
2097
+ } else {
2098
+ lines.push(` ${bp.name}: { type: "string" },`);
2099
+ }
2100
+ }
2101
+ lines.push(' } } } },');
2102
+ lines.push(' },');
1947
2103
  }
1948
- lines.push(' } } } },');
1949
- lines.push(' },');
1950
2104
  }
1951
2105
  }
1952
2106
 
1953
- // Response schema from return type
1954
- if (handlerDecl && handlerDecl.returnType) {
1955
- const rt = handlerDecl.returnType;
2107
+ // Response schema prefer route-level responseType (T9-2), fall back to handler return type
2108
+ const responseType = route.responseType || (handlerDecl && handlerDecl.returnType);
2109
+ if (responseType) {
2110
+ const rt = responseType;
1956
2111
  if (rt.type === 'ArrayTypeAnnotation' && rt.elementType) {
1957
2112
  const elName = rt.elementType.name;
1958
2113
  if (sharedTypes.has(elName)) {
1959
2114
  lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { "$ref": "#/components/schemas/${elName}" } } } } } },`);
1960
2115
  } else {
1961
- lines.push(' responses: { "200": { description: "Success" } },');
2116
+ lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: ${tovaTypeToJsonSchema(elName)} } } } } },`);
1962
2117
  }
1963
2118
  } else if (rt.type === 'TypeAnnotation' && sharedTypes.has(rt.name)) {
1964
2119
  lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: { "$ref": "#/components/schemas/${rt.name}" } } } } },`);
2120
+ } else if (rt.type === 'TypeAnnotation') {
2121
+ lines.push(` responses: { "200": { description: "Success", content: { "application/json": { schema: ${tovaTypeToJsonSchema(rt.name)} } } } },`);
1965
2122
  } else {
1966
2123
  lines.push(' responses: { "200": { description: "Success" } },');
1967
2124
  }
@@ -1988,6 +2145,32 @@ export class ServerCodegen extends BaseCodegen {
1988
2145
  lines.push('');
1989
2146
  }
1990
2147
 
2148
+ // T9-5: API versions endpoint — list available versions
2149
+ const versionedRoutes = routes.filter(r => r._version);
2150
+ if (versionedRoutes.length > 0) {
2151
+ const versionMap = new Map();
2152
+ for (const r of versionedRoutes) {
2153
+ const v = String(r._version.version || '');
2154
+ if (!versionMap.has(v)) {
2155
+ versionMap.set(v, { version: v, deprecated: !!r._version.deprecated, sunset: r._version.sunset || null });
2156
+ }
2157
+ }
2158
+ lines.push('// ── API Versions ──');
2159
+ lines.push('__addRoute("GET", "/api/versions", async () => {');
2160
+ lines.push(' return Response.json({');
2161
+ lines.push(' versions: [');
2162
+ for (const [, info] of versionMap) {
2163
+ const parts = [`version: ${JSON.stringify(info.version)}`];
2164
+ if (info.deprecated) parts.push('deprecated: true');
2165
+ if (info.sunset) parts.push(`sunset: ${JSON.stringify(info.sunset)}`);
2166
+ lines.push(` { ${parts.join(', ')} },`);
2167
+ }
2168
+ lines.push(' ]');
2169
+ lines.push(' });');
2170
+ lines.push('});');
2171
+ lines.push('');
2172
+ }
2173
+
1991
2174
  // Include __contains helper if needed
1992
2175
  if (this._needsContainsHelper) {
1993
2176
  lines.push(this.getContainsHelper());
@@ -2521,7 +2704,31 @@ export class ServerCodegen extends BaseCodegen {
2521
2704
 
2522
2705
  for (const block of testBlocks) {
2523
2706
  const name = block.name || 'Tests';
2707
+ const blockTimeout = block.timeout || null;
2524
2708
  lines.push(`describe(${JSON.stringify(name)}, () => {`);
2709
+
2710
+ // Emit beforeEach if defined
2711
+ if (block.beforeEach && block.beforeEach.length > 0) {
2712
+ lines.push(' beforeEach(async () => {');
2713
+ this.pushScope();
2714
+ for (const s of block.beforeEach) {
2715
+ lines.push(' ' + this.generateStatement(s));
2716
+ }
2717
+ this.popScope();
2718
+ lines.push(' });');
2719
+ }
2720
+
2721
+ // Emit afterEach if defined
2722
+ if (block.afterEach && block.afterEach.length > 0) {
2723
+ lines.push(' afterEach(async () => {');
2724
+ this.pushScope();
2725
+ for (const s of block.afterEach) {
2726
+ lines.push(' ' + this.generateStatement(s));
2727
+ }
2728
+ this.popScope();
2729
+ lines.push(' });');
2730
+ }
2731
+
2525
2732
  for (const stmt of block.body) {
2526
2733
  if (stmt.type === 'FunctionDeclaration') {
2527
2734
  const fnName = stmt.name;
@@ -2533,9 +2740,10 @@ export class ServerCodegen extends BaseCodegen {
2533
2740
  }
2534
2741
  const body = this.genBlockBody(stmt.body);
2535
2742
  this.popScope();
2743
+ const timeoutArg = blockTimeout ? `, ${blockTimeout}` : '';
2536
2744
  lines.push(` test(${JSON.stringify(displayName)}, async () => {`);
2537
2745
  lines.push(body);
2538
- lines.push(' });');
2746
+ lines.push(` }${timeoutArg});`);
2539
2747
  } else {
2540
2748
  lines.push(' ' + this.generateStatement(stmt));
2541
2749
  }
@@ -2547,6 +2755,58 @@ export class ServerCodegen extends BaseCodegen {
2547
2755
  return lines.join('\n');
2548
2756
  }
2549
2757
 
2758
+ generateBench(benchBlocks) {
2759
+ const lines = [];
2760
+ lines.push('// ── Tova Benchmark Runner ──');
2761
+ lines.push('');
2762
+ lines.push('async function __runBench(name, fn, runs) {');
2763
+ lines.push(' runs = runs || 100;');
2764
+ lines.push(' // Warmup');
2765
+ lines.push(' for (let i = 0; i < Math.min(10, runs); i++) await fn();');
2766
+ lines.push(' const times = [];');
2767
+ lines.push(' for (let i = 0; i < runs; i++) {');
2768
+ lines.push(' const start = performance.now();');
2769
+ lines.push(' await fn();');
2770
+ lines.push(' times.push(performance.now() - start);');
2771
+ lines.push(' }');
2772
+ lines.push(' times.sort((a, b) => a - b);');
2773
+ lines.push(' const sum = times.reduce((a, b) => a + b, 0);');
2774
+ lines.push(' const mean = sum / times.length;');
2775
+ lines.push(' const p50 = times[Math.floor(times.length * 0.5)];');
2776
+ lines.push(' const p99 = times[Math.floor(times.length * 0.99)];');
2777
+ lines.push(' console.log(`bench ${JSON.stringify(name)}: mean=${mean.toFixed(2)}ms p50=${p50.toFixed(2)}ms p99=${p99.toFixed(2)}ms (${runs} runs)`);');
2778
+ lines.push('}');
2779
+ lines.push('');
2780
+ lines.push('(async () => {');
2781
+
2782
+ for (const block of benchBlocks) {
2783
+ const name = block.name || 'Benchmark';
2784
+ lines.push(` console.log("── ${name.replace(/"/g, '\\"')} ──");`);
2785
+ for (const stmt of block.body) {
2786
+ if (stmt.type === 'FunctionDeclaration') {
2787
+ const fnName = stmt.name;
2788
+ const displayName = fnName.replace(/_/g, ' ');
2789
+ this.pushScope();
2790
+ for (const p of (stmt.params || [])) {
2791
+ const pName = typeof p === 'string' ? p : (p.name || p.identifier);
2792
+ if (pName) this.declareVar(pName);
2793
+ }
2794
+ const body = this.genBlockBody(stmt.body);
2795
+ this.popScope();
2796
+ lines.push(` await __runBench(${JSON.stringify(displayName)}, async () => {`);
2797
+ lines.push(body);
2798
+ lines.push(' });');
2799
+ } else {
2800
+ lines.push(' ' + this.generateStatement(stmt));
2801
+ }
2802
+ }
2803
+ lines.push('');
2804
+ }
2805
+
2806
+ lines.push('})();');
2807
+ return lines.join('\n');
2808
+ }
2809
+
2550
2810
  _getAiRuntime() {
2551
2811
  return `// AI Client Runtime
2552
2812
  function __createAI(config) {
@@ -2672,7 +2932,6 @@ function __createAI(config) {
2672
2932
  classify(text, categories, opts) { return __aiRequest('classify', [text, categories, opts || {}], opts); },
2673
2933
  };
2674
2934
  }
2675
- // Default AI object for one-off calls (no config block required)
2676
- const ai = typeof ai === 'undefined' ? __createAI({}) : ai;`;
2935
+ `;
2677
2936
  }
2678
2937
  }