@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 +26 -0
- package/dist/core.cjs +88 -4
- package/dist/index.cjs +280 -5
- package/package.json +1 -1
- package/src/cli.mjs +68 -66
- package/src/codegen.mjs +73 -4
- package/src/core.d.ts +1 -1
- package/src/core.mjs +1 -1
- package/src/emit.mjs +1 -1
- package/src/exporters.mjs +14 -5
- package/src/index.d.ts +1 -0
- package/src/index.mjs +1 -1
- package/src/mcp.mjs +125 -1
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.
|
|
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.
|
|
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.
|
|
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
|
+
"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
|
-
|
|
334
|
-
new [Name]
|
|
335
|
-
mission <Name> [cmd]
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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]
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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.
|
|
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.
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// (
|
|
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.
|
|
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.',
|