@skillstech/thunderlang 0.4.1 → 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/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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.2
7
+
8
+ The evidence-graph release. Additive; no breaking changes.
9
+
10
+ - **Evidence projections.** ThunderLang can now project its verification into `evidence-event-v1`
11
+ events (`evidenceType: tool_verified`) for the shared SkillsTech proof spine: `thunder evidence
12
+ <file>` emits `intent.proven`, and `--evidence` on `verify-diff` / `conform` / `drift` emits
13
+ `change.gated` / `conformance.verified` / `intent.drift`. The projections are pure, offline, and
14
+ safe-derived (ids, hashes, verdicts, diagnostic codes, and counts only , never source, secret
15
+ values, finding messages, or expected/actual values). The library exports `toEvidenceEvents`,
16
+ `verifyDiffToEvidence`, `conformToEvidence`, and `driftToEvidence`. The network push to the spine
17
+ is intentionally left to the org-layer/CI, so the compiler stays offline and deterministic.
18
+ - **Fix: `thunder lift` emits canonical `.thunder` drafts** instead of the legacy `.intent`
19
+ extension, on both single-file and repo lifts.
20
+
6
21
  ## 0.4.1
7
22
 
8
23
  The coherence release. Additive; no breaking changes. Version chosen to align with OpenThunder
package/README.md CHANGED
@@ -11,7 +11,7 @@ Part of [ThunderLang](https://thunderlang.dev), the intent language for AI-era s
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npm install -g @skillstech/thunderlang # then: intent check path/to/Mission.intent
14
+ npm install -g @skillstech/thunderlang # then: thunder check path/to/Mission.intent
15
15
  npx @skillstech/thunderlang help # or run without installing
16
16
  ```
17
17
 
@@ -19,36 +19,36 @@ npx @skillstech/thunderlang help # or run without installing
19
19
 
20
20
  ```bash
21
21
  # Author & check
22
- intent init [Name] # scaffold a runnable starter mission
23
- intent check <file|dir> # semantic diagnostics (exit 1 on error); a dir gates the repo
24
- intent fmt <file|dir> -w # canonical formatting (whitespace only; comments preserved)
25
- intent build <file> # docs, contract graph, test plan, .intent-proof.json
26
- intent graph <file> # the canonical Intent Graph (intent-graph-v1)
27
- intent check . --format sarif # SARIF 2.1.0 for GitHub/GitLab code scanning
28
- intent schema # emit the graph schema + diagnostic catalog
29
- intent rules · intent explain <CODE> # the full diagnostic catalog / one code
30
- intent proof --schema # the canonical proof envelope schema (intent-proof-v1)
22
+ thunder init [Name] # scaffold a runnable starter mission
23
+ thunder check <file|dir> # semantic diagnostics (exit 1 on error); a dir gates the repo
24
+ thunder fmt <file|dir> -w # canonical formatting (whitespace only; comments preserved)
25
+ thunder build <file> # docs, contract graph, test plan, .intent-proof.json
26
+ thunder graph <file> # the canonical Intent Graph (intent-graph-v1)
27
+ thunder check . --format sarif # SARIF 2.1.0 for GitHub/GitLab code scanning
28
+ thunder schema # emit the graph schema + diagnostic catalog
29
+ thunder rules · thunder explain <CODE> # the full diagnostic catalog / one code
30
+ thunder proof --schema # the canonical proof envelope schema (intent-proof-v1)
31
31
 
32
32
  # Execute (no AI, no generated code)
33
- intent run <file> --inputs '{"age":20}' # evaluate the decision(s) against inputs
34
- intent simulate <file> --events submit,approve # walk the lifecycle(s)
35
- intent test <file> # run in-file test blocks (case/scenario)
36
- intent outcomes <file> # evaluate outcome contracts vs results
37
- intent style <file> # resolve style intents vs the canonical token space
33
+ thunder run <file> --inputs '{"age":20}' # evaluate the decision(s) against inputs
34
+ thunder simulate <file> --events submit,approve # walk the lifecycle(s)
35
+ thunder test <file> # run in-file test blocks (case/scenario)
36
+ thunder outcomes <file> # evaluate outcome contracts vs results
37
+ thunder style <file> # resolve style intents vs the canonical token space
38
38
 
39
39
  # Interop (9 export formats)
40
- intent export <file> --format dmn|bpmn|smv|jsonschema|openapi|tokens|css|mermaid|playwright
41
- intent import <file> [--format dmn|bpmn] [--json] # lift DMN/BPMN into intent
42
- intent source <file|graph.json> # regenerate .intent from a graph
43
- intent migrate <graph.json> [--to <ver>] # upgrade a persisted graph
44
- intent verify <proof.json> [src] # confirm a proof matches its source
40
+ thunder export <file> --format dmn|bpmn|smv|jsonschema|openapi|tokens|css|mermaid|playwright
41
+ thunder import <file> [--format dmn|bpmn] [--json] # lift DMN/BPMN into intent
42
+ thunder source <file|graph.json> # regenerate .intent from a graph
43
+ thunder migrate <graph.json> [--to <ver>] # upgrade a persisted graph
44
+ thunder verify <proof.json> [src] # confirm a proof matches its source
45
45
 
46
46
  # Navigate & compare
47
- intent atlas <dir> [--search q] intent index <dir>
48
- intent diff <before> <after> intent merge <base> <ours> <theirs>
47
+ thunder atlas <dir> [--search q] thunder index <dir>
48
+ thunder diff <before> <after> thunder merge <base> <ours> <theirs>
49
49
 
50
50
  # Code <-> intent
51
- intent lift <file> intent approve <file> intent drift <file>
51
+ thunder lift <file> thunder approve <file> thunder drift <file>
52
52
  ```
53
53
 
54
54
  Run `thunder help` for the full reference.
@@ -75,8 +75,8 @@ test Eligibility
75
75
  ```
76
76
 
77
77
  ```bash
78
- intent run mission.intent --inputs '{"age":20,"score":90}' # -> Eligible, with a trace
79
- intent test mission.intent # -> 1/1 passed
78
+ thunder run mission.intent --inputs '{"age":20,"score":90}' # -> Eligible, with a trace
79
+ thunder test mission.intent # -> 1/1 passed
80
80
  ```
81
81
 
82
82
  No AI, no generated code. The intent itself decides.
package/dist/core.cjs CHANGED
@@ -4290,7 +4290,7 @@ function notesSummary(ast) {
4290
4290
  byLens
4291
4291
  };
4292
4292
  }
4293
- var COMPILER_VERSION = "0.4.1";
4293
+ var COMPILER_VERSION = "0.4.2";
4294
4294
  var PROOF_SCHEMA_VERSION = "0.1.0";
4295
4295
  var SOURCE_PRODUCT = "skillstech-compiler";
4296
4296
  function buildContractGraph(ast, generatedAt) {
@@ -5713,7 +5713,7 @@ function liftRepo(files, { language } = {}) {
5713
5713
  const base = slug(r.lifted.mission);
5714
5714
  const n = (usedNames.get(base) || 0) + 1;
5715
5715
  usedNames.set(base, n);
5716
- const outName = n === 1 ? `${base}.intent` : `${base}-${n}.intent`;
5716
+ const outName = n === 1 ? `${base}.thunder` : `${base}-${n}.thunder`;
5717
5717
  missions.push({
5718
5718
  mission: r.lifted.mission,
5719
5719
  sourceFile: file,
package/dist/index.cjs CHANGED
@@ -160,6 +160,7 @@ __export(index_exports, {
160
160
  comprehensionLevel: () => comprehensionLevel,
161
161
  comprehensionReport: () => comprehensionReport,
162
162
  confidenceFromClassification: () => confidenceFromClassification,
163
+ conformToEvidence: () => conformToEvidence,
163
164
  contractHash: () => contractHash,
164
165
  contradictionsView: () => contradictionsView,
165
166
  correctionsFor: () => correctionsFor,
@@ -168,6 +169,7 @@ __export(index_exports, {
168
169
  detectFormat: () => detectFormat,
169
170
  diffGraphs: () => diffGraphs,
170
171
  draftIntent: () => draftIntent,
172
+ driftToEvidence: () => driftToEvidence,
171
173
  dropNodeField: () => dropNodeField,
172
174
  emptyApprovals: () => emptyApprovals,
173
175
  emptyEventLog: () => emptyEventLog,
@@ -288,6 +290,7 @@ __export(index_exports, {
288
290
  toCss: () => toCss,
289
291
  toDMN: () => toDMN,
290
292
  toDesignTokens: () => toDesignTokens,
293
+ toEvidenceEvents: () => toEvidenceEvents,
291
294
  toFinding: () => toFinding,
292
295
  toJSONSchema: () => toJSONSchema,
293
296
  toJava: () => toJava,
@@ -309,6 +312,7 @@ __export(index_exports, {
309
312
  validateIR: () => validateIR,
310
313
  validateProof: () => validateProof,
311
314
  verifyDiff: () => verifyDiff,
315
+ verifyDiffToEvidence: () => verifyDiffToEvidence,
312
316
  verifyLedger: () => verifyLedger,
313
317
  violatesArchitecture: () => violatesArchitecture,
314
318
  whyBuilt: () => whyBuilt
@@ -328,8 +332,8 @@ function intentRefId(astOrName, { sourceHash } = {}) {
328
332
  const name = typeof astOrName === "string" ? astOrName : subjectName(astOrName);
329
333
  const base = `intent:${slug(name || "mission")}`;
330
334
  if (!sourceHash) return base;
331
- const short = String(sourceHash).replace(/^sha256:/, "").slice(0, 8);
332
- return short ? `${base}@${short}` : base;
335
+ const short2 = String(sourceHash).replace(/^sha256:/, "").slice(0, 8);
336
+ return short2 ? `${base}@${short2}` : base;
333
337
  }
334
338
  function skillRefId(name) {
335
339
  return `skill:${slug(name || "unknown")}`;
@@ -1225,6 +1229,103 @@ function parseIntent(source) {
1225
1229
  return ast;
1226
1230
  }
1227
1231
 
1232
+ // src/evidence.mjs
1233
+ var COUNTED_STATUSES = ["verified", "failed", "planned", "needs_verification"];
1234
+ var stripHash = (h) => String(h || "").replace(/^sha256:/, "");
1235
+ var short = (h, n = 16) => stripHash(h).slice(0, n);
1236
+ function envelope(eventType, evidenceId, ctx = {}, payload = {}) {
1237
+ return {
1238
+ schema: "evidence-event-v1",
1239
+ sourceProduct: "thunderlang",
1240
+ producer: { name: "thunderlang", version: ctx.compilerVersion || null },
1241
+ eventType,
1242
+ evidenceType: "tool_verified",
1243
+ evidenceId,
1244
+ occurredAt: ctx.occurredAt || null,
1245
+ visibility: ctx.visibility || "private",
1246
+ subject: { missionName: ctx.missionName || null, intentHash: ctx.intentHash || null },
1247
+ payload
1248
+ };
1249
+ }
1250
+ var safeClaim = (kind, c) => ({ id: c.id, kind, status: c.status, provenBy: c.provenBy || null });
1251
+ function countByStatus(claims) {
1252
+ const counts = Object.fromEntries(COUNTED_STATUSES.map((s) => [s, 0]));
1253
+ for (const c of claims) if (c && Object.prototype.hasOwnProperty.call(counts, c.status)) counts[c.status]++;
1254
+ return counts;
1255
+ }
1256
+ var safeFinding = (f) => ({ code: f.code, level: f.level, ...f.regression !== void 0 ? { regression: !!f.regression } : {}, ...f.line !== void 0 ? { line: f.line } : {} });
1257
+ function toEvidenceEvents(proof) {
1258
+ if (!proof || typeof proof !== "object") return [];
1259
+ const guarantees = Array.isArray(proof.guarantees) ? proof.guarantees : [];
1260
+ const neverRules = Array.isArray(proof.neverRules) ? proof.neverRules : [];
1261
+ const claims = [
1262
+ ...guarantees.map((c) => safeClaim("guarantee", c)),
1263
+ ...neverRules.map((c) => safeClaim("prohibition", c))
1264
+ ];
1265
+ const fr = proof.freshness || {};
1266
+ const intentHash2 = proof.freshness?.intentHash || proof.sourceHash || null;
1267
+ return [envelope("intent.proven", `tl-proof-${short(intentHash2)}`, {
1268
+ compilerVersion: proof.compilerVersion,
1269
+ occurredAt: proof.generatedAt,
1270
+ missionName: proof.missionName,
1271
+ intentHash: intentHash2
1272
+ }, {
1273
+ proofStatus: proof.proofStatus || null,
1274
+ total: claims.length,
1275
+ counts: countByStatus(claims),
1276
+ freshness: { implementation: fr.implementation || null, dependencies: fr.dependencies ? fr.dependencies.hash || null : null, environment: fr.environment || null },
1277
+ claims
1278
+ })];
1279
+ }
1280
+ function verifyDiffToEvidence(verdict2, ctx = {}) {
1281
+ if (!verdict2 || typeof verdict2 !== "object") return [];
1282
+ const findings2 = Array.isArray(verdict2.findings) ? verdict2.findings : [];
1283
+ const id = `tl-verifydiff-${short(ctx.intentHash)}${ctx.changeHash ? `-${short(ctx.changeHash, 8)}` : ""}`;
1284
+ return [envelope("change.gated", id, ctx, {
1285
+ verdict: verdict2.verdict || null,
1286
+ ok: !!verdict2.ok,
1287
+ blocking: verdict2.blocking || 0,
1288
+ regressions: verdict2.summary?.regressions ?? findings2.filter((f) => f.regression).length,
1289
+ findings: findings2.map(safeFinding)
1290
+ })];
1291
+ }
1292
+ function conformToEvidence(report, ctx = {}) {
1293
+ if (!report || typeof report !== "object") return [];
1294
+ const cases = Array.isArray(report.cases) ? report.cases : [];
1295
+ const columns = Array.isArray(report.columns) ? report.columns : [];
1296
+ const targets = {};
1297
+ for (const col of columns) {
1298
+ const t = { pass: 0, fail: 0, declared: 0 };
1299
+ for (const c of cases) {
1300
+ const s = c.targets?.[col]?.status;
1301
+ if (s === "pass") t.pass++;
1302
+ else if (s === "fail") t.fail++;
1303
+ else t.declared++;
1304
+ }
1305
+ targets[col] = t;
1306
+ }
1307
+ return [envelope("conformance.verified", `tl-conform-${short(ctx.intentHash)}`, ctx, {
1308
+ total: report.total || cases.length,
1309
+ columns,
1310
+ semanticFailures: report.semanticFailures || 0,
1311
+ graded: !!report.graded,
1312
+ targets,
1313
+ // failure locations by name only (case + target), never expected/actual values.
1314
+ failures: (Array.isArray(report.failures) ? report.failures : []).map((f) => ({ target: f.target, case: f.case }))
1315
+ })];
1316
+ }
1317
+ function driftToEvidence(report, ctx = {}) {
1318
+ if (!report || typeof report !== "object") return [];
1319
+ const findings2 = Array.isArray(report.findings) ? report.findings : [];
1320
+ const id = `tl-drift-${short(ctx.intentHash)}${ctx.codeHash ? `-${short(ctx.codeHash, 8)}` : ""}`;
1321
+ return [envelope("intent.drift", id, ctx, {
1322
+ status: report.status || null,
1323
+ total: findings2.length,
1324
+ blocking: report.summary?.blocking ?? 0,
1325
+ findings: findings2.map(safeFinding)
1326
+ })];
1327
+ }
1328
+
1228
1329
  // src/hash.mjs
1229
1330
  var K = new Uint32Array([
1230
1331
  1116352408,
@@ -2288,7 +2389,7 @@ function notesSummary(ast) {
2288
2389
  byLens
2289
2390
  };
2290
2391
  }
2291
- var COMPILER_VERSION = "0.4.1";
2392
+ var COMPILER_VERSION = "0.4.2";
2292
2393
  var PROOF_SCHEMA_VERSION = "0.1.0";
2293
2394
  var SOURCE_PRODUCT = "skillstech-compiler";
2294
2395
  function buildContractGraph(ast, generatedAt) {
@@ -4087,7 +4188,7 @@ function liftRepo(files, { language } = {}) {
4087
4188
  const base = slug(r.lifted.mission);
4088
4189
  const n = (usedNames.get(base) || 0) + 1;
4089
4190
  usedNames.set(base, n);
4090
- const outName = n === 1 ? `${base}.intent` : `${base}-${n}.intent`;
4191
+ const outName = n === 1 ? `${base}.thunder` : `${base}-${n}.thunder`;
4091
4192
  missions.push({
4092
4193
  mission: r.lifted.mission,
4093
4194
  sourceFile: file,
@@ -9648,6 +9749,7 @@ function startLspServer({ readable = process.stdin, writable = process.stdout }
9648
9749
  comprehensionLevel,
9649
9750
  comprehensionReport,
9650
9751
  confidenceFromClassification,
9752
+ conformToEvidence,
9651
9753
  contractHash,
9652
9754
  contradictionsView,
9653
9755
  correctionsFor,
@@ -9656,6 +9758,7 @@ function startLspServer({ readable = process.stdin, writable = process.stdout }
9656
9758
  detectFormat,
9657
9759
  diffGraphs,
9658
9760
  draftIntent,
9761
+ driftToEvidence,
9659
9762
  dropNodeField,
9660
9763
  emptyApprovals,
9661
9764
  emptyEventLog,
@@ -9776,6 +9879,7 @@ function startLspServer({ readable = process.stdin, writable = process.stdout }
9776
9879
  toCss,
9777
9880
  toDMN,
9778
9881
  toDesignTokens,
9882
+ toEvidenceEvents,
9779
9883
  toFinding,
9780
9884
  toJSONSchema,
9781
9885
  toJava,
@@ -9797,6 +9901,7 @@ function startLspServer({ readable = process.stdin, writable = process.stdout }
9797
9901
  validateIR,
9798
9902
  validateProof,
9799
9903
  verifyDiff,
9904
+ verifyDiffToEvidence,
9800
9905
  verifyLedger,
9801
9906
  violatesArchitecture,
9802
9907
  whyBuilt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillstech/thunderlang",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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",
@@ -70,11 +70,11 @@
70
70
  "homepage": "https://thunderlang.dev",
71
71
  "repository": {
72
72
  "type": "git",
73
- "url": "git+https://github.com/SkillsTechTalk/intent-language.git",
73
+ "url": "git+https://github.com/SkillsTechTalk/thunderlang.git",
74
74
  "directory": "compiler"
75
75
  },
76
76
  "bugs": {
77
- "url": "https://github.com/SkillsTechTalk/intent-language/issues"
77
+ "url": "https://github.com/SkillsTechTalk/thunderlang/issues"
78
78
  },
79
79
  "author": "Skills Tech Talk, LLC",
80
80
  "license": "Apache-2.0",
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];
@@ -370,6 +372,8 @@ Prove & verify intent
370
372
  proof <file> the .thunder-proof.json artifact (see also: prove, which emits verdicts)
371
373
  proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
372
374
  verify <proof.json> [src] re-check a proof; reports STALE when impl/deps/compiler moved
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)
373
377
  conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] conformance matrix across targets
374
378
 
375
379
  Real code vs intent (drift)
@@ -860,7 +864,7 @@ test Example
860
864
  if (!res.ok) { console.error(res.error); process.exit(1); }
861
865
  if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
862
866
  if (args.out) {
863
- 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);
864
868
  console.log(`thunder lift ${basename(file)} -> ${p.replace(process.cwd() + '/', '')}`);
865
869
  } else {
866
870
  console.log(res.intentText);
@@ -901,6 +905,11 @@ test Example
901
905
  const codeText = readFileSync(file, 'utf8');
902
906
  const language = args.from && args.from !== 'repo' ? args.from : languageForFile(file);
903
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
+ }
904
913
  if (args.json) { console.log(JSON.stringify(res, null, 2)); }
905
914
  else {
906
915
  console.log(`thunder drift: ${res.status.toUpperCase()} (${res.summary.blocking} blocking)`);
@@ -1505,6 +1514,34 @@ test Example
1505
1514
  return;
1506
1515
  }
1507
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
+
1508
1545
  // CONFORMANCE: the same test cases run against every target. The engine defines the canonical
1509
1546
  // result each case must produce; target outputs fed via --results are graded against it.
1510
1547
  // `thunder conform <file> [--targets ts,py] [--results <json|path>] [--json]`.
@@ -1542,6 +1579,11 @@ test Example
1542
1579
  const rep = buildConformance(ast, { targets, results });
1543
1580
  if (skippedTargets.length) rep.skipped = Array.from(new Set(skippedTargets));
1544
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
+ }
1545
1587
  if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1546
1588
  if (!rep.total) { console.log(`thunder conform ${basename(file)}: no test cases to conform.`); return; }
1547
1589
 
@@ -1752,6 +1794,11 @@ test Example
1752
1794
  const before = args.before ? readFileSync(args.before, 'utf8') : null;
1753
1795
  const language = args.from || languageForFile(args.after);
1754
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
+ }
1755
1802
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
1756
1803
  console.log(`thunder verify-diff ${basename(file)} vs ${basename(args.after)}: ${r.verdict} (${r.blocking} blocking, ${r.summary.regressions} regression(s))`);
1757
1804
  for (const f of r.findings) {
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.4.1';
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/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 {
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,