Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
be94b7e
Feat/portable logic foundation (#386)
cukas Jun 6, 2026
4d00fa5
build(deps): bump the minor-and-patch group with 5 updates (#388)
dependabot[bot] Jun 7, 2026
a5c1bf1
Feat/portable logic foundation (#389)
cukas Jun 7, 2026
e5cbb34
Fix guard typecheck findings
cukas Jun 7, 2026
270d442
Add KERN core runtime foundation
cukas Jun 7, 2026
4a8107a
feat(core): add kern object runtime foundation
cukas Jun 7, 2026
eb09948
Merge branch 'main' into feat/kern-core-runtime
cukas Jun 7, 2026
2011262
feat(core): add semantic substrate for review
cukas Jun 7, 2026
7f75f40
fix(core): harden semantic substrate typing
cukas Jun 7, 2026
b1d7cdc
fix(core): avoid unknown stdlib entries
cukas Jun 7, 2026
2307eac
feat(core): add class object semantic validation
cukas Jun 8, 2026
2d6a94e
feat(core): harden class runtime setters
cukas Jun 8, 2026
3cf2413
fix(core): resolve runtime merge and guard value ir fallback
cukas Jun 8, 2026
cdcb60a
feat(core): add static class member runtime
cukas Jun 8, 2026
82c4581
feat(core): enforce constructor super discipline
cukas Jun 8, 2026
274396b
feat(core): enforce declared class shapes
cukas Jun 8, 2026
4decdf4
feat(core): expose class semantic facts
cukas Jun 8, 2026
6ea95d1
test(core): expand class object conformance
cukas Jun 8, 2026
eebf294
test(core): add object protocol negative conformance
cukas Jun 8, 2026
732a8f9
feat(core): add rag language contracts
cukas Jun 8, 2026
27eff2e
feat(core): bind mcp tools to rag contracts
cukas Jun 8, 2026
335225a
feat(core): add mcp resource rag ingress
cukas Jun 8, 2026
bc6240f
Merge remote-tracking branch 'origin/main' into feat/kern-core-runtime
cukas Jun 8, 2026
177c695
feat(core): add typed rag retrieval outputs
cukas Jun 8, 2026
922b18b
feat(core): add rag eval contract cases
cukas Jun 8, 2026
99422a5
feat(core): harden rag eval contracts
cukas Jun 8, 2026
0d7b5f3
feat(core): add in-memory rag runtime
cukas Jun 8, 2026
970bb84
feat(core): evaluate rag runtime contracts
cukas Jun 8, 2026
479a88c
feat(core): add rag runtime provenance
cukas Jun 8, 2026
c9e1737
feat(core): add rag answer contracts
cukas Jun 8, 2026
398bcac
feat(core): add rag answer contract surface
cukas Jun 8, 2026
3aa8813
test(core): add rag contract conformance fixtures
cukas Jun 8, 2026
7d287b7
feat(core): add declared shape validators
cukas Jun 9, 2026
2a2059a
feat(core): harden declared shape validators
cukas Jun 9, 2026
36db5f0
feat(core): expose effective class member facts
cukas Jun 9, 2026
fc06188
feat(core): add class implements conformance facts
cukas Jun 9, 2026
1891221
feat(core): add constructor discipline facts
cukas Jun 9, 2026
fb1febf
feat(core): enforce class implements at runtime
cukas Jun 9, 2026
4d85443
feat(core): enforce interface method protocols
cukas Jun 9, 2026
f02dd28
feat(core): enforce static interface member protocols
cukas Jun 9, 2026
863f9b8
fix(core): pin core contract element type to satisfy strict tsc
cukas Jun 9, 2026
04a1e06
fix(core): clear kern-guard findings in class semantic validator
cukas Jun 9, 2026
750fd93
feat(python): lower KERN classes to pure Python
cukas Jun 9, 2026
377a8ee
test(python): lock single-source class codegen
cukas Jun 9, 2026
a2459ec
feat(python): per-instance field defaults and static field values
cukas Jun 9, 2026
c0e7fe6
test(python): cover field defaults and super-ordering
cukas Jun 9, 2026
fcd4b7b
test(conformance): CI-enforce class TS<->Python parity
cukas Jun 9, 2026
399777a
Merge branch 'main' into feat/kern-core-runtime
cukas Jun 9, 2026
bba9db8
feat(python): static accessors via per-class metaclass (with chaining)
cukas Jun 9, 2026
9f8615d
test(python): static-accessor metaclass + inheritance conformance
cukas Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"type": "module",
"packageManager": "pnpm@10.32.1",
"description": "KERN backend structure and portable route logic for TypeScript/Express and Python/FastAPI parity.",
"description": "KERN \u2014 backend structure and portable route logic for TypeScript/Express and Python/FastAPI parity.",
"author": "cukas",
"repository": {
"type": "git",
Expand All @@ -18,7 +18,7 @@
"test:non-semantics": "pnpm -r --filter '!kern-monorepo' --filter '!@kernlang/review-python' test --testPathIgnorePatterns=ir-semantics && pnpm test:prepush && pnpm check:rule-coverage",
"check:rule-coverage": "node ./scripts/check-rule-coverage.mjs",
"check:python-codegen": "pnpm --filter @kernlang/core --filter @kernlang/python build && node ./scripts/lift-rate-python.mjs --check",
"check:conformance": "pnpm --filter @kernlang/core --filter @kernlang/python --filter @kernlang/express build && node ./scripts/conformance.mjs",
"check:conformance": "pnpm --filter @kernlang/core --filter @kernlang/python --filter @kernlang/express build && node ./scripts/conformance.mjs && node ./scripts/class-conformance.mjs",
"docs:contracts": "pnpm --filter @kernlang/core build && node ./scripts/generate-ir-semantics-docs.mjs --format=markdown --out=-",
"docs:contracts:json": "pnpm --filter @kernlang/core build && node ./scripts/generate-ir-semantics-docs.mjs --format=json --out=generated/contracts/registry.json",
"docs:contracts:check": "pnpm --filter @kernlang/core build && node ./scripts/check-contract-docs.mjs",
Expand All @@ -32,7 +32,8 @@
"lint:fix": "biome check --fix",
"format": "biome format --write",
"prepush": "node ./scripts/pre-push.mjs",
"prepare": "node ./scripts/install-git-hooks.mjs"
"prepare": "node ./scripts/install-git-hooks.mjs",
"check:class-conformance": "pnpm --filter @kernlang/core --filter @kernlang/python build && node ./scripts/class-conformance.mjs"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
18 changes: 18 additions & 0 deletions packages/python/src/codegen-body-python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ export interface BodyEmitOptions {
* the KERN-form `userId` resolves to the snake_cased Python parameter.
* Identifiers not in the map pass through unchanged. */
symbolMap?: Record<string, string>;
/** When true, the handler is a class member body: identifier `super`
* lowers to Python `super()` (so `super.m()` -> `super().m()`) and a
* direct `super(...)` call lowers to `super().__init__(...)`. Paired with
* a `symbolMap` entry `this -> self` by the class generator. */
inClassBody?: boolean;
/** When true, the handler is specifically a constructor body, so a direct
* `super(...)` call lowers to `super().__init__(...)`. Outside a constructor
* `super(...)` is not a parent-constructor call and is left untouched. */
inConstructor?: boolean;
/** Slice 4a review fix (Gemini #5) — how to lower the `?` propagation
* hoist's err-branch return:
* - 'value' (default for `fn`): `return __k_tN` so the caller sees
Expand Down Expand Up @@ -137,6 +146,8 @@ interface BodyEmitContext {
* `each` pair-mode). Consumer emits each entry at module scope. */
helpers: Set<string>;
symbolMap: Record<string, string>;
inClassBody: boolean;
inConstructor: boolean;
shadowedSymbols: Set<string>;
localScopes: Array<Map<string, 'const' | 'let' | 'cell'>>;
regexScopes: Array<Map<string, Extract<ValueIR, { kind: 'regexLit' }> | null>>;
Expand Down Expand Up @@ -171,6 +182,8 @@ function freshCtx(options?: BodyEmitOptions): BodyEmitContext {
imports: new Set<string>(),
helpers: new Set<string>(),
symbolMap: options?.symbolMap ?? {},
inClassBody: options?.inClassBody ?? false,
inConstructor: options?.inConstructor ?? false,
shadowedSymbols: new Set<string>(),
localScopes: [],
regexScopes: [],
Expand Down Expand Up @@ -1695,6 +1708,7 @@ function emitPyExprCtx(node: ValueIR, ctx: BodyEmitContext): string {
// Python-form `user_id`. Identifiers not in the map (locals, globals,
// module names) pass through unchanged.
if (ctx.shadowedSymbols.has(node.name)) return node.name;
if (ctx.inClassBody && node.name === 'super') return 'super()';
return ctx.symbolMap[node.name] ?? node.name;
}
case 'member':
Expand Down Expand Up @@ -2101,6 +2115,10 @@ function lowerChain(node: ChainNode, ctx: BodyEmitContext): GuardedExpr {
if (regex !== null) return { guard: null, expr: regex };
const stdlib = applyStdlibLoweringPython(node, ctx);
if (stdlib !== null) return { guard: null, expr: stdlib };
if (ctx.inConstructor && node.callee.kind === 'ident' && node.callee.name === 'super') {
const superArgs = node.args.map((arg) => emitPyExprCtx(arg, ctx)).join(', ');
return { guard: null, expr: `super().__init__(${superArgs})` };
}
if (node.callee.kind === 'ident' && node.callee.name === 'String') {
if (node.args.length !== 1) {
throw new Error('String() portable coercion expects exactly one argument on Python target.');
Expand Down
3 changes: 3 additions & 0 deletions packages/python/src/codegen-python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
// Data layer generators (model, repository, cache, dependency, service, union)
import {
generatePythonCache,
generatePythonClass,
generatePythonDependency,
generatePythonModel,
generatePythonRepository,
Expand Down Expand Up @@ -180,6 +181,8 @@ export function generatePythonCoreNode(node: IRNode, options: PythonCodegenOptio
return generatePythonDependency(node);
case 'service':
return generatePythonService(node);
case 'class':
return generatePythonClass(node);
case 'union':
return generatePythonUnion(node);
// Backend infrastructure
Expand Down
213 changes: 209 additions & 4 deletions packages/python/src/generators/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import { mapTsTypeToPython, toSnakeCase } from '../type-map.js';
*
* When the handler is legacy raw, returns `{ code: handlerCode(method),
* imports: empty }`. */
function methodBodyCodePython(method: IRNode): { code: string; imports: Set<string>; helpers: Set<string> } {
function methodBodyCodePython(
method: IRNode,
opts?: { classBody?: boolean; isConstructor?: boolean; staticReceiver?: boolean },
): { code: string; imports: Set<string>; helpers: Set<string> } {
const handler = getFirstChild(method, 'handler');
if (!handler || getProps(handler).lang !== 'kern') {
return { code: handlerCode(method), imports: new Set(), helpers: new Set() };
Expand Down Expand Up @@ -54,7 +57,15 @@ function methodBodyCodePython(method: IRNode): { code: string; imports: Set<stri
for (const part of parseLegacyParamParts(rawParams)) recordParam(part.name);
}
}
const { code, imports, helpers } = emitNativeKernBodyPythonWithImports(handler, { symbolMap });
// Class member bodies: `this` resolves to `self`, and `super(...)`/`super.x`
// lower to `super().__init__(...)`/`super().x` via the inClassBody flag.
// In a static accessor (metaclass property) body `this` is the class -> `cls`.
if (opts?.classBody) symbolMap.this = opts?.staticReceiver ? 'cls' : 'self';
const { code, imports, helpers } = emitNativeKernBodyPythonWithImports(handler, {
symbolMap,
inClassBody: opts?.classBody ?? false,
inConstructor: opts?.isConstructor ?? false,
});
return { code, imports, helpers };
}

Expand All @@ -64,8 +75,11 @@ function methodBodyCodePython(method: IRNode): { code: string; imports: Set<stri
* scope absorbs them, and Python caches modules after first import.
* Returns the indented lines (4-space prefix) ready to push into the
* enclosing class definition. Empty body yields a single `pass`. */
function methodBodyLinesPython(method: IRNode): string[] {
const { code, imports, helpers } = methodBodyCodePython(method);
function methodBodyLinesPython(
method: IRNode,
opts?: { classBody?: boolean; isConstructor?: boolean; staticReceiver?: boolean },
): string[] {
const { code, imports, helpers } = methodBodyCodePython(method, opts);
const lines: string[] = [];
for (const mod of [...imports].sort()) {
lines.push(` import ${mod} as __k_${mod}`);
Expand Down Expand Up @@ -109,6 +123,25 @@ export function formatPythonDefault(value: string, kernType: string): string {
return trimmed;
}

/** Lower a field's default to a Python expression, or undefined when none.
* A `value={{ <expr> }}` block parses to `{ __expr: true, code: '<expr>' }`;
* a bare `default=...` is a raw string. `new X(...)` -> `X(...)`; literals go
* through formatPythonDefault (true/false/null/number/string handling). */
function fieldDefaultPython(field: IRNode): string | undefined {
const fp = p(field);
const v = fp.value as unknown;
let code: string | undefined;
if (v && typeof v === 'object' && (v as { __expr?: boolean }).__expr) {
code = (v as { code?: string }).code;
} else if (typeof v === 'string') {
code = v;
} else if (typeof fp.default === 'string') {
code = fp.default as string;
}
if (code === undefined) return undefined;
return formatPythonDefault(code.replace(/\bnew\s+/g, ''), (fp.type as string) || '');
}

// SQLModel column override: pydantic validator types -> plain DB types for column declarations
const SQLMODEL_COLUMN_OVERRIDE: Record<string, string> = {
Email: 'str',
Expand Down Expand Up @@ -455,6 +488,178 @@ export function generatePythonService(node: IRNode): string[] {
return lines;
}

// ── Class (single-source class slice, Python target) ────────────────────
// Phase 1: structural shell parity with the TS `emitClassBody`. Emits the
// class header + `extends` base, static fields as class attributes, the
// constructor as `__init__`, instance/static methods, and getters/setters via
// `@property`. Method/ctor bodies route through the shared
// `methodBodyLinesPython`; full class-body symbol translation
// (`this`->`self`, `super.m()`->`super().m()`, `new X()`->`X()`) is the next
// sub-problem the differential class fixtures will drive.
export function generatePythonClass(node: IRNode): string[] {
const props = p(node);
const name = emitIdentifier(props.name as string, 'UnknownClass', node);
const baseRaw = typeof props.extends === 'string' ? (props.extends as string) : '';
const base = baseRaw ? emitIdentifier(baseRaw, 'object', node) : '';

const isStatic = (n: IRNode): boolean => {
const np = p(n);
return np.static === 'true' || np.static === true;
};

const fields = kids(node, 'field');
const staticFields = fields.filter(isStatic);
const methods = kids(node, 'method');
const getters = kids(node, 'getter');
const setters = kids(node, 'setter');
const ctor = firstChild(node, 'constructor');

// Static accessors (static get/set) lower to a per-class metaclass: both
// `Box.label` reads and `Box.label = x` writes dispatch through the metaclass
// @property/.setter (a plain descriptor would be shadowed on assignment). The
// static backing field stays a class attribute. The metaclass extends
// `type(<base>)` so that when the base ALSO has static accessors the derived
// metaclass subclasses the base metaclass (no `metaclass conflict`, and the
// base's static accessors are inherited); when the base has none, `type(<base>)`
// is just `type`.
const staticGetters = getters.filter(isStatic);
const staticSetters = setters.filter(isStatic);
const metaName = `_${name}Meta`;
const metaLines: string[] = [];
if (staticGetters.length + staticSetters.length > 0) {
metaLines.push(`class ${metaName}(${base ? `type(${base})` : 'type'}):`);
const metaGetterNames = new Set<string>();
for (const g of staticGetters) {
const gp = p(g);
const gname = toSnakeCase((gp.name as string) || 'prop');
const returns = gp.returns ? ` -> ${mapTsTypeToPython(gp.returns as string)}` : '';
metaGetterNames.add(gname);
metaLines.push(' @property');
metaLines.push(` def ${gname}(cls)${returns}:`);
metaLines.push(...methodBodyLinesPython(g, { classBody: true, staticReceiver: true }));
metaLines.push('');
}
for (const s of staticSetters) {
const sname = toSnakeCase((p(s).name as string) || 'prop');
if (!metaGetterNames.has(sname)) {
metaLines.push(' @property');
metaLines.push(` def ${sname}(cls): # write-only static property`);
metaLines.push(' return None');
metaLines.push('');
metaGetterNames.add(sname);
}
metaLines.push(` @${sname}.setter`);
metaLines.push(` def ${sname}(cls, ${buildPythonParamList(s, { selfPrefix: false })}):`);
metaLines.push(...methodBodyLinesPython(s, { classBody: true, staticReceiver: true }));
metaLines.push('');
}
}
const baseParts = [base, metaLines.length > 0 ? `metaclass=${metaName}` : ''].filter(Boolean);
const header = baseParts.length > 0 ? `class ${name}(${baseParts.join(', ')}):` : `class ${name}:`;

const body: string[] = [];

// Static fields -> class-level attributes (shared across instances, like TS statics).
for (const f of staticFields) {
const fp = p(f);
const fname = toSnakeCase((fp.name as string) || 'field');
const ftype = fp.type ? mapTsTypeToPython(fp.type as string) : 'Any';
body.push(` ${fname}: ${ftype} = ${fieldDefaultPython(f) ?? 'None'}`);
}
if (staticFields.length > 0) body.push('');

// Constructor -> __init__. Instance-field defaults are emitted INSIDE __init__
// (never as class-level attributes) so each instance gets a fresh value —
// matching TS per-instance field initialization and avoiding Python's
// shared-mutable-default trap (a class-level `items = []` would be shared by
// every instance). Defaults precede the constructor body, which may reassign
// them (TS field-init-then-constructor order).
const instanceDefaults = fields.filter((f) => !isStatic(f) && fieldDefaultPython(f) !== undefined);
const defaultLines = instanceDefaults.map(
(f) => ` self.${toSnakeCase((p(f).name as string) || 'field')} = ${fieldDefaultPython(f)}`,
);
if (ctor) {
body.push(` def __init__(${buildPythonParamList(ctor, { selfPrefix: true })}):`);
const ctorLines = methodBodyLinesPython(ctor, { classBody: true, isConstructor: true });
// Field initializers run AFTER super().__init__() (TS field-init-after-super
// order), so inject defaults right after the super call when present, else at
// the top of the constructor body.
const superIdx = ctorLines.findIndex((line) => line.includes('super().__init__'));
if (superIdx >= 0) {
body.push(...ctorLines.slice(0, superIdx + 1), ...defaultLines, ...ctorLines.slice(superIdx + 1));
} else {
body.push(...defaultLines, ...ctorLines);
}
body.push('');
} else if (instanceDefaults.length > 0) {
// No explicit constructor. A derived class still forwards to its base
// initializer (TS subclasses without a constructor auto-forward args), then
// applies its own field defaults.
if (base) {
body.push(' def __init__(self, *args, **kwargs):');
body.push(' super().__init__(*args, **kwargs)');
} else {
body.push(' def __init__(self):');
}
body.push(...defaultLines);
body.push('');
}

// Methods (instance + static).
for (const m of methods) {
const mp = p(m);
const mname = toSnakeCase((mp.name as string) || 'method');
const asyncKw = mp.async === 'true' || mp.async === true ? 'async ' : '';
const returns = mp.returns ? ` -> ${mapTsTypeToPython(mp.returns as string)}` : '';
if (isStatic(m)) {
body.push(' @staticmethod');
body.push(` ${asyncKw}def ${mname}(${buildPythonParamList(m, { selfPrefix: false })})${returns}:`);
} else {
body.push(` ${asyncKw}def ${mname}(${buildPythonParamList(m, { selfPrefix: true })})${returns}:`);
}
body.push(...methodBodyLinesPython(m, { classBody: !isStatic(m) }));
body.push('');
}

// Getters -> @property. Static getters were already emitted on the metaclass.
const instanceGetterNames = new Set<string>();
for (const g of getters) {
if (isStatic(g)) continue;
const gp = p(g);
const gname = toSnakeCase((gp.name as string) || 'prop');
instanceGetterNames.add(gname);
const returns = gp.returns ? ` -> ${mapTsTypeToPython(gp.returns as string)}` : '';
body.push(' @property');
body.push(` def ${gname}(self)${returns}:`);
body.push(...methodBodyLinesPython(g, { classBody: true }));
body.push('');
}
// Setters -> @<name>.setter. Python requires a property to exist before its
// `.setter`; KERN allows setter-only properties, so synthesize a getter when
// none was declared (write-only -> returns None, matching a TS getter-less read).
for (const s of setters) {
if (isStatic(s)) continue; // static setters were already emitted on the metaclass
const sp = p(s);
const sname = toSnakeCase((sp.name as string) || 'prop');
if (!instanceGetterNames.has(sname)) {
body.push(' @property');
body.push(` def ${sname}(self): # write-only property (no getter declared in KERN)`);
body.push(' return None');
body.push('');
instanceGetterNames.add(sname);
}
body.push(` @${sname}.setter`);
body.push(` def ${sname}(${buildPythonParamList(s, { selfPrefix: true })}):`);
body.push(...methodBodyLinesPython(s, { classBody: true }));
body.push('');
}

if (body.length === 0) body.push(' pass');

// Metaclass (if any) must be defined before the class that references it.
return metaLines.length > 0 ? [...metaLines, header, ...body] : [header, ...body];
}

// ── Union (Pydantic Discriminated Union) ────────────────────────────────
// union name=ContentSegment discriminant=type
// variant name=prose
Expand Down
12 changes: 12 additions & 0 deletions packages/python/src/targets/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { emitModels } from '../core/emit-models.js';
import { collectFenceDiagnostics } from '../core/fence-diagnostics.js';
import { emitPureHandlers } from '../core/handlers/index.js';
import { findServerNode } from '../fastapi-utils.js';
import { generatePythonClass } from '../generators/data.js';

/**
* The PyDotDict / _DotList shim, emitted at the top of every `--emit=backend`
Expand Down Expand Up @@ -132,6 +133,11 @@ export function transpilePython(root: IRNode, config?: ResolvedKernConfig): Tran
target: 'python',
});

// 3b. Class declarations -> pure Python classes. Additive: files without
// `class` nodes (e.g. the models-only byte-invariance corpus) are untouched.
const classNodes = root.type === 'class' ? [root] : (root.children ?? []).filter((child) => child.type === 'class');
const classesCode = classNodes.map((node) => generatePythonClass(node).join('\n')).join('\n\n');

const lines: string[] = [];

// Sort and print imports
Expand Down Expand Up @@ -166,6 +172,12 @@ export function transpilePython(root: IRNode, config?: ResolvedKernConfig): Tran
lines.push(modelsCode);
}

// Class definitions (pure Python — not FastAPI/Pydantic).
if (classesCode.trim().length > 0) {
lines.push('');
lines.push(classesCode);
}

// Pure handlers (additive)
if (handlersCode) {
lines.push('');
Expand Down
Loading