@skillstech/thunderlang 0.3.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,32 @@
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
+
6
32
  ## 0.3.0
7
33
 
8
34
  The 14-language lift release. Additive; no breaking changes. Restores the OpenThunder -> ThunderLang
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.3.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) {
@@ -6671,7 +6673,7 @@ function toCSharp(ast) {
6671
6673
  try {
6672
6674
  cond = exprToCSharp(r.when, { inputs });
6673
6675
  } catch {
6674
- cond = `false /* TODO: "${r.when}" */`;
6676
+ cond = `false /* TODO: could not translate "${r.when}" */`;
6675
6677
  }
6676
6678
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
6677
6679
  }
@@ -6680,6 +6682,7 @@ function toCSharp(ast) {
6680
6682
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
6681
6683
  const outName = (ast.outputs || []).length ? `${subject}Output` : "void";
6682
6684
  L.push(` public static ${outName} Run(${inName ? `${inName} input` : ""})`, " {");
6685
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
6683
6686
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
6684
6687
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
6685
6688
  L.push(' throw new NotImplementedException("TODO: implement , the intent above defines what this must do.");', " }");
@@ -6740,7 +6743,7 @@ function toJava(ast) {
6740
6743
  try {
6741
6744
  cond = exprToJava(r.when, { inputs });
6742
6745
  } catch {
6743
- cond = `false /* TODO: "${r.when}" */`;
6746
+ cond = `false /* TODO: could not translate "${r.when}" */`;
6744
6747
  }
6745
6748
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
6746
6749
  }
@@ -6749,18 +6752,98 @@ function toJava(ast) {
6749
6752
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
6750
6753
  const outName = (ast.outputs || []).length ? `${subject}Output` : "void";
6751
6754
  L.push(` public static ${outName} run(${inName ? `${inName} input` : ""}) {`);
6755
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
6752
6756
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
6753
6757
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
6754
6758
  L.push(' throw new UnsupportedOperationException("TODO: implement , the intent above defines what this must do.");', " }");
6755
6759
  L.push("}", "");
6756
6760
  return L.join("\n");
6757
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
+ }
6758
6839
  var GENERATORS = {
6759
6840
  typescript: toTypeScript,
6760
6841
  ts: toTypeScript,
6761
6842
  csharp: toCSharp,
6762
6843
  cs: toCSharp,
6763
- java: toJava
6844
+ java: toJava,
6845
+ python: toPython,
6846
+ py: toPython
6764
6847
  };
6765
6848
 
6766
6849
  // src/changes.mjs
@@ -6977,6 +7060,7 @@ function changeReport(pairs) {
6977
7060
  toDesignTokens,
6978
7061
  toFinding,
6979
7062
  toJava,
7063
+ toPython,
6980
7064
  toTypeScript,
6981
7065
  twelveFactorReport,
6982
7066
  twelveFactorSummary,
package/dist/index.cjs CHANGED
@@ -294,6 +294,7 @@ __export(index_exports, {
294
294
  toMermaid: () => toMermaid,
295
295
  toOpenAPI: () => toOpenAPI,
296
296
  toPlaywright: () => toPlaywright,
297
+ toPython: () => toPython,
297
298
  toSMV: () => toSMV,
298
299
  toSarif: () => toSarif,
299
300
  toTypeScript: () => toTypeScript,
@@ -2287,7 +2288,7 @@ function notesSummary(ast) {
2287
2288
  byLens
2288
2289
  };
2289
2290
  }
2290
- var COMPILER_VERSION = "0.3.0";
2291
+ var COMPILER_VERSION = "0.4.1";
2291
2292
  var PROOF_SCHEMA_VERSION = "0.1.0";
2292
2293
  var SOURCE_PRODUCT = "skillstech-compiler";
2293
2294
  function buildContractGraph(ast, generatedAt) {
@@ -5147,7 +5148,9 @@ function toPlaywright(ast) {
5147
5148
  const exps = ast.experiences || [];
5148
5149
  const L = [];
5149
5150
  L.push("// Playwright test scaffold generated from experience intent by @skillstech/thunderlang.");
5150
- L.push("// SKELETON: fill in selectors and assertions. It is not a passing test until you do.");
5151
+ L.push("// SKELETON: fill in selectors and assertions. Every test starts with a test.fixme");
5152
+ L.push('// guard so an unimplemented scaffold reports "fixme" instead of passing vacuously;');
5153
+ L.push("// remove the guard from each test once its body is real.");
5151
5154
  L.push("import { test, expect } from '@playwright/test';");
5152
5155
  L.push("");
5153
5156
  if (!exps.length) {
@@ -5162,6 +5165,7 @@ function toPlaywright(ast) {
5162
5165
  }
5163
5166
  for (const j of exp.journeys || []) {
5164
5167
  L.push(` test(${jsStr(j.name || "journey")}, async ({ page }) => {`);
5168
+ L.push(` test.fixme(true, ${jsStr("scaffold: implement the steps, then remove this guard")});`);
5165
5169
  if (!(j.steps || []).length) L.push(" // TODO: no steps declared for this journey");
5166
5170
  for (const step of j.steps || []) {
5167
5171
  L.push(` await test.step(${jsStr(step)}, async () => {`);
@@ -5173,10 +5177,12 @@ function toPlaywright(ast) {
5173
5177
  for (const st of exp.states || []) {
5174
5178
  if (st.hasRecovery) {
5175
5179
  L.push(` test(${jsStr(`failure state "${st.name}" offers a recovery path`)}, async ({ page }) => {`);
5180
+ L.push(` test.fixme(true, ${jsStr("scaffold: implement this state check, then remove this guard")});`);
5176
5181
  L.push(` // TODO: drive the UI into "${st.name}", assert a recovery action is available`);
5177
5182
  L.push(" });");
5178
5183
  } else {
5179
5184
  L.push(` test(${jsStr(`reaches state: ${st.name}`)}, async ({ page }) => {`);
5185
+ L.push(` test.fixme(true, ${jsStr("scaffold: implement this state check, then remove this guard")});`);
5180
5186
  L.push(` // TODO: drive the UI to "${st.name}" and assert it is shown`);
5181
5187
  L.push(" });");
5182
5188
  }
@@ -6351,6 +6357,7 @@ function exprToCode(src, { inputs = [], dialect = "js" } = {}) {
6351
6357
  var exprToJs = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "js" });
6352
6358
  var exprToCSharp = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "csharp" });
6353
6359
  var exprToJava = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "java" });
6360
+ var exprToPython = (src, opts = {}) => exprToCode(src, { ...opts, dialect: "python" });
6354
6361
 
6355
6362
  // src/runtime.mjs
6356
6363
  var RUNTIME_SCHEMA = "intent-runtime-v1";
@@ -7848,7 +7855,7 @@ function toCSharp(ast) {
7848
7855
  try {
7849
7856
  cond = exprToCSharp(r.when, { inputs });
7850
7857
  } catch {
7851
- cond = `false /* TODO: "${r.when}" */`;
7858
+ cond = `false /* TODO: could not translate "${r.when}" */`;
7852
7859
  }
7853
7860
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
7854
7861
  }
@@ -7857,6 +7864,7 @@ function toCSharp(ast) {
7857
7864
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
7858
7865
  const outName = (ast.outputs || []).length ? `${subject}Output` : "void";
7859
7866
  L.push(` public static ${outName} Run(${inName ? `${inName} input` : ""})`, " {");
7867
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
7860
7868
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
7861
7869
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
7862
7870
  L.push(' throw new NotImplementedException("TODO: implement , the intent above defines what this must do.");', " }");
@@ -7917,7 +7925,7 @@ function toJava(ast) {
7917
7925
  try {
7918
7926
  cond = exprToJava(r.when, { inputs });
7919
7927
  } catch {
7920
- cond = `false /* TODO: "${r.when}" */`;
7928
+ cond = `false /* TODO: could not translate "${r.when}" */`;
7921
7929
  }
7922
7930
  L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
7923
7931
  }
@@ -7926,18 +7934,98 @@ function toJava(ast) {
7926
7934
  const inName = (ast.inputs || []).length ? `${subject}Input` : null;
7927
7935
  const outName = (ast.outputs || []).length ? `${subject}Output` : "void";
7928
7936
  L.push(` public static ${outName} run(${inName ? `${inName} input` : ""}) {`);
7937
+ for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
7929
7938
  for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
7930
7939
  for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
7931
7940
  L.push(' throw new UnsupportedOperationException("TODO: implement , the intent above defines what this must do.");', " }");
7932
7941
  L.push("}", "");
7933
7942
  return L.join("\n");
7934
7943
  }
7944
+ var PY_TYPES = {
7945
+ Email: "str",
7946
+ Url: "str",
7947
+ UserId: "str",
7948
+ AccountId: "str",
7949
+ OrderId: "str",
7950
+ InvoiceId: "str",
7951
+ CustomerId: "str",
7952
+ Secret: "str",
7953
+ Token: "str",
7954
+ Jwt: "str",
7955
+ IdempotencyKey: "str",
7956
+ Currency: "str",
7957
+ Date: "str",
7958
+ DateTime: "str",
7959
+ Money: "float",
7960
+ Percentage: "float",
7961
+ Count: "int",
7962
+ Duration: "int",
7963
+ Flag: "bool"
7964
+ };
7965
+ function pyType(type, domain) {
7966
+ if (!type) return "object";
7967
+ const list = /^List<(.+)>$/.exec(type);
7968
+ if (list) return `list[${pyType(list[1], domain)}]`;
7969
+ const base = type.replace(/\(.*\)/, "");
7970
+ if (PY_TYPES[base]) return PY_TYPES[base];
7971
+ domain.add(base);
7972
+ return base;
7973
+ }
7974
+ var snake = (s) => pascal2(s).replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
7975
+ function toPython(ast) {
7976
+ const subject = pascal2(subjectName(ast) || "Intent");
7977
+ const domain = /* @__PURE__ */ new Set();
7978
+ const L = [
7979
+ `# ${subject} , generated from ThunderLang (intent-codegen-v1). Deterministic, no AI.`,
7980
+ "# The dataclass contract and the decision logic are fully determined by the intent;",
7981
+ "# business logic marked TODO is yours, bound by the guarantees and never-rules below.",
7982
+ ""
7983
+ ];
7984
+ const hasData = (ast.inputs || []).length || (ast.outputs || []).length;
7985
+ if (hasData) L.push("from __future__ import annotations", "from dataclasses import dataclass", "");
7986
+ const cls = (name, fields) => fields.length ? ["@dataclass", `class ${name}:`, ...fields.map((f) => ` ${f.name}: ${pyType(f.type, domain)}`), ""] : [];
7987
+ if ((ast.inputs || []).length) L.push(...cls(`${subject}Input`, ast.inputs));
7988
+ if ((ast.outputs || []).length) L.push(...cls(`${subject}Output`, ast.outputs));
7989
+ for (const d of ast.decisions || []) {
7990
+ const inputs = d.inputs || [];
7991
+ L.push(`# decision ${d.name} , first matching rule wins.`);
7992
+ L.push(`def ${snake(d.name)}(${inputs.join(", ")}) -> str:`);
7993
+ for (const r of d.rules || []) {
7994
+ let cond, note = "";
7995
+ try {
7996
+ cond = exprToPython(r.when, { inputs });
7997
+ } catch {
7998
+ cond = "False";
7999
+ note = ` # TODO: could not translate "${r.when}"`;
8000
+ }
8001
+ L.push(` if ${cond}:${note}`);
8002
+ L.push(` return ${JSON.stringify(r.result)} # rule ${r.name}`);
8003
+ }
8004
+ L.push(` return ${JSON.stringify(d.default ?? "Undecided")} # default`, "");
8005
+ }
8006
+ const inName = (ast.inputs || []).length ? `${subject}Input` : null;
8007
+ const outName = (ast.outputs || []).length ? `${subject}Output` : "None";
8008
+ L.push(`def run(${inName ? `input: ${inName}` : ""}) -> ${outName}:`);
8009
+ for (const r of ast.requires || []) L.push(` # precondition: ${r} , TODO: validate`);
8010
+ for (const n of ast.neverRules || []) L.push(` # NEVER: ${n.statement}`);
8011
+ for (const g of ast.guarantees || []) L.push(` # GUARANTEE: ${g.statement}`);
8012
+ L.push(' raise NotImplementedError("TODO: implement , the intent above defines what this must do.")', "");
8013
+ const stubs = [...domain].filter((t) => !PY_TYPES[t] && /^[A-Z]/.test(t)).sort();
8014
+ if (stubs.length) {
8015
+ L.push("# Domain types referenced by the intent , complete these.");
8016
+ for (const t of stubs) L.push(`class ${t}: pass # TODO: fields`);
8017
+ L.push("");
8018
+ }
8019
+ return L.join("\n");
8020
+ }
7935
8021
  var GENERATORS = {
7936
8022
  typescript: toTypeScript,
7937
8023
  ts: toTypeScript,
7938
8024
  csharp: toCSharp,
7939
8025
  cs: toCSharp,
7940
- java: toJava
8026
+ java: toJava,
8027
+ python: toPython,
8028
+ py: toPython
7941
8029
  };
7942
8030
 
7943
8031
  // src/changes.mjs
@@ -8527,6 +8615,54 @@ function verifyDiff(intentText, { before = null, after, language = "typescript"
8527
8615
  };
8528
8616
  }
8529
8617
 
8618
+ // src/mcp.mjs
8619
+ var import_node_child_process = require("node:child_process");
8620
+ var import_node_fs = require("node:fs");
8621
+ var import_node_path = require("node:path");
8622
+
8623
+ // src/conformance.mjs
8624
+ function buildConformance(ast, { targets = [], results = null } = {}) {
8625
+ const sem = runTests(ast);
8626
+ const cases = (sem.results || []).map((r) => ({
8627
+ key: `${r.target} / ${r.case}`,
8628
+ test: r.target,
8629
+ case: r.case,
8630
+ inputs: r.inputs || {},
8631
+ expected: r.expected,
8632
+ semantic: r.actual,
8633
+ semanticPass: !!r.pass
8634
+ }));
8635
+ const columns = (targets.length ? targets : ast.targets || []).map((t) => String(t).toLowerCase());
8636
+ const rows = cases.map((c) => {
8637
+ const t = {};
8638
+ for (const col of columns) {
8639
+ const tr = results && results[col];
8640
+ if (!tr || !(c.key in tr)) {
8641
+ t[col] = { status: "declared" };
8642
+ continue;
8643
+ }
8644
+ const actual = tr[c.key];
8645
+ t[col] = { status: String(actual) === String(c.expected) ? "pass" : "fail", actual };
8646
+ }
8647
+ return { ...c, targets: t };
8648
+ });
8649
+ const failures = [];
8650
+ for (const row of rows) for (const col of columns) {
8651
+ const tr = row.targets[col];
8652
+ if (tr.status === "fail") failures.push({ target: col, case: row.key, expected: row.expected, actual: tr.actual });
8653
+ }
8654
+ return {
8655
+ schema: "thunder-conformance-v1",
8656
+ mission: ast.mission,
8657
+ total: cases.length,
8658
+ columns,
8659
+ semanticFailures: cases.filter((c) => !c.semanticPass).length,
8660
+ graded: !!results,
8661
+ failures,
8662
+ cases: rows
8663
+ };
8664
+ }
8665
+
8530
8666
  // src/draft.mjs
8531
8667
  var DRAFT_SCHEMA = "intent-draft-v1";
8532
8668
  var SECRET_TYPES2 = /* @__PURE__ */ new Set(["secret", "password", "passwd", "jwt", "token", "apikey", "api_key", "privatekey", "private_key", "credential", "cvv"]);
@@ -8618,6 +8754,75 @@ function draftIntent(brief = {}) {
8618
8754
  // src/mcp.mjs
8619
8755
  var PROTOCOL_VERSION = "2024-11-05";
8620
8756
  var str = { type: "string" };
8757
+ function resolveObligations(ast) {
8758
+ const t = runTests(ast);
8759
+ const testPass = /* @__PURE__ */ new Map();
8760
+ for (const res of t.results || []) {
8761
+ const cur = testPass.get(res.target);
8762
+ testPass.set(res.target, cur === void 0 ? !!res.pass : cur && !!res.pass);
8763
+ }
8764
+ const norm4 = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, "");
8765
+ const matchTest = (verifyText) => {
8766
+ const v = norm4(verifyText);
8767
+ if (!v) return null;
8768
+ for (const [name, pass] of testPass) {
8769
+ const n = norm4(name);
8770
+ if (n && (n === v || n.includes(v) || v.includes(n))) return { name, pass };
8771
+ }
8772
+ return null;
8773
+ };
8774
+ const build = (kind, o) => {
8775
+ const verify = o.verify || [];
8776
+ let status = "unverified", provenBy = null;
8777
+ if (verify.length) {
8778
+ let m = null;
8779
+ for (const vt of verify) {
8780
+ const x = matchTest(vt);
8781
+ if (x) {
8782
+ m = x;
8783
+ if (!x.pass) break;
8784
+ }
8785
+ }
8786
+ if (m) {
8787
+ status = m.pass ? "verified" : "failed";
8788
+ provenBy = m.name;
8789
+ } else status = "declared";
8790
+ }
8791
+ return { kind, id: o.id, status, provenBy };
8792
+ };
8793
+ const obligations = [...ast.guarantees.map((g) => build("guarantee", g)), ...ast.neverRules.map((n) => build("prohibition", n))];
8794
+ return { obligations, tests: t, failed: obligations.filter((o) => o.status === "failed").length };
8795
+ }
8796
+ var LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "poetry.lock", "Pipfile.lock", "Cargo.lock", "go.sum", "Gemfile.lock", "composer.lock"];
8797
+ function proofFreshness(proof, environment) {
8798
+ let head = null;
8799
+ try {
8800
+ head = (0, import_node_child_process.execSync)("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }).trim() || null;
8801
+ } catch {
8802
+ head = null;
8803
+ }
8804
+ let lock = null, dir = process.cwd();
8805
+ for (let i = 0; i < 6 && !lock; i++) {
8806
+ for (const lf of LOCKFILES) {
8807
+ const p = (0, import_node_path.join)(dir, lf);
8808
+ if ((0, import_node_fs.existsSync)(p)) {
8809
+ lock = p;
8810
+ break;
8811
+ }
8812
+ }
8813
+ const parent = (0, import_node_path.dirname)(dir);
8814
+ if (parent === dir) break;
8815
+ dir = parent;
8816
+ }
8817
+ return {
8818
+ intentHash: proof.sourceHash,
8819
+ compilerVersion: proof.compilerVersion,
8820
+ implementation: head,
8821
+ dependencies: lock ? { file: (0, import_node_path.basename)(lock), hash: sha256((0, import_node_fs.readFileSync)(lock, "utf8")) } : null,
8822
+ environment: environment || null,
8823
+ generatedAt: proof.generatedAt
8824
+ };
8825
+ }
8621
8826
  var TOOLS = [
8622
8827
  {
8623
8828
  name: "intent_check",
@@ -8643,6 +8848,75 @@ var TOOLS = [
8643
8848
  },
8644
8849
  run: ({ intent, after, before = null, language = "typescript" }) => verifyDiff(String(intent), { before: before == null ? null : String(before), after: String(after), language })
8645
8850
  },
8851
+ {
8852
+ name: "intent_prove",
8853
+ description: "Emit the durable intent-proof-v1 artifact for .intent source: per-claim verdicts (verified / failed / planned / needs_verification, each bound to the named test that proves it) plus the freshness tuple (intent hash, compiler version, commit, dependency lockfile) that lets `thunder verify` mark the proof STALE later. Mirrors `thunder prove`; an unverified claim never reads as passed. Call it after the gate passes to record WHAT was proven.",
8854
+ inputSchema: {
8855
+ type: "object",
8856
+ required: ["source"],
8857
+ properties: {
8858
+ source: { ...str, description: "the .intent source" },
8859
+ sourceFile: { ...str, description: "file name to record in the proof (default intent.thunder)" },
8860
+ environment: { ...str, description: "environment name to bind into the freshness tuple (optional)" }
8861
+ }
8862
+ },
8863
+ run: ({ source, sourceFile = "intent.thunder", environment }) => {
8864
+ const text = String(source);
8865
+ const ast = parseIntent(text);
8866
+ const diagnostics = semanticDiagnostics(ast);
8867
+ const resolved = resolveObligations(ast);
8868
+ const proof = buildProof(ast, {
8869
+ sourceFile: String(sourceFile),
8870
+ sourceHash: sha256(text),
8871
+ targetsRequested: ast.targets || [],
8872
+ targetsGenerated: [],
8873
+ diagnostics,
8874
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
8875
+ });
8876
+ const CLAIM_MAP = { verified: "verified", failed: "failed", declared: "planned", unverified: "needs_verification" };
8877
+ const byId = new Map(resolved.obligations.map((o) => [o.id, o]));
8878
+ const applyStatus = (claim) => {
8879
+ const o = byId.get(claim.id);
8880
+ return o ? { ...claim, status: CLAIM_MAP[o.status], provenBy: o.provenBy || null } : claim;
8881
+ };
8882
+ proof.guarantees = proof.guarantees.map(applyStatus);
8883
+ proof.neverRules = proof.neverRules.map(applyStatus);
8884
+ proof.freshness = proofFreshness(proof, environment);
8885
+ const ok = proof.verification.semanticPassed && resolved.tests.ok !== false && resolved.failed === 0;
8886
+ return { ok, proofId: `proof-${proof.sourceHash.replace("sha256:", "").slice(0, 6)}`, ...proof, tests: resolved.tests };
8887
+ }
8888
+ },
8889
+ {
8890
+ name: "intent_conform",
8891
+ description: 'Grade cross-target conformance (thunder-conformance-v1): the deterministic engine defines the canonical result every in-file test case must produce, and each target\'s outputs (passed via `results`) are graded against it. Same tests, every implementation , a diverging target case is a CONFORMANCE FAILURE. Mirrors `thunder conform`. Without `results`, targets honestly stay "declared", never "pass".',
8892
+ inputSchema: {
8893
+ type: "object",
8894
+ required: ["source"],
8895
+ properties: {
8896
+ source: { ...str, description: "the .intent source (with test blocks)" },
8897
+ targets: { type: "array", items: str, description: "target languages to grade (default: the targets the intent declares)" },
8898
+ results: { type: "object", description: 'per-target actual outputs to grade: {target: {"Test / case": result}}' }
8899
+ }
8900
+ },
8901
+ run: ({ source, targets = [], results = null }) => {
8902
+ const rep = buildConformance(parseIntent(String(source)), { targets: Array.isArray(targets) ? targets.map(String) : [], results });
8903
+ return { ok: rep.semanticFailures === 0 && rep.failures.length === 0, ...rep };
8904
+ }
8905
+ },
8906
+ {
8907
+ name: "intent_drift",
8908
+ description: "Check whether real code, as it exists TODAY, still satisfies its intent , the standing-guard complement to intent_verify_diff (no diff needed). Re-lifts the code and reports drift findings: guarantees with no matching evidence, never-rules with no guard, declared inputs missing from the signature, and new behavior the intent never declared. Mirrors `thunder drift`.",
8909
+ inputSchema: {
8910
+ type: "object",
8911
+ required: ["intent", "code"],
8912
+ properties: {
8913
+ intent: { ...str, description: "the approved .intent source (the contract)" },
8914
+ code: { ...str, description: "the implementation source as it exists now" },
8915
+ language: { ...str, description: `source language (default typescript). One of: ${SUPPORTED_LANGUAGES.join(", ")}` }
8916
+ }
8917
+ },
8918
+ run: ({ intent, code, language = "typescript" }) => checkDrift(String(intent), String(code), { language })
8919
+ },
8646
8920
  {
8647
8921
  name: "intent_draft",
8648
8922
  description: "Turn a STRUCTURED brief into a rigorous, canonically-formatted ThunderLang draft plus a review checklist of what a human must still fill in (unverified guarantees, unguarded secrets, missing goal). Use this after distilling a user request into structured fields; the draft is a proposal for human approval, never verified.",
@@ -9508,6 +9782,7 @@ function startLspServer({ readable = process.stdin, writable = process.stdout }
9508
9782
  toMermaid,
9509
9783
  toOpenAPI,
9510
9784
  toPlaywright,
9785
+ toPython,
9511
9786
  toSMV,
9512
9787
  toSarif,
9513
9788
  toTypeScript,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillstech/thunderlang",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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
@@ -327,81 +327,83 @@ function printDiagnostics(diags) {
327
327
 
328
328
  const HELP = `thunder , the ThunderLang compiler + engine (deterministic, no AI required)
329
329
 
330
+ The flow: author intent, inspect it, run and prove it, then keep real code aligned with it.
331
+
330
332
  usage: thunder <command> <file> [options]
331
333
  thunder mission <Name> <command> run any command on a mission by name
332
334
 
333
- Everyday
334
- new [Name] scaffold a runnable starter mission (Name.thunder) [alias: init]
335
- mission <Name> [cmd] resolve a mission by name and run a command on it (list | <Name> | <Name> <cmd>)
336
- check <file|dir> parse + lint + explainable diagnostics
337
- 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
338
359
  test <file> [--contracts | --properties | --scenarios | --mutate | --evals | --changed | --coverage] [--strict]
339
360
  test <file> --target typescript|python|csharp|java run the tests against the EXECUTED generated code
340
- test <file> --all-targets run the tests against every AVAILABLE target in one pass
341
- 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)
342
372
  verify <proof.json> [src] re-check a proof; reports STALE when impl/deps/compiler moved
343
- build <file> generate docs, contract graph, test plan, targets
344
-
345
- Author & check
346
- init [Name] scaffold a runnable starter mission (Name.thunder)
347
- draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
348
- check <file|dir> [--json|--format sarif] diagnostics for one file, or gate a whole dir
349
- report [dir] [--json] repo-wide intent health: severity + area counts, coverage
350
- scan [dir] [--json] [--ir <path>] Scanner: intent -> Intent IR -> Fable findings -> risk themes
351
- risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json] focused scan queries
352
- focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json] Intent Lens: focused graph + brief
353
- comprehension <file|dir> [--observed] [--learning] [--governed] [--json] the C0..C7 understanding level
354
- twelve-factor <file> [--json] score an intent against the 13 humanlayer/12-factor-agents principles
355
- gen <file> [--target typescript|csharp|java] [--out <dir>] deterministic code scaffold (types + decision logic + TODOs)
356
- changes [<base>..<head> | <base>] [--json] Change Lens: what a branch/PR changed by meaning
357
- guardian <before> <after> drift: what changed, what risk, what to reverify, what learning is stale
358
- impact <base> <proposed> Simulator: estimate a change's blast radius + risk BEFORE building it
359
- ledger <file.json> [--subject <id>] verify the tamper-evident history + explain why/who/what changed
360
- guard <file> [--json] preview the runtime guard (redacted fields, enforced decisions)
361
- fmt <file|dir> [--write|--check] canonical formatting (whitespace only; comments kept)
362
- edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
363
- lsp start the Language Server (LSP over stdio, for editors)
364
- mcp start the MCP server (for AI coding agents; stdio)
365
- build <file> docs, contract graph, test plan, and .thunder-proof.json
366
- graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
367
- proof <file> the .thunder-proof.json artifact
368
- proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
369
- verify <proof.json> [src] confirm a proof is well-formed and still matches its source
370
- conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] run the same cases against every target (conformance matrix)
371
- schema emit the canonical graph schema + diagnostic catalog
372
- explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
373
- rules [--json] list the whole canonical diagnostic catalog
374
- notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
375
- docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
376
- code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
377
- apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
378
-
379
- Execute (no AI, no generated code)
380
- run <file> --inputs '<json>' evaluate the decision(s) against inputs
381
- simulate <file> --events a,b,c walk the lifecycle(s) over events
382
- test <file> run the in-file test blocks (case/scenario)
383
- outcomes <file> evaluate outcome contracts vs delivery results
384
- 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
385
394
 
386
395
  Interop
387
396
  export <file> --format <dmn|bpmn|smv|jsonschema|openapi|tokens|mermaid|css|playwright> render to a standard format
388
- import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
389
- source <file|graph.json> regenerate .thunder from a graph
390
- migrate <graph.json> [--to <version>] upgrade a persisted graph
391
- validate <graph.json> [--json] check a graph is canonical (anti-fork)
397
+ import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
392
398
 
393
- Navigate & compare (over many missions)
394
- atlas <dir> [--search q | --expand id] the whole-system Atlas
395
- index <dir> the Mission Atlas inventory
396
- diff <before> <after> semantic diff (by meaning)
397
- 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)
398
403
 
399
- Code <-> intent
400
- lift <file> [--from <lang>] lift source code into inferred intent
401
- approve <file> --by <name> approve intent (drift baseline)
402
- drift <file> --from <code> check intent vs code drift
403
- verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
404
- 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)
405
407
 
406
408
  Common options: --out <dir>, --json, --no-ai. See https://thunderlang.dev/docs`;
407
409
 
@@ -648,7 +650,7 @@ function main() {
648
650
  return;
649
651
  }
650
652
 
651
- // `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
652
654
  // intent. Generates the typed contract + the decision logic (already executable) and leaves
653
655
  // honest TODO markers for business logic. No AI. Prints, or writes with --out.
654
656
  if (cmd === 'gen') {
@@ -659,7 +661,7 @@ function main() {
659
661
  const ast = parseIntent(readFileSync(file, 'utf8'));
660
662
  const code = generate(ast);
661
663
  if (args.outExplicit) {
662
- 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;
663
665
  const p = writeText(args.out, `${slug(subjectName(ast) || 'intent')}.${ext}`, code.endsWith('\n') ? code : code + '\n');
664
666
  console.log(`wrote ${p.replace(process.cwd() + '/', '')}`);
665
667
  return;
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.3.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/mcp.mjs CHANGED
@@ -6,19 +6,79 @@
6
6
  //
7
7
  // Start it with `thunder mcp`. Point an MCP client at that command.
8
8
 
9
+ import { execSync } from 'node:child_process';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { basename, dirname, join } from 'node:path';
9
12
  import { parseIntent } from './parse.mjs';
10
- import { semanticDiagnostics, COMPILER_VERSION } from './emit.mjs';
13
+ import { semanticDiagnostics, buildProof, COMPILER_VERSION } from './emit.mjs';
14
+ import { sha256 } from './hash.mjs';
11
15
  import { verifyDiff } from './verify-diff.mjs';
12
16
  import { liftSource, SUPPORTED_LANGUAGES } from './lift.mjs';
13
17
  import { runTests } from './testing.mjs';
14
18
  import { evaluateDecision } from './runtime.mjs';
15
19
  import { buildIntentGraph } from './intent-graph.mjs';
20
+ import { buildConformance } from './conformance.mjs';
21
+ import { checkDrift } from './drift.mjs';
16
22
  import { draftIntent } from './draft.mjs';
17
23
  import { ALL_DIAGNOSTICS } from './intent-schema.mjs';
18
24
 
19
25
  const PROTOCOL_VERSION = '2024-11-05';
20
26
  const str = { type: 'string' };
21
27
 
28
+ // ── prove helpers ────────────────────────────────────────────────────────────
29
+ // Per-claim resolution , mirrors resolveObligations in cli.mjs (shared there by `thunder test
30
+ // --contracts` and `thunder prove`) so the MCP proof and the CLI proof agree claim for claim.
31
+ // States: verified (proven by a passing named test), failed (its test fails), declared (a
32
+ // verification is named but not runnable in-file), unverified (nothing).
33
+ function resolveObligations(ast) {
34
+ const t = runTests(ast);
35
+ const testPass = new Map();
36
+ for (const res of (t.results || [])) {
37
+ const cur = testPass.get(res.target);
38
+ testPass.set(res.target, cur === undefined ? !!res.pass : cur && !!res.pass);
39
+ }
40
+ const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
41
+ const matchTest = (verifyText) => {
42
+ const v = norm(verifyText); if (!v) return null;
43
+ for (const [name, pass] of testPass) { const n = norm(name); if (n && (n === v || n.includes(v) || v.includes(n))) return { name, pass }; }
44
+ return null;
45
+ };
46
+ const build = (kind, o) => {
47
+ const verify = o.verify || [];
48
+ let status = 'unverified', provenBy = null;
49
+ if (verify.length) {
50
+ let m = null;
51
+ for (const vt of verify) { const x = matchTest(vt); if (x) { m = x; if (!x.pass) break; } }
52
+ if (m) { status = m.pass ? 'verified' : 'failed'; provenBy = m.name; } else status = 'declared';
53
+ }
54
+ return { kind, id: o.id, status, provenBy };
55
+ };
56
+ const obligations = [...ast.guarantees.map((g) => build('guarantee', g)), ...ast.neverRules.map((n) => build('prohibition', n))];
57
+ return { obligations, tests: t, failed: obligations.filter((o) => o.status === 'failed').length };
58
+ }
59
+
60
+ // Freshness tuple , the (intent, implementation, dependencies, compiler, environment) binding
61
+ // `thunder verify` recomputes later to mark a proof STALE. Mirrors freshnessFor in cli.mjs;
62
+ // implementation/dependencies are read from the server's working directory when available.
63
+ const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'poetry.lock', 'Pipfile.lock', 'Cargo.lock', 'go.sum', 'Gemfile.lock', 'composer.lock'];
64
+ function proofFreshness(proof, environment) {
65
+ let head = null;
66
+ try { head = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim() || null; } catch { head = null; }
67
+ let lock = null, dir = process.cwd();
68
+ for (let i = 0; i < 6 && !lock; i++) {
69
+ for (const lf of LOCKFILES) { const p = join(dir, lf); if (existsSync(p)) { lock = p; break; } }
70
+ const parent = dirname(dir); if (parent === dir) break; dir = parent;
71
+ }
72
+ return {
73
+ intentHash: proof.sourceHash,
74
+ compilerVersion: proof.compilerVersion,
75
+ implementation: head,
76
+ dependencies: lock ? { file: basename(lock), hash: sha256(readFileSync(lock, 'utf8')) } : null,
77
+ environment: environment || null,
78
+ generatedAt: proof.generatedAt,
79
+ };
80
+ }
81
+
22
82
  // Each tool is a pure function of its arguments. `run` returns a JSON-able value or a string.
23
83
  const TOOLS = [
24
84
  {
@@ -44,6 +104,70 @@ const TOOLS = [
44
104
  },
45
105
  run: ({ intent, after, before = null, language = 'typescript' }) => verifyDiff(String(intent), { before: before == null ? null : String(before), after: String(after), language }),
46
106
  },
107
+ {
108
+ name: 'intent_prove',
109
+ description: 'Emit the durable intent-proof-v1 artifact for .intent source: per-claim verdicts (verified / failed / planned / needs_verification, each bound to the named test that proves it) plus the freshness tuple (intent hash, compiler version, commit, dependency lockfile) that lets `thunder verify` mark the proof STALE later. Mirrors `thunder prove`; an unverified claim never reads as passed. Call it after the gate passes to record WHAT was proven.',
110
+ inputSchema: {
111
+ type: 'object', required: ['source'],
112
+ properties: {
113
+ source: { ...str, description: 'the .intent source' },
114
+ sourceFile: { ...str, description: 'file name to record in the proof (default intent.thunder)' },
115
+ environment: { ...str, description: 'environment name to bind into the freshness tuple (optional)' },
116
+ },
117
+ },
118
+ run: ({ source, sourceFile = 'intent.thunder', environment }) => {
119
+ const text = String(source);
120
+ const ast = parseIntent(text);
121
+ const diagnostics = semanticDiagnostics(ast);
122
+ const resolved = resolveObligations(ast);
123
+ const proof = buildProof(ast, {
124
+ sourceFile: String(sourceFile),
125
+ sourceHash: sha256(text),
126
+ targetsRequested: ast.targets || [],
127
+ targetsGenerated: [],
128
+ diagnostics,
129
+ generatedAt: new Date().toISOString(),
130
+ });
131
+ // Fold per-claim verdicts into the proof so it records real status, not just planned/needs_verification.
132
+ const CLAIM_MAP = { verified: 'verified', failed: 'failed', declared: 'planned', unverified: 'needs_verification' };
133
+ const byId = new Map(resolved.obligations.map((o) => [o.id, o]));
134
+ const applyStatus = (claim) => { const o = byId.get(claim.id); return o ? { ...claim, status: CLAIM_MAP[o.status], provenBy: o.provenBy || null } : claim; };
135
+ proof.guarantees = proof.guarantees.map(applyStatus);
136
+ proof.neverRules = proof.neverRules.map(applyStatus);
137
+ proof.freshness = proofFreshness(proof, environment);
138
+ const ok = proof.verification.semanticPassed && resolved.tests.ok !== false && resolved.failed === 0;
139
+ return { ok, proofId: `proof-${proof.sourceHash.replace('sha256:', '').slice(0, 6)}`, ...proof, tests: resolved.tests };
140
+ },
141
+ },
142
+ {
143
+ name: 'intent_conform',
144
+ description: 'Grade cross-target conformance (thunder-conformance-v1): the deterministic engine defines the canonical result every in-file test case must produce, and each target\'s outputs (passed via `results`) are graded against it. Same tests, every implementation , a diverging target case is a CONFORMANCE FAILURE. Mirrors `thunder conform`. Without `results`, targets honestly stay "declared", never "pass".',
145
+ inputSchema: {
146
+ type: 'object', required: ['source'],
147
+ properties: {
148
+ source: { ...str, description: 'the .intent source (with test blocks)' },
149
+ targets: { type: 'array', items: str, description: 'target languages to grade (default: the targets the intent declares)' },
150
+ results: { type: 'object', description: 'per-target actual outputs to grade: {target: {"Test / case": result}}' },
151
+ },
152
+ },
153
+ run: ({ source, targets = [], results = null }) => {
154
+ const rep = buildConformance(parseIntent(String(source)), { targets: Array.isArray(targets) ? targets.map(String) : [], results });
155
+ return { ok: rep.semanticFailures === 0 && rep.failures.length === 0, ...rep };
156
+ },
157
+ },
158
+ {
159
+ name: 'intent_drift',
160
+ description: 'Check whether real code, as it exists TODAY, still satisfies its intent , the standing-guard complement to intent_verify_diff (no diff needed). Re-lifts the code and reports drift findings: guarantees with no matching evidence, never-rules with no guard, declared inputs missing from the signature, and new behavior the intent never declared. Mirrors `thunder drift`.',
161
+ inputSchema: {
162
+ type: 'object', required: ['intent', 'code'],
163
+ properties: {
164
+ intent: { ...str, description: 'the approved .intent source (the contract)' },
165
+ code: { ...str, description: 'the implementation source as it exists now' },
166
+ language: { ...str, description: `source language (default typescript). One of: ${SUPPORTED_LANGUAGES.join(', ')}` },
167
+ },
168
+ },
169
+ run: ({ intent, code, language = 'typescript' }) => checkDrift(String(intent), String(code), { language }),
170
+ },
47
171
  {
48
172
  name: 'intent_draft',
49
173
  description: 'Turn a STRUCTURED brief into a rigorous, canonically-formatted ThunderLang draft plus a review checklist of what a human must still fill in (unverified guarantees, unguarded secrets, missing goal). Use this after distilling a user request into structured fields; the draft is a proposal for human approval, never verified.',