@skillstech/thunderlang 0.2.0 → 0.3.0

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,19 @@
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.3.0
7
+
8
+ The 14-language lift release. Additive; no breaking changes. Restores the OpenThunder -> ThunderLang
9
+ recover-intent loop for every language OpenThunder's archgraph core can now discover.
10
+
11
+ - **Kotlin, Scala, and Elixir lift adapters.** `thunder lift` now extracts candidate intent from
12
+ `.kt`/`.kts`, `.scala`/`.sc`, and `.ex`/`.exs`, bringing the supported-language count to 14. A new
13
+ `name: Type` parameter parser handles the JVM signature shape; Elixir joins the dynamic-language set.
14
+ - **Fix: repo-walk language coverage.** `LIFT_EXTS` was stale and silently skipped Python, Java, C#,
15
+ Go, C++, PHP, and Ruby during repo/`--all` lifts; it now mirrors the full adapter set.
16
+ - **Fix: single-file lift language detection.** `thunder lift <file>` now auto-detects the language by
17
+ extension instead of defaulting to TypeScript, consistent with `--all`/repo modes.
18
+
6
19
  ## 0.2.0
7
20
 
8
21
  The cross-language target execution release. Additive; no breaking changes.
package/dist/core.cjs CHANGED
@@ -4288,7 +4288,7 @@ function notesSummary(ast) {
4288
4288
  byLens
4289
4289
  };
4290
4290
  }
4291
- var COMPILER_VERSION = "0.2.0";
4291
+ var COMPILER_VERSION = "0.3.0";
4292
4292
  var PROOF_SCHEMA_VERSION = "0.1.0";
4293
4293
  var SOURCE_PRODUCT = "skillstech-compiler";
4294
4294
  function buildContractGraph(ast, generatedAt) {
@@ -5378,6 +5378,112 @@ function extractFactsRuby(source, file = "input.rb") {
5378
5378
  }
5379
5379
  return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "ruby", sourceRoot: file, functions, tests, errors };
5380
5380
  }
5381
+ function parseNameColonTypeParams(raw) {
5382
+ return splitTopLevel(raw, ",").map((p) => p.trim()).filter(Boolean).map((p) => {
5383
+ const cleaned = p.replace(/@\w+(\([^)]*\))?/g, "").replace(/^(?:val|var|vararg|implicit|final|lazy)\s+/, "").replace(/=.*$/, "").trim();
5384
+ const mm = cleaned.match(/^([A-Za-z_]\w*)\s*:\s*(.+)$/);
5385
+ if (mm) return { name: mm[1], type: mm[2].trim() };
5386
+ return { name: cleaned.replace(/[^\w].*$/, "") || cleaned, type: null };
5387
+ });
5388
+ }
5389
+ function extractFactsKotlin(source, file = "input.kt") {
5390
+ let m;
5391
+ const functions = [];
5392
+ const tests = [];
5393
+ const seen = /* @__PURE__ */ new Set();
5394
+ const testMethods = /* @__PURE__ */ new Set();
5395
+ const ta = /@Test\b[\s\S]{0,120}?\bfun\s+(?:`([^`]+)`|(\w+))\s*\(/g;
5396
+ while (m = ta.exec(source)) testMethods.add(m[1] || m[2]);
5397
+ const fnRe = /\bfun\s+(?:<[^>]*>\s*)?(?:[A-Za-z_][\w.]*\.)?(?:`([^`]+)`|([A-Za-z_]\w*))\s*\(([^)]*)\)\s*(?::\s*([^{=\n]+))?/g;
5398
+ while (m = fnRe.exec(source)) {
5399
+ const name = m[1] || m[2];
5400
+ if (testMethods.has(name)) {
5401
+ if (!tests.some((t) => t.name === name)) tests.push({ name, file, line: lineOf(source, m.index) });
5402
+ continue;
5403
+ }
5404
+ if (seen.has(name)) continue;
5405
+ seen.add(name);
5406
+ 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) }] });
5407
+ }
5408
+ const errors = [];
5409
+ const addErr = addErrOf(errors, /* @__PURE__ */ new Set(), { _src: source, _file: file });
5410
+ let mm;
5411
+ const ce = /class\s+(\w*(?:Exception|Error))\b/g;
5412
+ while (mm = ce.exec(source)) addErr(mm[1], mm.index);
5413
+ const th = /throw\s+(\w+)\s*\(/g;
5414
+ while (mm = th.exec(source)) addErr(mm[1], mm.index);
5415
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "kotlin", sourceRoot: file, functions, tests, errors };
5416
+ }
5417
+ function extractFactsScala(source, file = "input.scala") {
5418
+ let m;
5419
+ const functions = [];
5420
+ const tests = [];
5421
+ const seen = /* @__PURE__ */ new Set();
5422
+ const fnRe = /\bdef\s+([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\(([^)]*)\)\s*(?::\s*([^={\n]+))?/g;
5423
+ while (m = fnRe.exec(source)) {
5424
+ const name = m[1];
5425
+ if (seen.has(name)) continue;
5426
+ seen.add(name);
5427
+ 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) }] });
5428
+ }
5429
+ const seenT = /* @__PURE__ */ new Set();
5430
+ const addTest = (n, idx) => {
5431
+ const k = String(n).toLowerCase();
5432
+ if (n && !seenT.has(k)) {
5433
+ seenT.add(k);
5434
+ tests.push({ name: n, file, line: lineOf(source, idx) });
5435
+ }
5436
+ };
5437
+ const tr = /\btest\s*\(\s*"([^"]+)"/g;
5438
+ while (m = tr.exec(source)) addTest(m[1], m.index);
5439
+ const inRe = /"([^"]+)"\s+(?:in|should|must)\b/g;
5440
+ while (m = inRe.exec(source)) addTest(m[1], m.index);
5441
+ const errors = [];
5442
+ const addErr = addErrOf(errors, /* @__PURE__ */ new Set(), { _src: source, _file: file });
5443
+ let mm;
5444
+ const ce = /class\s+(\w*(?:Exception|Error))\b/g;
5445
+ while (mm = ce.exec(source)) addErr(mm[1], mm.index);
5446
+ const th = /throw\s+new\s+(\w+)\s*\(/g;
5447
+ while (mm = th.exec(source)) addErr(mm[1], mm.index);
5448
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "scala", sourceRoot: file, functions, tests, errors };
5449
+ }
5450
+ function extractFactsElixir(source, file = "input.ex") {
5451
+ let m;
5452
+ const functions = [];
5453
+ const tests = [];
5454
+ const seen = /* @__PURE__ */ new Set();
5455
+ const seenT = /* @__PURE__ */ new Set();
5456
+ const addTest = (n, idx) => {
5457
+ const k = String(n).toLowerCase();
5458
+ if (n && !seenT.has(k)) {
5459
+ seenT.add(k);
5460
+ tests.push({ name: n, file, line: lineOf(source, idx) });
5461
+ }
5462
+ };
5463
+ const tr = /\btest\s+"([^"]+)"/g;
5464
+ while (m = tr.exec(source)) addTest(m[1], m.index);
5465
+ const fnRe = /^[ \t]*defp?\s+([a-z_]\w*[!?]?)\s*(?:\(([^)]*)\))?/gm;
5466
+ while (m = fnRe.exec(source)) {
5467
+ const name = m[1];
5468
+ if (seen.has(name)) continue;
5469
+ seen.add(name);
5470
+ const parameters = splitTopLevel(m[2] || "", ",").map((p) => p.trim()).filter(Boolean).map((p) => ({ name: p.split(/\\\\/)[0].trim().replace(/^%\{?/, "").replace(/[^\w].*$/, "") || p, type: null }));
5471
+ 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) }] });
5472
+ }
5473
+ const errors = [];
5474
+ const seenErr = /* @__PURE__ */ new Set();
5475
+ const addErr = (n, idx) => {
5476
+ if (n && !seenErr.has(n)) {
5477
+ seenErr.add(n);
5478
+ errors.push({ name: n, file, line: lineOf(source, idx) });
5479
+ }
5480
+ };
5481
+ const modErr = /defmodule\s+([\w.]*(?:Error|Exception))\b/g;
5482
+ while (m = modErr.exec(source)) addErr(m[1].split(".").pop(), m.index);
5483
+ const raiseRe = /raise\s+([A-Z][\w.]*)/g;
5484
+ while (m = raiseRe.exec(source)) addErr(m[1].split(".").pop(), m.index);
5485
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "elixir", sourceRoot: file, functions, tests, errors };
5486
+ }
5381
5487
  var ADAPTERS = {
5382
5488
  typescript: extractFactsTypeScript,
5383
5489
  ts: extractFactsTypeScript,
@@ -5401,10 +5507,18 @@ var ADAPTERS = {
5401
5507
  cc: extractFactsCpp,
5402
5508
  php: extractFactsPhp,
5403
5509
  ruby: extractFactsRuby,
5404
- rb: extractFactsRuby
5510
+ rb: extractFactsRuby,
5511
+ kotlin: extractFactsKotlin,
5512
+ kt: extractFactsKotlin,
5513
+ kts: extractFactsKotlin,
5514
+ scala: extractFactsScala,
5515
+ sc: extractFactsScala,
5516
+ elixir: extractFactsElixir,
5517
+ ex: extractFactsElixir,
5518
+ exs: extractFactsElixir
5405
5519
  };
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"]);
5520
+ var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "java", "csharp", "go", "rust", "cpp", "php", "ruby", "perl", "kotlin", "scala", "elixir"];
5521
+ var DYNAMIC_LANGUAGES = /* @__PURE__ */ new Set(["perl", "javascript", "python", "ruby", "php", "elixir"]);
5408
5522
  var LANG_DISPLAY = {
5409
5523
  typescript: "TypeScript",
5410
5524
  javascript: "JavaScript",
@@ -5416,7 +5530,10 @@ var LANG_DISPLAY = {
5416
5530
  cpp: "C++",
5417
5531
  php: "PHP",
5418
5532
  ruby: "Ruby",
5419
- perl: "Perl"
5533
+ perl: "Perl",
5534
+ kotlin: "Kotlin",
5535
+ scala: "Scala",
5536
+ elixir: "Elixir"
5420
5537
  };
5421
5538
  function unwrapReturn(ret) {
5422
5539
  if (!ret) return { output: null, error: null };
@@ -5540,6 +5657,9 @@ function languageForFile(file) {
5540
5657
  if (/\.(cpp|cc|cxx|hpp|hh|c|h)$/i.test(file)) return "cpp";
5541
5658
  if (/\.php$/i.test(file)) return "php";
5542
5659
  if (/\.rb$/i.test(file)) return "ruby";
5660
+ if (/\.kts?$/i.test(file)) return "kotlin";
5661
+ if (/\.(scala|sc)$/i.test(file)) return "scala";
5662
+ if (/\.exs?$/i.test(file)) return "elixir";
5543
5663
  if (/\.(mjs|cjs|jsx?)$/i.test(file)) return "javascript";
5544
5664
  return "typescript";
5545
5665
  }
@@ -5547,7 +5667,7 @@ function isPublicFn(fn, language) {
5547
5667
  const name = fn.name || "";
5548
5668
  if (/(?:Internal|Impl|_impl|_helper|_test|Helper)$/.test(name)) return false;
5549
5669
  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);
5670
+ if (language === "python" || language === "ruby" || language === "elixir") return !name.startsWith("_") && (fn.indent == null || fn.indent <= 4);
5551
5671
  return !name.startsWith("_") && name !== "init" && name !== "constructor";
5552
5672
  }
5553
5673
  function liftAll(source, { language = "typescript", file = "", publicOnly = true } = {}) {
@@ -5622,7 +5742,10 @@ var LANG_EXT = {
5622
5742
  cpp: "cpp",
5623
5743
  php: "php",
5624
5744
  ruby: "rb",
5625
- perl: "pl"
5745
+ perl: "pl",
5746
+ kotlin: "kt",
5747
+ scala: "scala",
5748
+ elixir: "ex"
5626
5749
  };
5627
5750
  function liftSource(source, { language = "typescript", file = "", seeds = void 0 } = {}) {
5628
5751
  const key = String(language).toLowerCase();
package/dist/index.cjs CHANGED
@@ -2287,7 +2287,7 @@ function notesSummary(ast) {
2287
2287
  byLens
2288
2288
  };
2289
2289
  }
2290
- var COMPILER_VERSION = "0.2.0";
2290
+ var COMPILER_VERSION = "0.3.0";
2291
2291
  var PROOF_SCHEMA_VERSION = "0.1.0";
2292
2292
  var SOURCE_PRODUCT = "skillstech-compiler";
2293
2293
  function buildContractGraph(ast, generatedAt) {
@@ -3753,6 +3753,112 @@ function extractFactsRuby(source, file = "input.rb") {
3753
3753
  }
3754
3754
  return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "ruby", sourceRoot: file, functions, tests, errors };
3755
3755
  }
3756
+ function parseNameColonTypeParams(raw) {
3757
+ return splitTopLevel(raw, ",").map((p) => p.trim()).filter(Boolean).map((p) => {
3758
+ const cleaned = p.replace(/@\w+(\([^)]*\))?/g, "").replace(/^(?:val|var|vararg|implicit|final|lazy)\s+/, "").replace(/=.*$/, "").trim();
3759
+ const mm = cleaned.match(/^([A-Za-z_]\w*)\s*:\s*(.+)$/);
3760
+ if (mm) return { name: mm[1], type: mm[2].trim() };
3761
+ return { name: cleaned.replace(/[^\w].*$/, "") || cleaned, type: null };
3762
+ });
3763
+ }
3764
+ function extractFactsKotlin(source, file = "input.kt") {
3765
+ let m;
3766
+ const functions = [];
3767
+ const tests = [];
3768
+ const seen = /* @__PURE__ */ new Set();
3769
+ const testMethods = /* @__PURE__ */ new Set();
3770
+ const ta = /@Test\b[\s\S]{0,120}?\bfun\s+(?:`([^`]+)`|(\w+))\s*\(/g;
3771
+ while (m = ta.exec(source)) testMethods.add(m[1] || m[2]);
3772
+ const fnRe = /\bfun\s+(?:<[^>]*>\s*)?(?:[A-Za-z_][\w.]*\.)?(?:`([^`]+)`|([A-Za-z_]\w*))\s*\(([^)]*)\)\s*(?::\s*([^{=\n]+))?/g;
3773
+ while (m = fnRe.exec(source)) {
3774
+ const name = m[1] || m[2];
3775
+ if (testMethods.has(name)) {
3776
+ if (!tests.some((t) => t.name === name)) tests.push({ name, file, line: lineOf(source, m.index) });
3777
+ continue;
3778
+ }
3779
+ if (seen.has(name)) continue;
3780
+ seen.add(name);
3781
+ 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) }] });
3782
+ }
3783
+ const errors = [];
3784
+ const addErr = addErrOf(errors, /* @__PURE__ */ new Set(), { _src: source, _file: file });
3785
+ let mm;
3786
+ const ce = /class\s+(\w*(?:Exception|Error))\b/g;
3787
+ while (mm = ce.exec(source)) addErr(mm[1], mm.index);
3788
+ const th = /throw\s+(\w+)\s*\(/g;
3789
+ while (mm = th.exec(source)) addErr(mm[1], mm.index);
3790
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "kotlin", sourceRoot: file, functions, tests, errors };
3791
+ }
3792
+ function extractFactsScala(source, file = "input.scala") {
3793
+ let m;
3794
+ const functions = [];
3795
+ const tests = [];
3796
+ const seen = /* @__PURE__ */ new Set();
3797
+ const fnRe = /\bdef\s+([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\(([^)]*)\)\s*(?::\s*([^={\n]+))?/g;
3798
+ while (m = fnRe.exec(source)) {
3799
+ const name = m[1];
3800
+ if (seen.has(name)) continue;
3801
+ seen.add(name);
3802
+ 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) }] });
3803
+ }
3804
+ const seenT = /* @__PURE__ */ new Set();
3805
+ const addTest = (n, idx) => {
3806
+ const k = String(n).toLowerCase();
3807
+ if (n && !seenT.has(k)) {
3808
+ seenT.add(k);
3809
+ tests.push({ name: n, file, line: lineOf(source, idx) });
3810
+ }
3811
+ };
3812
+ const tr = /\btest\s*\(\s*"([^"]+)"/g;
3813
+ while (m = tr.exec(source)) addTest(m[1], m.index);
3814
+ const inRe = /"([^"]+)"\s+(?:in|should|must)\b/g;
3815
+ while (m = inRe.exec(source)) addTest(m[1], m.index);
3816
+ const errors = [];
3817
+ const addErr = addErrOf(errors, /* @__PURE__ */ new Set(), { _src: source, _file: file });
3818
+ let mm;
3819
+ const ce = /class\s+(\w*(?:Exception|Error))\b/g;
3820
+ while (mm = ce.exec(source)) addErr(mm[1], mm.index);
3821
+ const th = /throw\s+new\s+(\w+)\s*\(/g;
3822
+ while (mm = th.exec(source)) addErr(mm[1], mm.index);
3823
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "scala", sourceRoot: file, functions, tests, errors };
3824
+ }
3825
+ function extractFactsElixir(source, file = "input.ex") {
3826
+ let m;
3827
+ const functions = [];
3828
+ const tests = [];
3829
+ const seen = /* @__PURE__ */ new Set();
3830
+ const seenT = /* @__PURE__ */ new Set();
3831
+ const addTest = (n, idx) => {
3832
+ const k = String(n).toLowerCase();
3833
+ if (n && !seenT.has(k)) {
3834
+ seenT.add(k);
3835
+ tests.push({ name: n, file, line: lineOf(source, idx) });
3836
+ }
3837
+ };
3838
+ const tr = /\btest\s+"([^"]+)"/g;
3839
+ while (m = tr.exec(source)) addTest(m[1], m.index);
3840
+ const fnRe = /^[ \t]*defp?\s+([a-z_]\w*[!?]?)\s*(?:\(([^)]*)\))?/gm;
3841
+ while (m = fnRe.exec(source)) {
3842
+ const name = m[1];
3843
+ if (seen.has(name)) continue;
3844
+ seen.add(name);
3845
+ const parameters = splitTopLevel(m[2] || "", ",").map((p) => p.trim()).filter(Boolean).map((p) => ({ name: p.split(/\\\\/)[0].trim().replace(/^%\{?/, "").replace(/[^\w].*$/, "") || p, type: null }));
3846
+ 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) }] });
3847
+ }
3848
+ const errors = [];
3849
+ const seenErr = /* @__PURE__ */ new Set();
3850
+ const addErr = (n, idx) => {
3851
+ if (n && !seenErr.has(n)) {
3852
+ seenErr.add(n);
3853
+ errors.push({ name: n, file, line: lineOf(source, idx) });
3854
+ }
3855
+ };
3856
+ const modErr = /defmodule\s+([\w.]*(?:Error|Exception))\b/g;
3857
+ while (m = modErr.exec(source)) addErr(m[1].split(".").pop(), m.index);
3858
+ const raiseRe = /raise\s+([A-Z][\w.]*)/g;
3859
+ while (m = raiseRe.exec(source)) addErr(m[1].split(".").pop(), m.index);
3860
+ return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: "elixir", sourceRoot: file, functions, tests, errors };
3861
+ }
3756
3862
  var ADAPTERS = {
3757
3863
  typescript: extractFactsTypeScript,
3758
3864
  ts: extractFactsTypeScript,
@@ -3776,10 +3882,18 @@ var ADAPTERS = {
3776
3882
  cc: extractFactsCpp,
3777
3883
  php: extractFactsPhp,
3778
3884
  ruby: extractFactsRuby,
3779
- rb: extractFactsRuby
3885
+ rb: extractFactsRuby,
3886
+ kotlin: extractFactsKotlin,
3887
+ kt: extractFactsKotlin,
3888
+ kts: extractFactsKotlin,
3889
+ scala: extractFactsScala,
3890
+ sc: extractFactsScala,
3891
+ elixir: extractFactsElixir,
3892
+ ex: extractFactsElixir,
3893
+ exs: extractFactsElixir
3780
3894
  };
3781
- var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "java", "csharp", "go", "rust", "cpp", "php", "ruby", "perl"];
3782
- var DYNAMIC_LANGUAGES = /* @__PURE__ */ new Set(["perl", "javascript", "python", "ruby", "php"]);
3895
+ var SUPPORTED_LANGUAGES = ["typescript", "javascript", "python", "java", "csharp", "go", "rust", "cpp", "php", "ruby", "perl", "kotlin", "scala", "elixir"];
3896
+ var DYNAMIC_LANGUAGES = /* @__PURE__ */ new Set(["perl", "javascript", "python", "ruby", "php", "elixir"]);
3783
3897
  var LANG_DISPLAY = {
3784
3898
  typescript: "TypeScript",
3785
3899
  javascript: "JavaScript",
@@ -3791,7 +3905,10 @@ var LANG_DISPLAY = {
3791
3905
  cpp: "C++",
3792
3906
  php: "PHP",
3793
3907
  ruby: "Ruby",
3794
- perl: "Perl"
3908
+ perl: "Perl",
3909
+ kotlin: "Kotlin",
3910
+ scala: "Scala",
3911
+ elixir: "Elixir"
3795
3912
  };
3796
3913
  function unwrapReturn(ret) {
3797
3914
  if (!ret) return { output: null, error: null };
@@ -3915,6 +4032,9 @@ function languageForFile(file) {
3915
4032
  if (/\.(cpp|cc|cxx|hpp|hh|c|h)$/i.test(file)) return "cpp";
3916
4033
  if (/\.php$/i.test(file)) return "php";
3917
4034
  if (/\.rb$/i.test(file)) return "ruby";
4035
+ if (/\.kts?$/i.test(file)) return "kotlin";
4036
+ if (/\.(scala|sc)$/i.test(file)) return "scala";
4037
+ if (/\.exs?$/i.test(file)) return "elixir";
3918
4038
  if (/\.(mjs|cjs|jsx?)$/i.test(file)) return "javascript";
3919
4039
  return "typescript";
3920
4040
  }
@@ -3922,7 +4042,7 @@ function isPublicFn(fn, language) {
3922
4042
  const name = fn.name || "";
3923
4043
  if (/(?:Internal|Impl|_impl|_helper|_test|Helper)$/.test(name)) return false;
3924
4044
  if (language === "go" || language === "golang") return /^[A-Z]/.test(name) && name !== "Test";
3925
- if (language === "python" || language === "ruby") return !name.startsWith("_") && (fn.indent == null || fn.indent <= 4);
4045
+ if (language === "python" || language === "ruby" || language === "elixir") return !name.startsWith("_") && (fn.indent == null || fn.indent <= 4);
3926
4046
  return !name.startsWith("_") && name !== "init" && name !== "constructor";
3927
4047
  }
3928
4048
  function liftAll(source, { language = "typescript", file = "", publicOnly = true } = {}) {
@@ -3997,7 +4117,10 @@ var LANG_EXT = {
3997
4117
  cpp: "cpp",
3998
4118
  php: "php",
3999
4119
  ruby: "rb",
4000
- perl: "pl"
4120
+ perl: "pl",
4121
+ kotlin: "kt",
4122
+ scala: "scala",
4123
+ elixir: "ex"
4001
4124
  };
4002
4125
  function liftSource(source, { language = "typescript", file = "", seeds = void 0 } = {}) {
4003
4126
  const key = String(language).toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillstech/thunderlang",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Deterministic ThunderLang compiler, CLI, and Language Server. No AI required. Turns .thunder files into the canonical Intent Graph, docs, and a proof, and EXECUTES the intent: run decisions, simulate lifecycles, and check outcome contracts. Interops with DMN, BPMN, JSON Schema, and OpenAPI.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
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.
@@ -846,9 +852,9 @@ test Example
846
852
  return;
847
853
  }
848
854
 
849
- // Single-file mode.
855
+ // Single-file mode. Auto-detect language by extension (consistent with --all / repo modes).
850
856
  const src = readFileSync(file, 'utf8');
851
- const res = liftSource(src, { language: args.from || 'typescript', file: basename(file) });
857
+ const res = liftSource(src, { language: args.from || languageForFile(file), file: basename(file) });
852
858
  if (!res.ok) { console.error(res.error); process.exit(1); }
853
859
  if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
854
860
  if (args.out) {
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.3.0';
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/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
  /**