@skillstech/thunderlang 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.mjs CHANGED
@@ -84,6 +84,7 @@ function runLiveTarget(ast, target) {
84
84
  }
85
85
  import { importIntent, importReport, detectFormat, IMPORT_FORMATS } from './importers.mjs';
86
86
  import { runTests } from './testing.mjs';
87
+ import { toEvidenceEvents, verifyDiffToEvidence, conformToEvidence, driftToEvidence } from './evidence.mjs';
87
88
  import { evaluateOutcomes } from './outcome.mjs';
88
89
  import { analyzeStyle } from './style.mjs';
89
90
  import { intentProofJsonSchema, validateProof } from './proof-schema.mjs';
@@ -261,6 +262,7 @@ function parseArgs(argv) {
261
262
  else if (a === '--governed') args.governed = true;
262
263
  else if (a === '--target') args.target = argv[++i];
263
264
  else if (a === '--all-targets') args.allTargets = true;
265
+ else if (a === '--evidence') args.evidence = true;
264
266
  else if (a === '--env') args.env = argv[++i];
265
267
  else if (a === '--brief') args.brief = argv[++i];
266
268
  else if (a === '--after') args.after = argv[++i];
@@ -327,81 +329,85 @@ function printDiagnostics(diags) {
327
329
 
328
330
  const HELP = `thunder , the ThunderLang compiler + engine (deterministic, no AI required)
329
331
 
332
+ The flow: author intent, inspect it, run and prove it, then keep real code aligned with it.
333
+
330
334
  usage: thunder <command> <file> [options]
331
335
  thunder mission <Name> <command> run any command on a mission by name
332
336
 
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
337
+ Author
338
+ new [Name] scaffold a runnable starter mission (Name.thunder) [alias: init]
339
+ mission <Name> [cmd] resolve a mission by name and run a command on it (list | <Name> | <Name> <cmd>)
340
+ draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
341
+ edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
342
+ fmt <file|dir> [--write|--check] canonical formatting (whitespace only; comments kept)
343
+ source <file|graph.json> regenerate .thunder from a persisted graph
344
+
345
+ Inspect
346
+ check <file|dir> [--json|--format sarif] parse + lint + explainable diagnostics; gate a whole dir
347
+ report [dir] [--json] repo-wide intent health: severity + area counts, coverage
348
+ risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json] focused scan queries
349
+ focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json] Intent Lens: focused graph + brief
350
+ comprehension <file|dir> [--observed] [--learning] [--governed] [--json] the C0..C7 understanding level
351
+ twelve-factor <file> [--json] score against the 13 humanlayer/12-factor-agents principles [alias: 12factor]
352
+ explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
353
+ rules [--json] list the whole canonical diagnostic catalog
354
+ notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
355
+ docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
356
+ index <dir> the Mission Atlas inventory
357
+ atlas <dir> [--search q | --expand id] the whole-system Atlas
358
+
359
+ Run & test
360
+ run <file> --inputs '<json>' execute the decision(s) deterministically
338
361
  test <file> [--contracts | --properties | --scenarios | --mutate | --evals | --changed | --coverage] [--strict]
339
362
  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)
363
+ test <file> --all-targets run the tests against every AVAILABLE target in one pass
364
+ simulate <file> --events a,b,c walk the lifecycle(s) over events
365
+ outcomes <file> evaluate outcome contracts vs delivery results
366
+ style <file> resolve style intents vs the canonical token space
367
+ gen <file> [--target typescript|csharp|java|python] [--out <dir>] deterministic code scaffold (types + decision logic + TODOs)
368
+ build <file> generate docs, contract graph, test plan, and .thunder-proof.json
369
+
370
+ Prove & verify intent
371
+ prove <file> emit an intent-proof-v1 artifact (honest verdicts + freshness)
372
+ proof <file> the .thunder-proof.json artifact (see also: prove, which emits verdicts)
373
+ proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
342
374
  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
375
+ evidence <file> emit evidence-event-v1 JSON (tool_verified) for the shared proof spine
376
+ (also: prove/verify-diff/conform/drift accept --evidence to emit their own event)
377
+ conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] conformance matrix across targets
378
+
379
+ Real code vs intent (drift)
380
+ lift <file> [--from <lang>] lift source code into inferred intent
381
+ drift <codeFile> --intent <file.intent> [--from <lang>] check intent vs code drift
382
+ approve <file> --by <name> approve intent (drift baseline)
383
+ verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
384
+ changes [<base>..<head> | <base>] [--json] Change Lens: what a branch/PR changed by meaning
385
+ guardian <before> <after> drift: what changed, what risk, what to reverify, what learning is stale
386
+ impact <base> <proposed> Simulator: estimate a change's blast radius + risk BEFORE building it
387
+ diff <before> <after> semantic diff (by meaning)
388
+ merge <base> <ours> <theirs> deterministic 3-way semantic merge
389
+ handoff <file> the OpenThunder drift handoff
390
+ ledger <file.json> [--subject <id>] verify the tamper-evident history + explain why/who/what changed
391
+ guard <file> [--json] preview the runtime guard (redacted fields, enforced decisions)
392
+
393
+ Graph & IR
394
+ graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
395
+ migrate <graph.json> [--to <version>] upgrade a persisted graph
396
+ validate <graph.json> [--json] check a graph is canonical (anti-fork)
397
+ schema emit the canonical graph schema + diagnostic catalog
385
398
 
386
399
  Interop
387
400
  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)
401
+ import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
392
402
 
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
403
+ Fix
404
+ scan [dir] [--json] [--ir <path>] Scanner: intent -> Intent IR -> Fable findings -> risk themes
405
+ code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
406
+ apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
398
407
 
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
408
+ Servers
409
+ lsp start the Language Server (LSP over stdio, for editors)
410
+ mcp start the MCP server (for AI coding agents; stdio)
405
411
 
406
412
  Common options: --out <dir>, --json, --no-ai. See https://thunderlang.dev/docs`;
407
413
 
@@ -648,7 +654,7 @@ function main() {
648
654
  return;
649
655
  }
650
656
 
651
- // `thunder gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
657
+ // `thunder gen <file> [--target typescript|csharp|java|python] [--out <dir>]` , deterministic code scaffold from
652
658
  // intent. Generates the typed contract + the decision logic (already executable) and leaves
653
659
  // honest TODO markers for business logic. No AI. Prints, or writes with --out.
654
660
  if (cmd === 'gen') {
@@ -659,7 +665,7 @@ function main() {
659
665
  const ast = parseIntent(readFileSync(file, 'utf8'));
660
666
  const code = generate(ast);
661
667
  if (args.outExplicit) {
662
- const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target;
668
+ const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target === 'python' ? 'py' : target === 'csharp' ? 'cs' : target;
663
669
  const p = writeText(args.out, `${slug(subjectName(ast) || 'intent')}.${ext}`, code.endsWith('\n') ? code : code + '\n');
664
670
  console.log(`wrote ${p.replace(process.cwd() + '/', '')}`);
665
671
  return;
@@ -858,7 +864,7 @@ test Example
858
864
  if (!res.ok) { console.error(res.error); process.exit(1); }
859
865
  if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
860
866
  if (args.out) {
861
- const p = writeText(args.out, `${slug(res.lifted.mission)}.intent`, res.intentText);
867
+ const p = writeText(args.out, `${slug(res.lifted.mission)}.thunder`, res.intentText);
862
868
  console.log(`thunder lift ${basename(file)} -> ${p.replace(process.cwd() + '/', '')}`);
863
869
  } else {
864
870
  console.log(res.intentText);
@@ -899,6 +905,11 @@ test Example
899
905
  const codeText = readFileSync(file, 'utf8');
900
906
  const language = args.from && args.from !== 'repo' ? args.from : languageForFile(file);
901
907
  const res = checkDrift(intentText, codeText, { language });
908
+ if (args.evidence) {
909
+ const ctx = { compilerVersion: COMPILER_VERSION, occurredAt: new Date().toISOString(), missionName: parseIntent(intentText).mission || null, intentHash: sha256(intentText), codeHash: sha256(codeText) };
910
+ console.log(JSON.stringify(driftToEvidence(res, ctx), null, 2));
911
+ process.exit(res.status === 'drift' ? 1 : 0); return;
912
+ }
902
913
  if (args.json) { console.log(JSON.stringify(res, null, 2)); }
903
914
  else {
904
915
  console.log(`thunder drift: ${res.status.toUpperCase()} (${res.summary.blocking} blocking)`);
@@ -1503,6 +1514,34 @@ test Example
1503
1514
  return;
1504
1515
  }
1505
1516
 
1517
+ // `thunder evidence <file>` , project the intent's proof into evidence-event-v1 events for the
1518
+ // shared SkillsTech evidence graph (STW spine). Deterministic, offline, safe-derived; prints JSON.
1519
+ // The network push to the spine belongs to the org-layer/CI, not this CLI.
1520
+ if (cmd === 'evidence') {
1521
+ if (!file || !existsSync(file)) { console.error('usage: thunder evidence <file> # emit evidence-event-v1 JSON for the shared proof spine'); process.exit(2); return; }
1522
+ const source = readFileSync(file, 'utf8');
1523
+ const ast = parseIntent(source);
1524
+ const diagnostics = semanticDiagnostics(ast);
1525
+ const resolved = resolveObligations(ast);
1526
+ const proof = buildProof(ast, {
1527
+ sourceFile: basename(file),
1528
+ sourceHash: sha256(source),
1529
+ targetsRequested: ast.targets || [],
1530
+ targetsGenerated: [],
1531
+ diagnostics,
1532
+ generatedAt: new Date().toISOString(),
1533
+ });
1534
+ const CLAIM_MAP = { verified: 'verified', failed: 'failed', declared: 'planned', unverified: 'needs_verification' };
1535
+ const byId = new Map(resolved.obligations.map((o) => [o.id, o]));
1536
+ const applyStatus = (claim) => { const o = byId.get(claim.id); return o ? { ...claim, status: CLAIM_MAP[o.status], provenBy: o.provenBy || null } : claim; };
1537
+ proof.guarantees = proof.guarantees.map(applyStatus);
1538
+ proof.neverRules = proof.neverRules.map(applyStatus);
1539
+ proof.freshness = freshnessFor(file, proof, args.env);
1540
+ const events = toEvidenceEvents(proof);
1541
+ console.log(JSON.stringify(events, null, 2));
1542
+ return;
1543
+ }
1544
+
1506
1545
  // CONFORMANCE: the same test cases run against every target. The engine defines the canonical
1507
1546
  // result each case must produce; target outputs fed via --results are graded against it.
1508
1547
  // `thunder conform <file> [--targets ts,py] [--results <json|path>] [--json]`.
@@ -1540,6 +1579,11 @@ test Example
1540
1579
  const rep = buildConformance(ast, { targets, results });
1541
1580
  if (skippedTargets.length) rep.skipped = Array.from(new Set(skippedTargets));
1542
1581
  const bad = rep.semanticFailures > 0 || rep.failures.length > 0;
1582
+ if (args.evidence) {
1583
+ const ctx = { compilerVersion: COMPILER_VERSION, occurredAt: new Date().toISOString(), missionName: ast.mission || null, intentHash: sha256(readFileSync(file, 'utf8')) };
1584
+ console.log(JSON.stringify(conformToEvidence(rep, ctx), null, 2));
1585
+ process.exit(bad ? 1 : 0); return;
1586
+ }
1543
1587
  if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1544
1588
  if (!rep.total) { console.log(`thunder conform ${basename(file)}: no test cases to conform.`); return; }
1545
1589
 
@@ -1750,6 +1794,11 @@ test Example
1750
1794
  const before = args.before ? readFileSync(args.before, 'utf8') : null;
1751
1795
  const language = args.from || languageForFile(args.after);
1752
1796
  const r = verifyDiff(intentText, { before, after, language });
1797
+ if (args.evidence) {
1798
+ const ctx = { compilerVersion: COMPILER_VERSION, occurredAt: new Date().toISOString(), missionName: parseIntent(intentText).mission || null, intentHash: sha256(intentText), changeHash: sha256(after) };
1799
+ console.log(JSON.stringify(verifyDiffToEvidence(r, ctx), null, 2));
1800
+ process.exit(r.ok ? 0 : 1); return;
1801
+ }
1753
1802
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
1754
1803
  console.log(`thunder verify-diff ${basename(file)} vs ${basename(args.after)}: ${r.verdict} (${r.blocking} blocking, ${r.summary.regressions} regression(s))`);
1755
1804
  for (const f of r.findings) {
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.2';
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.
@@ -0,0 +1,116 @@
1
+ // ThunderLang -> shared evidence graph projection.
2
+ //
3
+ // Maps ThunderLang's verification artifacts into evidence-event-v1 events for the SkillsTech proof
4
+ // spine (owned by STW: siblings emit evidence-event-v1, STW composes proof-bundle-v1). ThunderLang is
5
+ // the deterministic, machine-checked layer, so its evidence is evidenceType `tool_verified`.
6
+ //
7
+ // These projections are PURE and OFFLINE. They perform no network I/O: the transport (POST to the
8
+ // spine) belongs to the hosted org-layer / CI, never the end-user CLI, so the compiler stays
9
+ // deterministic and phone-home-free. They are also SAFE-DERIVED: events carry only ids, hashes,
10
+ // verdicts, diagnostic codes, and counts , never source code, never secret values, never claim
11
+ // statement text, never data-element contents (no finding `message`, no expected/actual values).
12
+ //
13
+ // NOTE: the evidence-event-v1 envelope below is ThunderLang's PROPOSED shape, pending STW's canonical
14
+ // schema (see the TL->STW proposal on the comms bus). Field names may adjust once STW confirms.
15
+
16
+ const COUNTED_STATUSES = ['verified', 'failed', 'planned', 'needs_verification'];
17
+ const stripHash = (h) => String(h || '').replace(/^sha256:/, '');
18
+ const short = (h, n = 16) => stripHash(h).slice(0, n);
19
+
20
+ // The shared evidence-event-v1 envelope. `ctx` carries the cross-artifact context the individual
21
+ // artifacts do not all include (missionName, intentHash, compilerVersion, occurredAt).
22
+ function envelope(eventType, evidenceId, ctx = {}, payload = {}) {
23
+ return {
24
+ schema: 'evidence-event-v1',
25
+ sourceProduct: 'thunderlang',
26
+ producer: { name: 'thunderlang', version: ctx.compilerVersion || null },
27
+ eventType,
28
+ evidenceType: 'tool_verified',
29
+ evidenceId,
30
+ occurredAt: ctx.occurredAt || null,
31
+ visibility: ctx.visibility || 'private',
32
+ subject: { missionName: ctx.missionName || null, intentHash: ctx.intentHash || null },
33
+ payload,
34
+ };
35
+ }
36
+
37
+ const safeClaim = (kind, c) => ({ id: c.id, kind, status: c.status, provenBy: c.provenBy || null });
38
+ function countByStatus(claims) {
39
+ const counts = Object.fromEntries(COUNTED_STATUSES.map((s) => [s, 0]));
40
+ for (const c of claims) if (c && Object.prototype.hasOwnProperty.call(counts, c.status)) counts[c.status]++;
41
+ return counts;
42
+ }
43
+ // Safe-derived finding: diagnostic code + level (+ flags), never the human message (which can quote code).
44
+ const safeFinding = (f) => ({ code: f.code, level: f.level, ...(f.regression !== undefined ? { regression: !!f.regression } : {}), ...(f.line !== undefined ? { line: f.line } : {}) });
45
+
46
+ // intent.proven , from an intent-proof-v1 artifact.
47
+ export function toEvidenceEvents(proof) {
48
+ if (!proof || typeof proof !== 'object') return [];
49
+ const guarantees = Array.isArray(proof.guarantees) ? proof.guarantees : [];
50
+ const neverRules = Array.isArray(proof.neverRules) ? proof.neverRules : [];
51
+ const claims = [
52
+ ...guarantees.map((c) => safeClaim('guarantee', c)),
53
+ ...neverRules.map((c) => safeClaim('prohibition', c)),
54
+ ];
55
+ const fr = proof.freshness || {};
56
+ const intentHash = proof.freshness?.intentHash || proof.sourceHash || null;
57
+ return [envelope('intent.proven', `tl-proof-${short(intentHash)}`, {
58
+ compilerVersion: proof.compilerVersion, occurredAt: proof.generatedAt, missionName: proof.missionName, intentHash,
59
+ }, {
60
+ proofStatus: proof.proofStatus || null,
61
+ total: claims.length,
62
+ counts: countByStatus(claims),
63
+ freshness: { implementation: fr.implementation || null, dependencies: fr.dependencies ? fr.dependencies.hash || null : null, environment: fr.environment || null },
64
+ claims,
65
+ })];
66
+ }
67
+
68
+ // change.gated , from a verify-diff verdict ({ verdict, ok, findings, blocking, summary }).
69
+ export function verifyDiffToEvidence(verdict, ctx = {}) {
70
+ if (!verdict || typeof verdict !== 'object') return [];
71
+ const findings = Array.isArray(verdict.findings) ? verdict.findings : [];
72
+ const id = `tl-verifydiff-${short(ctx.intentHash)}${ctx.changeHash ? `-${short(ctx.changeHash, 8)}` : ''}`;
73
+ return [envelope('change.gated', id, ctx, {
74
+ verdict: verdict.verdict || null,
75
+ ok: !!verdict.ok,
76
+ blocking: verdict.blocking || 0,
77
+ regressions: verdict.summary?.regressions ?? findings.filter((f) => f.regression).length,
78
+ findings: findings.map(safeFinding),
79
+ })];
80
+ }
81
+
82
+ // conformance.verified , from a thunder-conformance-v1 report.
83
+ export function conformToEvidence(report, ctx = {}) {
84
+ if (!report || typeof report !== 'object') return [];
85
+ const cases = Array.isArray(report.cases) ? report.cases : [];
86
+ const columns = Array.isArray(report.columns) ? report.columns : [];
87
+ // Per-target pass/fail/declared counts, derived without leaking any expected/actual values.
88
+ const targets = {};
89
+ for (const col of columns) {
90
+ const t = { pass: 0, fail: 0, declared: 0 };
91
+ for (const c of cases) { const s = c.targets?.[col]?.status; if (s === 'pass') t.pass++; else if (s === 'fail') t.fail++; else t.declared++; }
92
+ targets[col] = t;
93
+ }
94
+ return [envelope('conformance.verified', `tl-conform-${short(ctx.intentHash)}`, ctx, {
95
+ total: report.total || cases.length,
96
+ columns,
97
+ semanticFailures: report.semanticFailures || 0,
98
+ graded: !!report.graded,
99
+ targets,
100
+ // failure locations by name only (case + target), never expected/actual values.
101
+ failures: (Array.isArray(report.failures) ? report.failures : []).map((f) => ({ target: f.target, case: f.case })),
102
+ })];
103
+ }
104
+
105
+ // intent.drift , from a checkDrift report ({ status, findings, summary }).
106
+ export function driftToEvidence(report, ctx = {}) {
107
+ if (!report || typeof report !== 'object') return [];
108
+ const findings = Array.isArray(report.findings) ? report.findings : [];
109
+ const id = `tl-drift-${short(ctx.intentHash)}${ctx.codeHash ? `-${short(ctx.codeHash, 8)}` : ''}`;
110
+ return [envelope('intent.drift', id, ctx, {
111
+ status: report.status || null,
112
+ total: findings.length,
113
+ blocking: report.summary?.blocking ?? 0,
114
+ findings: findings.map(safeFinding),
115
+ })];
116
+ }
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
@@ -8,6 +8,7 @@
8
8
 
9
9
  // Parsing
10
10
  export { parseIntent, slug, KNOWN_LENSES } from './parse.mjs';
11
+ export { toEvidenceEvents, verifyDiffToEvidence, conformToEvidence, driftToEvidence } from './evidence.mjs';
11
12
 
12
13
  // Semantic analysis, graphs, and proof
13
14
  export {
@@ -113,7 +114,7 @@ export { FOCUS_SCHEMA, SCOPE_TYPES, FOCUS_REASONS, makeScope, buildFocusGraph, i
113
114
  // Comprehension Contract , the C0..C7 understanding level (intent-comprehension-v1)
114
115
  export { COMPREHENSION_SCHEMA, LEVELS as COMPREHENSION_LEVELS, comprehensionLevel, comprehensionReport } from './comprehension.mjs';
115
116
  // Code generation , deterministic scaffolds from intent (intent-codegen-v1)
116
- export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava } from './codegen.mjs';
117
+ export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython } from './codegen.mjs';
117
118
  // Change Lens , what a branch/PR changed by meaning (intent-changes-v1)
118
119
  export { CHANGES_SCHEMA, changeReport } from './changes.mjs';
119
120
  export { exprToJs, exprToCSharp, exprToJava, exprToCode } from './expr.mjs';
package/src/lift.mjs CHANGED
@@ -696,7 +696,7 @@ export function liftRepo(files, { language } = {}) {
696
696
  const base = slug(r.lifted.mission);
697
697
  const n = (usedNames.get(base) || 0) + 1;
698
698
  usedNames.set(base, n);
699
- const outName = n === 1 ? `${base}.intent` : `${base}-${n}.intent`;
699
+ const outName = n === 1 ? `${base}.thunder` : `${base}-${n}.thunder`;
700
700
  missions.push({
701
701
  mission: r.lifted.mission, sourceFile: file, outName,
702
702
  intentText: r.intentText, summary: r.summary, diagnostics: r.diagnostics,