@kernlang/python 3.5.2

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 (63) hide show
  1. package/LICENSE +678 -0
  2. package/README.md +26 -0
  3. package/dist/codegen-body-python.d.ts +152 -0
  4. package/dist/codegen-body-python.js +1648 -0
  5. package/dist/codegen-body-python.js.map +1 -0
  6. package/dist/codegen-helpers.d.ts +21 -0
  7. package/dist/codegen-helpers.js +352 -0
  8. package/dist/codegen-helpers.js.map +1 -0
  9. package/dist/codegen-python.d.ts +17 -0
  10. package/dist/codegen-python.js +106 -0
  11. package/dist/codegen-python.js.map +1 -0
  12. package/dist/fastapi-middleware.d.ts +8 -0
  13. package/dist/fastapi-middleware.js +87 -0
  14. package/dist/fastapi-middleware.js.map +1 -0
  15. package/dist/fastapi-portable.d.ts +9 -0
  16. package/dist/fastapi-portable.js +295 -0
  17. package/dist/fastapi-portable.js.map +1 -0
  18. package/dist/fastapi-raw-handler.d.ts +28 -0
  19. package/dist/fastapi-raw-handler.js +282 -0
  20. package/dist/fastapi-raw-handler.js.map +1 -0
  21. package/dist/fastapi-response.d.ts +13 -0
  22. package/dist/fastapi-response.js +150 -0
  23. package/dist/fastapi-response.js.map +1 -0
  24. package/dist/fastapi-route.d.ts +12 -0
  25. package/dist/fastapi-route.js +629 -0
  26. package/dist/fastapi-route.js.map +1 -0
  27. package/dist/fastapi-types.d.ts +39 -0
  28. package/dist/fastapi-types.js +5 -0
  29. package/dist/fastapi-types.js.map +1 -0
  30. package/dist/fastapi-utils.d.ts +16 -0
  31. package/dist/fastapi-utils.js +99 -0
  32. package/dist/fastapi-utils.js.map +1 -0
  33. package/dist/fastapi-websocket.d.ts +6 -0
  34. package/dist/fastapi-websocket.js +77 -0
  35. package/dist/fastapi-websocket.js.map +1 -0
  36. package/dist/generators/core.d.ts +23 -0
  37. package/dist/generators/core.js +906 -0
  38. package/dist/generators/core.js.map +1 -0
  39. package/dist/generators/data.d.ts +15 -0
  40. package/dist/generators/data.js +443 -0
  41. package/dist/generators/data.js.map +1 -0
  42. package/dist/generators/ground.d.ts +20 -0
  43. package/dist/generators/ground.js +333 -0
  44. package/dist/generators/ground.js.map +1 -0
  45. package/dist/generators/infra.d.ts +8 -0
  46. package/dist/generators/infra.js +109 -0
  47. package/dist/generators/infra.js.map +1 -0
  48. package/dist/index.d.ts +6 -0
  49. package/dist/index.js +7 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/ir-semantics/python-leg.d.ts +45 -0
  52. package/dist/ir-semantics/python-leg.js +291 -0
  53. package/dist/ir-semantics/python-leg.js.map +1 -0
  54. package/dist/python-stdlib-preamble.d.ts +32 -0
  55. package/dist/python-stdlib-preamble.js +86 -0
  56. package/dist/python-stdlib-preamble.js.map +1 -0
  57. package/dist/transpiler-fastapi.d.ts +8 -0
  58. package/dist/transpiler-fastapi.js +593 -0
  59. package/dist/transpiler-fastapi.js.map +1 -0
  60. package/dist/type-map.d.ts +14 -0
  61. package/dist/type-map.js +288 -0
  62. package/dist/type-map.js.map +1 -0
  63. package/package.json +37 -0
@@ -0,0 +1,906 @@
1
+ /**
2
+ * Core generators — Python generation for KERN's base type system nodes:
3
+ * type, interface, fn, machine, error, config, store, test, event, import, const
4
+ */
5
+ import { emitIdentifier, handlerCode, shouldEmitImportForTarget } from '@kernlang/core';
6
+ import { emitNativeKernBodyPythonWithImports } from '../codegen-body-python.js';
7
+ import { buildPythonParamList, kids, p, parseLegacyParamParts } from '../codegen-helpers.js';
8
+ import { mapTsTypeToPython, toScreamingSnake, toSnakeCase } from '../type-map.js';
9
+ /** Slice 1 — native KERN handler bodies for Python target.
10
+ * Returns the emitted Python body when the fn's handler child opts in via
11
+ * `lang=kern`, otherwise returns the legacy raw body via `handlerCode`.
12
+ *
13
+ * Slice 3a — KERN bodies reference parameters in their original camelCase
14
+ * form (e.g., `userId`), but the Python signature snake-cases them
15
+ * (`user_id`). We build a `userId → user_id` map from the param list and
16
+ * hand it to the body emitter so identifier references resolve correctly.
17
+ *
18
+ * Slice 3b — the body emitter returns a per-handler set of required
19
+ * imports (`'math'` ⇒ `import math`); we inject them as the first lines
20
+ * of the function body before the user code. Inline-in-function imports
21
+ * are valid Python, idempotent (Python caches modules after first import),
22
+ * and avoid the cross-cutting refactor that module-level emission would
23
+ * require. */
24
+ function fnBodyCodePython(node) {
25
+ const handler = node.children?.find((c) => c.type === 'handler');
26
+ if (handler && handler.props?.lang === 'kern') {
27
+ const symbolMap = buildPythonSymbolMap(node);
28
+ const { code, imports, helpers } = emitNativeKernBodyPythonWithImports(handler, { symbolMap });
29
+ if (imports.size === 0 && helpers.size === 0)
30
+ return code;
31
+ // Stable ordering for deterministic output / test snapshots.
32
+ // Slice 3 review fix (Gemini): import-as-alias to avoid shadowing user
33
+ // bindings. KERN-stdlib templates reference the alias (`__k_math.floor`),
34
+ // so any user-defined `math` ident in the body remains accessible.
35
+ const importLines = [...imports].sort().map((mod) => `import ${mod} as __k_${mod}`);
36
+ // PR-4 — runtime helpers (e.g. `_kern_pairs`) follow the imports as
37
+ // function-local defs. They're idempotent and Python re-binds each call;
38
+ // module-level emission would require a cross-cutting refactor for the
39
+ // `fn` generator path, so we keep them inline alongside imports.
40
+ const helperBlocks = [...helpers];
41
+ const headerParts = [];
42
+ if (importLines.length > 0)
43
+ headerParts.push(importLines.join('\n'));
44
+ if (helperBlocks.length > 0)
45
+ headerParts.push(helperBlocks.join('\n\n'));
46
+ const header = headerParts.join('\n\n');
47
+ return code ? `${header}\n${code}` : header;
48
+ }
49
+ return handlerCode(node);
50
+ }
51
+ /** Slice 3a — collect KERN-form parameter names paired with their Python
52
+ * snake_case form. Mirrors the rename rules in `buildPythonParamList` (see
53
+ * packages/python/src/codegen-helpers.ts) so the body symbol-map and the
54
+ * Python signature stay in lockstep. Destructured params (children
55
+ * `binding`/`element`) are skipped — they have no single name to rename;
56
+ * their decomposed bindings are emitted in the body itself, not the
57
+ * signature, and remain a slice-4 follow-up.
58
+ *
59
+ * Returns an entry only when the snake-cased form differs from the KERN
60
+ * form, so the map stays a tight identity overlay (no work done at the
61
+ * ident-emit hot path for already-snake_case names like `id` or `count`). */
62
+ function buildPythonSymbolMap(node) {
63
+ const map = {};
64
+ // Slice 3 review fix (OpenCode + Gemini): detect when two distinct KERN
65
+ // params snake-case to the same Python name (e.g. `xCount` and `x_count`
66
+ // both → `x_count`) and throw with a descriptive error. Without this,
67
+ // Python emits a `def foo(x_count, x_count)` signature that fails at
68
+ // import time with `SyntaxError: duplicate argument`.
69
+ const usedSnake = new Set();
70
+ const claimSnake = (rawName, snake) => {
71
+ if (usedSnake.has(snake)) {
72
+ throw new Error(`KERN-Python codegen: parameter '${rawName}' snake-cases to '${snake}', which collides with another parameter on this function. ` +
73
+ 'Rename one of the parameters to disambiguate (KERN identifiers are case-sensitive; the Python target is not).');
74
+ }
75
+ usedSnake.add(snake);
76
+ };
77
+ const paramChildren = (node.children ?? []).filter((c) => c.type === 'param');
78
+ if (paramChildren.length > 0) {
79
+ for (const param of paramChildren) {
80
+ const hasDestructure = (param.children ?? []).some((c) => c.type === 'binding' || c.type === 'element');
81
+ if (hasDestructure)
82
+ continue;
83
+ const rawName = param.props?.name || '';
84
+ if (!rawName)
85
+ continue;
86
+ const snake = toSnakeCase(rawName);
87
+ claimSnake(rawName, snake);
88
+ if (snake !== rawName)
89
+ map[rawName] = snake;
90
+ }
91
+ return map;
92
+ }
93
+ const rawParams = node.props?.params || '';
94
+ if (!rawParams)
95
+ return map;
96
+ for (const part of parseLegacyParamParts(rawParams)) {
97
+ const rawName = part.name;
98
+ if (!rawName)
99
+ continue;
100
+ const snake = toSnakeCase(rawName);
101
+ claimSnake(rawName, snake);
102
+ if (snake !== rawName)
103
+ map[rawName] = snake;
104
+ }
105
+ return map;
106
+ }
107
+ // ── Type Alias ───────────────────────────────────────────────────────────
108
+ // type name=PlanState values="draft|approved|running"
109
+ // → PlanState = Literal["draft", "approved", "running"]
110
+ export function generateType(node) {
111
+ const { name, values, alias } = p(node);
112
+ if (values) {
113
+ const members = values
114
+ .split('|')
115
+ .map((v) => `"${v.trim()}"`)
116
+ .join(', ');
117
+ return [`${name} = Literal[${members}]`];
118
+ }
119
+ if (alias) {
120
+ return [`${name} = ${mapTsTypeToPython(alias)}`];
121
+ }
122
+ return [`${name} = Any`];
123
+ }
124
+ // ── Interface → Pydantic BaseModel ───────────────────────────────────────
125
+ // interface name=Track
126
+ // field name=id type=string
127
+ // field name=title type=string
128
+ // field name=duration type=number optional=true
129
+ // → class Track(BaseModel):
130
+ // id: str
131
+ // title: str
132
+ // duration: int | None = None
133
+ export function generateInterface(node) {
134
+ const props = p(node);
135
+ const name = emitIdentifier(props.name, 'Model', node);
136
+ const ext = props.extends ? emitIdentifier(props.extends, 'BaseModel', node) : 'BaseModel';
137
+ const lines = [];
138
+ lines.push(`class ${name}(${ext}):`);
139
+ const fields = kids(node, 'field');
140
+ if (fields.length === 0) {
141
+ lines.push(' pass');
142
+ return lines;
143
+ }
144
+ for (const field of fields) {
145
+ const fp = p(field);
146
+ const fname = toSnakeCase(fp.name);
147
+ const ftype = mapTsTypeToPython(fp.type);
148
+ const isOptional = fp.optional === 'true' || fp.optional === true;
149
+ if (isOptional) {
150
+ lines.push(` ${fname}: ${ftype} | None = None`);
151
+ }
152
+ else {
153
+ lines.push(` ${fname}: ${ftype}`);
154
+ }
155
+ }
156
+ return lines;
157
+ }
158
+ // ── Function ─────────────────────────────────────────────────────────────
159
+ // fn name=createTrack params="title:string,duration:number" returns=Track async=true
160
+ // → async def create_track(title: str, duration: int) -> Track:
161
+ // ...
162
+ export function generateFunction(node) {
163
+ const props = p(node);
164
+ const name = toSnakeCase(props.name);
165
+ const returns = props.returns;
166
+ const isAsync = props.async === 'true' || props.async === true;
167
+ const lines = [...emitPythonFunctionDecorators(node)];
168
+ // Slice 3c P2 follow-up: target-neutral helper reads structured `param`
169
+ // children when present, falls back to legacy `params="..."` otherwise.
170
+ const paramList = buildPythonParamList(node);
171
+ const retClause = returns ? ` -> ${mapTsTypeToPython(returns)}` : '';
172
+ const asyncKw = isAsync ? 'async ' : '';
173
+ const code = fnBodyCodePython(node);
174
+ lines.push(`${asyncKw}def ${name}(${paramList})${retClause}:`);
175
+ if (code) {
176
+ for (const line of code.split('\n')) {
177
+ lines.push(` ${line}`);
178
+ }
179
+ }
180
+ else {
181
+ lines.push(' pass');
182
+ }
183
+ return lines;
184
+ }
185
+ function emitPythonFunctionDecorators(node) {
186
+ const decorators = kids(node, 'decorator');
187
+ if (decorators.length === 0)
188
+ return [];
189
+ return decorators.map((decorator) => {
190
+ const props = p(decorator);
191
+ const rawName = String(props.name ?? '');
192
+ const hasArgs = props.args !== undefined;
193
+ const args = typeof props.args === 'string' ? props.args : '';
194
+ const routeMethod = /^http\.(get|post|put|patch|delete|head|options)$/u.exec(rawName);
195
+ if (routeMethod) {
196
+ return `@router.${routeMethod[1]}(${rewriteFastApiPathArgs(args)})`;
197
+ }
198
+ return hasArgs ? `@${rawName}(${args})` : `@${rawName}`;
199
+ });
200
+ }
201
+ function rewriteFastApiPathArgs(args) {
202
+ if (/^\s*["']/u.test(args))
203
+ return rewriteFirstPathString(args);
204
+ return args.replace(/(\bpath\s*=\s*)(["'])([^"']*)\2/u, (_match, prefix, quote, path) => {
205
+ const fastapiPath = path.replace(/:([A-Za-z_]\w*)/gu, '{$1}');
206
+ return `${prefix}${quote}${fastapiPath}${quote}`;
207
+ });
208
+ }
209
+ function rewriteFirstPathString(args) {
210
+ return args.replace(/(["'])([^"']*)\1/u, (_match, quote, path) => {
211
+ const fastapiPath = path.replace(/:([A-Za-z_]\w*)/gu, '{$1}');
212
+ return `${quote}${fastapiPath}${quote}`;
213
+ });
214
+ }
215
+ // ── Error Class ──────────────────────────────────────────────────────────
216
+ // error name=NotFoundError
217
+ // → class NotFoundError(Exception):
218
+ // def __init__(self, message: str):
219
+ // super().__init__(message)
220
+ export function generateError(node) {
221
+ const props = p(node);
222
+ const name = props.name;
223
+ const ext = props.extends || 'Exception';
224
+ const message = props.message;
225
+ const fields = kids(node, 'field');
226
+ const lines = [];
227
+ lines.push(`class ${name}(${ext}):`);
228
+ if (fields.length > 0) {
229
+ const paramParts = ['self'];
230
+ for (const field of fields) {
231
+ const fp = p(field);
232
+ const fname = toSnakeCase(fp.name);
233
+ const ftype = mapTsTypeToPython(fp.type);
234
+ paramParts.push(`${fname}: ${ftype}`);
235
+ }
236
+ lines.push(` def __init__(${paramParts.join(', ')}):`);
237
+ if (message) {
238
+ // Handle array fields that need formatting
239
+ const arrayFields = fields.filter((f) => {
240
+ const ft = p(f).type;
241
+ return ft.includes('[]') || ft.includes('string |') || ft.includes('| string');
242
+ });
243
+ for (const f of arrayFields) {
244
+ const fn = toSnakeCase(p(f).name);
245
+ lines.push(` ${fn}_str = " | ".join(${fn}) if isinstance(${fn}, list) else ${fn}`);
246
+ }
247
+ // Convert TS template literal ${var} to Python f-string {var}
248
+ const arrayFieldNames = new Set(arrayFields.map((f) => toSnakeCase(p(f).name)));
249
+ const pyMessage = message.replace(/\$\{(\w+)\}/g, (_, v) => {
250
+ const snaked = toSnakeCase(v);
251
+ return arrayFieldNames.has(snaked) ? `{${snaked}_str}` : `{${snaked}}`;
252
+ });
253
+ lines.push(` super().__init__(f"${pyMessage}")`);
254
+ }
255
+ else {
256
+ const hasMessageParam = p(fields[0]).name === 'message';
257
+ if (hasMessageParam) {
258
+ lines.push(' super().__init__(message)');
259
+ }
260
+ else {
261
+ lines.push(' super().__init__()');
262
+ }
263
+ }
264
+ // Store fields as attributes
265
+ for (const field of fields) {
266
+ const fp = p(field);
267
+ const fname = toSnakeCase(fp.name);
268
+ if (fname !== 'message') {
269
+ lines.push(` self.${fname} = ${fname}`);
270
+ }
271
+ }
272
+ }
273
+ else {
274
+ lines.push(' def __init__(self, message: str):');
275
+ lines.push(' super().__init__(message)');
276
+ }
277
+ return lines;
278
+ }
279
+ // ── State Machine ────────────────────────────────────────────────────────
280
+ // KERN's killer feature. Generates:
281
+ // - Enum class for states
282
+ // - Error class
283
+ // - Typed transition functions (snake_case)
284
+ export function generateMachine(node) {
285
+ const props = p(node);
286
+ const name = props.name;
287
+ const lines = [];
288
+ const states = kids(node, 'state');
289
+ const stateNames = states.map((s) => {
290
+ const sp = p(s);
291
+ return (sp.name || sp.value);
292
+ });
293
+ const stateType = `${name}State`;
294
+ const errorName = `${name}StateError`;
295
+ const snakeName = toSnakeCase(name);
296
+ // State enum
297
+ lines.push(`class ${stateType}(str, Enum):`);
298
+ for (const s of stateNames) {
299
+ lines.push(` ${s.toUpperCase()} = "${s}"`);
300
+ }
301
+ lines.push('');
302
+ // Error class
303
+ lines.push('');
304
+ lines.push(`class ${errorName}(Exception):`);
305
+ lines.push(` def __init__(self, expected: str | list[str], actual: str):`);
306
+ lines.push(` expected_str = " | ".join(expected) if isinstance(expected, list) else expected`);
307
+ lines.push(` super().__init__(f"Invalid ${snakeName} state: expected {expected_str}, got {actual}")`);
308
+ lines.push(` self.expected = expected`);
309
+ lines.push(` self.actual = actual`);
310
+ lines.push('');
311
+ // Transition functions
312
+ const transitions = kids(node, 'transition');
313
+ for (const t of transitions) {
314
+ const tp = p(t);
315
+ const tname = toSnakeCase(tp.name);
316
+ const from = tp.from;
317
+ const to = tp.to;
318
+ const fromStates = from.split('|').map((s) => s.trim());
319
+ const isMultiFrom = fromStates.length > 1;
320
+ const fnName = `${tname}_${snakeName}`;
321
+ const code = handlerCode(t);
322
+ lines.push('');
323
+ lines.push(`def ${fnName}(entity: dict) -> dict:`);
324
+ lines.push(` """${from} → ${to}"""`);
325
+ if (isMultiFrom) {
326
+ lines.push(` valid_states = [${fromStates.map((s) => `"${s}"`).join(', ')}]`);
327
+ lines.push(` if entity["state"] not in valid_states:`);
328
+ lines.push(` raise ${errorName}(valid_states, entity["state"])`);
329
+ }
330
+ else {
331
+ lines.push(` if entity["state"] != "${fromStates[0]}":`);
332
+ lines.push(` raise ${errorName}("${fromStates[0]}", entity["state"])`);
333
+ }
334
+ if (code) {
335
+ for (const line of code.split('\n')) {
336
+ lines.push(` ${line}`);
337
+ }
338
+ }
339
+ else {
340
+ lines.push(` return {**entity, "state": "${to}"}`);
341
+ }
342
+ }
343
+ return lines;
344
+ }
345
+ // ── Config → Pydantic BaseSettings ───────────────────────────────────────
346
+ // config name=AppConfig
347
+ // field name=timeout type=number default=120
348
+ // → class AppConfig(BaseSettings):
349
+ // timeout: int = 120
350
+ export function generateConfig(node) {
351
+ const props = p(node);
352
+ const name = props.name;
353
+ const fields = kids(node, 'field');
354
+ const lines = [];
355
+ lines.push(`class ${name}(BaseSettings):`);
356
+ if (fields.length === 0) {
357
+ lines.push(' pass');
358
+ return lines;
359
+ }
360
+ for (const field of fields) {
361
+ const fp = p(field);
362
+ const fname = toSnakeCase(fp.name);
363
+ const ftype = mapTsTypeToPython(fp.type);
364
+ const def = fp.default;
365
+ if (def !== undefined) {
366
+ // Determine if default needs quoting
367
+ const fOrigType = fp.type;
368
+ if (fOrigType === 'string') {
369
+ lines.push(` ${fname}: ${ftype} = "${def}"`);
370
+ }
371
+ else {
372
+ lines.push(` ${fname}: ${ftype} = ${def}`);
373
+ }
374
+ }
375
+ else {
376
+ lines.push(` ${fname}: ${ftype}`);
377
+ }
378
+ }
379
+ return lines;
380
+ }
381
+ // ── Store → pathlib CRUD ─────────────────────────────────────────────────
382
+ // store name=Plan path="~/.agon/plans" key=id
383
+ // → save_plan(), load_plan(), list_plans(), delete_plan()
384
+ export function generateStore(node) {
385
+ const props = p(node);
386
+ const name = props.name;
387
+ const storePath = props.path || '~/.data';
388
+ const key = toSnakeCase(props.key || 'id');
389
+ const _model = props.model || 'dict';
390
+ const lines = [];
391
+ const snakeName = toSnakeCase(name);
392
+ const dirConst = `${toScreamingSnake(name)}_DIR`;
393
+ const resolvedPath = storePath.startsWith('~/') ? `Path.home() / "${storePath.slice(2)}"` : `Path("${storePath}")`;
394
+ lines.push('import json');
395
+ lines.push('from pathlib import Path');
396
+ lines.push('');
397
+ lines.push(`${dirConst} = ${resolvedPath}`);
398
+ lines.push('');
399
+ lines.push('');
400
+ lines.push(`def _ensure_${snakeName}_dir() -> None:`);
401
+ lines.push(` ${dirConst}.mkdir(parents=True, exist_ok=True)`);
402
+ lines.push('');
403
+ lines.push('');
404
+ lines.push(`def _safe_${snakeName}_path(id: str) -> Path:`);
405
+ lines.push(` import re`);
406
+ lines.push(` sanitized = re.sub(r"[^a-zA-Z0-9_-]", "", id)`);
407
+ lines.push(` full = (${dirConst} / f"{sanitized}.json").resolve()`);
408
+ lines.push(` if not str(full).startswith(str(${dirConst}.resolve())):`);
409
+ lines.push(` raise ValueError(f"Invalid ID: {id}")`);
410
+ lines.push(` return full`);
411
+ lines.push('');
412
+ lines.push('');
413
+ lines.push(`def save_${snakeName}(item: dict) -> None:`);
414
+ lines.push(` _ensure_${snakeName}_dir()`);
415
+ lines.push(` path = _safe_${snakeName}_path(item["${key}"])`);
416
+ lines.push(` path.write_text(json.dumps(item, indent=2) + "\\n")`);
417
+ lines.push('');
418
+ lines.push('');
419
+ lines.push(`def load_${snakeName}(id: str) -> dict | None:`);
420
+ lines.push(` try:`);
421
+ lines.push(` return json.loads(_safe_${snakeName}_path(id).read_text())`);
422
+ lines.push(` except (FileNotFoundError, json.JSONDecodeError):`);
423
+ lines.push(` return None`);
424
+ lines.push('');
425
+ lines.push('');
426
+ lines.push(`def list_${snakeName}s(limit: int = 20) -> list[dict]:`);
427
+ lines.push(` _ensure_${snakeName}_dir()`);
428
+ lines.push(` try:`);
429
+ lines.push(` items = [`);
430
+ lines.push(` json.loads(f.read_text())`);
431
+ lines.push(` for f in ${dirConst}.glob("*.json")`);
432
+ lines.push(` ]`);
433
+ lines.push(` items.sort(key=lambda x: x.get("updated_at", ""), reverse=True)`);
434
+ lines.push(` return items[:limit]`);
435
+ lines.push(` except Exception:`);
436
+ lines.push(` return []`);
437
+ lines.push('');
438
+ lines.push('');
439
+ lines.push(`def delete_${snakeName}(id: str) -> bool:`);
440
+ lines.push(` try:`);
441
+ lines.push(` _safe_${snakeName}_path(id).unlink()`);
442
+ lines.push(` return True`);
443
+ lines.push(` except FileNotFoundError:`);
444
+ lines.push(` return False`);
445
+ return lines;
446
+ }
447
+ // ── Test → pytest ────────────────────────────────────────────────────────
448
+ // test name="Plan Transitions"
449
+ // describe name=approve_plan
450
+ // it name="transitions draft → approved"
451
+ // handler <<<
452
+ // assert approve_plan(make_plan("draft"))["state"] == "approved"
453
+ // >>>
454
+ export function generateTest(node) {
455
+ const props = p(node);
456
+ const name = props.name;
457
+ const className = name.replace(/[^a-zA-Z0-9]/g, '');
458
+ const lines = [];
459
+ lines.push('import pytest');
460
+ lines.push('');
461
+ // Top-level setup handler
462
+ const setup = handlerCode(node);
463
+ if (setup) {
464
+ for (const line of setup.split('\n'))
465
+ lines.push(line);
466
+ lines.push('');
467
+ }
468
+ lines.push(`class Test${className}:`);
469
+ for (const desc of kids(node, 'describe')) {
470
+ const dname = p(desc).name;
471
+ const dclass = dname.replace(/[^a-zA-Z0-9]/g, '');
472
+ lines.push(` class Test${dclass}:`);
473
+ for (const test of kids(desc, 'it')) {
474
+ const tname = p(test).name;
475
+ const fname = toSnakeCase(tname
476
+ .replace(/[^a-zA-Z0-9\s]/g, '')
477
+ .trim()
478
+ .replace(/\s+/g, '_'));
479
+ const code = handlerCode(test);
480
+ lines.push(` def test_${fname}(self):`);
481
+ if (code) {
482
+ for (const line of code.split('\n'))
483
+ lines.push(` ${line}`);
484
+ }
485
+ else {
486
+ lines.push(' pass');
487
+ }
488
+ lines.push('');
489
+ }
490
+ }
491
+ // Top-level it blocks
492
+ for (const test of kids(node, 'it')) {
493
+ const tname = p(test).name;
494
+ const fname = toSnakeCase(tname
495
+ .replace(/[^a-zA-Z0-9\s]/g, '')
496
+ .trim()
497
+ .replace(/\s+/g, '_'));
498
+ const code = handlerCode(test);
499
+ lines.push(` def test_${fname}(self):`);
500
+ if (code) {
501
+ for (const line of code.split('\n'))
502
+ lines.push(` ${line}`);
503
+ }
504
+ else {
505
+ lines.push(' pass');
506
+ }
507
+ lines.push('');
508
+ }
509
+ return lines;
510
+ }
511
+ // ── Event → Literal + TypedDict ──────────────────────────────────────────
512
+ // event name=TrackEvent
513
+ // type name="track:created" data="{ title: string }"
514
+ // → TrackEventType = Literal["track:created", ...]
515
+ // → class TrackCreatedData(TypedDict): ...
516
+ export function generateEvent(node) {
517
+ const props = p(node);
518
+ const name = props.name;
519
+ const types = kids(node, 'type');
520
+ const lines = [];
521
+ // Event type union
522
+ lines.push(`${name}Type = Literal[${types.map((t) => `"${(p(t).name || p(t).value)}"`).join(', ')}]`);
523
+ lines.push('');
524
+ // Event TypedDict
525
+ lines.push(`class ${name}(TypedDict):`);
526
+ lines.push(` type: ${name}Type`);
527
+ lines.push(` engine_id: str`);
528
+ lines.push(` data: dict[str, Any]`);
529
+ lines.push('');
530
+ // Event data classes
531
+ for (const t of types) {
532
+ const tp = p(t);
533
+ const tname = (tp.name || tp.value);
534
+ const data = tp.data;
535
+ if (data) {
536
+ const className = `${tname
537
+ .split(':')
538
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
539
+ .join('')}Data`;
540
+ lines.push(`class ${className}(TypedDict):`);
541
+ // Parse simple {key: type} format
542
+ const inner = data.replace(/^\{|\}$/g, '').trim();
543
+ if (inner) {
544
+ for (const pair of inner.split(',')) {
545
+ const [k, ...vparts] = pair.split(':');
546
+ if (k && vparts.length > 0) {
547
+ lines.push(` ${toSnakeCase(k.trim())}: ${mapTsTypeToPython(vparts.join(':').trim())}`);
548
+ }
549
+ }
550
+ }
551
+ else {
552
+ lines.push(' pass');
553
+ }
554
+ lines.push('');
555
+ }
556
+ }
557
+ return lines;
558
+ }
559
+ // ── Import / Use ─────────────────────────────────────────────────────────
560
+ // import from="pathlib" names="Path"
561
+ // → from pathlib import Path
562
+ //
563
+ // use path="./helper.kern"
564
+ // from name=parseUser
565
+ // → from .helper import parseUser
566
+ function emitPythonImportIdent(value, fallback, node) {
567
+ return emitIdentifier(value, fallback, node);
568
+ }
569
+ function pythonModuleSpecifier(raw, node, options) {
570
+ if (!raw)
571
+ throw new Error('Python import specifier cannot be empty');
572
+ if (raw.includes("'") || raw.includes('"') || raw.includes('`') || raw.includes('\\')) {
573
+ throw new Error(`Invalid Python import specifier '${raw.slice(0, 80)}' — contains quote or escape characters`);
574
+ }
575
+ if (raw.includes(';') || raw.includes('$') || raw.includes('\n')) {
576
+ throw new Error(`Invalid Python import specifier '${raw.slice(0, 80)}' — contains unsafe characters`);
577
+ }
578
+ if (raw.startsWith('/')) {
579
+ throw new Error(`Invalid Python import specifier '${raw.slice(0, 80)}' — absolute paths are not importable`);
580
+ }
581
+ const resolved = options?.resolveKernModuleSpec?.(raw, node);
582
+ if (resolved) {
583
+ const moduleParts = resolved.replace(/^\.+/u, '').split('.').filter(Boolean);
584
+ for (const part of moduleParts) {
585
+ emitPythonImportIdent(part, 'module', node);
586
+ }
587
+ return resolved;
588
+ }
589
+ const withoutExt = raw.replace(/\.(kern|py|js|ts)$/u, '');
590
+ const pathParts = withoutExt.split('/').filter((part) => part.length > 0 && part !== '.');
591
+ const relativePrefix = pathParts.filter((part) => part === '..').length;
592
+ const moduleParts = pathParts
593
+ .filter((part) => part !== '..')
594
+ .flatMap((part) => part.split('.').filter(Boolean))
595
+ .map((part) => part.replace(/-/gu, '_'));
596
+ for (const part of moduleParts) {
597
+ emitPythonImportIdent(part, 'module', node);
598
+ }
599
+ if (raw.startsWith('./') || raw.startsWith('../')) {
600
+ return `${'.'.repeat(relativePrefix + 1)}${moduleParts.join('.')}`;
601
+ }
602
+ return moduleParts.join('.');
603
+ }
604
+ function emitPythonImportBinding(child) {
605
+ const cp = p(child);
606
+ const sourceName = emitPythonImportIdent(cp.name, 'imported', child);
607
+ const targetName = pythonTargetName(cp.targetNames, cp.name);
608
+ const emittedName = targetName
609
+ ? emitPythonImportIdent(targetName, 'imported', child)
610
+ : pythonExportedSymbolName(sourceName, cp.kind);
611
+ const localName = cp.as ? emitPythonImportIdent(cp.as, 'alias', child) : sourceName;
612
+ return emittedName === localName ? emittedName : `${emittedName} as ${localName}`;
613
+ }
614
+ function pythonExportedSymbolName(name, kind) {
615
+ switch ((kind ?? '').toLowerCase()) {
616
+ case 'fn':
617
+ case 'function':
618
+ case 'derive':
619
+ case 'transform':
620
+ case 'action':
621
+ case 'expect':
622
+ case 'dependency':
623
+ return toSnakeCase(name);
624
+ default:
625
+ return name;
626
+ }
627
+ }
628
+ function exportSymbolKinds(raw) {
629
+ const pairs = new Map();
630
+ if (typeof raw !== 'string')
631
+ return pairs;
632
+ for (const item of raw.split(',')) {
633
+ const [name, kind] = item.split(':').map((part) => part.trim());
634
+ if (name && kind)
635
+ pairs.set(name, kind);
636
+ }
637
+ return pairs;
638
+ }
639
+ function targetNamePairs(raw) {
640
+ const pairs = new Map();
641
+ if (typeof raw !== 'string')
642
+ return pairs;
643
+ for (const item of raw.split(',')) {
644
+ const [name, target, value] = item.split(':').map((part) => part.trim());
645
+ if (!name || !target || !value)
646
+ continue;
647
+ const targetNames = pairs.get(name) ?? {};
648
+ targetNames[target] = value;
649
+ pairs.set(name, targetNames);
650
+ }
651
+ return pairs;
652
+ }
653
+ function pythonTargetName(raw, symbolName) {
654
+ if (!symbolName)
655
+ return undefined;
656
+ return targetNamePairs(raw).get(symbolName)?.python;
657
+ }
658
+ function parsePythonExportBinding(raw) {
659
+ const match = raw.trim().match(/^([A-Za-z_]\w*)(?:\s+as\s+([A-Za-z_]\w*))?$/u);
660
+ if (!match)
661
+ return null;
662
+ return { source: match[1], alias: match[2] };
663
+ }
664
+ function emitPythonExportedName(raw, symbolKinds, symbolTargets, node) {
665
+ const binding = parsePythonExportBinding(raw);
666
+ if (!binding) {
667
+ const safeName = emitPythonImportIdent(raw, 'export', node);
668
+ return (symbolTargets.get(raw)?.python ??
669
+ pythonExportedSymbolName(safeName, symbolKinds.get(raw) ?? symbolKinds.get(safeName)));
670
+ }
671
+ const safeSource = emitPythonImportIdent(binding.source, 'export', node);
672
+ const emittedSource = symbolTargets.get(binding.source)?.python ??
673
+ pythonExportedSymbolName(safeSource, symbolKinds.get(binding.source) ?? symbolKinds.get(safeSource));
674
+ if (!binding.alias)
675
+ return emittedSource;
676
+ const safeAlias = emitPythonImportIdent(binding.alias, 'export alias', node);
677
+ return `${emittedSource} as ${safeAlias}`;
678
+ }
679
+ function emitPythonLocalExport(raw, symbolKinds, symbolTargets, node) {
680
+ const binding = parsePythonExportBinding(raw);
681
+ if (!binding) {
682
+ const safeName = emitPythonImportIdent(raw, 'export', node);
683
+ return {
684
+ publicName: symbolTargets.get(raw)?.python ??
685
+ pythonExportedSymbolName(safeName, symbolKinds.get(raw) ?? symbolKinds.get(safeName)),
686
+ };
687
+ }
688
+ const safeSource = emitPythonImportIdent(binding.source, 'export', node);
689
+ const emittedSource = symbolTargets.get(binding.source)?.python ??
690
+ pythonExportedSymbolName(safeSource, symbolKinds.get(binding.source) ?? symbolKinds.get(safeSource));
691
+ if (!binding.alias)
692
+ return { publicName: emittedSource };
693
+ const safeAlias = emitPythonImportIdent(binding.alias, 'export alias', node);
694
+ return { publicName: safeAlias, aliasLine: `${safeAlias} = ${emittedSource}` };
695
+ }
696
+ function emitPythonModuleImport(moduleSpec, alias) {
697
+ if (!moduleSpec.startsWith('.')) {
698
+ return alias ? [`import ${moduleSpec} as ${alias}`] : [`import ${moduleSpec}`];
699
+ }
700
+ const match = moduleSpec.match(/^(\.+)(.*)$/u);
701
+ const relativePrefix = match?.[1] ?? '.';
702
+ const rest = match?.[2] ?? '';
703
+ const parts = rest.split('.').filter(Boolean);
704
+ const moduleName = parts.pop();
705
+ const packageSpec = parts.length > 0 ? `${relativePrefix}${parts.join('.')}` : relativePrefix;
706
+ if (!moduleName)
707
+ return [];
708
+ return alias
709
+ ? [`from ${packageSpec} import ${moduleName} as ${alias}`]
710
+ : [`from ${packageSpec} import ${moduleName}`];
711
+ }
712
+ export function generateImport(node) {
713
+ const props = p(node);
714
+ const rawFrom = props.from;
715
+ const names = props.names;
716
+ const legacyName = props.name;
717
+ const defaultImport = props.default === true || props.default === 'true'
718
+ ? legacyName
719
+ : typeof props.default === 'string'
720
+ ? props.default
721
+ : undefined;
722
+ if (!rawFrom)
723
+ return [];
724
+ if (!shouldEmitImportForTarget(props, 'python'))
725
+ return [];
726
+ const from = pythonModuleSpecifier(rawFrom, node);
727
+ if (!from)
728
+ return [];
729
+ if (defaultImport && names) {
730
+ const safeDefault = emitPythonImportIdent(defaultImport, 'default', node);
731
+ const safeNames = names
732
+ .split(',')
733
+ .map((s) => emitPythonImportIdent(s.trim(), 'import', node))
734
+ .join(', ');
735
+ return [...emitPythonModuleImport(from, safeDefault), `from ${from} import ${safeNames}`];
736
+ }
737
+ if (defaultImport) {
738
+ const safeDefault = emitPythonImportIdent(defaultImport, 'default', node);
739
+ return emitPythonModuleImport(from, safeDefault);
740
+ }
741
+ if (names) {
742
+ const safeNames = names
743
+ .split(',')
744
+ .map((s) => emitPythonImportIdent(s.trim(), 'import', node))
745
+ .join(', ');
746
+ return [`from ${from} import ${safeNames}`];
747
+ }
748
+ return emitPythonModuleImport(from);
749
+ }
750
+ function externChildImport(node, child) {
751
+ const props = p(node);
752
+ const packageName = props.package;
753
+ const childProps = child?.props ?? {};
754
+ return {
755
+ type: 'import',
756
+ props: {
757
+ ...(child
758
+ ? childProps
759
+ : {
760
+ names: props.names,
761
+ default: props.default,
762
+ types: props.types,
763
+ }),
764
+ registry: props.registry,
765
+ target: props.target,
766
+ package: packageName,
767
+ from: childProps.from ?? packageName,
768
+ },
769
+ children: child?.children ?? [],
770
+ loc: child?.loc ?? node.loc,
771
+ };
772
+ }
773
+ export function generateExtern(node) {
774
+ const children = kids(node, 'import');
775
+ const props = p(node);
776
+ const hasInlineBinding = Boolean(props.names || props.default);
777
+ const inlineImport = hasInlineBinding ? generateImport(externChildImport(node)) : [];
778
+ const childImports = children.flatMap((child) => generateImport(externChildImport(node, child)));
779
+ return [...new Set([...inlineImport, ...childImports])];
780
+ }
781
+ export function generateUse(node, options) {
782
+ const props = p(node);
783
+ const rawPath = props.path;
784
+ if (!rawPath)
785
+ return [];
786
+ const moduleSpec = pythonModuleSpecifier(rawPath, node, options);
787
+ const fromChildren = kids(node, 'from');
788
+ if (fromChildren.length > 0) {
789
+ const bindings = fromChildren.map(emitPythonImportBinding);
790
+ return bindings.length > 0 ? [`from ${moduleSpec} import ${bindings.join(', ')}`] : [];
791
+ }
792
+ return emitPythonModuleImport(moduleSpec);
793
+ }
794
+ export function generateModule(node, dispatch, options) {
795
+ const props = p(node);
796
+ const lines = [`# -- Module: ${props.name || 'unknown'} --`, ''];
797
+ const localSymbolKinds = new Map();
798
+ const publicExports = [];
799
+ const lateAliasLines = [];
800
+ for (const child of node.children ?? []) {
801
+ const cp = p(child);
802
+ const name = cp.name;
803
+ if (typeof name !== 'string')
804
+ continue;
805
+ if (child.type === 'fn')
806
+ localSymbolKinds.set(name, 'fn');
807
+ else if ([
808
+ 'type',
809
+ 'interface',
810
+ 'union',
811
+ 'enum',
812
+ 'class',
813
+ 'service',
814
+ 'model',
815
+ 'repository',
816
+ 'derive',
817
+ 'transform',
818
+ 'action',
819
+ 'expect',
820
+ 'dependency',
821
+ ].includes(child.type)) {
822
+ localSymbolKinds.set(name, child.type);
823
+ }
824
+ }
825
+ for (const child of node.children ?? []) {
826
+ if (child.type === 'export') {
827
+ const ep = p(child);
828
+ const from = ep.from ? pythonModuleSpecifier(ep.from, child, options) : '';
829
+ const symbolKinds = new Map([...localSymbolKinds, ...exportSymbolKinds(ep.symbolKinds)]);
830
+ const symbolTargets = targetNamePairs(ep.targetNames);
831
+ const rawNames = ep.names
832
+ ?.split(',')
833
+ .map((s) => s.trim())
834
+ .filter(Boolean);
835
+ const rawTypeNames = ep.types
836
+ ?.split(',')
837
+ .map((s) => s.trim())
838
+ .filter(Boolean);
839
+ const nameList = from
840
+ ? rawNames?.map((s) => emitPythonExportedName(s, symbolKinds, symbolTargets, child)).filter(Boolean)
841
+ : undefined;
842
+ const typeNameList = from
843
+ ? rawTypeNames?.map((s) => emitPythonExportedName(s, symbolKinds, symbolTargets, child)).filter(Boolean)
844
+ : undefined;
845
+ const names = nameList?.join(', ');
846
+ const typeNames = typeNameList?.join(', ');
847
+ const star = ep.star === true || ep.star === 'true';
848
+ const defaultName = ep.default ? emitPythonImportIdent(ep.default, 'default', child) : '';
849
+ if (from && star)
850
+ lines.push(`from ${from} import *`);
851
+ if (from && names)
852
+ lines.push(`from ${from} import ${names}`);
853
+ if (from && typeNames)
854
+ lines.push(`from ${from} import ${typeNames}`);
855
+ if (from) {
856
+ for (const rawExport of [...(rawNames ?? []), ...(rawTypeNames ?? [])]) {
857
+ publicExports.push(emitPythonLocalExport(rawExport, symbolKinds, symbolTargets, child).publicName);
858
+ }
859
+ }
860
+ if (!from) {
861
+ for (const rawExport of [...(rawNames ?? []), ...(rawTypeNames ?? [])]) {
862
+ const localExport = emitPythonLocalExport(rawExport, symbolKinds, symbolTargets, child);
863
+ if (localExport.aliasLine)
864
+ lateAliasLines.push(localExport.aliasLine);
865
+ publicExports.push(localExport.publicName);
866
+ }
867
+ }
868
+ if (from && defaultName) {
869
+ // Python modules do not have a JS-style `default` export symbol.
870
+ lines.push(`# KERN TODO: default re-export '${defaultName}' from ${from} is not representable in Python`);
871
+ }
872
+ continue;
873
+ }
874
+ const childLines = dispatch(child);
875
+ if (childLines.length > 0) {
876
+ lines.push(...childLines);
877
+ lines.push('');
878
+ }
879
+ }
880
+ if (lateAliasLines.length > 0) {
881
+ lines.push(...lateAliasLines);
882
+ }
883
+ if (publicExports.length > 0) {
884
+ lines.push(`__all__ = [${[...new Set(publicExports)].map((name) => JSON.stringify(name)).join(', ')}]`);
885
+ }
886
+ return lines;
887
+ }
888
+ // ── Const ────────────────────────────────────────────────────────────────
889
+ // const name=MAX_RETRIES type=number value=3
890
+ // → MAX_RETRIES: int = 3
891
+ export function generateConst(node) {
892
+ const props = p(node);
893
+ const name = props.name;
894
+ const constType = props.type;
895
+ const value = props.value;
896
+ const code = handlerCode(node);
897
+ const typeAnnotation = constType ? `: ${mapTsTypeToPython(constType)}` : '';
898
+ if (code) {
899
+ return [`${name}${typeAnnotation} = ${code.trim()}`];
900
+ }
901
+ if (value) {
902
+ return [`${name}${typeAnnotation} = ${value}`];
903
+ }
904
+ return [`${name}${typeAnnotation} = None`];
905
+ }
906
+ //# sourceMappingURL=core.js.map