@skillstech/thunderlang 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.mjs CHANGED
@@ -5,9 +5,9 @@
5
5
  // NOT `dist/` , OpenThunder's scanner excludes dist/node_modules, so proof artifacts must live in a
6
6
  // committed, scannable location. `.intent/` mirrors the ecosystem's dot-dir convention (.openthunder/).
7
7
  //
8
- // intent check <file> parse + semantic diagnostics (exit 1 on error)
9
- // intent graph <file> [--out .intent] contract-graph.json + architecture-graph.json
10
- // intent proof <file> [--out .intent] .intent-proof.json
8
+ // thunder check <file> parse + semantic diagnostics (exit 1 on error)
9
+ // thunder graph <file> [--out .intent] contract-graph.json + architecture-graph.json
10
+ // intent proof <file> [--out .intent] .thunder-proof.json
11
11
  // intent build <file> [--out .intent] [--no-ai] all artifacts + docs + mermaid + testplan
12
12
  //
13
13
  // --no-ai is the default and only mode today; the flag is accepted for forward-compatibility.
@@ -39,18 +39,49 @@ import { liftSource, liftAll, liftRepo, languageForFile } from './lift.mjs';
39
39
  import { approveIntent, checkDrift, buildDriftHandoff } from './drift.mjs';
40
40
  import { buildMissionIndex } from './atlas.mjs';
41
41
  import { parseSelection, regionMetrics, selectCandidate } from './select.mjs';
42
- import { buildIntentGraph } from './intent-graph.mjs';
42
+ import { buildIntentGraph, safeGraph } from './intent-graph.mjs';
43
43
  import { buildAtlas, searchAtlas, expandNode } from './intent-atlas.mjs';
44
44
  import { buildFocusGraph, intentBrief, makeScope } from './focus.mjs';
45
45
  import { comprehensionLevel, comprehensionReport, LEVELS as COMPREHENSION_LEVELS } from './comprehension.mjs';
46
46
  import { twelveFactorReport } from './twelve-factor.mjs';
47
47
  import { GENERATORS } from './codegen.mjs';
48
48
  import { changeReport } from './changes.mjs';
49
- import { execSync } from 'node:child_process';
49
+ import { execSync, spawnSync } from 'node:child_process';
50
50
  import { diffGraphs, mergeGraphs } from './semantic-diff.mjs';
51
51
  import { applyWaivers, governanceDiagnostics } from './governance.mjs';
52
52
  import { exportIntent, EXPORT_FORMATS } from './exporters.mjs';
53
53
  import { evaluateDecision, simulateLifecycle } from './runtime.mjs';
54
+ import { runProperties } from './property.mjs';
55
+ import { runMutations } from './mutate.mjs';
56
+ import { buildConformance } from './conformance.mjs';
57
+ import { semanticCoverage } from './coverage.mjs';
58
+ import { runTypescriptTarget } from './target-ts.mjs';
59
+ import { runPythonTarget, pythonAvailable } from './target-py.mjs';
60
+ import { runCSharpTarget, csharpAvailable } from './target-cs.mjs';
61
+ import { runJavaTarget, javaAvailable } from './target-java.mjs';
62
+
63
+ // The live-target registry: one entry per canonical target that can be compiled + executed.
64
+ // `available()` is a (cached) toolchain probe; TypeScript/JS runs in-process so it is always live.
65
+ const LIVE_TARGETS = [
66
+ { key: 'typescript', run: runTypescriptTarget, available: () => true },
67
+ { key: 'python', run: runPythonTarget, available: pythonAvailable },
68
+ { key: 'csharp', run: runCSharpTarget, available: csharpAvailable },
69
+ { key: 'java', run: runJavaTarget, available: javaAvailable },
70
+ ];
71
+ const LIVE_BY_KEY = new Map(LIVE_TARGETS.map((t) => [t.key, t]));
72
+ LIVE_BY_KEY.set('javascript', LIVE_BY_KEY.get('typescript')); // js is the same in-process runner
73
+ // All canonical targets available to run right now (used by --all-targets).
74
+ const availableLiveTargets = () => LIVE_TARGETS.filter((t) => t.available());
75
+ const RUNNABLE_TARGETS = new Set(['typescript', 'ts', 'javascript', 'js', 'python', 'py', 'csharp', 'cs', 'c#', 'java']);
76
+ // Map an alias to its canonical target key + the adapter that executes it.
77
+ const TARGET_ALIASES = { ts: 'typescript', js: 'javascript', py: 'python', cs: 'csharp', 'c#': 'csharp' };
78
+ const canonicalTarget = (t) => TARGET_ALIASES[String(t).toLowerCase()] || String(t).toLowerCase();
79
+ // Execute a live target. Returns { "Test / case": actual } or null if the target can't run
80
+ // (e.g. the runtime/SDK is not installed). Unknown targets return null.
81
+ function runLiveTarget(ast, target) {
82
+ const entry = LIVE_BY_KEY.get(canonicalTarget(target));
83
+ return entry ? entry.run(ast) : null;
84
+ }
54
85
  import { importIntent, importReport, detectFormat, IMPORT_FORMATS } from './importers.mjs';
55
86
  import { runTests } from './testing.mjs';
56
87
  import { evaluateOutcomes } from './outcome.mjs';
@@ -69,6 +100,11 @@ import { parseEventLog, serializeEventLog, recordEvent, timeline } from './ai-ev
69
100
  // Recursively collect supported source files, skipping vendored / build dirs.
70
101
  const LIFT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.rs', '.pl', '.pm'];
71
102
  const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build', '.intent', 'coverage', '.vercel']);
103
+ // ThunderLang source files. `.thunder` is the canonical public extension; `.tl` is an
104
+ // accepted shorthand; `.intent` stays supported so legacy IntentLang sources keep working.
105
+ const SOURCE_EXTS = ['.thunder', '.tl', '.intent'];
106
+ const isSourceFile = (name) => SOURCE_EXTS.some((e) => name.endsWith(e));
107
+ const stripSourceExt = (name) => name.replace(/\.(thunder|tl|intent)$/i, '');
72
108
  function collectFiles(root, acc = []) {
73
109
  for (const name of readdirSync(root)) {
74
110
  if (SKIP_DIRS.has(name)) continue;
@@ -80,19 +116,90 @@ function collectFiles(root, acc = []) {
80
116
  return acc;
81
117
  }
82
118
 
83
- // Walk a directory for authored .intent files (skips the .intent/ output dir).
119
+ // Walk a directory for authored source files (skips the .intent/ output dir).
84
120
  function collectIntents(root, acc = []) {
85
121
  const st = statSync(root);
86
- if (!st.isDirectory()) return root.endsWith('.intent') ? [root] : acc;
122
+ if (!st.isDirectory()) return isSourceFile(root) ? [root] : acc;
87
123
  for (const name of readdirSync(root)) {
88
124
  if (SKIP_DIRS.has(name)) continue;
89
125
  const full = join(root, name);
90
126
  if (statSync(full).isDirectory()) collectIntents(full, acc);
91
- else if (name.endsWith('.intent')) acc.push(full);
127
+ else if (isSourceFile(name)) acc.push(full);
92
128
  }
93
129
  return acc;
94
130
  }
95
131
 
132
+ // Resolve every guarantee/never obligation against the SPECIFIC test that verifies it.
133
+ // Shared by `thunder test --contracts` and `thunder prove` so both agree per-claim.
134
+ // States: verified (proven by a passing named test), failed (its test fails), declared
135
+ // (a verification is named/classified but not runnable in-file), unverified (nothing).
136
+ function resolveObligations(ast) {
137
+ const t = runTests(ast);
138
+ const testPass = new Map();
139
+ for (const res of (t.results || [])) {
140
+ const cur = testPass.get(res.target);
141
+ testPass.set(res.target, cur === undefined ? !!res.pass : cur && !!res.pass);
142
+ }
143
+ const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
144
+ const matchTest = (verifyText) => {
145
+ const v = norm(verifyText); if (!v) return null;
146
+ for (const [name, pass] of testPass) { const n = norm(name); if (n && (n === v || n.includes(v) || v.includes(n))) return { name, pass }; }
147
+ return null;
148
+ };
149
+ const build = (kind, o) => {
150
+ const verify = o.verify || [];
151
+ const kinds = [...new Set((o.verifications || []).map((v) => v.kind).filter((k) => k && k !== 'unclassified'))];
152
+ let status, provenBy = null;
153
+ if (!verify.length) status = 'unverified';
154
+ else {
155
+ let m = null;
156
+ for (const vt of verify) { const x = matchTest(vt); if (x) { m = x; if (!x.pass) break; } }
157
+ if (m) { status = m.pass ? 'verified' : 'failed'; provenBy = m.name; } else status = 'declared';
158
+ }
159
+ return { kind, id: o.id, text: o.statement, status, verify, kinds, provenBy };
160
+ };
161
+ const obligations = [
162
+ ...ast.guarantees.map((g) => build('guarantee', g)),
163
+ ...ast.neverRules.map((n) => build('prohibition', n)),
164
+ ];
165
+ const count = (s) => obligations.filter((o) => o.status === s).length;
166
+ return { obligations, tests: t, verified: count('verified'), declared: count('declared'), unverified: count('unverified'), failed: count('failed') };
167
+ }
168
+
169
+ // Proof freshness: a proof is valid only for a specific (intent, implementation, dependencies,
170
+ // compiler, environment) tuple. These helpers capture that tuple so `verify` can mark a proof STALE.
171
+ const gitCmd = (c) => { try { return execSync(`git ${c}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim() || null; } catch { return null; } };
172
+ const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'poetry.lock', 'Pipfile.lock', 'Cargo.lock', 'go.sum', 'Gemfile.lock', 'composer.lock'];
173
+ function findLockfile(startDir) {
174
+ let dir = startDir || '.';
175
+ for (let i = 0; i < 6; i++) {
176
+ for (const lf of LOCKFILES) { const p = join(dir, lf); if (existsSync(p)) return p; }
177
+ const parent = dirname(dir); if (parent === dir) break; dir = parent;
178
+ }
179
+ return null;
180
+ }
181
+ function freshnessFor(file, proof, envName) {
182
+ const dir = dirname(file) || '.';
183
+ const lock = findLockfile(dir);
184
+ return {
185
+ intentHash: proof.sourceHash,
186
+ compilerVersion: proof.compilerVersion,
187
+ implementation: gitCmd('rev-parse HEAD'),
188
+ dependencies: lock ? { file: basename(lock), hash: sha256(readFileSync(lock, 'utf8')) } : null,
189
+ environment: envName || null,
190
+ generatedAt: proof.generatedAt,
191
+ };
192
+ }
193
+ // Given a proof's recorded freshness, recompute the current tuple and list what has drifted.
194
+ function stalenessReasons(freshness, srcDir) {
195
+ const reasons = [];
196
+ if (!freshness) return reasons;
197
+ if (freshness.implementation) { const now = gitCmd('rev-parse HEAD'); if (now && now !== freshness.implementation) reasons.push(`implementation moved: proof at ${freshness.implementation.slice(0, 7)}, now ${now.slice(0, 7)}`); }
198
+ if (freshness.dependencies) { const lock = findLockfile(srcDir); const now = lock ? sha256(readFileSync(lock, 'utf8')) : null; if (now && now !== freshness.dependencies.hash) reasons.push(`dependency lockfile changed (${freshness.dependencies.file})`); }
199
+ if (freshness.compilerVersion && freshness.compilerVersion !== COMPILER_VERSION) reasons.push(`compiler upgraded: proof ${freshness.compilerVersion}, now ${COMPILER_VERSION}`);
200
+ return reasons;
201
+ }
202
+
96
203
  function parseArgs(argv) {
97
204
  const args = { _: [], out: '.intent', noAi: false };
98
205
  for (let i = 0; i < argv.length; i++) {
@@ -124,6 +231,19 @@ function parseArgs(argv) {
124
231
  else if (a === '--check') args.check = true;
125
232
  else if (a === '--schema') args.schema = true;
126
233
  else if (a === '--all') args.all = true;
234
+ else if (a === '--contracts') args.contracts = true;
235
+ else if (a === '--strict') args.strict = true;
236
+ else if (a === '--changed') args.changed = true;
237
+ else if (a === '--properties') args.properties = true;
238
+ else if (a === '--scenarios') args.scenarios = true;
239
+ else if (a === '--mutate') args.mutate = true;
240
+ else if (a === '--safe') args.safe = true;
241
+ else if (a === '--evals') args.evals = true;
242
+ else if (a === '--coverage') args.coverage = true;
243
+ else if (a === '--run') args.run = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean);
244
+ else if (a === '--results') args.results = argv[++i];
245
+ else if (a === '--cases') args.cases = argv[++i];
246
+ else if (a === '--seed') args.seed = argv[++i];
127
247
  else if (a === '--ir') args.ir = argv[++i];
128
248
  else if (a === '--subject') args.subject = argv[++i];
129
249
  else if (a === '--lens') args.lens = argv[++i];
@@ -134,6 +254,8 @@ function parseArgs(argv) {
134
254
  else if (a === '--learning') args.learning = true;
135
255
  else if (a === '--governed') args.governed = true;
136
256
  else if (a === '--target') args.target = argv[++i];
257
+ else if (a === '--all-targets') args.allTargets = true;
258
+ else if (a === '--env') args.env = argv[++i];
137
259
  else if (a === '--brief') args.brief = argv[++i];
138
260
  else if (a === '--after') args.after = argv[++i];
139
261
  else if (a === '--before') args.before = argv[++i];
@@ -197,12 +319,25 @@ function printDiagnostics(diags) {
197
319
  return errors;
198
320
  }
199
321
 
200
- const HELP = `intent , the deterministic ThunderLang compiler (no AI required)
322
+ const HELP = `thunder , the ThunderLang compiler + engine (deterministic, no AI required)
323
+
324
+ usage: thunder <command> <file> [options]
325
+ thunder mission <Name> <command> run any command on a mission by name
201
326
 
202
- usage: intent <command> <file> [options]
327
+ Everyday
328
+ new [Name] scaffold a runnable starter mission (Name.thunder) [alias: init]
329
+ mission <Name> [cmd] resolve a mission by name and run a command on it (list | <Name> | <Name> <cmd>)
330
+ check <file|dir> parse + lint + explainable diagnostics
331
+ run <file> --inputs '<json>' execute the decision(s) deterministically
332
+ test <file> [--contracts | --properties | --scenarios | --mutate | --evals | --changed | --coverage] [--strict]
333
+ test <file> --target typescript|python|csharp|java run the tests against the EXECUTED generated code
334
+ test <file> --all-targets run the tests against every AVAILABLE target in one pass
335
+ prove <file> emit an intent-proof-v1 artifact (honest verdicts + freshness)
336
+ verify <proof.json> [src] re-check a proof; reports STALE when impl/deps/compiler moved
337
+ build <file> generate docs, contract graph, test plan, targets
203
338
 
204
339
  Author & check
205
- init [Name] scaffold a runnable starter mission (Name.intent)
340
+ init [Name] scaffold a runnable starter mission (Name.thunder)
206
341
  draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
207
342
  check <file|dir> [--json|--format sarif] diagnostics for one file, or gate a whole dir
208
343
  report [dir] [--json] repo-wide intent health: severity + area counts, coverage
@@ -221,15 +356,16 @@ Author & check
221
356
  edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
222
357
  lsp start the Language Server (LSP over stdio, for editors)
223
358
  mcp start the MCP server (for AI coding agents; stdio)
224
- build <file> docs, contract graph, test plan, and .intent-proof.json
225
- graph <file> the canonical Intent Graph (intent-graph-v1)
226
- proof <file> the .intent-proof.json artifact
359
+ build <file> docs, contract graph, test plan, and .thunder-proof.json
360
+ graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
361
+ proof <file> the .thunder-proof.json artifact
227
362
  proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
228
363
  verify <proof.json> [src] confirm a proof is well-formed and still matches its source
364
+ conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] run the same cases against every target (conformance matrix)
229
365
  schema emit the canonical graph schema + diagnostic catalog
230
366
  explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
231
367
  rules [--json] list the whole canonical diagnostic catalog
232
- notes <file> [--lens <lens>] [--json] IntentLens: the compiled note blocks by lens (not verification)
368
+ notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
233
369
  docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
234
370
  code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
235
371
  apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
@@ -244,7 +380,7 @@ Execute (no AI, no generated code)
244
380
  Interop
245
381
  export <file> --format <dmn|bpmn|smv|jsonschema|openapi|tokens|mermaid|css|playwright> render to a standard format
246
382
  import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
247
- source <file|graph.json> regenerate .intent from a graph
383
+ source <file|graph.json> regenerate .thunder from a graph
248
384
  migrate <graph.json> [--to <version>] upgrade a persisted graph
249
385
  validate <graph.json> [--json] check a graph is canonical (anti-fork)
250
386
 
@@ -281,21 +417,21 @@ function main() {
281
417
  else console.log(JSON.stringify(out, null, 2));
282
418
  return;
283
419
  }
284
- // `intent proof --schema` emits the canonical proof envelope JSON Schema (no file needed).
420
+ // `thunder proof --schema` emits the canonical proof envelope JSON Schema (no file needed).
285
421
  // This is the "shared envelope" schema siblings sign (STW) and re-verify (RM/OT) against.
286
422
  if (cmd === 'proof' && args.schema) {
287
423
  console.log(JSON.stringify(intentProofJsonSchema(), null, 2));
288
424
  return;
289
425
  }
290
- // Verify a .intent-proof.json against its source: the source hash still matches (no
426
+ // Verify a .thunder-proof.json against its source: the source hash still matches (no
291
427
  // drift / tampering) and the proof's claims re-derive from the source.
292
428
  if (cmd === 'verify') {
293
429
  const proofPath = args._[0];
294
- if (!proofPath) { console.error('usage: intent verify <proof.json> [<source.intent>]'); process.exit(2); return; }
430
+ if (!proofPath) { console.error('usage: thunder verify <proof.json> [<source.intent>]'); process.exit(2); return; }
295
431
  let proof;
296
- try { proof = JSON.parse(readFileSync(proofPath, 'utf8')); } catch { console.error('intent verify: proof is not valid JSON'); process.exit(2); return; }
432
+ try { proof = JSON.parse(readFileSync(proofPath, 'utf8')); } catch { console.error('thunder verify: proof is not valid JSON'); process.exit(2); return; }
297
433
  const srcPath = args._[1] || proof.sourceFile;
298
- if (!srcPath || !existsSync(srcPath)) { console.error(`intent verify: source not found (${srcPath || 'none'}). Pass it: intent verify <proof.json> <source.intent>`); process.exit(2); return; }
434
+ if (!srcPath || !existsSync(srcPath)) { console.error(`thunder verify: source not found (${srcPath || 'none'}). Pass it: thunder verify <proof.json> <source.intent>`); process.exit(2); return; }
299
435
  const src = readFileSync(srcPath, 'utf8');
300
436
  const ast = parseIntent(src);
301
437
  const diags = semanticDiagnostics(ast);
@@ -307,26 +443,32 @@ function main() {
307
443
  // Structural gate: the envelope must be a well-formed intent-proof-v1 document first.
308
444
  const structure = validateProof(proof);
309
445
  const valid = structure.valid && hashMatch && semanticMatch && guaranteesMatch && neverMatch;
310
- const result = { schema: 'intent-verify-v1', proof: proofPath, source: srcPath, valid, checks: { wellFormed: structure.valid, hashMatch, semanticMatch, guaranteesMatch, neverMatch }, structureErrors: structure.errors };
311
- if (args.json) { console.log(JSON.stringify(result, null, 2)); process.exit(valid ? 0 : 1); return; }
312
- console.log(`intent verify ${basename(proofPath)}: ${valid ? 'VALID' : 'FAILED'} (source ${basename(srcPath)})`);
446
+ // Freshness: even if the intent still matches, the proof is STALE if the implementation,
447
+ // dependencies, or compiler moved since it was generated. A stale proof must not read green.
448
+ const staleReasons = valid ? stalenessReasons(proof.freshness, dirname(srcPath) || '.') : [];
449
+ const stale = staleReasons.length > 0;
450
+ const status = !valid ? 'FAILED' : stale ? 'STALE' : 'VALID';
451
+ const result = { schema: 'intent-verify-v1', proof: proofPath, source: srcPath, valid, stale, status, staleReasons, checks: { wellFormed: structure.valid, hashMatch, semanticMatch, guaranteesMatch, neverMatch }, structureErrors: structure.errors };
452
+ if (args.json) { console.log(JSON.stringify(result, null, 2)); process.exit(valid && !stale ? 0 : 1); return; }
453
+ console.log(`thunder verify ${basename(proofPath)}: ${status} (source ${basename(srcPath)})`);
313
454
  if (!structure.valid) { console.log(` X proof is not a well-formed intent-proof-v1 document:`); for (const e of structure.errors) console.log(` ${e.path || '(root)'}: ${e.message}`); }
314
455
  if (!hashMatch) console.log(' X source hash does not match , the source has changed since the proof was generated (drift or tampering).');
315
456
  if (!semanticMatch) console.log(' X the proof claims a different semantic result than the source produces now.');
316
457
  if (!guaranteesMatch) console.log(' X guarantee count differs from the proof.');
317
458
  if (!neverMatch) console.log(' X never-rule count differs from the proof.');
318
- if (valid) console.log(` ok proof matches source (hash + ${(proof.guarantees || []).length} guarantee(s), ${(proof.neverRules || []).length} never-rule(s)).`);
319
- process.exit(valid ? 0 : 1);
459
+ if (stale) { console.log(' ! proof is STALE , the intent still matches, but the world moved since it was generated:'); for (const r of staleReasons) console.log(` ${r}`); console.log(' regenerate: thunder prove ' + basename(srcPath)); }
460
+ if (valid && !stale) console.log(` ok proof matches source and is fresh (hash + ${(proof.guarantees || []).length} guarantee(s), ${(proof.neverRules || []).length} never-rule(s)).`);
461
+ process.exit(valid && !stale ? 0 : 1);
320
462
  return;
321
463
  }
322
464
 
323
- // Explain a diagnostic code from the canonical catalog. `intent explain IL-DEC-001`.
465
+ // Explain a diagnostic code from the canonical catalog. `thunder explain IL-DEC-001`.
324
466
  if (cmd === 'explain') {
325
467
  const code = file;
326
- if (!code) { console.error('usage: intent explain <IL-CODE>'); process.exit(2); return; }
468
+ if (!code) { console.error('usage: thunder explain <IL-CODE>'); process.exit(2); return; }
327
469
  const rule = ALL_DIAGNOSTICS.find((r) => r.ruleId.toLowerCase() === code.toLowerCase());
328
470
  if (args.json) { console.log(JSON.stringify(rule || { ruleId: code, found: false }, null, 2)); process.exit(rule ? 0 : 1); return; }
329
- if (!rule) { console.error(`intent explain: "${code}" is not in the diagnostic catalog. Run "intent rules" for the full list.`); process.exit(1); return; }
471
+ if (!rule) { console.error(`thunder explain: "${code}" is not in the diagnostic catalog. Run "intent rules" for the full list.`); process.exit(1); return; }
330
472
  console.log(`${rule.ruleId} (area: ${rule.area})`);
331
473
  console.log(` ${rule.summary}`);
332
474
  console.log(` severity: ${rule.severity}${rule.blocks && rule.blocks.length ? ` | blocks: ${rule.blocks.join(', ')}` : ' | does not block a phase'}`);
@@ -338,7 +480,7 @@ function main() {
338
480
  if (args.json) { console.log(JSON.stringify(ALL_DIAGNOSTICS, null, 2)); return; }
339
481
  const byArea = {};
340
482
  for (const r of ALL_DIAGNOSTICS) (byArea[r.area] ||= []).push(r);
341
- console.log(`intent rules: ${ALL_DIAGNOSTICS.length} diagnostics in ${Object.keys(byArea).length} areas\n`);
483
+ console.log(`thunder rules: ${ALL_DIAGNOSTICS.length} diagnostics in ${Object.keys(byArea).length} areas\n`);
342
484
  for (const area of Object.keys(byArea).sort()) {
343
485
  console.log(`${area}`);
344
486
  for (const r of byArea[area]) {
@@ -350,7 +492,7 @@ function main() {
350
492
  return;
351
493
  }
352
494
 
353
- // `intent notes <file> [--lens <lens>] [--json]` , the IntentLens report: the compiled
495
+ // `thunder notes <file> [--lens <lens>] [--json]` , the ThunderLens report: the compiled
354
496
  // semantic comments (`note <lens>:`) grouped by lens, each with its target and source
355
497
  // line. Notes explain meaning for a reader; they are NEVER verification.
356
498
  if (cmd === 'notes') {
@@ -363,7 +505,7 @@ function main() {
363
505
  return;
364
506
  }
365
507
  const scope = args.lens ? ` (lens: ${args.lens})` : '';
366
- console.log(`intent notes ${basename(file)}${scope}: ${notes.length} note${notes.length === 1 ? '' : 's'}`);
508
+ console.log(`thunder notes ${basename(file)}${scope}: ${notes.length} note${notes.length === 1 ? '' : 's'}`);
367
509
  const known = new Set(KNOWN_LENSES);
368
510
  const byLens = {};
369
511
  for (const n of notes) (byLens[n.lens] ||= []).push(n);
@@ -378,7 +520,7 @@ function main() {
378
520
  return;
379
521
  }
380
522
 
381
- // `intent docs <file> [--lens <lens>] [--out <dir>]` , render a mission as Markdown docs.
523
+ // `thunder docs <file> [--lens <lens>] [--out <dir>]` , render a mission as Markdown docs.
382
524
  // With --lens, produce an audience-specific doc with that lens's notes woven inline.
383
525
  if (cmd === 'docs') {
384
526
  if (!file) { console.error('usage: intent docs <file> [--lens <lens>] [--out <dir>]'); process.exit(2); return; }
@@ -394,19 +536,19 @@ function main() {
394
536
  return;
395
537
  }
396
538
 
397
- // `intent code-actions <file> [--json]` , the available quick-fixes, each safety-graded
539
+ // `thunder code-actions <file> [--json]` , the available quick-fixes, each safety-graded
398
540
  // (safe autocorrects + reviewable diagnostic fixes). The IDE lightbulb's data source.
399
541
  if (cmd === 'code-actions') {
400
542
  if (!file) { console.error('usage: intent code-actions <file> [--json]'); process.exit(2); return; }
401
543
  const source = readFileSync(file, 'utf8');
402
544
  const actions = getCodeActions(source, semanticDiagnostics(parseIntent(source)));
403
545
  if (args.json) { console.log(JSON.stringify({ schema: 'intent-code-actions-v1', count: actions.length, actions }, null, 2)); return; }
404
- console.log(`intent code-actions ${basename(file)}: ${actions.length} action${actions.length === 1 ? '' : 's'}`);
546
+ console.log(`thunder code-actions ${basename(file)}: ${actions.length} action${actions.length === 1 ? '' : 's'}`);
405
547
  for (const a of actions) console.log(` [${a.safety}] ${a.kind}${a.line ? ` (line ${a.line})` : ''} ${a.title}`);
406
548
  return;
407
549
  }
408
550
 
409
- // `intent apply-fix <file> [--write]` , apply the SAFE autocorrects only (header aliases,
551
+ // `thunder apply-fix <file> [--write]` , apply the SAFE autocorrects only (header aliases,
410
552
  // stray colons). Reviewable quick-fixes are reported, never applied blindly.
411
553
  if (cmd === 'apply-fix') {
412
554
  if (!file) { console.error('usage: intent apply-fix <file> [--write]'); process.exit(2); return; }
@@ -415,7 +557,7 @@ function main() {
415
557
  const reviewable = getCodeActions(source, semanticDiagnostics(parseIntent(source))).filter((a) => a.safety !== 'safe');
416
558
  if (args.json) { console.log(JSON.stringify({ applied: changes, reviewableRemaining: reviewable.length, changed: fixed !== source }, null, 2)); }
417
559
  else {
418
- console.log(`intent apply-fix ${basename(file)}: ${changes.length} safe fix${changes.length === 1 ? '' : 'es'}${args.write ? ' applied' : ' (dry run; pass --write)'}`);
560
+ console.log(`thunder apply-fix ${basename(file)}: ${changes.length} safe fix${changes.length === 1 ? '' : 'es'}${args.write ? ' applied' : ' (dry run; pass --write)'}`);
419
561
  for (const c of changes) console.log(` line ${c.line}: "${c.from}" -> "${c.to}" [${c.rule}]`);
420
562
  if (reviewable.length) console.log(` ${reviewable.length} reviewable quick-fix(es) left for a human (run: intent code-actions ${basename(file)})`);
421
563
  }
@@ -423,12 +565,12 @@ function main() {
423
565
  return;
424
566
  }
425
567
 
426
- // `intent focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json]` , Intent Lens:
568
+ // `thunder focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json]` , Intent Lens:
427
569
  // a focused Focus Graph + Intent Brief around a selected scope, built over the Atlas.
428
570
  if (cmd === 'focus') {
429
571
  const dir = args.dir || '.';
430
572
  const files = existsSync(dir) && statSync(dir).isDirectory() ? collectIntents(dir) : (file && existsSync(file) ? [file] : []);
431
- if (!files.length) { console.error(`intent focus: no .intent files under ${dir}`); process.exit(2); return; }
573
+ if (!files.length) { console.error(`thunder focus: no .intent files under ${dir}`); process.exit(2); return; }
432
574
  const atlas = buildAtlas(files.map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8')))));
433
575
  // Resolve seeds: --nodes ids, or a mission/feature query, or (default) all missions.
434
576
  let seeds = [];
@@ -437,14 +579,14 @@ function main() {
437
579
  if (args.nodes) { seeds = args.nodes.split(',').map((s) => s.trim()).filter(Boolean); scopeType = 'custom'; scopeTitle = `${seeds.length} selected node(s)`; }
438
580
  else if (file && !existsSync(file)) {
439
581
  const hit = searchAtlas(atlas, file)[0];
440
- if (!hit) { console.error(`intent focus: no Atlas node matches "${file}"`); process.exit(1); return; }
582
+ if (!hit) { console.error(`thunder focus: no Atlas node matches "${file}"`); process.exit(1); return; }
441
583
  seeds = [hit.id]; scopeType = hit.type === 'Mission' ? 'mission' : 'feature'; scopeTitle = hit.title || hit.id;
442
584
  } else { seeds = atlas.missions.map((m) => m.id); scopeType = 'capability'; scopeTitle = 'whole project'; }
443
585
  const scope = makeScope({ type: scopeType, title: scopeTitle, seeds, createdAt: null });
444
586
  const focus = buildFocusGraph(atlas, { seeds, depth: args.depth ? Number(args.depth) : 2, scope });
445
587
  const brief = intentBrief(focus);
446
588
  if (args.json) { console.log(JSON.stringify({ scope, brief, focus }, null, 2)); return; }
447
- console.log(`intent focus , ${scope.title} [${scope.type}] (${scope.scopeId})`);
589
+ console.log(`thunder focus , ${scope.title} [${scope.type}] (${scope.scopeId})`);
448
590
  console.log(` what: ${brief.what || '(unnamed)'}${brief.confidence ? ` confidence: ${brief.confidence}` : ''}`);
449
591
  if (brief.who.length) console.log(` who: ${brief.who.join(', ')}`);
450
592
  console.log(` focus graph: ${focus.overview.nodes} node(s), ${focus.overview.relationships} edge(s), depth ${focus.depth}`);
@@ -457,19 +599,19 @@ function main() {
457
599
  return;
458
600
  }
459
601
 
460
- // `intent comprehension <file|dir> [--observed] [--learning] [--governed] [--json]` , the C0..C7
602
+ // `thunder comprehension <file|dir> [--observed] [--learning] [--governed] [--json]` , the C0..C7
461
603
  // understanding level of each mission (Comprehension Contract). IL scores the intent side (C1..C4);
462
604
  // --observed (OpenThunder/runtime), --learning (Skills Tech Talk), --governed (Guardian) lift the
463
605
  // joint level to C5/C6/C7 when a sibling attaches its evidence.
464
606
  if (cmd === 'comprehension') {
465
607
  const root = file || '.';
466
608
  const files = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : (existsSync(root) ? [root] : []);
467
- if (!files.length) { console.error(`intent comprehension: no .intent files under ${root}`); process.exit(2); return; }
609
+ if (!files.length) { console.error(`thunder comprehension: no .intent files under ${root}`); process.exit(2); return; }
468
610
  const opts = { observed: !!args.observed, learningPath: !!args.learning, governed: !!args.governed };
469
611
  const asts = files.map((f) => parseIntent(readFileSync(f, 'utf8')));
470
612
  const report = comprehensionReport(asts, opts);
471
613
  if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
472
- console.log(`intent comprehension ${root}: ${report.count} mission(s)`);
614
+ console.log(`thunder comprehension ${root}: ${report.count} mission(s)`);
473
615
  console.log(` distribution: ${COMPREHENSION_LEVELS.map((l) => `${l.level}:${report.byLevel[l.level]}`).join(' ')}`);
474
616
  for (const m of report.missions) {
475
617
  console.log(`\n ${m.mission || '(unnamed)'} , ${m.level} ${m.levelName}`);
@@ -482,7 +624,7 @@ function main() {
482
624
  return;
483
625
  }
484
626
 
485
- // `intent twelve-factor <file> [--json]` , score an intent against the 13 principles of
627
+ // `thunder twelve-factor <file> [--json]` , score an intent against the 13 principles of
486
628
  // humanlayer/12-factor-agents (deterministic conformance lens). Per-factor verdict + score.
487
629
  if (cmd === 'twelve-factor' || cmd === '12factor') {
488
630
  if (!file) { console.error('usage: intent twelve-factor <file> [--json]'); process.exit(2); return; }
@@ -500,14 +642,14 @@ function main() {
500
642
  return;
501
643
  }
502
644
 
503
- // `intent gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
645
+ // `thunder gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
504
646
  // intent. Generates the typed contract + the decision logic (already executable) and leaves
505
647
  // honest TODO markers for business logic. No AI. Prints, or writes with --out.
506
648
  if (cmd === 'gen') {
507
649
  if (!file) { console.error(`usage: intent gen <file> [--target ${Object.keys(GENERATORS).join('|')}] [--out <dir>]`); process.exit(2); return; }
508
650
  const target = (args.target || 'typescript').toLowerCase();
509
651
  const generate = GENERATORS[target];
510
- if (!generate) { console.error(`intent gen: unknown target "${target}" (have: ${Object.keys(GENERATORS).join(', ')})`); process.exit(2); return; }
652
+ if (!generate) { console.error(`thunder gen: unknown target "${target}" (have: ${Object.keys(GENERATORS).join(', ')})`); process.exit(2); return; }
511
653
  const ast = parseIntent(readFileSync(file, 'utf8'));
512
654
  const code = generate(ast);
513
655
  if (args.outExplicit) {
@@ -520,27 +662,27 @@ function main() {
520
662
  return;
521
663
  }
522
664
 
523
- // `intent changes [<base>..<head> | <base>] [--json]` , Change Lens: what a branch / PR / commit
665
+ // `thunder changes [<base>..<head> | <base>] [--json]` , Change Lens: what a branch / PR / commit
524
666
  // range changed BY MEANING. git-diffs the .intent files, semantic-diffs each, and reports the
525
667
  // behavior-level changes + regression risk. Default: HEAD vs the working tree.
526
668
  if (cmd === 'changes') {
527
669
  const git = (c) => { try { return execSync(`git ${c}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); } catch { return null; } };
528
- if (git('rev-parse --is-inside-work-tree') === null) { console.error('intent changes: not a git repository'); process.exit(2); return; }
670
+ if (git('rev-parse --is-inside-work-tree') === null) { console.error('thunder changes: not a git repository'); process.exit(2); return; }
529
671
  const range = file || '';
530
672
  let base, head; // head === null means "the working tree"
531
673
  if (range.includes('..')) { [base, head] = range.split('..'); head = head || 'HEAD'; }
532
674
  else if (range) { base = range; head = null; }
533
675
  else { base = 'HEAD'; head = null; }
534
676
  const diffSpec = head ? `${base} ${head}` : base;
535
- const names = (git(`diff --name-only ${diffSpec} -- "*.intent"`) || '').split('\n').map((s) => s.trim()).filter(Boolean);
536
- if (!names.length) { console.log(`intent changes ${range || `${base}..working-tree`}: no .intent files changed`); return; }
677
+ const names = (git(`diff --name-only ${diffSpec} -- "*.thunder" "*.tl" "*.intent"`) || '').split('\n').map((s) => s.trim()).filter(Boolean);
678
+ if (!names.length) { console.log(`thunder changes ${range || `${base}..working-tree`}: no source files changed`); return; }
537
679
  const readAt = (ref, p) => (ref === null ? (existsSync(p) ? readFileSync(p, 'utf8') : null) : git(`show ${ref}:${p}`));
538
680
  const toGraph = (src) => (src != null ? buildIntentGraph(parseIntent(src)) : null);
539
681
  const pairs = names.map((p) => ({ path: p, before: toGraph(readAt(base, p)), after: toGraph(readAt(head, p)) }));
540
682
  const report = changeReport(pairs);
541
683
  if (args.json) { console.log(JSON.stringify(report, null, 2)); process.exit(report.verdict === 'review' ? 1 : 0); return; }
542
684
  const t = report.totals;
543
- console.log(`intent changes ${range || `${base}..working-tree`}: ${report.verdict.toUpperCase()}`);
685
+ console.log(`thunder changes ${range || `${base}..working-tree`}: ${report.verdict.toUpperCase()}`);
544
686
  console.log(` ${t.files} file(s) , +${t.added} / -${t.removed} / ~${t.changed} nodes${t.invalidatedApprovals ? `, ${t.invalidatedApprovals} approval(s) invalidated` : ''}`);
545
687
  if (report.regressions.length) {
546
688
  console.log(' regression risk (a promise or its proof was removed or weakened):');
@@ -559,18 +701,18 @@ function main() {
559
701
  }
560
702
 
561
703
  // MCP server (Model Context Protocol over stdio) , makes ThunderLang a native tool for AI
562
- // coding agents. Long-running; no file argument. Point an MCP client at `intent mcp`.
704
+ // coding agents. Long-running; no file argument. Point an MCP client at `thunder mcp`.
563
705
  if (cmd === 'mcp') {
564
706
  startMcpServer();
565
707
  return; // keep the process alive on stdin
566
708
  }
567
709
 
568
- // Scaffold a runnable starter mission (deterministic, no AI). `intent init [Name]`.
569
- if (cmd === 'init') {
570
- const name = (file || 'Mission').replace(/\.intent$/i, '');
571
- const target = join(args.out && args.out !== '.intent' ? args.out : '.', `${name}.intent`);
710
+ // Scaffold a runnable starter mission (deterministic, no AI). `thunder init [Name]`.
711
+ if (cmd === 'init' || cmd === 'new') {
712
+ const name = stripSourceExt(file || 'Mission');
713
+ const target = join(args.out && args.out !== '.intent' ? args.out : '.', `${name}.thunder`);
572
714
  if (existsSync(target) && !args.force) {
573
- console.error(`intent init: ${target} already exists (use --force to overwrite).`);
715
+ console.error(`thunder init: ${target} already exists (use --force to overwrite).`);
574
716
  process.exit(1); return;
575
717
  }
576
718
  const starter = `mission ${name}
@@ -586,7 +728,7 @@ guarantee an example property that must always hold
586
728
  never
587
729
  do something this mission must never do
588
730
 
589
- # A runnable decision. Try: intent run ${name}.intent --inputs '{"age":20}'
731
+ # A runnable decision. Try: thunder run ${name}.intent --inputs '{"age":20}'
590
732
  decision Example
591
733
  inputs
592
734
  age
@@ -596,7 +738,7 @@ decision Example
596
738
  default
597
739
  return Blocked
598
740
 
599
- # Tests live in the file. Try: intent test ${name}.intent
741
+ # Tests live in the file. Try: thunder test ${name}.intent
600
742
  test Example
601
743
  case adult
602
744
  given age 20
@@ -607,15 +749,61 @@ test Example
607
749
  `;
608
750
  if (target.includes('/')) mkdirSync(dirname(target), { recursive: true });
609
751
  writeFileSync(target, starter);
610
- console.log(`intent init: wrote ${target}`);
611
- console.log(` next: intent check ${target} | intent run ${target} --inputs '{"age":20}' | intent test ${target}`);
752
+ console.log(`thunder ${cmd}: wrote ${target}`);
753
+ console.log(` next: thunder check ${target} | thunder run ${target} --inputs '{"age":20}' | thunder test ${target}`);
612
754
  return;
613
755
  }
614
756
 
615
- if (!file && cmd !== 'draft') {
616
- console.error(`intent ${cmd}: missing a file argument. Run "intent help" for usage.`);
757
+ if (!file && cmd !== 'draft' && cmd !== 'mission' && !(cmd === 'test' && args.changed)) {
758
+ console.error(`thunder ${cmd}: missing a file argument. Run "intent help" for usage.`);
617
759
  process.exit(2);
618
760
  }
761
+
762
+ // `thunder mission <Name> [<verb> ...]` , noun-first ergonomics: resolve a mission by name
763
+ // anywhere in the project, then run any verb on it without typing a path.
764
+ // `thunder mission list` lists every mission. `thunder mission <Name>` prints a summary.
765
+ if (cmd === 'mission') {
766
+ const scanRoot = process.cwd();
767
+ const parsed = collectIntents(scanRoot)
768
+ .map((f) => { try { return { file: f, ast: parseIntent(readFileSync(f, 'utf8')) }; } catch { return null; } })
769
+ .filter((x) => x && x.ast && x.ast.mission);
770
+ const raw = process.argv.slice(3); // [Name|list, verb?, ...passthrough]
771
+ const nameArg = raw[0];
772
+
773
+ if (!nameArg || nameArg === 'list') {
774
+ if (!parsed.length) { console.log('thunder mission list: no missions found under the current directory.'); return; }
775
+ console.log(`thunder mission list: ${parsed.length} mission(s)`);
776
+ for (const { file: f, ast } of parsed.sort((a, b) => (a.ast.mission || '').localeCompare(b.ast.mission || ''))) {
777
+ const g = (ast.guarantees || []).length, n = (ast.neverRules || []).length, t = (ast.tests || []).length;
778
+ console.log(` ${(ast.mission || '(unnamed)').padEnd(26)} ${relative(scanRoot, f)} (${g} guarantee(s), ${n} never, ${t} test(s))`);
779
+ }
780
+ return;
781
+ }
782
+
783
+ const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
784
+ const matches = parsed.filter((x) => x.ast.mission === nameArg);
785
+ const hit = matches[0] || parsed.find((x) => norm(x.ast.mission) === norm(nameArg));
786
+ if (!hit) { console.error(`thunder mission: no mission named "${nameArg}". Try: thunder mission list`); process.exit(2); return; }
787
+ if (matches.length > 1) console.error(`thunder mission: ${matches.length} missions named "${nameArg}"; using ${relative(scanRoot, hit.file)}`);
788
+
789
+ const verb = raw[1];
790
+ if (!verb) {
791
+ const a = hit.ast;
792
+ console.log(`mission ${a.mission} (${relative(scanRoot, hit.file)})`);
793
+ if (a.goal) console.log(` goal: ${a.goal}`);
794
+ if ((a.guarantees || []).length) { console.log(' guarantees:'); for (const g of a.guarantees) console.log(` - ${g.statement}`); }
795
+ if ((a.neverRules || []).length) { console.log(' never:'); for (const nr of a.neverRules) console.log(` - ${nr.statement}`); }
796
+ if ((a.targets || []).length) console.log(` targets: ${a.targets.join(', ')}`);
797
+ if ((a.tests || []).length) console.log(` tests: ${a.tests.length}`);
798
+ console.log(` try: thunder mission ${a.mission} check | run | test | prove | build`);
799
+ return;
800
+ }
801
+
802
+ // Delegate to `thunder <verb> <file> <passthrough>` in a child so every flag is honored exactly.
803
+ const res = spawnSync(process.execPath, [process.argv[1], verb, hit.file, ...raw.slice(2)], { stdio: 'inherit' });
804
+ process.exit(res.status == null ? 0 : res.status);
805
+ return;
806
+ }
619
807
  // IntentLift: lift source CODE into inferred .intent drafts (not intent parsing).
620
808
  if (cmd === 'lift') {
621
809
  // Repo mode: walk a directory, lift each file, emit drafts + a repo summary.
@@ -638,10 +826,10 @@ test Example
638
826
  }
639
827
  if (args.out) {
640
828
  for (const m of res.missions) writeText(args.out, m.outName, m.intentText);
641
- console.log(`intent lift repo ${root} -> ${res.missionsGenerated} mission(s) in ${args.out}`);
829
+ console.log(`thunder lift repo ${root} -> ${res.missionsGenerated} mission(s) in ${args.out}`);
642
830
  console.log(` confidence: ${JSON.stringify(res.confidenceSummary)} | ${res.unknowns} unknown(s) total`);
643
831
  } else {
644
- console.log(`intent lift repo ${root}: ${res.missionsGenerated} mission(s)`);
832
+ console.log(`thunder lift repo ${root}: ${res.missionsGenerated} mission(s)`);
645
833
  for (const m of res.missions) console.log(` ${m.mission} (${m.summary.confidence}) <- ${m.sourceFile}`);
646
834
  }
647
835
  return;
@@ -653,7 +841,7 @@ test Example
653
841
  const res = liftAll(src, { language: args.from || languageForFile(file), file: basename(file) });
654
842
  if (!res.ok) { console.error(res.error); process.exit(1); }
655
843
  if (args.json) { console.log(JSON.stringify(res, null, 2)); return; }
656
- console.log(`intent lift --all ${basename(file)}: ${res.count} mission(s) inferred`);
844
+ console.log(`thunder lift --all ${basename(file)}: ${res.count} mission(s) inferred`);
657
845
  for (const m of res.missions) console.log(` ${m.mission} (${m.fn}, confidence ${m.confidence})`);
658
846
  return;
659
847
  }
@@ -665,7 +853,7 @@ test Example
665
853
  if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
666
854
  if (args.out) {
667
855
  const p = writeText(args.out, `${slug(res.lifted.mission)}.intent`, res.intentText);
668
- console.log(`intent lift ${basename(file)} -> ${p.replace(process.cwd() + '/', '')}`);
856
+ console.log(`thunder lift ${basename(file)} -> ${p.replace(process.cwd() + '/', '')}`);
669
857
  } else {
670
858
  console.log(res.intentText);
671
859
  }
@@ -680,7 +868,7 @@ test Example
680
868
  // args.out defaults to '.intent'; approve writes back to the file unless a real --out was given.
681
869
  const target = args.out && args.out !== '.intent' ? args.out : file;
682
870
  writeFileSync(target, res.text);
683
- console.log(`intent approve ${basename(file)} -> reviewed: true (${res.approval.source_hash.slice(0, 24)}...)`);
871
+ console.log(`thunder approve ${basename(file)} -> reviewed: true (${res.approval.source_hash.slice(0, 24)}...)`);
684
872
  return;
685
873
  }
686
874
 
@@ -691,7 +879,7 @@ test Example
691
879
  const out = JSON.stringify(pack, null, 2);
692
880
  if (args.out && args.out !== '.intent') {
693
881
  const p = writeText(args.out, `${slug(pack.mission)}.drift-handoff.json`, out);
694
- console.log(`intent handoff ${basename(file)} -> ${p.replace(process.cwd() + '/', '')} (kind ${pack.kind}, approved ${pack.approved})`);
882
+ console.log(`thunder handoff ${basename(file)} -> ${p.replace(process.cwd() + '/', '')} (kind ${pack.kind}, approved ${pack.approved})`);
695
883
  } else {
696
884
  console.log(out);
697
885
  }
@@ -707,7 +895,7 @@ test Example
707
895
  const res = checkDrift(intentText, codeText, { language });
708
896
  if (args.json) { console.log(JSON.stringify(res, null, 2)); }
709
897
  else {
710
- console.log(`intent drift: ${res.status.toUpperCase()} (${res.summary.blocking} blocking)`);
898
+ console.log(`thunder drift: ${res.status.toUpperCase()} (${res.summary.blocking} blocking)`);
711
899
  for (const f of res.findings) console.log(` [${f.level}] ${f.code}: ${f.message}`);
712
900
  }
713
901
  process.exit(res.status === 'drift' ? 1 : 0);
@@ -725,7 +913,7 @@ test Example
725
913
  });
726
914
  const manifest = buildManifest(parsed, { projectId: args.product });
727
915
  if (args.json) { console.log(JSON.stringify(manifest, null, 2)); return; }
728
- console.log(`intent ai list ${root}: ${manifest.summary.total} AI implementation(s)`);
916
+ console.log(`thunder ai list ${root}: ${manifest.summary.total} AI implementation(s)`);
729
917
  for (const im of manifest.implementations) {
730
918
  console.log(` ${im.id.padEnd(28)} ${im.status.padEnd(9)} risk:${im.risk} approval:${im.approval} scope:${im.scope}`);
731
919
  }
@@ -738,7 +926,7 @@ test Example
738
926
  const file = args._[1];
739
927
  if (!file) { console.error('usage: intent ai generate <file.intent> [--from <lang>]'); process.exit(2); return; }
740
928
  const ast = parseIntent(readFileSync(file, 'utf8'));
741
- if (!ast.implementation) { console.error(`intent ai generate: ${basename(file)} has no "implement with ai" block.`); process.exit(2); return; }
929
+ if (!ast.implementation) { console.error(`thunder ai generate: ${basename(file)} has no "implement with ai" block.`); process.exit(2); return; }
742
930
  const prompt = buildImplementationPrompt(ast, { language: args.from || 'typescript' });
743
931
  if (args.out && args.out !== '.intent') { const p = writeText(args.out, `${slug(ast.mission)}.prompt.md`, prompt); console.log(`wrote ${p.replace(process.cwd() + '/', '')}`); }
744
932
  else console.log(prompt);
@@ -765,7 +953,7 @@ test Example
765
953
  });
766
954
  const gate = productionGate(resolved, { allowPending: args.allowPending });
767
955
  if (args.json) { console.log(JSON.stringify({ ...gate, mode: args.mode || 'production', resolved }, null, 2)); process.exit(gate.ok ? 0 : 1); return; }
768
- console.log(`intent ai gate ${root} (${args.mode || 'production'}): ${gate.ok ? 'PASS' : 'BLOCKED'} , ${resolved.length} implementation(s)`);
956
+ console.log(`thunder ai gate ${root} (${args.mode || 'production'}): ${gate.ok ? 'PASS' : 'BLOCKED'} , ${resolved.length} implementation(s)`);
769
957
  for (const r of resolved) console.log(` ${r.id.padEnd(28)} ${r.status}${r.reasons?.[0] ? ` , ${r.reasons[0].code}: ${r.reasons[0].message}` : ''}`);
770
958
  if (!gate.ok) console.log(` ${gate.blocking.length} implementation(s) block production. Use --allow-pending for dev builds.`);
771
959
  process.exit(gate.ok ? 0 : 1);
@@ -777,9 +965,9 @@ test Example
777
965
  if (!file || !id) { console.error('usage: intent ai adopt <targetFile> <id> [--from <lang>]'); process.exit(2); return; }
778
966
  const code = readFileSync(file, 'utf8');
779
967
  const res = adoptRegion(code, id, args.from || languageForFile(file));
780
- if (!res) { console.error(`intent ai adopt: no AI-managed region "${id}" in ${basename(file)}.`); process.exit(2); return; }
968
+ if (!res) { console.error(`thunder ai adopt: no AI-managed region "${id}" in ${basename(file)}.`); process.exit(2); return; }
781
969
  writeFileSync(file, res.code);
782
- console.log(`intent ai adopt ${id} -> human-owned (origin="ai" ownership="human") in ${basename(file)}`);
970
+ console.log(`thunder ai adopt ${id} -> human-owned (origin="ai" ownership="human") in ${basename(file)}`);
783
971
  return;
784
972
  }
785
973
  if (sub === 'approve' || sub === 'reject') {
@@ -790,7 +978,7 @@ test Example
790
978
  const manifest = buildManifest(parsed.map((p) => ({ path: relative(root, p.file), source: '', ast: p.ast })), {});
791
979
  const im = manifest.implementations.find((x) => x.id === id);
792
980
  const ast = parsed.find((p) => (p.ast.implementation?.id || slug(p.ast.mission || '')) === id)?.ast;
793
- if (!im || !ast) { console.error(`intent ai ${sub}: no implementation "${id}" found under ${root}.`); process.exit(2); return; }
981
+ if (!im || !ast) { console.error(`thunder ai ${sub}: no implementation "${id}" found under ${root}.`); process.exit(2); return; }
794
982
  let region = null;
795
983
  if (im.targetLocation && existsSync(join(root, im.targetLocation))) region = parseMarkers(readFileSync(join(root, im.targetLocation), 'utf8')).regions.find((r) => r.id === id) || null;
796
984
  const pf = join(root, im.proofLocation);
@@ -809,7 +997,7 @@ test Example
809
997
  by: args.by, role: args.role, note: args.note,
810
998
  contractHash: contractHash(ast), implementationHash: implementationHash(region.code), at,
811
999
  });
812
- if (error) { console.error(`intent ai ${sub}: ${error}`); process.exit(2); return; }
1000
+ if (error) { console.error(`thunder ai ${sub}: ${error}`); process.exit(2); return; }
813
1001
  writeJson(join(root, '.intent'), 'ai-approvals.json', next);
814
1002
  const event = makeEvent(sub === 'approve' ? 'IntentAiImplementationApproved' : 'IntentAiImplementationRejected', {
815
1003
  implementationId: id, missionId: ast.mission, contractHash: contractHash(ast), implementationHash: implementationHash(region.code),
@@ -817,18 +1005,18 @@ test Example
817
1005
  newStatus: sub === 'approve' ? 'APPROVED' : 'REJECTED',
818
1006
  });
819
1007
  const logPath = sinkEvent(root, event);
820
- console.log(`intent ai ${sub} ${id} by ${args.by || '(anonymous)'}${args.role ? ` [${args.role}]` : ''} -> ${sub === 'approve' ? 'APPROVED' : 'REJECTED'} (bound to current hashes)`);
1008
+ console.log(`thunder ai ${sub} ${id} by ${args.by || '(anonymous)'}${args.role ? ` [${args.role}]` : ''} -> ${sub === 'approve' ? 'APPROVED' : 'REJECTED'} (bound to current hashes)`);
821
1009
  console.log(` logged ${event.type} to ${logPath.replace(process.cwd() + '/', '')}`);
822
1010
  if (args.json) console.log(JSON.stringify(event, null, 2));
823
1011
  return;
824
1012
  }
825
- // `intent ai events [dir] [--subject <implId>] [--json]` , read the append-only audit log.
1013
+ // `thunder ai events [dir] [--subject <implId>] [--json]` , read the append-only audit log.
826
1014
  if (sub === 'events') {
827
1015
  const root = args._[1] || '.';
828
1016
  const log = readEventLog(root);
829
1017
  const events = args.subject ? log.events.filter((e) => e.implementationId === args.subject) : log.events;
830
1018
  if (args.json) { console.log(JSON.stringify({ ...log, events }, null, 2)); return; }
831
- console.log(`intent ai events ${root}: ${events.length} event${events.length === 1 ? '' : 's'}${args.subject ? ` for ${args.subject}` : ''}`);
1019
+ console.log(`thunder ai events ${root}: ${events.length} event${events.length === 1 ? '' : 's'}${args.subject ? ` for ${args.subject}` : ''}`);
832
1020
  for (const e of events) {
833
1021
  const who = e.actorId ? ` by ${e.actorId}` : '';
834
1022
  const move = e.previousStatus || e.newStatus ? ` (${e.previousStatus || '?'} -> ${e.newStatus || '?'})` : '';
@@ -845,7 +1033,7 @@ test Example
845
1033
  .find((a) => (a.implementation?.id || slug(a.mission || '')) === id);
846
1034
  const policy = parseSelection(ast?.selection || []);
847
1035
  const cdir = join(root, '.intent', 'candidates', id);
848
- if (!existsSync(cdir)) { console.error(`intent ai select: no candidates at ${relative(process.cwd(), cdir)}.`); process.exit(2); return; }
1036
+ if (!existsSync(cdir)) { console.error(`thunder ai select: no candidates at ${relative(process.cwd(), cdir)}.`); process.exit(2); return; }
849
1037
  const candidates = readdirSync(cdir).filter((n) => !n.endsWith('.proof.json') && !n.endsWith('.json')).map((name) => {
850
1038
  const code = readFileSync(join(cdir, name), 'utf8');
851
1039
  const region = parseMarkers(code).regions.find((r) => r.id === id);
@@ -856,12 +1044,12 @@ test Example
856
1044
  });
857
1045
  const result = selectCandidate(candidates, policy);
858
1046
  if (args.json) { console.log(JSON.stringify({ ...result, policy }, null, 2)); return; }
859
- console.log(`intent ai select ${id}: ${result.winner ? result.winner.id : '(none eligible)'} wins (${result.eligibleCount}/${candidates.length} eligible)`);
1047
+ console.log(`thunder ai select ${id}: ${result.winner ? result.winner.id : '(none eligible)'} wins (${result.eligibleCount}/${candidates.length} eligible)`);
860
1048
  console.log(` policy: prefer ${result.prefer.map((p) => `${p.direction} ${p.metric}`).join(', ')}${policy.requireAllChecks ? '; require all checks' : ''}`);
861
1049
  for (const c of result.ranking) console.log(` ${c.id.padEnd(20)} ${JSON.stringify(c.metrics)}${c.checksPassed === false ? ' [checks FAILED]' : ''}`);
862
1050
  return;
863
1051
  }
864
- console.error(`intent ai ${sub || ''}: IL supports list | generate | gate | adopt | approve | reject | select. OpenThunder runs verification.`);
1052
+ console.error(`thunder ai ${sub || ''}: IL supports list | generate | gate | adopt | approve | reject | select. OpenThunder runs verification.`);
865
1053
  process.exit(2);
866
1054
  return;
867
1055
  }
@@ -869,14 +1057,14 @@ test Example
869
1057
  // Semantic diff: compare two snapshots (dirs or .intent files) by meaning.
870
1058
  if (cmd === 'diff') {
871
1059
  const b = args._[0]; const a = args._[1];
872
- if (!b || !a) { console.error('usage: intent diff <before-dir|file> <after-dir|file> [--json]'); process.exit(2); return; }
1060
+ if (!b || !a) { console.error('usage: thunder diff <before-dir|file> <after-dir|file> [--json]'); process.exit(2); return; }
873
1061
  const snap = (p) => {
874
1062
  if (statSync(p).isDirectory()) return buildAtlas(collectIntents(p).map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8')))));
875
1063
  return buildIntentGraph(parseIntent(readFileSync(p, 'utf8')));
876
1064
  };
877
1065
  const diff = diffGraphs(snap(b), snap(a));
878
1066
  if (args.json) { console.log(JSON.stringify(diff, null, 2)); return; }
879
- console.log(`intent diff ${b} -> ${a}: +${diff.summary.added} / -${diff.summary.removed} / ~${diff.summary.changed} node(s), +${diff.summary.relationshipsAdded} / -${diff.summary.relationshipsRemoved} edge(s)`);
1067
+ console.log(`thunder diff ${b} -> ${a}: +${diff.summary.added} / -${diff.summary.removed} / ~${diff.summary.changed} node(s), +${diff.summary.relationshipsAdded} / -${diff.summary.relationshipsRemoved} edge(s)`);
880
1068
  if (Object.keys(diff.summary.addedByType).length) console.log(` added: ${JSON.stringify(diff.summary.addedByType)}`);
881
1069
  if (Object.keys(diff.summary.removedByType).length) console.log(` removed: ${JSON.stringify(diff.summary.removedByType)}`);
882
1070
  for (const c of diff.changedNodes.slice(0, 8)) console.log(` ~ ${c.type} ${c.id}`);
@@ -893,7 +1081,7 @@ test Example
893
1081
  : buildIntentGraph(parseIntent(readFileSync(p, 'utf8')));
894
1082
  const res = mergeGraphs(snap(bp), snap(op), snap(tp));
895
1083
  if (args.json) { console.log(JSON.stringify(res, null, 2)); process.exit(res.clean ? 0 : 1); return; }
896
- console.log(`intent merge: ${res.clean ? 'CLEAN' : 'CONFLICTS'} , ${res.summary.nodes} node(s), ${res.summary.conflicts} conflict(s)`);
1084
+ console.log(`thunder merge: ${res.clean ? 'CLEAN' : 'CONFLICTS'} , ${res.summary.nodes} node(s), ${res.summary.conflicts} conflict(s)`);
897
1085
  for (const c of res.conflicts) console.log(` conflict: ${c.type} ${c.id} (changed differently on both sides)`);
898
1086
  process.exit(res.clean ? 0 : 1);
899
1087
  return;
@@ -903,10 +1091,10 @@ test Example
903
1091
  if (cmd === 'run') {
904
1092
  const ast = parseIntent(readFileSync(file, 'utf8'));
905
1093
  let inputs = {};
906
- if (args.inputs) { try { inputs = JSON.parse(args.inputs); } catch { console.error('intent run: --inputs must be JSON'); process.exit(2); return; } }
1094
+ if (args.inputs) { try { inputs = JSON.parse(args.inputs); } catch { console.error('thunder run: --inputs must be JSON'); process.exit(2); return; } }
907
1095
  let decisions = ast.decisions || [];
908
1096
  if (args.decision) decisions = decisions.filter((d) => slug(d.name) === slug(args.decision));
909
- if (!decisions.length) { console.error('intent run: no decision to run' + (args.decision ? ` matching "${args.decision}"` : '')); process.exit(2); return; }
1097
+ if (!decisions.length) { console.error('thunder run: no decision to run' + (args.decision ? ` matching "${args.decision}"` : '')); process.exit(2); return; }
910
1098
  const runs = decisions.map((d) => evaluateDecision(d, inputs));
911
1099
  if (args.json) { console.log(JSON.stringify(runs.length === 1 ? runs[0] : runs, null, 2)); return; }
912
1100
  for (const r of runs) {
@@ -922,8 +1110,8 @@ test Example
922
1110
  const ast = parseIntent(readFileSync(file, 'utf8'));
923
1111
  const r = evaluateOutcomes(ast);
924
1112
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.missed > 0 ? 1 : 0); return; }
925
- if (r.total === 0) { console.log(`intent outcomes ${basename(file)}: no outcome contracts found.`); return; }
926
- console.log(`intent outcomes ${basename(file)}: ${r.met} met, ${r.missed} missed, ${r.pending} pending`);
1113
+ if (r.total === 0) { console.log(`thunder outcomes ${basename(file)}: no outcome contracts found.`); return; }
1114
+ console.log(`thunder outcomes ${basename(file)}: ${r.met} met, ${r.missed} missed, ${r.pending} pending`);
927
1115
  for (const e of r.evaluations) {
928
1116
  const tag = e.status === 'met' ? 'MET ' : e.status === 'missed' ? 'MISSED' : 'PENDING';
929
1117
  const detail = e.comparable ? `actual ${e.actual} vs target ${e.target} (${e.direction})${e.improvement != null ? `, ${e.improvement >= 0 ? '+' : ''}${e.improvement} vs baseline` : ''}` : `no measured result yet (target ${e.target})`;
@@ -938,8 +1126,8 @@ test Example
938
1126
  const ast = parseIntent(readFileSync(file, 'utf8'));
939
1127
  const r = analyzeStyle(ast);
940
1128
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.diagnostics.some((d) => d.severity === 'blocker') ? 1 : 0); return; }
941
- if (r.styleIntents.length === 0) { console.log(`intent style ${basename(file)}: no style intents found.`); return; }
942
- console.log(`intent style ${basename(file)}: ${r.styleIntents.length} style intent(s)`);
1129
+ if (r.styleIntents.length === 0) { console.log(`thunder style ${basename(file)}: no style intents found.`); return; }
1130
+ console.log(`thunder style ${basename(file)}: ${r.styleIntents.length} style intent(s)`);
943
1131
  for (const s of r.styleIntents) {
944
1132
  const a11y = s.accessibility ? `${s.accessibility.target} (${s.accessibility.classification}, verified=${s.accessibility.verified})` : 'no target';
945
1133
  console.log(` ${s.name} , a11y ${a11y}, ${s.tokens.length} token(s)${s.appliesTo ? `, applies_to ${s.appliesTo}` : ''}`);
@@ -952,11 +1140,289 @@ test Example
952
1140
 
953
1141
  // Self-verifying intent: run the `test` blocks in a .intent file (decisions + lifecycles).
954
1142
  if (cmd === 'test') {
1143
+ // Change-impact selection: run the tests for what a diff actually affects, using the Intent
1144
+ // Graph (not just the modified files). Selects changed intents plus any intent that shares an
1145
+ // event / service / api symbol with a changed one. `thunder test --changed [<range>]`.
1146
+ if (args.changed) {
1147
+ const gitc = (c) => { try { return execSync(`git ${c}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); } catch { return null; } };
1148
+ const top = gitc('rev-parse --show-toplevel');
1149
+ if (top === null) { console.error('thunder test --changed: not a git repository'); process.exit(2); return; }
1150
+ const root = top.trim();
1151
+ const range = file || '';
1152
+ let base, head;
1153
+ if (range.includes('..')) { [base, head] = range.split('..'); head = head || 'HEAD'; }
1154
+ else if (range) { base = range; head = null; }
1155
+ else { base = 'HEAD'; head = null; }
1156
+ const diffSpec = head ? `${base} ${head}` : base;
1157
+ const changed = (gitc(`diff --name-only ${diffSpec} -- "*.thunder" "*.tl" "*.intent"`) || '').split('\n').map((s) => s.trim()).filter(Boolean);
1158
+ const label = range || `${base}..working-tree`;
1159
+ if (!changed.length) { console.log(`thunder test --changed ${label}: no source files changed`); return; }
1160
+
1161
+ const symbolsOf = (ast) => {
1162
+ const s = new Set();
1163
+ if (!ast) return s;
1164
+ if (ast.mission) s.add(`system:${ast.mission}`);
1165
+ for (const e of ast.events || []) s.add(`event:${e.name}`);
1166
+ for (const sv of ast.services || []) { s.add(`service:${sv.name}`); (sv.publishes || []).forEach((p) => s.add(`event:${p}`)); (sv.consumes || []).forEach((p) => s.add(`event:${p}`)); }
1167
+ for (const ap of ast.apis || []) s.add(`api:${ap.name}`);
1168
+ return s;
1169
+ };
1170
+ const rel = (p) => relative(root, p);
1171
+ const astAt = (relpath) => { const abs = join(root, relpath); if (!existsSync(abs)) return null; try { return parseIntent(readFileSync(abs, 'utf8')); } catch { return null; } };
1172
+
1173
+ // Seed symbols from the changed intents, then find every repo intent that shares one.
1174
+ const changedAsts = new Map(changed.map((p) => [p, astAt(p)]));
1175
+ const seed = new Set();
1176
+ for (const ast of changedAsts.values()) for (const sym of symbolsOf(ast)) seed.add(sym);
1177
+ const selected = new Map();
1178
+ for (const p of changed) selected.set(p, 'changed');
1179
+ for (const abs of collectIntents(root)) {
1180
+ const p = rel(abs);
1181
+ if (selected.has(p)) continue;
1182
+ const shared = [...symbolsOf(parseIntent(readFileSync(abs, 'utf8')))].filter((x) => seed.has(x));
1183
+ if (shared.length) selected.set(p, `impacted via ${shared[0].replace(':', ' ')}`);
1184
+ }
1185
+
1186
+ let anyFail = false;
1187
+ const results = [];
1188
+ for (const [p, reason] of selected) {
1189
+ const ast = changedAsts.get(p) || astAt(p);
1190
+ if (!ast) { results.push({ file: p, reason, deleted: true }); continue; }
1191
+ const r = runTests(ast);
1192
+ const res = resolveObligations(ast);
1193
+ const testsOk = r.total === 0 || r.ok;
1194
+ if (!testsOk || res.failed > 0) anyFail = true;
1195
+ results.push({ file: p, reason, tests: { passed: r.passed, total: r.total, ok: testsOk }, unverified: res.unverified, failedClaims: res.failed });
1196
+ }
1197
+ if (args.json) { console.log(JSON.stringify({ schema: 'thunder-changed-v1', range: label, changed, selected: results }, null, 2)); process.exit(anyFail ? 1 : 0); return; }
1198
+ console.log(`thunder test --changed ${label}: ${changed.length} changed, ${selected.size} intent(s) selected`);
1199
+ for (const r of results) {
1200
+ if (r.deleted) { console.log(` , ${r.file} [${r.reason}, deleted]`); continue; }
1201
+ const mark = r.tests.ok && !r.failedClaims ? 'PASS' : 'FAIL';
1202
+ console.log(` ${mark.padEnd(6)} ${r.file} [${r.reason}] tests ${r.tests.passed}/${r.tests.total}${r.unverified ? `, ${r.unverified} unverified` : ''}`);
1203
+ }
1204
+ process.exit(anyFail ? 1 : 0);
1205
+ return;
1206
+ }
1207
+
955
1208
  const ast = parseIntent(readFileSync(file, 'utf8'));
1209
+
1210
+ // Property-based testing: generate many cases for each `property`, evaluate the decision,
1211
+ // assert the `expect` clauses, and shrink failures. `thunder test <file> --properties`.
1212
+ if (args.properties) {
1213
+ const cases = Number.isFinite(Number(args.cases)) && Number(args.cases) > 0 ? Math.min(Math.floor(Number(args.cases)), 100000) : 100;
1214
+ const seed = Number.isFinite(Number(args.seed)) ? Math.floor(Number(args.seed)) : 424242;
1215
+ const rep = runProperties(ast, { cases, seed });
1216
+ const bad = rep.passed !== rep.total;
1217
+ if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1218
+ if (!rep.total) { console.log(`thunder test ${basename(file)} --properties: no property blocks found.`); return; }
1219
+ console.log(`thunder test ${basename(file)} --properties: ${rep.total} propert${rep.total === 1 ? 'y' : 'ies'}, ${rep.passed} passed`);
1220
+ for (const r of rep.results) {
1221
+ if (r.error) { console.log(` FAIL ${r.property} , ${r.error}`); continue; }
1222
+ if (r.ok) { console.log(` PASS ${r.property} (${r.cases} cases, seed ${r.seed})`); continue; }
1223
+ const f = r.failure;
1224
+ const inputs = Object.entries(f.inputs).map(([k, v]) => `${k}=${v}`).join(', ');
1225
+ console.log(` FAIL ${r.property} (seed ${r.seed})`);
1226
+ console.log(` smallest failure: ${inputs} -> ${f.expect} (got ${f.actual})`);
1227
+ }
1228
+ process.exit(bad ? 1 : 0);
1229
+ return;
1230
+ }
1231
+
1232
+ // Scenario tests: workflow-level given/when/then/never. Deterministically flags a scenario
1233
+ // that both expects and prohibits the same outcome; otherwise DECLARED (needs runtime evidence).
1234
+ if (args.scenarios) {
1235
+ const scenarios = ast.scenarios || [];
1236
+ const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
1237
+ const results = scenarios.map((sc) => {
1238
+ const neverSet = new Set(sc.never.map(norm));
1239
+ const positives = [...sc.then, ...sc.eventually.flatMap((e) => e.clauses)];
1240
+ const contradictions = positives.filter((p) => neverSet.has(norm(p)));
1241
+ return { scenario: sc.name, status: contradictions.length ? 'failed' : 'declared', given: sc.given.length, then: positives.length, never: sc.never.length, contradictions };
1242
+ });
1243
+ const failed = results.filter((r) => r.status === 'failed').length;
1244
+ const rep = { schema: 'thunder-scenarios-v1', mission: ast.mission, file: basename(file), total: results.length, failed, results };
1245
+ if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(failed ? 1 : 0); return; }
1246
+ if (!rep.total) { console.log(`thunder test ${basename(file)} --scenarios: no scenario blocks found.`); return; }
1247
+ console.log(`thunder test ${basename(file)} --scenarios: ${rep.total} scenario(s), ${rep.total - failed} consistent${failed ? `, ${failed} contradictory` : ''}`);
1248
+ for (const r of results) {
1249
+ if (r.status === 'failed') console.log(` FAIL ${r.scenario} , contradiction: ${r.contradictions.map((c) => `"${c}"`).join(', ')} both expected and prohibited`);
1250
+ else console.log(` DECLARED ${r.scenario} (${r.given} given, ${r.then} then, ${r.never} never) , needs runtime evidence`);
1251
+ }
1252
+ process.exit(failed ? 1 : 0);
1253
+ return;
1254
+ }
1255
+
1256
+ // All-targets mode: run the tests against EVERY target whose toolchain is available, in one
1257
+ // pass, and report each target's pass count side by side. `thunder test <file> --all-targets`.
1258
+ if (args.allTargets) {
1259
+ const gradeCases = (actual) => {
1260
+ const cs = [];
1261
+ for (const t of ast.tests || []) {
1262
+ if (!(ast.decisions || []).some((d) => d.name === t.name)) continue;
1263
+ for (const c of t.cases || []) {
1264
+ const key = `${t.name} / ${c.name || 'case'}`;
1265
+ const act = actual[key];
1266
+ cs.push({ key, expected: c.expect, actual: act, pass: c.expect == null || String(act) === String(c.expect) });
1267
+ }
1268
+ }
1269
+ return cs;
1270
+ };
1271
+ const report = LIVE_TARGETS.map((t) => {
1272
+ const actual = t.available() ? t.run(ast) : null;
1273
+ if (actual === null) return { target: t.key, status: 'skipped', reason: `${t.key} toolchain not available` };
1274
+ const cases = gradeCases(actual);
1275
+ const passed = cases.filter((c) => c.pass).length;
1276
+ return { target: t.key, status: passed === cases.length ? 'pass' : 'fail', total: cases.length, passed, cases };
1277
+ });
1278
+ const bad = report.some((r) => r.status === 'fail');
1279
+ if (args.json) { console.log(JSON.stringify({ schema: 'thunder-all-targets-v1', mission: ast.mission, file: basename(file), targets: report }, null, 2)); process.exit(bad ? 1 : 0); return; }
1280
+ const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
1281
+ const ran = report.filter((r) => r.status !== 'skipped');
1282
+ console.log(`thunder test ${basename(file)} --all-targets: ${ran.length}/${report.length} target(s) executed`);
1283
+ for (const r of report) {
1284
+ if (r.status === 'skipped') { console.log(` SKIP ${cap(r.target).padEnd(12)} (toolchain not available)`); continue; }
1285
+ console.log(` ${r.status === 'pass' ? 'PASS' : 'FAIL'} ${cap(r.target).padEnd(12)} ${r.passed}/${r.total} passed (executed generated code)`);
1286
+ for (const c of r.cases) if (!c.pass) console.log(` FAIL ${c.key} , expected ${c.expected}, got ${c.actual}`);
1287
+ }
1288
+ process.exit(bad ? 1 : 0);
1289
+ return;
1290
+ }
1291
+
1292
+ // Target mode: run the tests against the GENERATED TypeScript (executed for real), not the
1293
+ // semantic engine. Proves the generated implementation is faithful to the intent.
1294
+ // `thunder test <file> --target typescript`.
1295
+ if (args.target && RUNNABLE_TARGETS.has(String(args.target).toLowerCase())) {
1296
+ const targetKey = canonicalTarget(args.target);
1297
+ const actual = runLiveTarget(ast, targetKey);
1298
+ // Live target could not run (e.g. python3 not installed) , skip cleanly, don't fail.
1299
+ if (actual === null) {
1300
+ if (args.json) { console.log(JSON.stringify({ schema: 'thunder-target-v1', target: targetKey, skipped: true, reason: `${targetKey} runtime unavailable` }, null, 2)); return; }
1301
+ console.log(`thunder test ${basename(file)} --target ${targetKey}: skipped (the ${targetKey} runtime is not available on this machine).`);
1302
+ return;
1303
+ }
1304
+ const cases = [];
1305
+ for (const t of ast.tests || []) {
1306
+ const dec = (ast.decisions || []).find((d) => d.name === t.name);
1307
+ if (!dec) continue;
1308
+ for (const c of t.cases || []) {
1309
+ const key = `${t.name} / ${c.name || 'case'}`;
1310
+ const act = actual[key];
1311
+ cases.push({ key, expected: c.expect, actual: act, pass: c.expect == null || String(act) === String(c.expect) });
1312
+ }
1313
+ }
1314
+ const passed = cases.filter((c) => c.pass).length;
1315
+ const bad = passed !== cases.length;
1316
+ if (args.json) { console.log(JSON.stringify({ schema: 'thunder-target-v1', target: targetKey, total: cases.length, passed, cases }, null, 2)); process.exit(bad ? 1 : 0); return; }
1317
+ if (!cases.length) { console.log(`thunder test ${basename(file)} --target ${targetKey}: no decision test cases to run.`); return; }
1318
+ console.log(`thunder test ${basename(file)} --target ${targetKey}: ${passed}/${cases.length} passed (executed generated code)`);
1319
+ for (const c of cases) console.log(` ${c.pass ? 'PASS' : 'FAIL'} ${c.key}${c.pass ? '' : ` , expected ${c.expected}, got ${c.actual}`}`);
1320
+ process.exit(bad ? 1 : 0);
1321
+ return;
1322
+ }
1323
+
1324
+ // Semantic coverage: which goals, decision rules, guarantees, prohibitions, scenarios, and
1325
+ // targets are actually exercised , meaning-level, not lines. `thunder test <file> --coverage`.
1326
+ if (args.coverage) {
1327
+ const rep = semanticCoverage(ast);
1328
+ const bad = args.strict && rep.unverified.length > 0;
1329
+ if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1330
+ console.log(`thunder test ${basename(file)} --coverage: ${rep.overall}% overall`);
1331
+ console.log('');
1332
+ for (const m of rep.metrics) console.log(` ${m.name.padEnd(16)} ${`${m.covered}/${m.total}`.padStart(7)} ${String(m.pct).padStart(3)}%`);
1333
+ if (rep.unverified.length) {
1334
+ console.log('');
1335
+ console.log(' Unverified:');
1336
+ for (const u of rep.unverified) console.log(` - ${u}`);
1337
+ }
1338
+ process.exit(bad ? 1 : 0);
1339
+ return;
1340
+ }
1341
+
1342
+ // AI evaluations: probabilistic behavior graded against a dataset by metric thresholds.
1343
+ // Declare the bar in ThunderLang; feed measured metrics via --results to gate on them.
1344
+ // `thunder test <file> --evals [--results <json|path>] [--strict]`.
1345
+ if (args.evals) {
1346
+ let results = null;
1347
+ if (args.results) {
1348
+ try { results = existsSync(args.results) ? JSON.parse(readFileSync(args.results, 'utf8')) : JSON.parse(args.results); }
1349
+ catch { console.error('thunder test --evals: --results must be a JSON file path or inline JSON of {metric: value}'); process.exit(2); return; }
1350
+ }
1351
+ const cmp = { '>=': (a, b) => a >= b, '<=': (a, b) => a <= b, '>': (a, b) => a > b, '<': (a, b) => a < b, '==': (a, b) => a === b, '!=': (a, b) => a !== b };
1352
+ const evals = (ast.evaluations || []).map((ev) => {
1353
+ if (!results) return { name: ev.name, dataset: ev.dataset, status: 'declared', requires: ev.requires };
1354
+ const checks = ev.requires.map((r) => {
1355
+ const have = results[r.metric];
1356
+ if (have === undefined) return { ...r, actual: null, pass: false, missing: true };
1357
+ return { ...r, actual: Number(have), pass: cmp[r.op](Number(have), r.threshold) };
1358
+ });
1359
+ return { name: ev.name, dataset: ev.dataset, status: checks.every((c) => c.pass) ? 'passed' : 'failed', checks };
1360
+ });
1361
+ const failed = evals.filter((e) => e.status === 'failed').length;
1362
+ const declared = evals.filter((e) => e.status === 'declared').length;
1363
+ const bad = failed > 0 || (args.strict && declared > 0);
1364
+ const rep = { schema: 'thunder-evals-v1', mission: ast.mission, file: basename(file), total: evals.length, passed: evals.filter((e) => e.status === 'passed').length, declared, failed, results: evals };
1365
+ if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1366
+ if (!rep.total) { console.log(`thunder test ${basename(file)} --evals: no evaluation blocks found.`); return; }
1367
+ console.log(`thunder test ${basename(file)} --evals: ${rep.total} evaluation(s)${results ? `, ${rep.passed} passed${failed ? `, ${failed} failed` : ''}` : ' (declared , provide --results to grade)'}`);
1368
+ for (const e of evals) {
1369
+ const label = e.status === 'passed' ? 'PASS' : e.status === 'failed' ? 'FAIL' : 'DECLARED';
1370
+ console.log(` ${label.padEnd(9)} ${e.name}${e.dataset ? ` (dataset ${e.dataset})` : ''}`);
1371
+ for (const r of (e.checks || e.requires)) {
1372
+ const mark = e.checks ? (r.pass ? '✓' : '✗') : '·';
1373
+ const actual = e.checks ? (r.missing ? ' (metric not in results)' : ` (${r.actual})`) : '';
1374
+ console.log(` ${mark} ${r.metric} ${r.op} ${r.threshold}${actual}`);
1375
+ }
1376
+ }
1377
+ process.exit(bad ? 1 : 0);
1378
+ return;
1379
+ }
1380
+
1381
+ // Mutation testing: inject faults into decisions and check the tests catch them.
1382
+ // A survived mutant is a weak spot. `thunder test <file> --mutate [--strict]`.
1383
+ if (args.mutate) {
1384
+ const rep = runMutations(ast);
1385
+ const bad = args.strict && rep.survived > 0;
1386
+ if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1387
+ if (rep.total === 0) { console.log(`thunder test ${basename(file)} --mutate: ${rep.note}`); return; }
1388
+ console.log(`thunder test ${basename(file)} --mutate: mutation score ${rep.score}% (${rep.killed} killed, ${rep.survived} survived of ${rep.total})`);
1389
+ const survivors = rep.results.filter((r) => !r.killed);
1390
+ if (survivors.length) { console.log(' survived , the tests did not catch these (weak spots):'); for (const s of survivors) console.log(` - ${s.describe}`); }
1391
+ process.exit(bad ? 1 : 0);
1392
+ return;
1393
+ }
1394
+
1395
+ // Contract-test derivation: every `guarantee` and `never` is a test obligation.
1396
+ // Honest resolution: no verification -> UNVERIFIED (never silently PASS); a declared
1397
+ // verification with the file's checks green -> PASS; a failing in-file check -> FAIL.
1398
+ // `--strict` also fails the run on any UNVERIFIED obligation (CI: nothing left unproven).
1399
+ if (args.contracts) {
1400
+ const res = resolveObligations(ast);
1401
+ const obligations = res.obligations;
1402
+ const rep = { schema: 'thunder-contracts-v1', mission: ast.mission, file: basename(file),
1403
+ total: obligations.length, verified: res.verified, declared: res.declared,
1404
+ unverified: res.unverified, failed: res.failed, obligations };
1405
+ const bad = rep.failed > 0 || (args.strict && rep.unverified > 0);
1406
+ if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1407
+ if (!rep.total) { console.log(`thunder test ${basename(file)} --contracts: no guarantees or prohibitions declared.`); return; }
1408
+ console.log(`thunder test ${basename(file)} --contracts: ${rep.total} obligation(s), ${rep.verified} verified, ${rep.declared} declared, ${rep.unverified} unverified${rep.failed ? `, ${rep.failed} failed` : ''}`);
1409
+ const LABEL = { verified: 'PASS', failed: 'FAIL', declared: 'DECLARED', unverified: 'UNVERIFIED' };
1410
+ for (const o of obligations) {
1411
+ const by = o.kinds.length ? ` by ${o.kinds.join('+')}` : '';
1412
+ const detail = o.provenBy ? ` proven by test ${o.provenBy}`
1413
+ : o.status === 'declared' ? ' (declared, runs in target mode)'
1414
+ : o.status === 'unverified' ? ' (nothing verifies this)' : '';
1415
+ console.log(` ${LABEL[o.status].padEnd(10)} ${o.kind} ${o.id}: ${o.text}${by}${detail}`);
1416
+ }
1417
+ if (args.strict && rep.unverified) console.log(`\n strict: ${rep.unverified} unverified obligation(s) , run fails until every claim carries a verification.`);
1418
+ process.exit(bad ? 1 : 0);
1419
+ return;
1420
+ }
1421
+
956
1422
  const r = runTests(ast);
957
1423
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
958
- if (r.total === 0) { console.log(`intent test ${basename(file)}: no test blocks found.`); return; }
959
- console.log(`intent test ${basename(file)}: ${r.passed}/${r.total} passed`);
1424
+ if (r.total === 0) { console.log(`thunder test ${basename(file)}: no test blocks found.`); return; }
1425
+ console.log(`thunder test ${basename(file)}: ${r.passed}/${r.total} passed`);
960
1426
  for (const c of r.results) {
961
1427
  const detail = c.error ? ` ${c.error}`
962
1428
  : c.kind === 'lifecycle' ? `expected ${c.expected ?? '(any)'}, got ${c.actual} (valid=${c.valid})`
@@ -967,6 +1433,141 @@ test Example
967
1433
  return;
968
1434
  }
969
1435
 
1436
+ // PROVE: run the tests, evaluate guarantee / prohibition obligations, and emit a durable
1437
+ // intent-proof-v1 artifact with honest statuses. Unverified claims never read as passed.
1438
+ // `thunder prove <file> [--out <dir>] [--json]`.
1439
+ if (cmd === 'prove') {
1440
+ if (!file || !existsSync(file)) { console.error('usage: thunder prove <file> [--out <dir>] [--json]'); process.exit(2); return; }
1441
+ const source = readFileSync(file, 'utf8');
1442
+ const ast = parseIntent(source);
1443
+ const diagnostics = semanticDiagnostics(ast);
1444
+ const resolved = resolveObligations(ast);
1445
+ const tests = resolved.tests;
1446
+ const proof = buildProof(ast, {
1447
+ sourceFile: basename(file),
1448
+ sourceHash: sha256(source),
1449
+ targetsRequested: ast.targets || [],
1450
+ targetsGenerated: [],
1451
+ diagnostics,
1452
+ generatedAt: new Date().toISOString(),
1453
+ });
1454
+ // Fold per-claim verdicts into the proof so it records real status, not just planned/needs_verification.
1455
+ const CLAIM_MAP = { verified: 'verified', failed: 'failed', declared: 'planned', unverified: 'needs_verification' };
1456
+ const byId = new Map(resolved.obligations.map((o) => [o.id, o]));
1457
+ const applyStatus = (claim) => { const o = byId.get(claim.id); return o ? { ...claim, status: CLAIM_MAP[o.status], provenBy: o.provenBy || null } : claim; };
1458
+ proof.guarantees = proof.guarantees.map(applyStatus);
1459
+ proof.neverRules = proof.neverRules.map(applyStatus);
1460
+ // Freshness tuple: what `thunder verify` recomputes to mark this proof STALE later.
1461
+ proof.freshness = freshnessFor(file, proof, args.env);
1462
+ const proofId = `proof-${proof.sourceHash.replace('sha256:', '').slice(0, 6)}`;
1463
+ const fail = !proof.verification.semanticPassed || !tests.ok || resolved.failed > 0;
1464
+
1465
+ if (args.json) {
1466
+ console.log(JSON.stringify({ proofId, ...proof, tests }, null, 2));
1467
+ process.exit(fail ? 1 : 0);
1468
+ return;
1469
+ }
1470
+
1471
+ console.log(`Proof created: ${proofId}`);
1472
+ console.log('');
1473
+ console.log(` Intent: ${proof.missionName || stripSourceExt(basename(file))}`);
1474
+ console.log(` Intent hash: ${proof.sourceHash}`);
1475
+ console.log(` Compiler: ThunderLang ${proof.compilerVersion}`);
1476
+ console.log(` Generated: ${proof.generatedAt}`);
1477
+ console.log(` Syntax: ${proof.verification.syntaxPassed ? 'PASS' : 'FAIL'}`);
1478
+ console.log(` Semantics: ${proof.verification.semanticPassed ? 'PASS' : 'FAIL'}`);
1479
+ console.log(` Tests: ${tests.total ? `${tests.passed}/${tests.total} passed` : 'none'}`);
1480
+ console.log(` Claims: ${resolved.verified} verified, ${resolved.declared} declared, ${resolved.unverified} UNVERIFIED${resolved.failed ? `, ${resolved.failed} FAILED` : ''} (of ${resolved.obligations.length})`);
1481
+ console.log(` Proof status: ${String(proof.proofStatus).toUpperCase()}${(resolved.unverified || resolved.failed) ? ' (not fully proven)' : ''}`);
1482
+ const fr = proof.freshness;
1483
+ console.log(` Bound to: commit ${fr.implementation ? fr.implementation.slice(0, 7) : 'n/a'}${fr.dependencies ? ` · deps ${fr.dependencies.hash.replace('sha256:', '').slice(0, 7)} (${fr.dependencies.file})` : ''}${fr.environment ? ` · env ${fr.environment}` : ''}`);
1484
+ const problems = resolved.obligations.filter((o) => o.status === 'unverified' || o.status === 'failed');
1485
+ if (problems.length) {
1486
+ console.log('');
1487
+ console.log(' Not proven (this is where drift hides , a claim like this never counts as proven):');
1488
+ for (const o of problems) console.log(` - ${o.status === 'failed' ? 'FAILED ' : 'UNVERIFIED'} ${o.kind} ${o.id}: ${o.text}`);
1489
+ }
1490
+ const outName = `${slug(ast.mission || stripSourceExt(basename(file)))}.thunder-proof.json`;
1491
+ const outDir = args.out && args.out !== '.intent' ? args.out : dirname(file);
1492
+ const outPath = join(outDir, outName);
1493
+ writeFileSync(outPath, JSON.stringify(proof, null, 2));
1494
+ console.log('');
1495
+ console.log(` wrote ${relative(process.cwd(), outPath)}`);
1496
+ process.exit(fail ? 1 : 0);
1497
+ return;
1498
+ }
1499
+
1500
+ // CONFORMANCE: the same test cases run against every target. The engine defines the canonical
1501
+ // result each case must produce; target outputs fed via --results are graded against it.
1502
+ // `thunder conform <file> [--targets ts,py] [--results <json|path>] [--json]`.
1503
+ if (cmd === 'conform') {
1504
+ const ast = parseIntent(readFileSync(file, 'utf8'));
1505
+ let results = null;
1506
+ if (args.results) {
1507
+ try { results = existsSync(args.results) ? JSON.parse(readFileSync(args.results, 'utf8')) : JSON.parse(args.results); }
1508
+ catch { console.error('thunder conform: --results must be JSON of {target: {"Test / case": result}} (file path or inline)'); process.exit(2); return; }
1509
+ }
1510
+ let targets = (args.targets && args.targets.length ? args.targets : (ast.targets || [])).map((t) => canonicalTarget(t));
1511
+ const skippedTargets = [];
1512
+ // --all-targets: execute EVERY target whose toolchain is available in one pass, and show all
1513
+ // live targets as columns (unavailable ones stay "declared" with a skipped note).
1514
+ if (args.allTargets) {
1515
+ results = results || {};
1516
+ targets = Array.from(new Set([...targets, ...LIVE_TARGETS.map((t) => t.key)]));
1517
+ for (const t of LIVE_TARGETS) {
1518
+ if (!t.available()) { skippedTargets.push(t.key); continue; }
1519
+ const actual = t.run(ast);
1520
+ if (actual !== null) results[t.key] = actual; else skippedTargets.push(t.key);
1521
+ }
1522
+ }
1523
+ // --run <targets>: execute specific live targets (TypeScript/JS/Python/C#/Java) and grade real
1524
+ // outputs. A null result (e.g. the SDK is not installed) is skipped, leaving it "declared".
1525
+ if (args.run && args.run.length) {
1526
+ results = results || {};
1527
+ for (const rt of args.run) {
1528
+ if (!RUNNABLE_TARGETS.has(rt.toLowerCase())) continue;
1529
+ const key = canonicalTarget(rt);
1530
+ const actual = runLiveTarget(ast, key);
1531
+ if (actual !== null) results[key] = actual;
1532
+ }
1533
+ }
1534
+ const rep = buildConformance(ast, { targets, results });
1535
+ if (skippedTargets.length) rep.skipped = Array.from(new Set(skippedTargets));
1536
+ const bad = rep.semanticFailures > 0 || rep.failures.length > 0;
1537
+ if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
1538
+ if (!rep.total) { console.log(`thunder conform ${basename(file)}: no test cases to conform.`); return; }
1539
+
1540
+ const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
1541
+ const cols = ['Semantic', ...rep.columns.map(cap)];
1542
+ const labelW = Math.max(12, ...rep.cases.map((c) => c.key.length));
1543
+ const cw = (h) => Math.max(h.length, 8);
1544
+ const cell = (v, w) => String(v).padEnd(w);
1545
+ console.log(`thunder conform ${basename(file)}: ${rep.total} case(s) · semantic + ${rep.columns.length} target(s)${rep.graded ? '' : ' (declared , provide --results to grade targets)'}`);
1546
+ console.log(` ${cell('', labelW)} ${cols.map((h) => cell(h, cw(h))).join(' ')}`);
1547
+ for (const c of rep.cases) {
1548
+ const sem = c.semanticPass ? 'PASS' : 'FAIL';
1549
+ const tcells = rep.columns.map((col) => {
1550
+ const s = c.targets[col].status;
1551
+ return cell(s === 'pass' ? 'PASS' : s === 'fail' ? 'FAIL' : '—', cw(cap(col)));
1552
+ });
1553
+ console.log(` ${cell(c.key, labelW)} ${cell(sem, cw('Semantic'))} ${tcells.join(' ')}`);
1554
+ }
1555
+ for (const f of rep.failures) {
1556
+ console.log('');
1557
+ console.log(' CONFORMANCE FAILURE');
1558
+ console.log(` Target: ${cap(f.target)}`);
1559
+ console.log(` Case: ${f.case}`);
1560
+ console.log(` Expected: ${f.expected}`);
1561
+ console.log(` Actual: ${f.actual}`);
1562
+ }
1563
+ if (rep.skipped && rep.skipped.length) {
1564
+ console.log('');
1565
+ console.log(` Skipped (toolchain not available, left declared): ${rep.skipped.map(cap).join(', ')}`);
1566
+ }
1567
+ process.exit(bad ? 1 : 0);
1568
+ return;
1569
+ }
1570
+
970
1571
  // Intent Runtime: SIMULATE a lifecycle against a sequence of events.
971
1572
  if (cmd === 'simulate') {
972
1573
  const ast = parseIntent(readFileSync(file, 'utf8'));
@@ -993,7 +1594,7 @@ test Example
993
1594
  if (!graph || !Array.isArray(graph.nodes)) { console.error('intent validate: not an Intent Graph (no nodes[])'); process.exit(2); return; }
994
1595
  const v = validateGraph(graph);
995
1596
  if (args.json) { console.log(JSON.stringify(v, null, 2)); process.exit(v.valid ? 0 : 1); return; }
996
- console.log(`intent validate ${basename(file)}: ${v.valid ? 'VALID' : `${v.issues.length} issue(s)`} (${v.version})`);
1597
+ console.log(`thunder validate ${basename(file)}: ${v.valid ? 'VALID' : `${v.issues.length} issue(s)`} (${v.version})`);
997
1598
  for (const i of v.issues) console.log(` [${i.code}] ${i.message}${i.id ? ` (${i.id})` : ''}`);
998
1599
  process.exit(v.valid ? 0 : 1);
999
1600
  return;
@@ -1002,14 +1603,14 @@ test Example
1002
1603
  if (cmd === 'migrate') {
1003
1604
  const raw = readFileSync(file, 'utf8');
1004
1605
  let graph;
1005
- try { graph = JSON.parse(raw); } catch { console.error('intent migrate: input is not valid JSON'); process.exit(2); return; }
1006
- if (!graph || !Array.isArray(graph.nodes)) { console.error('intent migrate: not an Intent Graph (no nodes[])'); process.exit(2); return; }
1606
+ try { graph = JSON.parse(raw); } catch { console.error('thunder migrate: input is not valid JSON'); process.exit(2); return; }
1607
+ if (!graph || !Array.isArray(graph.nodes)) { console.error('thunder migrate: not an Intent Graph (no nodes[])'); process.exit(2); return; }
1007
1608
  let result;
1008
1609
  try { result = migrateGraph(graph, args.to ? { to: args.to } : {}); }
1009
- catch (e) { console.error(`intent migrate: ${e instanceof Error ? e.message : e}`); process.exit(2); return; }
1610
+ catch (e) { console.error(`thunder migrate: ${e instanceof Error ? e.message : e}`); process.exit(2); return; }
1010
1611
  const v = validateGraph(result.graph);
1011
1612
  if (args.json) { console.log(JSON.stringify({ ...result, validation: v }, null, 2)); process.exit(v.valid ? 0 : 1); return; }
1012
- console.log(`intent migrate: ${result.from} -> ${result.to} (${result.migrated ? result.applied.length + ' step(s)' : 'already current'})`);
1613
+ console.log(`thunder migrate: ${result.from} -> ${result.to} (${result.migrated ? result.applied.length + ' step(s)' : 'already current'})`);
1013
1614
  for (const a of result.applied) console.log(` applied ${a.from} -> ${a.to}: ${a.description}`);
1014
1615
  console.log(` validation: ${v.valid ? 'OK' : `${v.issues.length} issue(s)`}`);
1015
1616
  for (const i of v.issues.slice(0, 10)) console.log(` [${i.code}] ${i.message}`);
@@ -1032,7 +1633,7 @@ test Example
1032
1633
  const src = graphToSource(graph);
1033
1634
  if (args.out && args.out !== '.intent') {
1034
1635
  const base = (graph.nodes.find((n) => n.type === 'Mission')?.title) || basename(file).replace(/\.[^.]+$/, '');
1035
- console.log(`intent source: wrote ${writeText(args.out, `${slug(base)}.intent`, src)}`);
1636
+ console.log(`thunder source: wrote ${writeText(args.out, `${slug(base)}.intent`, src)}`);
1036
1637
  } else {
1037
1638
  process.stdout.write(src);
1038
1639
  }
@@ -1044,22 +1645,22 @@ test Example
1044
1645
  const xml = readFileSync(file, 'utf8');
1045
1646
  const fmt = args.format || detectFormat(xml);
1046
1647
  if (!fmt || !IMPORT_FORMATS.includes(fmt)) {
1047
- console.error(`intent import: could not detect format; pass --format <${IMPORT_FORMATS.join('|')}>`);
1648
+ console.error(`thunder import: could not detect format; pass --format <${IMPORT_FORMATS.join('|')}>`);
1048
1649
  process.exit(2); return;
1049
1650
  }
1050
1651
  const report = importReport(xml, fmt);
1051
- if (report == null) { console.error(`intent import: unsupported format "${fmt}"`); process.exit(2); return; }
1652
+ if (report == null) { console.error(`thunder import: unsupported format "${fmt}"`); process.exit(2); return; }
1052
1653
  if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
1053
1654
  const src = report.source;
1054
1655
  if (args.out && args.out !== '.intent') {
1055
1656
  const base = basename(file).replace(/\.[^.]+$/, '');
1056
1657
  const p = writeText(args.out, `${slug(base)}.intent`, src);
1057
- console.log(`intent import: wrote ${p}`);
1658
+ console.log(`thunder import: wrote ${p}`);
1058
1659
  } else {
1059
1660
  process.stdout.write(src.endsWith('\n') ? src : src + '\n');
1060
1661
  }
1061
1662
  // Fidelity warnings go to stderr, so stdout stays clean for piping the source.
1062
- for (const w of report.warnings) console.error(`intent import: [${w.code}] ${w.message}`);
1663
+ for (const w of report.warnings) console.error(`thunder import: [${w.code}] ${w.message}`);
1063
1664
  return;
1064
1665
  }
1065
1666
 
@@ -1073,9 +1674,9 @@ test Example
1073
1674
  const ast = parseIntent(readFileSync(file, 'utf8'));
1074
1675
  const res = exportIntent(ast, fmt);
1075
1676
  if (args.out && args.out !== '.intent') {
1076
- const name = `${slug(ast.mission || basename(file, '.intent'))}.${res.ext}`;
1677
+ const name = `${slug(ast.mission || stripSourceExt(basename(file)))}.${res.ext}`;
1077
1678
  const p = writeText(args.out, name, res.content);
1078
- console.log(`intent export: wrote ${p}`);
1679
+ console.log(`thunder export: wrote ${p}`);
1079
1680
  } else {
1080
1681
  process.stdout.write(res.content);
1081
1682
  }
@@ -1090,13 +1691,13 @@ test Example
1090
1691
  if (args.search) {
1091
1692
  const hits = searchAtlas(atlas, args.search, { type: args.from });
1092
1693
  if (args.json) { console.log(JSON.stringify(hits, null, 2)); return; }
1093
- console.log(`intent atlas search "${args.search}": ${hits.length} hit(s)`);
1694
+ console.log(`thunder atlas search "${args.search}": ${hits.length} hit(s)`);
1094
1695
  for (const h of hits) console.log(` ${h.type.padEnd(18)} ${h.id}${h.title ? ` , ${h.title}` : ''}`);
1095
1696
  return;
1096
1697
  }
1097
1698
  if (args.expand) {
1098
1699
  const ex = expandNode(atlas, args.expand);
1099
- if (!ex) { console.error(`intent atlas: no node "${args.expand}".`); process.exit(2); return; }
1700
+ if (!ex) { console.error(`thunder atlas: no node "${args.expand}".`); process.exit(2); return; }
1100
1701
  if (args.json) { console.log(JSON.stringify(ex, null, 2)); return; }
1101
1702
  console.log(`${ex.node.type} ${ex.node.id}${ex.node.title ? ` , ${ex.node.title}` : ''}`);
1102
1703
  for (const e of ex.out) console.log(` -> ${e.rel.padEnd(16)} ${e.node.id}`);
@@ -1104,10 +1705,10 @@ test Example
1104
1705
  return;
1105
1706
  }
1106
1707
  if (args.json) { console.log(JSON.stringify(atlas, null, 2)); return; }
1107
- console.log(`intent atlas ${root}: ${atlas.overview.missions} mission(s), ${atlas.overview.nodes} node(s), ${atlas.overview.relationships} edge(s)`);
1708
+ console.log(`thunder atlas ${root}: ${atlas.overview.missions} mission(s), ${atlas.overview.nodes} node(s), ${atlas.overview.relationships} edge(s)`);
1108
1709
  console.log(` ${JSON.stringify(atlas.overview.byType)}`);
1109
1710
  for (const m of atlas.missions) console.log(` mission ${m.id}${m.title ? ` , ${m.title}` : ''}`);
1110
- console.log(' expand a node: intent atlas . --expand <id> | search: --search <query>');
1711
+ console.log(' expand a node: thunder atlas . --expand <id> | search: --search <query>');
1111
1712
  return;
1112
1713
  }
1113
1714
 
@@ -1118,13 +1719,13 @@ test Example
1118
1719
  try {
1119
1720
  intentFiles = collectIntents(root).map((f) => ({ path: relative(root, f), source: readFileSync(f, 'utf8') }));
1120
1721
  } catch (e) {
1121
- console.error(`intent index: cannot read "${root}": ${e instanceof Error ? e.message : e}`);
1722
+ console.error(`thunder index: cannot read "${root}": ${e instanceof Error ? e.message : e}`);
1122
1723
  process.exit(2);
1123
1724
  return;
1124
1725
  }
1125
1726
  const index = buildMissionIndex(intentFiles, { product: args.product });
1126
1727
  if (args.json) { console.log(JSON.stringify(index, null, 2)); return; }
1127
- console.log(`intent index ${root}: ${index.summary.missions} mission(s)`);
1728
+ console.log(`thunder index ${root}: ${index.summary.missions} mission(s)`);
1128
1729
  console.log(` ${JSON.stringify(index.summary.byArea)}`);
1129
1730
  for (const m of index.missions) {
1130
1731
  console.log(` ${m.mission.padEnd(24)} ${String(m.risk).padEnd(7)} G:${m.guarantees} N:${m.neverRules} verify:${m.verification}${m.reviewed ? ' reviewed' : ''}`);
@@ -1134,17 +1735,17 @@ test Example
1134
1735
  return;
1135
1736
  }
1136
1737
 
1137
- // `intent verify-diff <intent> --after <code> [--before <code>]` , the AI-loop gate: prove
1738
+ // `thunder verify-diff <intent> --after <code> [--before <code>]` , the AI-loop gate: prove
1138
1739
  // deterministically which of the intent's guarantees/never-rules a code change upholds or breaks.
1139
1740
  if (cmd === 'verify-diff') {
1140
1741
  const intentText = readFileSync(file, 'utf8');
1141
- if (!args.after) { console.error('usage: intent verify-diff <intent> --after <codeFile> [--before <codeFile>] [--from <lang>]'); process.exit(2); return; }
1742
+ if (!args.after) { console.error('usage: thunder verify-diff <intent> --after <codeFile> [--before <codeFile>] [--from <lang>]'); process.exit(2); return; }
1142
1743
  const after = readFileSync(args.after, 'utf8');
1143
1744
  const before = args.before ? readFileSync(args.before, 'utf8') : null;
1144
1745
  const language = args.from || languageForFile(args.after);
1145
1746
  const r = verifyDiff(intentText, { before, after, language });
1146
1747
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
1147
- console.log(`intent verify-diff ${basename(file)} vs ${basename(args.after)}: ${r.verdict} (${r.blocking} blocking, ${r.summary.regressions} regression(s))`);
1748
+ console.log(`thunder verify-diff ${basename(file)} vs ${basename(args.after)}: ${r.verdict} (${r.blocking} blocking, ${r.summary.regressions} regression(s))`);
1148
1749
  for (const f of r.findings) {
1149
1750
  const tag = f.code === 'INTENT_VERIFY_NEVER_VIOLATED' ? 'VIOLATION' : f.regression ? 'REGRESSION' : f.level.toUpperCase();
1150
1751
  console.log(` [${tag}] ${f.message}${f.line ? ` (line ${f.line})` : ''}`);
@@ -1154,7 +1755,7 @@ test Example
1154
1755
  return;
1155
1756
  }
1156
1757
 
1157
- // `intent draft --brief <json|->` , scaffold a rigorous intent draft from a structured brief,
1758
+ // `thunder draft --brief <json|->` , scaffold a rigorous intent draft from a structured brief,
1158
1759
  // plus a review checklist of what a human must still fill in. Prints the draft; --write saves it.
1159
1760
  if (cmd === 'draft') {
1160
1761
  const briefPath = args.brief || (cmd === 'draft' && file && file.endsWith('.json') ? file : null);
@@ -1164,19 +1765,19 @@ test Example
1164
1765
  try { brief = JSON.parse(raw); } catch { console.error('intent draft: --brief is not valid JSON'); process.exit(2); return; }
1165
1766
  const r = draftIntent(brief);
1166
1767
  if (args.json) { console.log(JSON.stringify(r, null, 2)); return; }
1167
- if (args.write) { writeFileSync(args.write, r.source); console.error(`intent draft: wrote ${args.write}`); }
1768
+ if (args.write) { writeFileSync(args.write, r.source); console.error(`thunder draft: wrote ${args.write}`); }
1168
1769
  else process.stdout.write(r.source);
1169
1770
  if (r.review.length) { console.error('\nreview (fill these in , the draft is a proposal, not verified):'); for (const x of r.review) console.error(` - ${x.message}`); }
1170
1771
  return;
1171
1772
  }
1172
1773
 
1173
- // `intent guard <file>` , preview what a runtime guard compiled from this intent enforces:
1774
+ // `thunder guard <file>` , preview what a runtime guard compiled from this intent enforces:
1174
1775
  // which fields it redacts (secrets/PII) and which decisions it can gate at runtime.
1175
1776
  if (cmd === 'guard') {
1176
1777
  const ast = parseIntent(readFileSync(file, 'utf8'));
1177
1778
  const g = guardSummary(ast);
1178
1779
  if (args.json) { console.log(JSON.stringify(g, null, 2)); return; }
1179
- console.log(`intent guard ${basename(file)}:`);
1780
+ console.log(`thunder guard ${basename(file)}:`);
1180
1781
  console.log(` redacts fields ${g.redactsFields.length ? g.redactsFields.join(', ') : '(none declared secret/PII)'}`);
1181
1782
  console.log(` enforces decisions ${g.enforcesDecisions.length ? g.enforcesDecisions.join(', ') : '(none)'}`);
1182
1783
  if (g.neverRules.length) { console.log(' never rules:'); for (const n of g.neverRules) console.log(` - ${n}`); }
@@ -1184,7 +1785,7 @@ test Example
1184
1785
  return;
1185
1786
  }
1186
1787
 
1187
- // `intent ledger <file.json>` , verify the tamper-evident chain, and explain a subject's history
1788
+ // `thunder ledger <file.json>` , verify the tamper-evident chain, and explain a subject's history
1188
1789
  // (why it was built, who approved it, what was assumed/corrected/verified, which risks accepted).
1189
1790
  if (cmd === 'ledger') {
1190
1791
  if (!file) { console.error('usage: intent ledger <ledger.json> [--subject <id>] [--json]'); process.exit(2); return; }
@@ -1194,7 +1795,7 @@ test Example
1194
1795
  if (args.subject) {
1195
1796
  const ex = ledgerExplain(ledger, args.subject);
1196
1797
  if (args.json) { console.log(JSON.stringify({ chain, ...ex }, null, 2)); process.exit(chain.valid ? 0 : 1); return; }
1197
- console.log(`intent ledger ${basename(file)} , ${args.subject} (chain ${chain.valid ? 'VALID' : 'BROKEN'})`);
1798
+ console.log(`thunder ledger ${basename(file)} , ${args.subject} (chain ${chain.valid ? 'VALID' : 'BROKEN'})`);
1198
1799
  if (ex.why.length) { console.log(' why built:'); for (const w of ex.why) console.log(` - ${w}`); }
1199
1800
  if (ex.approvedBy.length) console.log(` approved by: ${ex.approvedBy.join(', ')}`);
1200
1801
  if (ex.assumptions.length) console.log(` assumptions: ${ex.assumptions.length}`);
@@ -1207,12 +1808,12 @@ test Example
1207
1808
  }
1208
1809
  if (args.json) { console.log(JSON.stringify(chain, null, 2)); process.exit(chain.valid ? 0 : 1); return; }
1209
1810
  const n = (ledger.entries || []).length;
1210
- console.log(`intent ledger ${basename(file)}: ${n} entr${n === 1 ? 'y' : 'ies'}, chain ${chain.valid ? 'VALID (tamper-evident)' : `BROKEN at #${chain.brokenAt} , ${chain.reason}`}`);
1811
+ console.log(`thunder ledger ${basename(file)}: ${n} entr${n === 1 ? 'y' : 'ies'}, chain ${chain.valid ? 'VALID (tamper-evident)' : `BROKEN at #${chain.brokenAt} , ${chain.reason}`}`);
1211
1812
  process.exit(chain.valid ? 0 : 1);
1212
1813
  return;
1213
1814
  }
1214
1815
 
1215
- // `intent impact <base> <proposed>` , the Simulator: estimate a change's impact BEFORE building it
1816
+ // `thunder impact <base> <proposed>` , the Simulator: estimate a change's impact BEFORE building it
1216
1817
  // , the deterministic blast radius, the risk it would introduce, contradictions, release risk.
1217
1818
  if (cmd === 'impact') {
1218
1819
  const baseArg = args._[0]; const propArg = args._[1];
@@ -1220,7 +1821,7 @@ test Example
1220
1821
  const collect = (p) => (existsSync(p) && statSync(p).isDirectory() ? collectIntents(p) : [p]).map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') }));
1221
1822
  const r = simulateChange(collect(baseArg), collect(propArg));
1222
1823
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.summary.safe ? 0 : 1); return; }
1223
- console.log(`intent impact: ${r.summary.safe ? 'SAFE' : 'REVIEW'} (${baseArg} -> ${propArg})`);
1824
+ console.log(`thunder impact: ${r.summary.safe ? 'SAFE' : 'REVIEW'} (${baseArg} -> ${propArg})`);
1224
1825
  console.log(` change touches ${r.changedNodes} node(s); ripples to ${r.deterministicImpact.total} dependent(s)`);
1225
1826
  const bt = Object.entries(r.deterministicImpact.byType);
1226
1827
  if (bt.length) console.log(' deterministic impact by type: ' + bt.map(([t, ns]) => `${ns.length} ${t}`).join(', '));
@@ -1232,7 +1833,7 @@ test Example
1232
1833
  return;
1233
1834
  }
1234
1835
 
1235
- // `intent guardian <before> <after>` , drift detection: what a change did to the intent , which
1836
+ // `thunder guardian <before> <after>` , drift detection: what a change did to the intent , which
1236
1837
  // intent it affects, what risk it introduced, what must be reverified, what learning is stale.
1237
1838
  if (cmd === 'guardian') {
1238
1839
  const beforeArg = args._[0]; const afterArg = args._[1];
@@ -1241,7 +1842,7 @@ test Example
1241
1842
  const r = guardianReport(collect(beforeArg), collect(afterArg));
1242
1843
  if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.verdict === 'needs-attention' ? 1 : 0); return; }
1243
1844
  const c = r.changed;
1244
- console.log(`intent guardian: ${r.verdict.toUpperCase()} (${beforeArg} -> ${afterArg})`);
1845
+ console.log(`thunder guardian: ${r.verdict.toUpperCase()} (${beforeArg} -> ${afterArg})`);
1245
1846
  console.log(` changed +${c.nodesAdded} / -${c.nodesRemoved} / ~${c.nodesChanged} nodes, +${c.relationshipsAdded} / -${c.relationshipsRemoved} relationships`);
1246
1847
  if (r.affectedIntent.length) console.log(` affected ${r.affectedIntent.map((n) => n.title || n.id).join(', ')}`);
1247
1848
  if (r.introducedRisk.length) { console.log(` introduced risk (${r.introducedRisk.length}):`); for (const f of r.introducedRisk.slice(0, 6)) console.log(` [${f.severity}] ${f.ruleId} , ${f.detected}`); }
@@ -1252,31 +1853,31 @@ test Example
1252
1853
  return;
1253
1854
  }
1254
1855
 
1255
- // `intent scan [dir]` , the Scanner spine: intent -> Intent IR -> explainable Fable findings ->
1856
+ // `thunder scan [dir]` , the Scanner spine: intent -> Intent IR -> explainable Fable findings ->
1256
1857
  // risk themes. Deterministic, no AI. --json for the machine report; --ir writes the merged IR.
1257
1858
  // Part 3 focused scan queries , one question each over the Intent IR + Fable findings:
1258
- // `intent risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json]`.
1859
+ // `thunder risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json]`.
1259
1860
  if (VIEWS[cmd]) {
1260
1861
  const root = file || '.';
1261
1862
  const targets = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : [root];
1262
- if (!targets.length || !existsSync(targets[0])) { console.error(`intent ${cmd}: no .intent files under ${root}`); process.exit(2); return; }
1863
+ if (!targets.length || !existsSync(targets[0])) { console.error(`thunder ${cmd}: no .intent files under ${root}`); process.exit(2); return; }
1263
1864
  const scan = scanProject(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
1264
1865
  const v = VIEWS[cmd](scan);
1265
1866
  if (args.json) { console.log(JSON.stringify(v, null, 2)); return; }
1266
1867
  if (cmd === 'coverage') {
1267
- console.log(`intent coverage ${root}: ${v.verified}/${v.total} claims verified (${v.coverage}%)`);
1868
+ console.log(`thunder coverage ${root}: ${v.verified}/${v.total} claims verified (${v.coverage}%)`);
1268
1869
  for (const c of v.unverified) console.log(` unverified [${c.type}] ${c.title}`);
1269
1870
  process.exit(v.coverage === 100 ? 0 : 1); return;
1270
1871
  }
1271
1872
  if (cmd === 'risks') {
1272
1873
  const s = v.bySeverity;
1273
- console.log(`intent risks ${root}: ${v.count} risk theme(s) , ${s.blocker || 0} blocker, ${s.error || 0} error, ${s.warning || 0} warning, ${s.info || 0} info`);
1874
+ console.log(`thunder risks ${root}: ${v.count} risk theme(s) , ${s.blocker || 0} blocker, ${s.error || 0} error, ${s.warning || 0} warning, ${s.info || 0} info`);
1274
1875
  for (const t of v.themes) console.log(` ${String(t.count).padStart(3)} ${t.category}${t.blocker ? ` (${t.blocker} blocker)` : ''}`);
1275
1876
  for (const r of v.remediationSequence.slice(0, 5)) console.log(` fix [${r.severity}] ${r.ruleId} (${r.count}x) , ${r.remediation}`);
1276
1877
  process.exit((s.blocker || 0) + (s.error || 0) === 0 ? 0 : 1); return;
1277
1878
  }
1278
1879
  // gaps / unverified / unknowns / contradictions , a uniform list
1279
- console.log(`intent ${cmd} ${root}: ${v.count}`);
1880
+ console.log(`thunder ${cmd} ${root}: ${v.count}`);
1280
1881
  for (const g of v.gaps || []) console.log(` [${g.severity}] ${g.ruleId} , ${g.detected}`);
1281
1882
  for (const c of v.claims || []) console.log(` [${c.type}] ${c.title}`);
1282
1883
  for (const u of v.unknowns || []) console.log(` [${u.type}] ${u.title}${u.confidence ? ` (${u.confidence})` : ''}`);
@@ -1291,10 +1892,10 @@ test Example
1291
1892
  const root = file || '.';
1292
1893
  const targets = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : [root];
1293
1894
  const result = scanProject(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
1294
- if (args.ir) { writeFileSync(args.ir, JSON.stringify(result.ir, null, 2)); console.error(`intent scan: wrote Intent IR (${result.ir.nodes.length} nodes) to ${args.ir}`); }
1895
+ if (args.ir) { writeFileSync(args.ir, JSON.stringify(result.ir, null, 2)); console.error(`thunder scan: wrote Intent IR (${result.ir.nodes.length} nodes) to ${args.ir}`); }
1295
1896
  if (args.json) { console.log(JSON.stringify(result, null, 2)); process.exit(result.ok ? 0 : 1); return; }
1296
1897
  const s = result.bySeverity;
1297
- console.log(`intent scan ${root}: ${result.totals.findings} finding(s) across ${result.totals.missions} mission(s) in ${result.totals.files} file(s)`);
1898
+ console.log(`thunder scan ${root}: ${result.totals.findings} finding(s) across ${result.totals.missions} mission(s) in ${result.totals.files} file(s)`);
1298
1899
  console.log(` severity ${s.blocker} blocker, ${s.error} error, ${s.warning} warning, ${s.info} info , Intent IR: ${result.ir.nodes.length} nodes`);
1299
1900
  if (result.risks.length) {
1300
1901
  console.log(' risk themes:');
@@ -1308,7 +1909,7 @@ test Example
1308
1909
  return;
1309
1910
  }
1310
1911
 
1311
- // `intent report [dir]` , a repo-wide intent health summary (aggregates every .intent file).
1912
+ // `thunder report [dir]` , a repo-wide intent health summary (aggregates every .intent file).
1312
1913
  // Distinct from `check` (pass/fail gate): counts by severity + area, top codes, and coverage.
1313
1914
  if (cmd === 'report') {
1314
1915
  const root = file || '.';
@@ -1316,7 +1917,7 @@ test Example
1316
1917
  const rep = buildReport(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
1317
1918
  if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(0); return; }
1318
1919
  const c = rep.coverage;
1319
- console.log(`intent report ${root}: ${rep.totals.missions} mission(s) in ${rep.totals.files} file(s), ${rep.totals.diagnostics} diagnostic(s)`);
1920
+ console.log(`thunder report ${root}: ${rep.totals.missions} mission(s) in ${rep.totals.files} file(s), ${rep.totals.diagnostics} diagnostic(s)`);
1320
1921
  console.log(` severity ${rep.bySeverity.blocker} blocker, ${rep.bySeverity.error} error, ${rep.bySeverity.warning} warning, ${rep.bySeverity.info} info`);
1321
1922
  console.log(' coverage '
1322
1923
  + `guarantees verified ${c.guaranteesVerified}/${c.guarantees}${c.guaranteeVerifyRate != null ? ` (${c.guaranteeVerifyRate}%)` : ''}, `
@@ -1334,9 +1935,9 @@ test Example
1334
1935
  return;
1335
1936
  }
1336
1937
 
1337
- // `intent check <file|dir> --format sarif` emits a SARIF 2.1.0 log so ThunderLang
1938
+ // `thunder check <file|dir> --format sarif` emits a SARIF 2.1.0 log so ThunderLang
1338
1939
  // diagnostics show up natively in GitHub/GitLab code scanning and SARIF-aware IDEs.
1339
- // This is a REPORT (exit 0); gate the build with a plain `intent check .` step.
1940
+ // This is a REPORT (exit 0); gate the build with a plain `thunder check .` step.
1340
1941
  if (cmd === 'check' && args.format === 'sarif') {
1341
1942
  const targets = existsSync(file) && statSync(file).isDirectory() ? collectIntents(file) : [file];
1342
1943
  const reports = targets.map((f) => {
@@ -1352,7 +1953,7 @@ test Example
1352
1953
  return;
1353
1954
  }
1354
1955
 
1355
- // `intent check <dir>` recurses and gates every .intent file (self-contained CI, no
1956
+ // `thunder check <dir>` recurses and gates every .intent file (self-contained CI, no
1356
1957
  // wrapper script needed). Errors fail the run; warnings do not.
1357
1958
  if (cmd === 'check' && existsSync(file) && statSync(file).isDirectory()) {
1358
1959
  const files = collectIntents(file);
@@ -1371,12 +1972,12 @@ test Example
1371
1972
  console.log(JSON.stringify({ schema: 'intent-check-batch-v1', root: file, total: reports.length, failed: failed.length, ok: failed.length === 0, files: reports }, null, 2));
1372
1973
  process.exit(failed.length ? 1 : 0);
1373
1974
  }
1374
- console.log(`intent check ${file}: ${reports.length - failed.length}/${reports.length} passed`);
1975
+ console.log(`thunder check ${file}: ${reports.length - failed.length}/${reports.length} passed`);
1375
1976
  for (const r of reports) console.log(` ${r.ok ? 'ok ' : 'ERR'} ${r.file}${r.errors ? ` , ${r.errors} error(s)` : ''}${r.warnings ? ` (${r.warnings} warning(s))` : ''}`);
1376
1977
  process.exit(failed.length ? 1 : 0);
1377
1978
  }
1378
1979
 
1379
- // `intent edit <file>` , apply structural field edits (intent-patch-v1) to the source,
1980
+ // `thunder edit <file>` , apply structural field edits (intent-patch-v1) to the source,
1380
1981
  // preserving comments + formatting. Edits come from --edits <json|-> and/or convenience
1381
1982
  // flags. Prints the result to stdout, or --write applies it in place.
1382
1983
  if (cmd === 'edit') {
@@ -1401,7 +2002,7 @@ test Example
1401
2002
  for (const s of result.skipped) console.error(` skipped: ${s.reason}`);
1402
2003
  if (args.write) {
1403
2004
  if (result.source !== src.replace(/\r\n?/g, '\n')) writeFileSync(file, result.source);
1404
- console.error(`intent edit ${basename(file)}: applied ${result.applied.length}, skipped ${result.skipped.length}.`);
2005
+ console.error(`thunder edit ${basename(file)}: applied ${result.applied.length}, skipped ${result.skipped.length}.`);
1405
2006
  } else if (!args.json) {
1406
2007
  process.stdout.write(result.source);
1407
2008
  }
@@ -1409,7 +2010,7 @@ test Example
1409
2010
  return;
1410
2011
  }
1411
2012
 
1412
- // `intent fmt <file|dir>` , canonical formatting (whitespace only; content + comments
2013
+ // `thunder fmt <file|dir>` , canonical formatting (whitespace only; content + comments
1413
2014
  // preserved). Prints to stdout, or --write in place, or --check for CI (exit 1 if any
1414
2015
  // file is not already formatted).
1415
2016
  if (cmd === 'fmt') {
@@ -1425,18 +2026,18 @@ test Example
1425
2026
  process.stdout.write(formatted);
1426
2027
  }
1427
2028
  if (args.check) {
1428
- if (unformatted.length) { console.error(`intent fmt --check: ${unformatted.length} file(s) not formatted:`); for (const u of unformatted) console.error(` ${u}`); process.exit(1); }
1429
- console.log('intent fmt --check: all formatted.');
2029
+ if (unformatted.length) { console.error(`thunder fmt --check: ${unformatted.length} file(s) not formatted:`); for (const u of unformatted) console.error(` ${u}`); process.exit(1); }
2030
+ console.log('thunder fmt --check: all formatted.');
1430
2031
  process.exit(0);
1431
2032
  }
1432
- if (args.write) console.log(`intent fmt: formatted ${changed} file(s), ${targets.length - changed} already clean.`);
2033
+ if (args.write) console.log(`thunder fmt: formatted ${changed} file(s), ${targets.length - changed} already clean.`);
1433
2034
  return;
1434
2035
  }
1435
2036
 
1436
2037
  const { source, ast, sourceHash, sourceFile } = load(file);
1437
2038
  const generatedAt = new Date().toISOString();
1438
2039
  const diagnostics = semanticDiagnostics(ast);
1439
- const outDir = join(args.out, slug(ast.mission || basename(file, '.intent')));
2040
+ const outDir = join(args.out, slug(ast.mission || stripSourceExt(basename(file))));
1440
2041
 
1441
2042
  // Production gate: a build --mode production refuses to proceed while an AI
1442
2043
  // implementation is not shippable. --allow-pending is for dev builds only.
@@ -1497,11 +2098,18 @@ test Example
1497
2098
  console.log(JSON.stringify(out, null, 2));
1498
2099
  process.exit(errors > 0 ? 1 : 0);
1499
2100
  }
1500
- console.log(`intent check ${sourceFile} (mission: ${ast.mission})`);
2101
+ console.log(`thunder check ${sourceFile} (mission: ${ast.mission})`);
1501
2102
  process.exit(printDiagnostics(diags) > 0 ? 1 : 0);
1502
2103
  }
1503
2104
 
1504
2105
  const generated = [];
2106
+ // `thunder graph <file> --safe` , a display-safe intent-graph on stdout, for external viewers
2107
+ // (e.g. STT rendering the Intent Atlas). Strips owner/source provenance and redacts sensitive
2108
+ // classifications; carries no source code.
2109
+ if (cmd === 'graph' && args.safe) {
2110
+ console.log(JSON.stringify(safeGraph(buildIntentGraph(ast)), null, 2));
2111
+ return;
2112
+ }
1505
2113
  if (cmd === 'graph' || cmd === 'build') {
1506
2114
  generated.push(writeJson(outDir, 'contract-graph.json', buildContractGraph(ast, generatedAt)));
1507
2115
  generated.push(writeJson(outDir, 'architecture-graph.json', buildArchitectureGraph(ast, generatedAt)));
@@ -1520,13 +2128,13 @@ test Example
1520
2128
  targetsGenerated: generated.map((p) => p.replace(process.cwd() + '/', '')),
1521
2129
  diagnostics,
1522
2130
  });
1523
- generated.push(writeJson(outDir, '.intent-proof.json', proof));
2131
+ generated.push(writeJson(outDir, '.thunder-proof.json', proof));
1524
2132
  }
1525
2133
  if (!['graph', 'proof', 'build'].includes(cmd)) {
1526
2134
  console.error(`unknown command: ${cmd}`);
1527
2135
  process.exit(2);
1528
2136
  }
1529
- console.log(`intent ${cmd} ${sourceFile} -> ${outDir}`);
2137
+ console.log(`thunder ${cmd} ${sourceFile} -> ${outDir}`);
1530
2138
  for (const p of generated) console.log(` wrote ${p.replace(process.cwd() + '/', '')}`);
1531
2139
  printDiagnostics(diagnostics);
1532
2140
  }