@skillstech/thunderlang 0.2.0 → 0.4.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.
package/src/cli.mjs CHANGED
@@ -98,7 +98,13 @@ import {
98
98
  import { parseEventLog, serializeEventLog, recordEvent, timeline } from './ai-events.mjs';
99
99
 
100
100
  // Recursively collect supported source files, skipping vendored / build dirs.
101
- const LIFT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.rs', '.pl', '.pm'];
101
+ // Kept in sync with lift.mjs languageForFile(): every language the lift adapters support,
102
+ // so repo-mode discovery matches the advertised multi-language lift surface.
103
+ const LIFT_EXTS = [
104
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.rs', '.pl', '.pm', '.t',
105
+ '.py', '.pyi', '.java', '.cs', '.go', '.cpp', '.cc', '.cxx', '.hpp', '.hh', '.c', '.h',
106
+ '.php', '.rb', '.kt', '.kts', '.scala', '.sc', '.ex', '.exs',
107
+ ];
102
108
  const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build', '.intent', 'coverage', '.vercel']);
103
109
  // ThunderLang source files. `.thunder` is the canonical public extension; `.tl` is an
104
110
  // accepted shorthand; `.intent` stays supported so legacy IntentLang sources keep working.
@@ -321,81 +327,83 @@ function printDiagnostics(diags) {
321
327
 
322
328
  const HELP = `thunder , the ThunderLang compiler + engine (deterministic, no AI required)
323
329
 
330
+ The flow: author intent, inspect it, run and prove it, then keep real code aligned with it.
331
+
324
332
  usage: thunder <command> <file> [options]
325
333
  thunder mission <Name> <command> run any command on a mission by name
326
334
 
327
- Everyday
328
- new [Name] scaffold a runnable starter mission (Name.thunder) [alias: init]
329
- mission <Name> [cmd] resolve a mission by name and run a command on it (list | <Name> | <Name> <cmd>)
330
- check <file|dir> parse + lint + explainable diagnostics
331
- run <file> --inputs '<json>' execute the decision(s) deterministically
335
+ Author
336
+ new [Name] scaffold a runnable starter mission (Name.thunder) [alias: init]
337
+ mission <Name> [cmd] resolve a mission by name and run a command on it (list | <Name> | <Name> <cmd>)
338
+ draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
339
+ edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
340
+ fmt <file|dir> [--write|--check] canonical formatting (whitespace only; comments kept)
341
+ source <file|graph.json> regenerate .thunder from a persisted graph
342
+
343
+ Inspect
344
+ check <file|dir> [--json|--format sarif] parse + lint + explainable diagnostics; gate a whole dir
345
+ report [dir] [--json] repo-wide intent health: severity + area counts, coverage
346
+ risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json] focused scan queries
347
+ focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json] Intent Lens: focused graph + brief
348
+ comprehension <file|dir> [--observed] [--learning] [--governed] [--json] the C0..C7 understanding level
349
+ twelve-factor <file> [--json] score against the 13 humanlayer/12-factor-agents principles [alias: 12factor]
350
+ explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
351
+ rules [--json] list the whole canonical diagnostic catalog
352
+ notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
353
+ docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
354
+ index <dir> the Mission Atlas inventory
355
+ atlas <dir> [--search q | --expand id] the whole-system Atlas
356
+
357
+ Run & test
358
+ run <file> --inputs '<json>' execute the decision(s) deterministically
332
359
  test <file> [--contracts | --properties | --scenarios | --mutate | --evals | --changed | --coverage] [--strict]
333
360
  test <file> --target typescript|python|csharp|java run the tests against the EXECUTED generated code
334
- test <file> --all-targets run the tests against every AVAILABLE target in one pass
335
- prove <file> emit an intent-proof-v1 artifact (honest verdicts + freshness)
361
+ test <file> --all-targets run the tests against every AVAILABLE target in one pass
362
+ simulate <file> --events a,b,c walk the lifecycle(s) over events
363
+ outcomes <file> evaluate outcome contracts vs delivery results
364
+ style <file> resolve style intents vs the canonical token space
365
+ gen <file> [--target typescript|csharp|java|python] [--out <dir>] deterministic code scaffold (types + decision logic + TODOs)
366
+ build <file> generate docs, contract graph, test plan, and .thunder-proof.json
367
+
368
+ Prove & verify intent
369
+ prove <file> emit an intent-proof-v1 artifact (honest verdicts + freshness)
370
+ proof <file> the .thunder-proof.json artifact (see also: prove, which emits verdicts)
371
+ proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
336
372
  verify <proof.json> [src] re-check a proof; reports STALE when impl/deps/compiler moved
337
- build <file> generate docs, contract graph, test plan, targets
338
-
339
- Author & check
340
- init [Name] scaffold a runnable starter mission (Name.thunder)
341
- draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
342
- check <file|dir> [--json|--format sarif] diagnostics for one file, or gate a whole dir
343
- report [dir] [--json] repo-wide intent health: severity + area counts, coverage
344
- scan [dir] [--json] [--ir <path>] Scanner: intent -> Intent IR -> Fable findings -> risk themes
345
- risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json] focused scan queries
346
- focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json] Intent Lens: focused graph + brief
347
- comprehension <file|dir> [--observed] [--learning] [--governed] [--json] the C0..C7 understanding level
348
- twelve-factor <file> [--json] score an intent against the 13 humanlayer/12-factor-agents principles
349
- gen <file> [--target typescript|csharp|java] [--out <dir>] deterministic code scaffold (types + decision logic + TODOs)
350
- changes [<base>..<head> | <base>] [--json] Change Lens: what a branch/PR changed by meaning
351
- guardian <before> <after> drift: what changed, what risk, what to reverify, what learning is stale
352
- impact <base> <proposed> Simulator: estimate a change's blast radius + risk BEFORE building it
353
- ledger <file.json> [--subject <id>] verify the tamper-evident history + explain why/who/what changed
354
- guard <file> [--json] preview the runtime guard (redacted fields, enforced decisions)
355
- fmt <file|dir> [--write|--check] canonical formatting (whitespace only; comments kept)
356
- edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
357
- lsp start the Language Server (LSP over stdio, for editors)
358
- mcp start the MCP server (for AI coding agents; stdio)
359
- build <file> docs, contract graph, test plan, and .thunder-proof.json
360
- graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
361
- proof <file> the .thunder-proof.json artifact
362
- proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
363
- verify <proof.json> [src] confirm a proof is well-formed and still matches its source
364
- conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] run the same cases against every target (conformance matrix)
365
- schema emit the canonical graph schema + diagnostic catalog
366
- explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
367
- rules [--json] list the whole canonical diagnostic catalog
368
- notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
369
- docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
370
- code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
371
- apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
372
-
373
- Execute (no AI, no generated code)
374
- run <file> --inputs '<json>' evaluate the decision(s) against inputs
375
- simulate <file> --events a,b,c walk the lifecycle(s) over events
376
- test <file> run the in-file test blocks (case/scenario)
377
- outcomes <file> evaluate outcome contracts vs delivery results
378
- style <file> resolve style intents vs the canonical token space
373
+ conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] conformance matrix across targets
374
+
375
+ Real code vs intent (drift)
376
+ lift <file> [--from <lang>] lift source code into inferred intent
377
+ drift <codeFile> --intent <file.intent> [--from <lang>] check intent vs code drift
378
+ approve <file> --by <name> approve intent (drift baseline)
379
+ verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
380
+ changes [<base>..<head> | <base>] [--json] Change Lens: what a branch/PR changed by meaning
381
+ guardian <before> <after> drift: what changed, what risk, what to reverify, what learning is stale
382
+ impact <base> <proposed> Simulator: estimate a change's blast radius + risk BEFORE building it
383
+ diff <before> <after> semantic diff (by meaning)
384
+ merge <base> <ours> <theirs> deterministic 3-way semantic merge
385
+ handoff <file> the OpenThunder drift handoff
386
+ ledger <file.json> [--subject <id>] verify the tamper-evident history + explain why/who/what changed
387
+ guard <file> [--json] preview the runtime guard (redacted fields, enforced decisions)
388
+
389
+ Graph & IR
390
+ graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
391
+ migrate <graph.json> [--to <version>] upgrade a persisted graph
392
+ validate <graph.json> [--json] check a graph is canonical (anti-fork)
393
+ schema emit the canonical graph schema + diagnostic catalog
379
394
 
380
395
  Interop
381
396
  export <file> --format <dmn|bpmn|smv|jsonschema|openapi|tokens|mermaid|css|playwright> render to a standard format
382
- import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
383
- source <file|graph.json> regenerate .thunder from a graph
384
- migrate <graph.json> [--to <version>] upgrade a persisted graph
385
- validate <graph.json> [--json] check a graph is canonical (anti-fork)
397
+ import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
386
398
 
387
- Navigate & compare (over many missions)
388
- atlas <dir> [--search q | --expand id] the whole-system Atlas
389
- index <dir> the Mission Atlas inventory
390
- diff <before> <after> semantic diff (by meaning)
391
- merge <base> <ours> <theirs> deterministic 3-way semantic merge
399
+ Fix
400
+ scan [dir] [--json] [--ir <path>] Scanner: intent -> Intent IR -> Fable findings -> risk themes
401
+ code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
402
+ apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
392
403
 
393
- Code <-> intent
394
- lift <file> [--from <lang>] lift source code into inferred intent
395
- approve <file> --by <name> approve intent (drift baseline)
396
- drift <file> --from <code> check intent vs code drift
397
- verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
398
- handoff <file> the OpenThunder drift handoff
404
+ Servers
405
+ lsp start the Language Server (LSP over stdio, for editors)
406
+ mcp start the MCP server (for AI coding agents; stdio)
399
407
 
400
408
  Common options: --out <dir>, --json, --no-ai. See https://thunderlang.dev/docs`;
401
409
 
@@ -642,7 +650,7 @@ function main() {
642
650
  return;
643
651
  }
644
652
 
645
- // `thunder gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
653
+ // `thunder gen <file> [--target typescript|csharp|java|python] [--out <dir>]` , deterministic code scaffold from
646
654
  // intent. Generates the typed contract + the decision logic (already executable) and leaves
647
655
  // honest TODO markers for business logic. No AI. Prints, or writes with --out.
648
656
  if (cmd === 'gen') {
@@ -653,7 +661,7 @@ function main() {
653
661
  const ast = parseIntent(readFileSync(file, 'utf8'));
654
662
  const code = generate(ast);
655
663
  if (args.outExplicit) {
656
- const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target;
664
+ const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target === 'python' ? 'py' : target === 'csharp' ? 'cs' : target;
657
665
  const p = writeText(args.out, `${slug(subjectName(ast) || 'intent')}.${ext}`, code.endsWith('\n') ? code : code + '\n');
658
666
  console.log(`wrote ${p.replace(process.cwd() + '/', '')}`);
659
667
  return;
@@ -846,9 +854,9 @@ test Example
846
854
  return;
847
855
  }
848
856
 
849
- // Single-file mode.
857
+ // Single-file mode. Auto-detect language by extension (consistent with --all / repo modes).
850
858
  const src = readFileSync(file, 'utf8');
851
- const res = liftSource(src, { language: args.from || 'typescript', file: basename(file) });
859
+ const res = liftSource(src, { language: args.from || languageForFile(file), file: basename(file) });
852
860
  if (!res.ok) { console.error(res.error); process.exit(1); }
853
861
  if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
854
862
  if (args.out) {
package/src/codegen.mjs CHANGED
@@ -5,9 +5,9 @@
5
5
  // the "see how it works, then change it" surface for the playground and `thunder gen`.
6
6
  //
7
7
  // Pure and browser-safe so the playground can render it. TypeScript first; the same adapter
8
- // shape (a type map + a body walk) extends to C# / Java.
8
+ // shape (a type map + a body walk) extends to C# / Java / Python.
9
9
 
10
- import { exprToJs, exprToCSharp, exprToJava } from './expr.mjs';
10
+ import { exprToJs, exprToCSharp, exprToJava, exprToPython } from './expr.mjs';
11
11
  import { subjectName } from './parse.mjs';
12
12
 
13
13
  export const CODEGEN_SCHEMA = 'intent-codegen-v1';
@@ -138,7 +138,7 @@ export function toCSharp(ast) {
138
138
  L.push(` public static string ${pascal(d.name)}(${inputs.map((i) => `dynamic ${i}`).join(', ')})`);
139
139
  L.push(' {');
140
140
  for (const r of d.rules || []) {
141
- let cond; try { cond = exprToCSharp(r.when, { inputs }); } catch { cond = `false /* TODO: "${r.when}" */`; }
141
+ let cond; try { cond = exprToCSharp(r.when, { inputs }); } catch { cond = `false /* TODO: could not translate "${r.when}" */`; }
142
142
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
143
143
  }
144
144
  L.push(` return ${JSON.stringify(d.default ?? 'Undecided')}; // default`, ' }');
@@ -146,6 +146,7 @@ export function toCSharp(ast) {
146
146
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
147
147
  const outName = (ast.outputs || []).length ? `${subject}Output` : 'void';
148
148
  L.push(` public static ${outName} Run(${inName ? `${inName} input` : ''})`, ' {');
149
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
149
150
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
150
151
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
151
152
  L.push(' throw new NotImplementedException("TODO: implement , the intent above defines what this must do.");', ' }');
@@ -192,7 +193,7 @@ export function toJava(ast) {
192
193
  L.push(` // decision ${d.name} , first matching rule wins.`);
193
194
  L.push(` public static String ${lower(pascal(d.name))}(${inputs.map((i) => `Object ${i}`).join(', ')}) {`);
194
195
  for (const r of d.rules || []) {
195
- let cond; try { cond = exprToJava(r.when, { inputs }); } catch { cond = `false /* TODO: "${r.when}" */`; }
196
+ let cond; try { cond = exprToJava(r.when, { inputs }); } catch { cond = `false /* TODO: could not translate "${r.when}" */`; }
196
197
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
197
198
  }
198
199
  L.push(` return ${JSON.stringify(d.default ?? 'Undecided')}; // default`, ' }');
@@ -200,6 +201,7 @@ export function toJava(ast) {
200
201
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
201
202
  const outName = (ast.outputs || []).length ? `${subject}Output` : 'void';
202
203
  L.push(` public static ${outName} run(${inName ? `${inName} input` : ''}) {`);
204
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
203
205
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
204
206
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
205
207
  L.push(' throw new UnsupportedOperationException("TODO: implement , the intent above defines what this must do.");', ' }');
@@ -207,8 +209,75 @@ export function toJava(ast) {
207
209
  return L.join('\n');
208
210
  }
209
211
 
212
+ // ── Python ───────────────────────────────────────────────────────────────────
213
+ const PY_TYPES = {
214
+ Email: 'str', Url: 'str', UserId: 'str', AccountId: 'str', OrderId: 'str',
215
+ InvoiceId: 'str', CustomerId: 'str', Secret: 'str', Token: 'str', Jwt: 'str',
216
+ IdempotencyKey: 'str', Currency: 'str', Date: 'str', DateTime: 'str',
217
+ Money: 'float', Percentage: 'float', Count: 'int', Duration: 'int', Flag: 'bool',
218
+ };
219
+ function pyType(type, domain) {
220
+ if (!type) return 'object';
221
+ const list = /^List<(.+)>$/.exec(type);
222
+ if (list) return `list[${pyType(list[1], domain)}]`;
223
+ const base = type.replace(/\(.*\)/, '');
224
+ if (PY_TYPES[base]) return PY_TYPES[base];
225
+ domain.add(base); return base;
226
+ }
227
+ // PascalCase/camelCase -> snake_case, for idiomatic Python function names. Field and decision
228
+ // input names stay verbatim: the translated conditions reference them by their intent names.
229
+ const snake = (s) => pascal(s).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
230
+
231
+ /** Generate a deterministic Python scaffold from an intent AST. */
232
+ export function toPython(ast) {
233
+ const subject = pascal(subjectName(ast) || 'Intent');
234
+ const domain = new Set();
235
+ const L = [
236
+ `# ${subject} , generated from ThunderLang (intent-codegen-v1). Deterministic, no AI.`,
237
+ '# The dataclass contract and the decision logic are fully determined by the intent;',
238
+ '# business logic marked TODO is yours, bound by the guarantees and never-rules below.',
239
+ '',
240
+ ];
241
+ const hasData = (ast.inputs || []).length || (ast.outputs || []).length;
242
+ // Postponed annotations so a dataclass may reference a domain stub declared below it.
243
+ if (hasData) L.push('from __future__ import annotations', 'from dataclasses import dataclass', '');
244
+ const cls = (name, fields) => fields.length
245
+ ? ['@dataclass', `class ${name}:`, ...fields.map((f) => ` ${f.name}: ${pyType(f.type, domain)}`), '']
246
+ : [];
247
+ if ((ast.inputs || []).length) L.push(...cls(`${subject}Input`, ast.inputs));
248
+ if ((ast.outputs || []).length) L.push(...cls(`${subject}Output`, ast.outputs));
249
+ for (const d of ast.decisions || []) {
250
+ const inputs = d.inputs || [];
251
+ L.push(`# decision ${d.name} , first matching rule wins.`);
252
+ L.push(`def ${snake(d.name)}(${inputs.join(', ')}) -> str:`);
253
+ for (const r of d.rules || []) {
254
+ let cond, note = '';
255
+ try { cond = exprToPython(r.when, { inputs }); }
256
+ catch { cond = 'False'; note = ` # TODO: could not translate "${r.when}"`; }
257
+ L.push(` if ${cond}:${note}`);
258
+ L.push(` return ${JSON.stringify(r.result)} # rule ${r.name}`);
259
+ }
260
+ L.push(` return ${JSON.stringify(d.default ?? 'Undecided')} # default`, '');
261
+ }
262
+ const inName = (ast.inputs || []).length ? `${subject}Input` : null;
263
+ const outName = (ast.outputs || []).length ? `${subject}Output` : 'None';
264
+ L.push(`def run(${inName ? `input: ${inName}` : ''}) -> ${outName}:`);
265
+ for (const r of ast.requires || []) L.push(` # precondition: ${r} , TODO: validate`);
266
+ for (const n of ast.neverRules || []) L.push(` # NEVER: ${n.statement}`);
267
+ for (const g of ast.guarantees || []) L.push(` # GUARANTEE: ${g.statement}`);
268
+ L.push(' raise NotImplementedError("TODO: implement , the intent above defines what this must do.")', '');
269
+ const stubs = [...domain].filter((t) => !PY_TYPES[t] && /^[A-Z]/.test(t)).sort();
270
+ if (stubs.length) {
271
+ L.push('# Domain types referenced by the intent , complete these.');
272
+ for (const t of stubs) L.push(`class ${t}: pass # TODO: fields`);
273
+ L.push('');
274
+ }
275
+ return L.join('\n');
276
+ }
277
+
210
278
  export const GENERATORS = {
211
279
  typescript: toTypeScript, ts: toTypeScript,
212
280
  csharp: toCSharp, cs: toCSharp,
213
281
  java: toJava,
282
+ python: toPython, py: toPython,
214
283
  };
package/src/core.d.ts CHANGED
@@ -50,7 +50,7 @@ export {
50
50
  } from './index';
51
51
 
52
52
  // Code generation , deterministic scaffolds from intent.
53
- export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, subjectName, intentRefId, skillRefId } from './index';
53
+ export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython, subjectName, intentRefId, skillRefId } from './index';
54
54
 
55
55
  // 12-Factor Agents conformance lens (twelve-factor-v1).
56
56
  export { TWELVE_FACTOR_SCHEMA, TwelveFactorResult, twelveFactorReport, twelveFactorSummary } from './index';
package/src/core.mjs CHANGED
@@ -80,7 +80,7 @@ export { FOCUS_SCHEMA, SCOPE_TYPES, FOCUS_REASONS, makeScope, buildFocusGraph, i
80
80
  // Comprehension Contract: the C0..C7 understanding level (browser-safe; every product reads it).
81
81
  export { COMPREHENSION_SCHEMA, LEVELS as COMPREHENSION_LEVELS, comprehensionLevel, comprehensionReport } from './comprehension.mjs';
82
82
  // Code generation: deterministic scaffolds from intent (browser-safe, so the playground renders it).
83
- export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava } from './codegen.mjs';
83
+ export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython } from './codegen.mjs';
84
84
  // Change Lens: what a branch/PR changed by meaning (pure; the CLI supplies the git-diffed graphs).
85
85
  export { CHANGES_SCHEMA, changeReport } from './changes.mjs';
86
86
  export { subjectName, intentRefId, skillRefId } from './parse.mjs';
package/src/emit.mjs CHANGED
@@ -30,7 +30,7 @@ export function notesSummary(ast) {
30
30
  };
31
31
  }
32
32
 
33
- export const COMPILER_VERSION = '0.2.0';
33
+ export const COMPILER_VERSION = '0.4.1';
34
34
  export const PROOF_SCHEMA_VERSION = '0.1.0';
35
35
  // Identifies which ecosystem product emitted this proof. Consumed by SkillsTech
36
36
  // Certified to key cert proofs to the compiler. Stable slug per the coordination bus.
package/src/exporters.mjs CHANGED
@@ -180,17 +180,23 @@ export function toMermaid(ast) {
180
180
 
181
181
  // ── Playwright (experience -> E2E test scaffold) ─────────────────────────────
182
182
  // A SKELETON, not a passing test: declared experiences/journeys/states become structured
183
- // Playwright stubs (describe/test/test.step) with TODOs for selectors + assertions. This is
184
- // the test-plan target for the experience profile , it turns "what the UI must do" into the
185
- // shape of the test that proves it, deterministically. Consistent with the compiler's scope
186
- // (test scaffolds, not production code).
183
+ // Playwright stubs (describe/test/test.step) with TODOs for selectors + assertions. The
184
+ // TODOs are intentional and stay: a deterministic compiler cannot know the app's selectors,
185
+ // so it must not fabricate them. To keep the scaffold honest, every generated test opens
186
+ // with a test.fixme(true, ...) guard: Playwright reports it as "fixme" (not a vacuous pass)
187
+ // until a human fills in the body and removes the guard. This is the test-plan target for
188
+ // the experience profile , it turns "what the UI must do" into the shape of the test that
189
+ // proves it, deterministically. Consistent with the compiler's scope (test scaffolds, not
190
+ // production code).
187
191
  const jsStr = (s) => `'${String(s ?? '').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\r?\n/g, ' ')}'`;
188
192
 
189
193
  export function toPlaywright(ast) {
190
194
  const exps = ast.experiences || [];
191
195
  const L = [];
192
196
  L.push('// Playwright test scaffold generated from experience intent by @skillstech/thunderlang.');
193
- L.push('// SKELETON: fill in selectors and assertions. It is not a passing test until you do.');
197
+ L.push('// SKELETON: fill in selectors and assertions. Every test starts with a test.fixme');
198
+ L.push('// guard so an unimplemented scaffold reports "fixme" instead of passing vacuously;');
199
+ L.push('// remove the guard from each test once its body is real.');
194
200
  L.push("import { test, expect } from '@playwright/test';");
195
201
  L.push('');
196
202
  if (!exps.length) {
@@ -204,6 +210,7 @@ export function toPlaywright(ast) {
204
210
  }
205
211
  for (const j of exp.journeys || []) {
206
212
  L.push(` test(${jsStr(j.name || 'journey')}, async ({ page }) => {`);
213
+ L.push(` test.fixme(true, ${jsStr('scaffold: implement the steps, then remove this guard')});`);
207
214
  if (!(j.steps || []).length) L.push(' // TODO: no steps declared for this journey');
208
215
  for (const step of j.steps || []) {
209
216
  L.push(` await test.step(${jsStr(step)}, async () => {`);
@@ -215,10 +222,12 @@ export function toPlaywright(ast) {
215
222
  for (const st of exp.states || []) {
216
223
  if (st.hasRecovery) {
217
224
  L.push(` test(${jsStr(`failure state "${st.name}" offers a recovery path`)}, async ({ page }) => {`);
225
+ L.push(` test.fixme(true, ${jsStr('scaffold: implement this state check, then remove this guard')});`);
218
226
  L.push(` // TODO: drive the UI into "${st.name}", assert a recovery action is available`);
219
227
  L.push(' });');
220
228
  } else {
221
229
  L.push(` test(${jsStr(`reaches state: ${st.name}`)}, async ({ page }) => {`);
230
+ L.push(` test.fixme(true, ${jsStr('scaffold: implement this state check, then remove this guard')});`);
222
231
  L.push(` // TODO: drive the UI to "${st.name}" and assert it is shown`);
223
232
  L.push(' });');
224
233
  }
package/src/index.d.ts CHANGED
@@ -691,6 +691,7 @@ export const CODEGEN_SCHEMA: string;
691
691
  export function toTypeScript(ast: IntentAst): string;
692
692
  export function toCSharp(ast: IntentAst): string;
693
693
  export function toJava(ast: IntentAst): string;
694
+ export function toPython(ast: IntentAst): string;
694
695
  export const GENERATORS: Record<string, (ast: IntentAst) => string>;
695
696
  export function exprToJs(src: string, opts?: { inputs?: string[] }): string;
696
697
  export function subjectName(ast: IntentAst): string | null;
package/src/index.mjs CHANGED
@@ -113,7 +113,7 @@ export { FOCUS_SCHEMA, SCOPE_TYPES, FOCUS_REASONS, makeScope, buildFocusGraph, i
113
113
  // Comprehension Contract , the C0..C7 understanding level (intent-comprehension-v1)
114
114
  export { COMPREHENSION_SCHEMA, LEVELS as COMPREHENSION_LEVELS, comprehensionLevel, comprehensionReport } from './comprehension.mjs';
115
115
  // Code generation , deterministic scaffolds from intent (intent-codegen-v1)
116
- export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava } from './codegen.mjs';
116
+ export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython } from './codegen.mjs';
117
117
  // Change Lens , what a branch/PR changed by meaning (intent-changes-v1)
118
118
  export { CHANGES_SCHEMA, changeReport } from './changes.mjs';
119
119
  export { exprToJs, exprToCSharp, exprToJava, exprToCode } from './expr.mjs';
package/src/lift.mjs CHANGED
@@ -393,6 +393,74 @@ export function extractFactsRuby(source, file = 'input.rb') {
393
393
  return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'ruby', sourceRoot: file, functions, tests, errors };
394
394
  }
395
395
 
396
+ // Params written "name: Type" (Kotlin, Scala). Top-level comma split so generics/tuples
397
+ // like Map<String, Int> or (A, B) don't split mid-type. Strips val/var/vararg/implicit and defaults.
398
+ function parseNameColonTypeParams(raw) {
399
+ return splitTopLevel(raw, ',').map((p) => p.trim()).filter(Boolean).map((p) => {
400
+ const cleaned = p.replace(/@\w+(\([^)]*\))?/g, '').replace(/^(?:val|var|vararg|implicit|final|lazy)\s+/, '').replace(/=.*$/, '').trim();
401
+ const mm = cleaned.match(/^([A-Za-z_]\w*)\s*:\s*(.+)$/);
402
+ if (mm) return { name: mm[1], type: mm[2].trim() };
403
+ return { name: cleaned.replace(/[^\w].*$/, '') || cleaned, type: null };
404
+ });
405
+ }
406
+
407
+ // ── Kotlin adapter (JVM: fun name(p: Type): Ret, @Test, *Exception) ──────────
408
+ export function extractFactsKotlin(source, file = 'input.kt') {
409
+ let m; const functions = []; const tests = []; const seen = new Set();
410
+ const testMethods = new Set(); const ta = /@Test\b[\s\S]{0,120}?\bfun\s+(?:`([^`]+)`|(\w+))\s*\(/g;
411
+ while ((m = ta.exec(source))) testMethods.add(m[1] || m[2]);
412
+ const fnRe = /\bfun\s+(?:<[^>]*>\s*)?(?:[A-Za-z_][\w.]*\.)?(?:`([^`]+)`|([A-Za-z_]\w*))\s*\(([^)]*)\)\s*(?::\s*([^{=\n]+))?/g;
413
+ while ((m = fnRe.exec(source))) {
414
+ const name = m[1] || m[2];
415
+ if (testMethods.has(name)) { if (!tests.some((t) => t.name === name)) tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
416
+ if (seen.has(name)) continue; seen.add(name);
417
+ functions.push({ name, file, line: lineOf(source, m.index), parameters: parseNameColonTypeParams(m[3] || ''), returnType: m[4] ? m[4].trim() : null, evidence: [{ kind: 'fun', file, line: lineOf(source, m.index) }] });
418
+ }
419
+ const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
420
+ let mm; const ce = /class\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
421
+ const th = /throw\s+(\w+)\s*\(/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index); // Kotlin: no `new`
422
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'kotlin', sourceRoot: file, functions, tests, errors };
423
+ }
424
+
425
+ // ── Scala adapter (def name(p: Type): Ret, ScalaTest, *Exception) ─────────────
426
+ export function extractFactsScala(source, file = 'input.scala') {
427
+ let m; const functions = []; const tests = []; const seen = new Set();
428
+ const fnRe = /\bdef\s+([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\(([^)]*)\)\s*(?::\s*([^={\n]+))?/g;
429
+ while ((m = fnRe.exec(source))) {
430
+ const name = m[1];
431
+ if (seen.has(name)) continue; seen.add(name);
432
+ functions.push({ name, file, line: lineOf(source, m.index), parameters: parseNameColonTypeParams(m[2] || ''), returnType: m[3] ? m[3].trim() : null, evidence: [{ kind: 'def', file, line: lineOf(source, m.index) }] });
433
+ }
434
+ // ScalaTest: `test("desc")` (FunSuite) and `"desc" in { }` / `"desc" should`/`must` (WordSpec/FlatSpec).
435
+ const seenT = new Set(); const addTest = (n, idx) => { const k = String(n).toLowerCase(); if (n && !seenT.has(k)) { seenT.add(k); tests.push({ name: n, file, line: lineOf(source, idx) }); } };
436
+ const tr = /\btest\s*\(\s*"([^"]+)"/g; while ((m = tr.exec(source))) addTest(m[1], m.index);
437
+ const inRe = /"([^"]+)"\s+(?:in|should|must)\b/g; while ((m = inRe.exec(source))) addTest(m[1], m.index);
438
+ const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
439
+ let mm; const ce = /class\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
440
+ const th = /throw\s+new\s+(\w+)\s*\(/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index);
441
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'scala', sourceRoot: file, functions, tests, errors };
442
+ }
443
+
444
+ // ── Elixir adapter (dynamic: def/defp name(args), ExUnit test, raise/defexception)
445
+ export function extractFactsElixir(source, file = 'input.ex') {
446
+ let m; const functions = []; const tests = []; const seen = new Set();
447
+ const seenT = new Set(); const addTest = (n, idx) => { const k = String(n).toLowerCase(); if (n && !seenT.has(k)) { seenT.add(k); tests.push({ name: n, file, line: lineOf(source, idx) }); } };
448
+ const tr = /\btest\s+"([^"]+)"/g; while ((m = tr.exec(source))) addTest(m[1], m.index); // ExUnit: test "desc" do
449
+ const fnRe = /^[ \t]*defp?\s+([a-z_]\w*[!?]?)\s*(?:\(([^)]*)\))?/gm;
450
+ while ((m = fnRe.exec(source))) {
451
+ const name = m[1];
452
+ if (seen.has(name)) continue; seen.add(name);
453
+ // Elixir params are pattern matches (conn, %{id: id}, x \\ default); take the leading binding name.
454
+ const parameters = splitTopLevel(m[2] || '', ',').map((p) => p.trim()).filter(Boolean)
455
+ .map((p) => ({ name: p.split(/\\\\/)[0].trim().replace(/^%\{?/, '').replace(/[^\w].*$/, '') || p, type: null }));
456
+ functions.push({ name, file, line: lineOf(source, m.index), indent: (m[0].match(/^[ \t]*/) || [''])[0].length, parameters, returnType: null, evidence: [{ kind: 'def', file, line: lineOf(source, m.index) }] });
457
+ }
458
+ const errors = []; const seenErr = new Set(); const addErr = (n, idx) => { if (n && !seenErr.has(n)) { seenErr.add(n); errors.push({ name: n, file, line: lineOf(source, idx) }); } };
459
+ const modErr = /defmodule\s+([\w.]*(?:Error|Exception))\b/g; while ((m = modErr.exec(source))) addErr(m[1].split('.').pop(), m.index);
460
+ const raiseRe = /raise\s+([A-Z][\w.]*)/g; while ((m = raiseRe.exec(source))) addErr(m[1].split('.').pop(), m.index);
461
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'elixir', sourceRoot: file, functions, tests, errors };
462
+ }
463
+
396
464
  const ADAPTERS = {
397
465
  typescript: extractFactsTypeScript, ts: extractFactsTypeScript,
398
466
  javascript: extractFactsTypeScript, js: extractFactsTypeScript,
@@ -405,13 +473,17 @@ const ADAPTERS = {
405
473
  cpp: extractFactsCpp, 'c++': extractFactsCpp, c: extractFactsCpp, cc: extractFactsCpp,
406
474
  php: extractFactsPhp,
407
475
  ruby: extractFactsRuby, rb: extractFactsRuby,
476
+ kotlin: extractFactsKotlin, kt: extractFactsKotlin, kts: extractFactsKotlin,
477
+ scala: extractFactsScala, sc: extractFactsScala,
478
+ elixir: extractFactsElixir, ex: extractFactsElixir, exs: extractFactsElixir,
408
479
  };
409
- export const SUPPORTED_LANGUAGES = ['typescript', 'javascript', 'python', 'java', 'csharp', 'go', 'rust', 'cpp', 'php', 'ruby', 'perl'];
410
- const DYNAMIC_LANGUAGES = new Set(['perl', 'javascript', 'python', 'ruby', 'php']);
480
+ export const SUPPORTED_LANGUAGES = ['typescript', 'javascript', 'python', 'java', 'csharp', 'go', 'rust', 'cpp', 'php', 'ruby', 'perl', 'kotlin', 'scala', 'elixir'];
481
+ const DYNAMIC_LANGUAGES = new Set(['perl', 'javascript', 'python', 'ruby', 'php', 'elixir']);
411
482
 
412
483
  const LANG_DISPLAY = {
413
484
  typescript: 'TypeScript', javascript: 'JavaScript', python: 'Python', java: 'Java',
414
485
  csharp: 'C#', go: 'Go', rust: 'Rust', cpp: 'C++', php: 'PHP', ruby: 'Ruby', perl: 'Perl',
486
+ kotlin: 'Kotlin', scala: 'Scala', elixir: 'Elixir',
415
487
  };
416
488
 
417
489
  // Unwrap Result<T, E> / Promise<T> / T -> { output, error }
@@ -558,6 +630,9 @@ export function languageForFile(file) {
558
630
  if (/\.(cpp|cc|cxx|hpp|hh|c|h)$/i.test(file)) return 'cpp';
559
631
  if (/\.php$/i.test(file)) return 'php';
560
632
  if (/\.rb$/i.test(file)) return 'ruby';
633
+ if (/\.kts?$/i.test(file)) return 'kotlin';
634
+ if (/\.(scala|sc)$/i.test(file)) return 'scala';
635
+ if (/\.exs?$/i.test(file)) return 'elixir';
561
636
  if (/\.(mjs|cjs|jsx?)$/i.test(file)) return 'javascript';
562
637
  return 'typescript';
563
638
  }
@@ -577,7 +652,7 @@ function isPublicFn(fn, language) {
577
652
  // Common private-helper naming conventions, across languages.
578
653
  if (/(?:Internal|Impl|_impl|_helper|_test|Helper)$/.test(name)) return false;
579
654
  if (language === 'go' || language === 'golang') return /^[A-Z]/.test(name) && name !== 'Test';
580
- if (language === 'python' || language === 'ruby') return !name.startsWith('_') && (fn.indent == null || fn.indent <= 4);
655
+ if (language === 'python' || language === 'ruby' || language === 'elixir') return !name.startsWith('_') && (fn.indent == null || fn.indent <= 4);
581
656
  return !name.startsWith('_') && name !== 'init' && name !== 'constructor';
582
657
  }
583
658
 
@@ -643,6 +718,7 @@ export function liftRepo(files, { language } = {}) {
643
718
  const LANG_EXT = {
644
719
  typescript: 'ts', javascript: 'js', python: 'py', java: 'java', csharp: 'cs',
645
720
  go: 'go', rust: 'rs', cpp: 'cpp', php: 'php', ruby: 'rb', perl: 'pl',
721
+ kotlin: 'kt', scala: 'scala', elixir: 'ex',
646
722
  };
647
723
 
648
724
  /**