@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/CHANGELOG.md CHANGED
@@ -3,6 +3,45 @@
3
3
  All notable changes to `@skillstech/thunderlang`. Pre-1.0: the language and the
4
4
  `intent-graph-v1` schema version independently and may still change.
5
5
 
6
+ ## 0.4.1
7
+
8
+ The coherence release. Additive; no breaking changes. Version chosen to align with OpenThunder
9
+ (TL and OT version in lockstep; OT is moving from 0.4.0 to match).
10
+
11
+ - **Python code generation.** `thunder gen <file> --target python` now emits a typed Python 3
12
+ scaffold, aligning the codegen axis with live execution. Also fixes a real gap: C# and Java `gen`
13
+ silently dropped `requires` preconditions that TypeScript emitted; they are now emitted as guided
14
+ TODOs.
15
+ - **Language support matrix.** New `docs/language-support-matrix.md` documents the three axes , lift
16
+ (14 languages), gen, and live target execution (4) , and makes them discoverable.
17
+ - **VS Code extension.** `editors/vscode/` ships a real extension: syntax highlighting and a Language
18
+ Server client that launches `thunder lsp`. Packageable with `vsce package`.
19
+ - **Grouped, scannable CLI help.** `thunder --help` is reorganized into nine labeled groups with a
20
+ one-line mental model. Command behavior is unchanged (help text only); a test guards that no
21
+ command was dropped.
22
+ - **Verify-real-code loop, productized.** New MCP tools `intent_prove`, `intent_conform`, and
23
+ `intent_drift` let AI agents drive the full loop. `docs/verifying-ai-changes.md` is rewritten as
24
+ the headline workflow (author intent, write code, prove conformance, gate the merge) with a CI
25
+ recipe.
26
+ - **Honest Playwright scaffolds.** The exported Playwright tests now open with `test.fixme` so an
27
+ unfilled scaffold cannot pass vacuously.
28
+ - **intent-graph-v1 stability policy.** New `docs/schema-stability.md` commits to additive-only type
29
+ sets within v1, an IR version that moves independently of the package, a deprecation policy, and a
30
+ road to 1.0, so consumers like OpenThunder can depend on the IR safely.
31
+
32
+ ## 0.3.0
33
+
34
+ The 14-language lift release. Additive; no breaking changes. Restores the OpenThunder -> ThunderLang
35
+ recover-intent loop for every language OpenThunder's archgraph core can now discover.
36
+
37
+ - **Kotlin, Scala, and Elixir lift adapters.** `thunder lift` now extracts candidate intent from
38
+ `.kt`/`.kts`, `.scala`/`.sc`, and `.ex`/`.exs`, bringing the supported-language count to 14. A new
39
+ `name: Type` parameter parser handles the JVM signature shape; Elixir joins the dynamic-language set.
40
+ - **Fix: repo-walk language coverage.** `LIFT_EXTS` was stale and silently skipped Python, Java, C#,
41
+ Go, C++, PHP, and Ruby during repo/`--all` lifts; it now mirrors the full adapter set.
42
+ - **Fix: single-file lift language detection.** `thunder lift <file>` now auto-detects the language by
43
+ extension instead of defaulting to TypeScript, consistent with `--all`/repo modes.
44
+
6
45
  ## 0.2.0
7
46
 
8
47
  The cross-language target execution release. Additive; no breaking changes.
package/dist/core.cjs CHANGED
@@ -161,6 +161,7 @@ __export(core_exports, {
161
161
  toDesignTokens: () => toDesignTokens,
162
162
  toFinding: () => toFinding,
163
163
  toJava: () => toJava,
164
+ toPython: () => toPython,
164
165
  toTypeScript: () => toTypeScript,
165
166
  twelveFactorReport: () => twelveFactorReport,
166
167
  twelveFactorSummary: () => twelveFactorSummary,
@@ -3336,6 +3337,7 @@ function exprToCode(src, { inputs = [], dialect = "js" } = {}) {
3336
3337
  var exprToJs = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "js" });
3337
3338
  var exprToCSharp = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "csharp" });
3338
3339
  var exprToJava = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "java" });
3340
+ var exprToPython = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "python" });
3339
3341
 
3340
3342
  // src/runtime.mjs
3341
3343
  var RUNTIME_SCHEMA = "intent-runtime-v1";
@@ -4288,7 +4290,7 @@ function notesSummary(ast) {
4288
4290
  byLens
4289
4291
  };
4290
4292
  }
4291
- var COMPILER_VERSION = "0.2.0";
4293
+ var COMPILER_VERSION = "0.4.1";
4292
4294
  var PROOF_SCHEMA_VERSION = "0.1.0";
4293
4295
  var SOURCE_PRODUCT = "skillstech-compiler";
4294
4296
  function buildContractGraph(ast, generatedAt) {
@@ -5378,6 +5380,112 @@ function extractFactsRuby(source, file = "input.rb") {
5378
5380
  }
5379
5381
  return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "ruby", sourceRoot: file, functions, tests, errors };
5380
5382
  }
5383
+ function parseNameColonTypeParams(raw) {
5384
+ return splitTopLevel(raw, ",").map((p) => p.trim()).filter(Boolean).map((p) => {
5385
+ const cleaned = p.replace(/@\w+(\([^)]*\))?/g, "").replace(/^(?:val|var|vararg|implicit|final|lazy)\s+/, "").replace(/=.*$/, "").trim();
5386
+ const mm = cleaned.match(/^([A-Za-z_]\w*)\s*:\s*(.+)$/);
5387
+ if (mm) return { name: mm[1], type: mm[2].trim() };
5388
+ return { name: cleaned.replace(/[^\w].*$/, "") || cleaned, type: null };
5389
+ });
5390
+ }
5391
+ function extractFactsKotlin(source, file = "input.kt") {
5392
+ let m;
5393
+ const functions = [];
5394
+ const tests = [];
5395
+ const seen = /* @__PURE__ */ new Set();
5396
+ const testMethods = /* @__PURE__ */ new Set();
5397
+ const ta = /@Test\b[\s\S]{0,120}?\bfun\s+(?:`([^`]+)`|(\w+))\s*\(/g;
5398
+ while (m = ta.exec(source)) testMethods.add(m[1] || m[2]);
5399
+ const fnRe = /\bfun\s+(?:<[^>]*>\s*)?(?:[A-Za-z_][\w.]*\.)?(?:`([^`]+)`|([A-Za-z_]\w*))\s*\(([^)]*)\)\s*(?::\s*([^{=\n]+))?/g;
5400
+ while (m = fnRe.exec(source)) {
5401
+ const name = m[1] || m[2];
5402
+ if (testMethods.has(name)) {
5403
+ if (!tests.some((t) => t.name === name)) tests.push({ name, file, line: lineOf(source, m.index) });
5404
+ continue;
5405
+ }
5406
+ if (seen.has(name)) continue;
5407
+ seen.add(name);
5408
+ 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) }] });
5409
+ }
5410
+ const errors = [];
5411
+ const addErr = addErrOf(errors, /* @__PURE__ */ new Set(), { _src: source, _file: file });
5412
+ let mm;
5413
+ const ce = /class\s+(\w*(?:Exception|Error))\b/g;
5414
+ while (mm = ce.exec(source)) addErr(mm[1], mm.index);
5415
+ const th = /throw\s+(\w+)\s*\(/g;
5416
+ while (mm = th.exec(source)) addErr(mm[1], mm.index);
5417
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "kotlin", sourceRoot: file, functions, tests, errors };
5418
+ }
5419
+ function extractFactsScala(source, file = "input.scala") {
5420
+ let m;
5421
+ const functions = [];
5422
+ const tests = [];
5423
+ const seen = /* @__PURE__ */ new Set();
5424
+ const fnRe = /\bdef\s+([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\(([^)]*)\)\s*(?::\s*([^={\n]+))?/g;
5425
+ while (m = fnRe.exec(source)) {
5426
+ const name = m[1];
5427
+ if (seen.has(name)) continue;
5428
+ seen.add(name);
5429
+ 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) }] });
5430
+ }
5431
+ const seenT = /* @__PURE__ */ new Set();
5432
+ const addTest = (n, idx) => {
5433
+ const k = String(n).toLowerCase();
5434
+ if (n && !seenT.has(k)) {
5435
+ seenT.add(k);
5436
+ tests.push({ name: n, file, line: lineOf(source, idx) });
5437
+ }
5438
+ };
5439
+ const tr = /\btest\s*\(\s*"([^"]+)"/g;
5440
+ while (m = tr.exec(source)) addTest(m[1], m.index);
5441
+ const inRe = /"([^"]+)"\s+(?:in|should|must)\b/g;
5442
+ while (m = inRe.exec(source)) addTest(m[1], m.index);
5443
+ const errors = [];
5444
+ const addErr = addErrOf(errors, /* @__PURE__ */ new Set(), { _src: source, _file: file });
5445
+ let mm;
5446
+ const ce = /class\s+(\w*(?:Exception|Error))\b/g;
5447
+ while (mm = ce.exec(source)) addErr(mm[1], mm.index);
5448
+ const th = /throw\s+new\s+(\w+)\s*\(/g;
5449
+ while (mm = th.exec(source)) addErr(mm[1], mm.index);
5450
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "scala", sourceRoot: file, functions, tests, errors };
5451
+ }
5452
+ function extractFactsElixir(source, file = "input.ex") {
5453
+ let m;
5454
+ const functions = [];
5455
+ const tests = [];
5456
+ const seen = /* @__PURE__ */ new Set();
5457
+ const seenT = /* @__PURE__ */ new Set();
5458
+ const addTest = (n, idx) => {
5459
+ const k = String(n).toLowerCase();
5460
+ if (n && !seenT.has(k)) {
5461
+ seenT.add(k);
5462
+ tests.push({ name: n, file, line: lineOf(source, idx) });
5463
+ }
5464
+ };
5465
+ const tr = /\btest\s+"([^"]+)"/g;
5466
+ while (m = tr.exec(source)) addTest(m[1], m.index);
5467
+ const fnRe = /^[ \t]*defp?\s+([a-z_]\w*[!?]?)\s*(?:\(([^)]*)\))?/gm;
5468
+ while (m = fnRe.exec(source)) {
5469
+ const name = m[1];
5470
+ if (seen.has(name)) continue;
5471
+ seen.add(name);
5472
+ const parameters = splitTopLevel(m[2] || "", ",").map((p) => p.trim()).filter(Boolean).map((p) => ({ name: p.split(/\\\\/)[0].trim().replace(/^%\{?/, "").replace(/[^\w].*$/, "") || p, type: null }));
5473
+ 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) }] });
5474
+ }
5475
+ const errors = [];
5476
+ const seenErr = /* @__PURE__ */ new Set();
5477
+ const addErr = (n, idx) => {
5478
+ if (n && !seenErr.has(n)) {
5479
+ seenErr.add(n);
5480
+ errors.push({ name: n, file, line: lineOf(source, idx) });
5481
+ }
5482
+ };
5483
+ const modErr = /defmodule\s+([\w.]*(?:Error|Exception))\b/g;
5484
+ while (m = modErr.exec(source)) addErr(m[1].split(".").pop(), m.index);
5485
+ const raiseRe = /raise\s+([A-Z][\w.]*)/g;
5486
+ while (m = raiseRe.exec(source)) addErr(m[1].split(".").pop(), m.index);
5487
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "elixir", sourceRoot: file, functions, tests, errors };
5488
+ }
5381
5489
  var ADAPTERS = {
5382
5490
  typescript: extractFactsTypeScript,
5383
5491
  ts: extractFactsTypeScript,
@@ -5401,10 +5509,18 @@ var ADAPTERS = {
5401
5509
  cc: extractFactsCpp,
5402
5510
  php: extractFactsPhp,
5403
5511
  ruby: extractFactsRuby,
5404
- rb: extractFactsRuby
5512
+ rb: extractFactsRuby,
5513
+ kotlin: extractFactsKotlin,
5514
+ kt: extractFactsKotlin,
5515
+ kts: extractFactsKotlin,
5516
+ scala: extractFactsScala,
5517
+ sc: extractFactsScala,
5518
+ elixir: extractFactsElixir,
5519
+ ex: extractFactsElixir,
5520
+ exs: extractFactsElixir
5405
5521
  };
5406
- var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "java", "csharp", "go", "rust", "cpp", "php", "ruby", "perl"];
5407
- var DYNAMIC_LANGUAGES = /* @__PURE__ */ new Set(["perl", "javascript", "python", "ruby", "php"]);
5522
+ var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "java", "csharp", "go", "rust", "cpp", "php", "ruby", "perl", "kotlin", "scala", "elixir"];
5523
+ var DYNAMIC_LANGUAGES = /* @__PURE__ */ new Set(["perl", "javascript", "python", "ruby", "php", "elixir"]);
5408
5524
  var LANG_DISPLAY = {
5409
5525
  typescript: "TypeScript",
5410
5526
  javascript: "JavaScript",
@@ -5416,7 +5532,10 @@ var LANG_DISPLAY = {
5416
5532
  cpp: "C++",
5417
5533
  php: "PHP",
5418
5534
  ruby: "Ruby",
5419
- perl: "Perl"
5535
+ perl: "Perl",
5536
+ kotlin: "Kotlin",
5537
+ scala: "Scala",
5538
+ elixir: "Elixir"
5420
5539
  };
5421
5540
  function unwrapReturn(ret) {
5422
5541
  if (!ret) return { output: null, error: null };
@@ -5540,6 +5659,9 @@ function languageForFile(file) {
5540
5659
  if (/\.(cpp|cc|cxx|hpp|hh|c|h)$/i.test(file)) return "cpp";
5541
5660
  if (/\.php$/i.test(file)) return "php";
5542
5661
  if (/\.rb$/i.test(file)) return "ruby";
5662
+ if (/\.kts?$/i.test(file)) return "kotlin";
5663
+ if (/\.(scala|sc)$/i.test(file)) return "scala";
5664
+ if (/\.exs?$/i.test(file)) return "elixir";
5543
5665
  if (/\.(mjs|cjs|jsx?)$/i.test(file)) return "javascript";
5544
5666
  return "typescript";
5545
5667
  }
@@ -5547,7 +5669,7 @@ function isPublicFn(fn, language) {
5547
5669
  const name = fn.name || "";
5548
5670
  if (/(?:Internal|Impl|_impl|_helper|_test|Helper)$/.test(name)) return false;
5549
5671
  if (language === "go" || language === "golang") return /^[A-Z]/.test(name) && name !== "Test";
5550
- if (language === "python" || language === "ruby") return !name.startsWith("_") && (fn.indent == null || fn.indent <= 4);
5672
+ if (language === "python" || language === "ruby" || language === "elixir") return !name.startsWith("_") && (fn.indent == null || fn.indent <= 4);
5551
5673
  return !name.startsWith("_") && name !== "init" && name !== "constructor";
5552
5674
  }
5553
5675
  function liftAll(source, { language = "typescript", file = "", publicOnly = true } = {}) {
@@ -5622,7 +5744,10 @@ var LANG_EXT = {
5622
5744
  cpp: "cpp",
5623
5745
  php: "php",
5624
5746
  ruby: "rb",
5625
- perl: "pl"
5747
+ perl: "pl",
5748
+ kotlin: "kt",
5749
+ scala: "scala",
5750
+ elixir: "ex"
5626
5751
  };
5627
5752
  function liftSource(source, { language = "typescript", file = "", seeds = void 0 } = {}) {
5628
5753
  const key = String(language).toLowerCase();
@@ -6548,7 +6673,7 @@ function toCSharp(ast) {
6548
6673
  try {
6549
6674
  cond = exprToCSharp(r.when, { inputs });
6550
6675
  } catch {
6551
- cond = `false /* TODO: "${r.when}" */`;
6676
+ cond = `false /* TODO: could not translate "${r.when}" */`;
6552
6677
  }
6553
6678
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
6554
6679
  }
@@ -6557,6 +6682,7 @@ function toCSharp(ast) {
6557
6682
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
6558
6683
  const outName = (ast.outputs || []).length ? `${subject}Output` : "void";
6559
6684
  L.push(` public static ${outName} Run(${inName ? `${inName} input` : ""})`, " {");
6685
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
6560
6686
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
6561
6687
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
6562
6688
  L.push(' throw new NotImplementedException("TODO: implement , the intent above defines what this must do.");', " }");
@@ -6617,7 +6743,7 @@ function toJava(ast) {
6617
6743
  try {
6618
6744
  cond = exprToJava(r.when, { inputs });
6619
6745
  } catch {
6620
- cond = `false /* TODO: "${r.when}" */`;
6746
+ cond = `false /* TODO: could not translate "${r.when}" */`;
6621
6747
  }
6622
6748
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
6623
6749
  }
@@ -6626,18 +6752,98 @@ function toJava(ast) {
6626
6752
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
6627
6753
  const outName = (ast.outputs || []).length ? `${subject}Output` : "void";
6628
6754
  L.push(` public static ${outName} run(${inName ? `${inName} input` : ""}) {`);
6755
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
6629
6756
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
6630
6757
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
6631
6758
  L.push(' throw new UnsupportedOperationException("TODO: implement , the intent above defines what this must do.");', " }");
6632
6759
  L.push("}", "");
6633
6760
  return L.join("\n");
6634
6761
  }
6762
+ var PY_TYPES = {
6763
+ Email: "str",
6764
+ Url: "str",
6765
+ UserId: "str",
6766
+ AccountId: "str",
6767
+ OrderId: "str",
6768
+ InvoiceId: "str",
6769
+ CustomerId: "str",
6770
+ Secret: "str",
6771
+ Token: "str",
6772
+ Jwt: "str",
6773
+ IdempotencyKey: "str",
6774
+ Currency: "str",
6775
+ Date: "str",
6776
+ DateTime: "str",
6777
+ Money: "float",
6778
+ Percentage: "float",
6779
+ Count: "int",
6780
+ Duration: "int",
6781
+ Flag: "bool"
6782
+ };
6783
+ function pyType(type, domain) {
6784
+ if (!type) return "object";
6785
+ const list = /^List<(.+)>$/.exec(type);
6786
+ if (list) return `list[${pyType(list[1], domain)}]`;
6787
+ const base = type.replace(/\(.*\)/, "");
6788
+ if (PY_TYPES[base]) return PY_TYPES[base];
6789
+ domain.add(base);
6790
+ return base;
6791
+ }
6792
+ var snake = (s) => pascal2(s).replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
6793
+ function toPython(ast) {
6794
+ const subject = pascal2(subjectName(ast) || "Intent");
6795
+ const domain = /* @__PURE__ */ new Set();
6796
+ const L = [
6797
+ `# ${subject} , generated from ThunderLang (intent-codegen-v1). Deterministic, no AI.`,
6798
+ "# The dataclass contract and the decision logic are fully determined by the intent;",
6799
+ "# business logic marked TODO is yours, bound by the guarantees and never-rules below.",
6800
+ ""
6801
+ ];
6802
+ const hasData = (ast.inputs || []).length || (ast.outputs || []).length;
6803
+ if (hasData) L.push("from __future__ import annotations", "from dataclasses import dataclass", "");
6804
+ const cls = (name, fields) => fields.length ? ["@dataclass", `class ${name}:`, ...fields.map((f) => ` ${f.name}: ${pyType(f.type, domain)}`), ""] : [];
6805
+ if ((ast.inputs || []).length) L.push(...cls(`${subject}Input`, ast.inputs));
6806
+ if ((ast.outputs || []).length) L.push(...cls(`${subject}Output`, ast.outputs));
6807
+ for (const d of ast.decisions || []) {
6808
+ const inputs = d.inputs || [];
6809
+ L.push(`# decision ${d.name} , first matching rule wins.`);
6810
+ L.push(`def ${snake(d.name)}(${inputs.join(", ")}) -> str:`);
6811
+ for (const r of d.rules || []) {
6812
+ let cond, note = "";
6813
+ try {
6814
+ cond = exprToPython(r.when, { inputs });
6815
+ } catch {
6816
+ cond = "False";
6817
+ note = ` # TODO: could not translate "${r.when}"`;
6818
+ }
6819
+ L.push(` if ${cond}:${note}`);
6820
+ L.push(` return ${JSON.stringify(r.result)} # rule ${r.name}`);
6821
+ }
6822
+ L.push(` return ${JSON.stringify(d.default ?? "Undecided")} # default`, "");
6823
+ }
6824
+ const inName = (ast.inputs || []).length ? `${subject}Input` : null;
6825
+ const outName = (ast.outputs || []).length ? `${subject}Output` : "None";
6826
+ L.push(`def run(${inName ? `input: ${inName}` : ""}) -> ${outName}:`);
6827
+ for (const r of ast.requires || []) L.push(` # precondition: ${r} , TODO: validate`);
6828
+ for (const n of ast.neverRules || []) L.push(` # NEVER: ${n.statement}`);
6829
+ for (const g of ast.guarantees || []) L.push(` # GUARANTEE: ${g.statement}`);
6830
+ L.push(' raise NotImplementedError("TODO: implement , the intent above defines what this must do.")', "");
6831
+ const stubs = [...domain].filter((t) => !PY_TYPES[t] && /^[A-Z]/.test(t)).sort();
6832
+ if (stubs.length) {
6833
+ L.push("# Domain types referenced by the intent , complete these.");
6834
+ for (const t of stubs) L.push(`class ${t}: pass # TODO: fields`);
6835
+ L.push("");
6836
+ }
6837
+ return L.join("\n");
6838
+ }
6635
6839
  var GENERATORS = {
6636
6840
  typescript: toTypeScript,
6637
6841
  ts: toTypeScript,
6638
6842
  csharp: toCSharp,
6639
6843
  cs: toCSharp,
6640
- java: toJava
6844
+ java: toJava,
6845
+ python: toPython,
6846
+ py: toPython
6641
6847
  };
6642
6848
 
6643
6849
  // src/changes.mjs
@@ -6854,6 +7060,7 @@ function changeReport(pairs) {
6854
7060
  toDesignTokens,
6855
7061
  toFinding,
6856
7062
  toJava,
7063
+ toPython,
6857
7064
  toTypeScript,
6858
7065
  twelveFactorReport,
6859
7066
  twelveFactorSummary,