@skillstech/thunderlang 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -47
- package/LICENSE +201 -21
- package/NOTICE +15 -0
- package/README.md +5 -8
- package/dist/core.cjs +12 -12
- package/dist/index.cjs +16 -16
- package/package.json +12 -12
- package/src/ai-events.mjs +1 -1
- package/src/cli.mjs +139 -134
- package/src/codegen.mjs +1 -1
- package/src/compile.mjs +3 -3
- package/src/drift.mjs +3 -3
- package/src/emit.mjs +2 -2
- package/src/focus.mjs +1 -1
- package/src/guard.mjs +3 -3
- package/src/intellisense.mjs +1 -1
- package/src/intent-schema.mjs +4 -4
- package/src/ledger.mjs +1 -1
- package/src/lift.mjs +2 -2
- package/src/lsp.mjs +1 -1
- package/src/mcp.mjs +1 -1
- package/src/parse.mjs +3 -3
- package/src/proof-schema.mjs +2 -2
- package/src/report.mjs +1 -1
- package/src/sarif.mjs +1 -1
- package/src/scan-queries.mjs +1 -1
- package/src/style.mjs +3 -3
- package/src/twelve-factor.mjs +2 -2
package/src/cli.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
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
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// thunder check <file> parse + semantic diagnostics (exit 1 on error)
|
|
9
|
+
// thunder graph <file> [--out .intent] contract-graph.json + architecture-graph.json
|
|
10
10
|
// intent proof <file> [--out .intent] .intent-proof.json
|
|
11
11
|
// intent build <file> [--out .intent] [--no-ai] all artifacts + docs + mermaid + testplan
|
|
12
12
|
//
|
|
@@ -69,6 +69,11 @@ import { parseEventLog, serializeEventLog, recordEvent, timeline } from './ai-ev
|
|
|
69
69
|
// Recursively collect supported source files, skipping vendored / build dirs.
|
|
70
70
|
const LIFT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.rs', '.pl', '.pm'];
|
|
71
71
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build', '.intent', 'coverage', '.vercel']);
|
|
72
|
+
// ThunderLang source files. `.thunder` is the canonical public extension; `.tl` is an
|
|
73
|
+
// accepted shorthand; `.intent` stays supported so legacy IntentLang sources keep working.
|
|
74
|
+
const SOURCE_EXTS = ['.thunder', '.tl', '.intent'];
|
|
75
|
+
const isSourceFile = (name) => SOURCE_EXTS.some((e) => name.endsWith(e));
|
|
76
|
+
const stripSourceExt = (name) => name.replace(/\.(thunder|tl|intent)$/i, '');
|
|
72
77
|
function collectFiles(root, acc = []) {
|
|
73
78
|
for (const name of readdirSync(root)) {
|
|
74
79
|
if (SKIP_DIRS.has(name)) continue;
|
|
@@ -80,15 +85,15 @@ function collectFiles(root, acc = []) {
|
|
|
80
85
|
return acc;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
// Walk a directory for authored
|
|
88
|
+
// Walk a directory for authored source files (skips the .intent/ output dir).
|
|
84
89
|
function collectIntents(root, acc = []) {
|
|
85
90
|
const st = statSync(root);
|
|
86
|
-
if (!st.isDirectory()) return root
|
|
91
|
+
if (!st.isDirectory()) return isSourceFile(root) ? [root] : acc;
|
|
87
92
|
for (const name of readdirSync(root)) {
|
|
88
93
|
if (SKIP_DIRS.has(name)) continue;
|
|
89
94
|
const full = join(root, name);
|
|
90
95
|
if (statSync(full).isDirectory()) collectIntents(full, acc);
|
|
91
|
-
else if (name
|
|
96
|
+
else if (isSourceFile(name)) acc.push(full);
|
|
92
97
|
}
|
|
93
98
|
return acc;
|
|
94
99
|
}
|
|
@@ -197,7 +202,7 @@ function printDiagnostics(diags) {
|
|
|
197
202
|
return errors;
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
const HELP = `
|
|
205
|
+
const HELP = `thunder , the deterministic ThunderLang compiler (no AI required)
|
|
201
206
|
|
|
202
207
|
usage: intent <command> <file> [options]
|
|
203
208
|
|
|
@@ -229,7 +234,7 @@ Author & check
|
|
|
229
234
|
schema emit the canonical graph schema + diagnostic catalog
|
|
230
235
|
explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
|
|
231
236
|
rules [--json] list the whole canonical diagnostic catalog
|
|
232
|
-
notes <file> [--lens <lens>] [--json]
|
|
237
|
+
notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
|
|
233
238
|
docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
|
|
234
239
|
code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
|
|
235
240
|
apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
|
|
@@ -281,7 +286,7 @@ function main() {
|
|
|
281
286
|
else console.log(JSON.stringify(out, null, 2));
|
|
282
287
|
return;
|
|
283
288
|
}
|
|
284
|
-
// `
|
|
289
|
+
// `thunder proof --schema` emits the canonical proof envelope JSON Schema (no file needed).
|
|
285
290
|
// This is the "shared envelope" schema siblings sign (STW) and re-verify (RM/OT) against.
|
|
286
291
|
if (cmd === 'proof' && args.schema) {
|
|
287
292
|
console.log(JSON.stringify(intentProofJsonSchema(), null, 2));
|
|
@@ -291,11 +296,11 @@ function main() {
|
|
|
291
296
|
// drift / tampering) and the proof's claims re-derive from the source.
|
|
292
297
|
if (cmd === 'verify') {
|
|
293
298
|
const proofPath = args._[0];
|
|
294
|
-
if (!proofPath) { console.error('usage:
|
|
299
|
+
if (!proofPath) { console.error('usage: thunder verify <proof.json> [<source.intent>]'); process.exit(2); return; }
|
|
295
300
|
let proof;
|
|
296
|
-
try { proof = JSON.parse(readFileSync(proofPath, 'utf8')); } catch { console.error('
|
|
301
|
+
try { proof = JSON.parse(readFileSync(proofPath, 'utf8')); } catch { console.error('thunder verify: proof is not valid JSON'); process.exit(2); return; }
|
|
297
302
|
const srcPath = args._[1] || proof.sourceFile;
|
|
298
|
-
if (!srcPath || !existsSync(srcPath)) { console.error(`
|
|
303
|
+
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
304
|
const src = readFileSync(srcPath, 'utf8');
|
|
300
305
|
const ast = parseIntent(src);
|
|
301
306
|
const diags = semanticDiagnostics(ast);
|
|
@@ -309,7 +314,7 @@ function main() {
|
|
|
309
314
|
const valid = structure.valid && hashMatch && semanticMatch && guaranteesMatch && neverMatch;
|
|
310
315
|
const result = { schema: 'intent-verify-v1', proof: proofPath, source: srcPath, valid, checks: { wellFormed: structure.valid, hashMatch, semanticMatch, guaranteesMatch, neverMatch }, structureErrors: structure.errors };
|
|
311
316
|
if (args.json) { console.log(JSON.stringify(result, null, 2)); process.exit(valid ? 0 : 1); return; }
|
|
312
|
-
console.log(`
|
|
317
|
+
console.log(`thunder verify ${basename(proofPath)}: ${valid ? 'VALID' : 'FAILED'} (source ${basename(srcPath)})`);
|
|
313
318
|
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
319
|
if (!hashMatch) console.log(' X source hash does not match , the source has changed since the proof was generated (drift or tampering).');
|
|
315
320
|
if (!semanticMatch) console.log(' X the proof claims a different semantic result than the source produces now.');
|
|
@@ -320,13 +325,13 @@ function main() {
|
|
|
320
325
|
return;
|
|
321
326
|
}
|
|
322
327
|
|
|
323
|
-
// Explain a diagnostic code from the canonical catalog. `
|
|
328
|
+
// Explain a diagnostic code from the canonical catalog. `thunder explain IL-DEC-001`.
|
|
324
329
|
if (cmd === 'explain') {
|
|
325
330
|
const code = file;
|
|
326
|
-
if (!code) { console.error('usage:
|
|
331
|
+
if (!code) { console.error('usage: thunder explain <IL-CODE>'); process.exit(2); return; }
|
|
327
332
|
const rule = ALL_DIAGNOSTICS.find((r) => r.ruleId.toLowerCase() === code.toLowerCase());
|
|
328
333
|
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(`
|
|
334
|
+
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
335
|
console.log(`${rule.ruleId} (area: ${rule.area})`);
|
|
331
336
|
console.log(` ${rule.summary}`);
|
|
332
337
|
console.log(` severity: ${rule.severity}${rule.blocks && rule.blocks.length ? ` | blocks: ${rule.blocks.join(', ')}` : ' | does not block a phase'}`);
|
|
@@ -338,7 +343,7 @@ function main() {
|
|
|
338
343
|
if (args.json) { console.log(JSON.stringify(ALL_DIAGNOSTICS, null, 2)); return; }
|
|
339
344
|
const byArea = {};
|
|
340
345
|
for (const r of ALL_DIAGNOSTICS) (byArea[r.area] ||= []).push(r);
|
|
341
|
-
console.log(`
|
|
346
|
+
console.log(`thunder rules: ${ALL_DIAGNOSTICS.length} diagnostics in ${Object.keys(byArea).length} areas\n`);
|
|
342
347
|
for (const area of Object.keys(byArea).sort()) {
|
|
343
348
|
console.log(`${area}`);
|
|
344
349
|
for (const r of byArea[area]) {
|
|
@@ -350,7 +355,7 @@ function main() {
|
|
|
350
355
|
return;
|
|
351
356
|
}
|
|
352
357
|
|
|
353
|
-
// `
|
|
358
|
+
// `thunder notes <file> [--lens <lens>] [--json]` , the ThunderLens report: the compiled
|
|
354
359
|
// semantic comments (`note <lens>:`) grouped by lens, each with its target and source
|
|
355
360
|
// line. Notes explain meaning for a reader; they are NEVER verification.
|
|
356
361
|
if (cmd === 'notes') {
|
|
@@ -363,7 +368,7 @@ function main() {
|
|
|
363
368
|
return;
|
|
364
369
|
}
|
|
365
370
|
const scope = args.lens ? ` (lens: ${args.lens})` : '';
|
|
366
|
-
console.log(`
|
|
371
|
+
console.log(`thunder notes ${basename(file)}${scope}: ${notes.length} note${notes.length === 1 ? '' : 's'}`);
|
|
367
372
|
const known = new Set(KNOWN_LENSES);
|
|
368
373
|
const byLens = {};
|
|
369
374
|
for (const n of notes) (byLens[n.lens] ||= []).push(n);
|
|
@@ -378,7 +383,7 @@ function main() {
|
|
|
378
383
|
return;
|
|
379
384
|
}
|
|
380
385
|
|
|
381
|
-
// `
|
|
386
|
+
// `thunder docs <file> [--lens <lens>] [--out <dir>]` , render a mission as Markdown docs.
|
|
382
387
|
// With --lens, produce an audience-specific doc with that lens's notes woven inline.
|
|
383
388
|
if (cmd === 'docs') {
|
|
384
389
|
if (!file) { console.error('usage: intent docs <file> [--lens <lens>] [--out <dir>]'); process.exit(2); return; }
|
|
@@ -394,19 +399,19 @@ function main() {
|
|
|
394
399
|
return;
|
|
395
400
|
}
|
|
396
401
|
|
|
397
|
-
// `
|
|
402
|
+
// `thunder code-actions <file> [--json]` , the available quick-fixes, each safety-graded
|
|
398
403
|
// (safe autocorrects + reviewable diagnostic fixes). The IDE lightbulb's data source.
|
|
399
404
|
if (cmd === 'code-actions') {
|
|
400
405
|
if (!file) { console.error('usage: intent code-actions <file> [--json]'); process.exit(2); return; }
|
|
401
406
|
const source = readFileSync(file, 'utf8');
|
|
402
407
|
const actions = getCodeActions(source, semanticDiagnostics(parseIntent(source)));
|
|
403
408
|
if (args.json) { console.log(JSON.stringify({ schema: 'intent-code-actions-v1', count: actions.length, actions }, null, 2)); return; }
|
|
404
|
-
console.log(`
|
|
409
|
+
console.log(`thunder code-actions ${basename(file)}: ${actions.length} action${actions.length === 1 ? '' : 's'}`);
|
|
405
410
|
for (const a of actions) console.log(` [${a.safety}] ${a.kind}${a.line ? ` (line ${a.line})` : ''} ${a.title}`);
|
|
406
411
|
return;
|
|
407
412
|
}
|
|
408
413
|
|
|
409
|
-
// `
|
|
414
|
+
// `thunder apply-fix <file> [--write]` , apply the SAFE autocorrects only (header aliases,
|
|
410
415
|
// stray colons). Reviewable quick-fixes are reported, never applied blindly.
|
|
411
416
|
if (cmd === 'apply-fix') {
|
|
412
417
|
if (!file) { console.error('usage: intent apply-fix <file> [--write]'); process.exit(2); return; }
|
|
@@ -415,7 +420,7 @@ function main() {
|
|
|
415
420
|
const reviewable = getCodeActions(source, semanticDiagnostics(parseIntent(source))).filter((a) => a.safety !== 'safe');
|
|
416
421
|
if (args.json) { console.log(JSON.stringify({ applied: changes, reviewableRemaining: reviewable.length, changed: fixed !== source }, null, 2)); }
|
|
417
422
|
else {
|
|
418
|
-
console.log(`
|
|
423
|
+
console.log(`thunder apply-fix ${basename(file)}: ${changes.length} safe fix${changes.length === 1 ? '' : 'es'}${args.write ? ' applied' : ' (dry run; pass --write)'}`);
|
|
419
424
|
for (const c of changes) console.log(` line ${c.line}: "${c.from}" -> "${c.to}" [${c.rule}]`);
|
|
420
425
|
if (reviewable.length) console.log(` ${reviewable.length} reviewable quick-fix(es) left for a human (run: intent code-actions ${basename(file)})`);
|
|
421
426
|
}
|
|
@@ -423,12 +428,12 @@ function main() {
|
|
|
423
428
|
return;
|
|
424
429
|
}
|
|
425
430
|
|
|
426
|
-
// `
|
|
431
|
+
// `thunder focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json]` , Intent Lens:
|
|
427
432
|
// a focused Focus Graph + Intent Brief around a selected scope, built over the Atlas.
|
|
428
433
|
if (cmd === 'focus') {
|
|
429
434
|
const dir = args.dir || '.';
|
|
430
435
|
const files = existsSync(dir) && statSync(dir).isDirectory() ? collectIntents(dir) : (file && existsSync(file) ? [file] : []);
|
|
431
|
-
if (!files.length) { console.error(`
|
|
436
|
+
if (!files.length) { console.error(`thunder focus: no .intent files under ${dir}`); process.exit(2); return; }
|
|
432
437
|
const atlas = buildAtlas(files.map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8')))));
|
|
433
438
|
// Resolve seeds: --nodes ids, or a mission/feature query, or (default) all missions.
|
|
434
439
|
let seeds = [];
|
|
@@ -437,14 +442,14 @@ function main() {
|
|
|
437
442
|
if (args.nodes) { seeds = args.nodes.split(',').map((s) => s.trim()).filter(Boolean); scopeType = 'custom'; scopeTitle = `${seeds.length} selected node(s)`; }
|
|
438
443
|
else if (file && !existsSync(file)) {
|
|
439
444
|
const hit = searchAtlas(atlas, file)[0];
|
|
440
|
-
if (!hit) { console.error(`
|
|
445
|
+
if (!hit) { console.error(`thunder focus: no Atlas node matches "${file}"`); process.exit(1); return; }
|
|
441
446
|
seeds = [hit.id]; scopeType = hit.type === 'Mission' ? 'mission' : 'feature'; scopeTitle = hit.title || hit.id;
|
|
442
447
|
} else { seeds = atlas.missions.map((m) => m.id); scopeType = 'capability'; scopeTitle = 'whole project'; }
|
|
443
448
|
const scope = makeScope({ type: scopeType, title: scopeTitle, seeds, createdAt: null });
|
|
444
449
|
const focus = buildFocusGraph(atlas, { seeds, depth: args.depth ? Number(args.depth) : 2, scope });
|
|
445
450
|
const brief = intentBrief(focus);
|
|
446
451
|
if (args.json) { console.log(JSON.stringify({ scope, brief, focus }, null, 2)); return; }
|
|
447
|
-
console.log(`
|
|
452
|
+
console.log(`thunder focus , ${scope.title} [${scope.type}] (${scope.scopeId})`);
|
|
448
453
|
console.log(` what: ${brief.what || '(unnamed)'}${brief.confidence ? ` confidence: ${brief.confidence}` : ''}`);
|
|
449
454
|
if (brief.who.length) console.log(` who: ${brief.who.join(', ')}`);
|
|
450
455
|
console.log(` focus graph: ${focus.overview.nodes} node(s), ${focus.overview.relationships} edge(s), depth ${focus.depth}`);
|
|
@@ -457,19 +462,19 @@ function main() {
|
|
|
457
462
|
return;
|
|
458
463
|
}
|
|
459
464
|
|
|
460
|
-
// `
|
|
465
|
+
// `thunder comprehension <file|dir> [--observed] [--learning] [--governed] [--json]` , the C0..C7
|
|
461
466
|
// understanding level of each mission (Comprehension Contract). IL scores the intent side (C1..C4);
|
|
462
467
|
// --observed (OpenThunder/runtime), --learning (Skills Tech Talk), --governed (Guardian) lift the
|
|
463
468
|
// joint level to C5/C6/C7 when a sibling attaches its evidence.
|
|
464
469
|
if (cmd === 'comprehension') {
|
|
465
470
|
const root = file || '.';
|
|
466
471
|
const files = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : (existsSync(root) ? [root] : []);
|
|
467
|
-
if (!files.length) { console.error(`
|
|
472
|
+
if (!files.length) { console.error(`thunder comprehension: no .intent files under ${root}`); process.exit(2); return; }
|
|
468
473
|
const opts = { observed: !!args.observed, learningPath: !!args.learning, governed: !!args.governed };
|
|
469
474
|
const asts = files.map((f) => parseIntent(readFileSync(f, 'utf8')));
|
|
470
475
|
const report = comprehensionReport(asts, opts);
|
|
471
476
|
if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
472
|
-
console.log(`
|
|
477
|
+
console.log(`thunder comprehension ${root}: ${report.count} mission(s)`);
|
|
473
478
|
console.log(` distribution: ${COMPREHENSION_LEVELS.map((l) => `${l.level}:${report.byLevel[l.level]}`).join(' ')}`);
|
|
474
479
|
for (const m of report.missions) {
|
|
475
480
|
console.log(`\n ${m.mission || '(unnamed)'} , ${m.level} ${m.levelName}`);
|
|
@@ -482,7 +487,7 @@ function main() {
|
|
|
482
487
|
return;
|
|
483
488
|
}
|
|
484
489
|
|
|
485
|
-
// `
|
|
490
|
+
// `thunder twelve-factor <file> [--json]` , score an intent against the 13 principles of
|
|
486
491
|
// humanlayer/12-factor-agents (deterministic conformance lens). Per-factor verdict + score.
|
|
487
492
|
if (cmd === 'twelve-factor' || cmd === '12factor') {
|
|
488
493
|
if (!file) { console.error('usage: intent twelve-factor <file> [--json]'); process.exit(2); return; }
|
|
@@ -500,14 +505,14 @@ function main() {
|
|
|
500
505
|
return;
|
|
501
506
|
}
|
|
502
507
|
|
|
503
|
-
// `
|
|
508
|
+
// `thunder gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
|
|
504
509
|
// intent. Generates the typed contract + the decision logic (already executable) and leaves
|
|
505
510
|
// honest TODO markers for business logic. No AI. Prints, or writes with --out.
|
|
506
511
|
if (cmd === 'gen') {
|
|
507
512
|
if (!file) { console.error(`usage: intent gen <file> [--target ${Object.keys(GENERATORS).join('|')}] [--out <dir>]`); process.exit(2); return; }
|
|
508
513
|
const target = (args.target || 'typescript').toLowerCase();
|
|
509
514
|
const generate = GENERATORS[target];
|
|
510
|
-
if (!generate) { console.error(`
|
|
515
|
+
if (!generate) { console.error(`thunder gen: unknown target "${target}" (have: ${Object.keys(GENERATORS).join(', ')})`); process.exit(2); return; }
|
|
511
516
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
512
517
|
const code = generate(ast);
|
|
513
518
|
if (args.outExplicit) {
|
|
@@ -520,27 +525,27 @@ function main() {
|
|
|
520
525
|
return;
|
|
521
526
|
}
|
|
522
527
|
|
|
523
|
-
// `
|
|
528
|
+
// `thunder changes [<base>..<head> | <base>] [--json]` , Change Lens: what a branch / PR / commit
|
|
524
529
|
// range changed BY MEANING. git-diffs the .intent files, semantic-diffs each, and reports the
|
|
525
530
|
// behavior-level changes + regression risk. Default: HEAD vs the working tree.
|
|
526
531
|
if (cmd === 'changes') {
|
|
527
532
|
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('
|
|
533
|
+
if (git('rev-parse --is-inside-work-tree') === null) { console.error('thunder changes: not a git repository'); process.exit(2); return; }
|
|
529
534
|
const range = file || '';
|
|
530
535
|
let base, head; // head === null means "the working tree"
|
|
531
536
|
if (range.includes('..')) { [base, head] = range.split('..'); head = head || 'HEAD'; }
|
|
532
537
|
else if (range) { base = range; head = null; }
|
|
533
538
|
else { base = 'HEAD'; head = null; }
|
|
534
539
|
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(`
|
|
540
|
+
const names = (git(`diff --name-only ${diffSpec} -- "*.thunder" "*.tl" "*.intent"`) || '').split('\n').map((s) => s.trim()).filter(Boolean);
|
|
541
|
+
if (!names.length) { console.log(`thunder changes ${range || `${base}..working-tree`}: no source files changed`); return; }
|
|
537
542
|
const readAt = (ref, p) => (ref === null ? (existsSync(p) ? readFileSync(p, 'utf8') : null) : git(`show ${ref}:${p}`));
|
|
538
543
|
const toGraph = (src) => (src != null ? buildIntentGraph(parseIntent(src)) : null);
|
|
539
544
|
const pairs = names.map((p) => ({ path: p, before: toGraph(readAt(base, p)), after: toGraph(readAt(head, p)) }));
|
|
540
545
|
const report = changeReport(pairs);
|
|
541
546
|
if (args.json) { console.log(JSON.stringify(report, null, 2)); process.exit(report.verdict === 'review' ? 1 : 0); return; }
|
|
542
547
|
const t = report.totals;
|
|
543
|
-
console.log(`
|
|
548
|
+
console.log(`thunder changes ${range || `${base}..working-tree`}: ${report.verdict.toUpperCase()}`);
|
|
544
549
|
console.log(` ${t.files} file(s) , +${t.added} / -${t.removed} / ~${t.changed} nodes${t.invalidatedApprovals ? `, ${t.invalidatedApprovals} approval(s) invalidated` : ''}`);
|
|
545
550
|
if (report.regressions.length) {
|
|
546
551
|
console.log(' regression risk (a promise or its proof was removed or weakened):');
|
|
@@ -559,18 +564,18 @@ function main() {
|
|
|
559
564
|
}
|
|
560
565
|
|
|
561
566
|
// 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 `
|
|
567
|
+
// coding agents. Long-running; no file argument. Point an MCP client at `thunder mcp`.
|
|
563
568
|
if (cmd === 'mcp') {
|
|
564
569
|
startMcpServer();
|
|
565
570
|
return; // keep the process alive on stdin
|
|
566
571
|
}
|
|
567
572
|
|
|
568
|
-
// Scaffold a runnable starter mission (deterministic, no AI). `
|
|
573
|
+
// Scaffold a runnable starter mission (deterministic, no AI). `thunder init [Name]`.
|
|
569
574
|
if (cmd === 'init') {
|
|
570
|
-
const name = (file || 'Mission')
|
|
571
|
-
const target = join(args.out && args.out !== '.intent' ? args.out : '.', `${name}.
|
|
575
|
+
const name = stripSourceExt(file || 'Mission');
|
|
576
|
+
const target = join(args.out && args.out !== '.intent' ? args.out : '.', `${name}.thunder`);
|
|
572
577
|
if (existsSync(target) && !args.force) {
|
|
573
|
-
console.error(`
|
|
578
|
+
console.error(`thunder init: ${target} already exists (use --force to overwrite).`);
|
|
574
579
|
process.exit(1); return;
|
|
575
580
|
}
|
|
576
581
|
const starter = `mission ${name}
|
|
@@ -586,7 +591,7 @@ guarantee an example property that must always hold
|
|
|
586
591
|
never
|
|
587
592
|
do something this mission must never do
|
|
588
593
|
|
|
589
|
-
# A runnable decision. Try:
|
|
594
|
+
# A runnable decision. Try: thunder run ${name}.intent --inputs '{"age":20}'
|
|
590
595
|
decision Example
|
|
591
596
|
inputs
|
|
592
597
|
age
|
|
@@ -596,7 +601,7 @@ decision Example
|
|
|
596
601
|
default
|
|
597
602
|
return Blocked
|
|
598
603
|
|
|
599
|
-
# Tests live in the file. Try:
|
|
604
|
+
# Tests live in the file. Try: thunder test ${name}.intent
|
|
600
605
|
test Example
|
|
601
606
|
case adult
|
|
602
607
|
given age 20
|
|
@@ -607,13 +612,13 @@ test Example
|
|
|
607
612
|
`;
|
|
608
613
|
if (target.includes('/')) mkdirSync(dirname(target), { recursive: true });
|
|
609
614
|
writeFileSync(target, starter);
|
|
610
|
-
console.log(`
|
|
611
|
-
console.log(` next:
|
|
615
|
+
console.log(`thunder init: wrote ${target}`);
|
|
616
|
+
console.log(` next: thunder check ${target} | thunder run ${target} --inputs '{"age":20}' | thunder test ${target}`);
|
|
612
617
|
return;
|
|
613
618
|
}
|
|
614
619
|
|
|
615
620
|
if (!file && cmd !== 'draft') {
|
|
616
|
-
console.error(`
|
|
621
|
+
console.error(`thunder ${cmd}: missing a file argument. Run "intent help" for usage.`);
|
|
617
622
|
process.exit(2);
|
|
618
623
|
}
|
|
619
624
|
// IntentLift: lift source CODE into inferred .intent drafts (not intent parsing).
|
|
@@ -638,10 +643,10 @@ test Example
|
|
|
638
643
|
}
|
|
639
644
|
if (args.out) {
|
|
640
645
|
for (const m of res.missions) writeText(args.out, m.outName, m.intentText);
|
|
641
|
-
console.log(`
|
|
646
|
+
console.log(`thunder lift repo ${root} -> ${res.missionsGenerated} mission(s) in ${args.out}`);
|
|
642
647
|
console.log(` confidence: ${JSON.stringify(res.confidenceSummary)} | ${res.unknowns} unknown(s) total`);
|
|
643
648
|
} else {
|
|
644
|
-
console.log(`
|
|
649
|
+
console.log(`thunder lift repo ${root}: ${res.missionsGenerated} mission(s)`);
|
|
645
650
|
for (const m of res.missions) console.log(` ${m.mission} (${m.summary.confidence}) <- ${m.sourceFile}`);
|
|
646
651
|
}
|
|
647
652
|
return;
|
|
@@ -653,7 +658,7 @@ test Example
|
|
|
653
658
|
const res = liftAll(src, { language: args.from || languageForFile(file), file: basename(file) });
|
|
654
659
|
if (!res.ok) { console.error(res.error); process.exit(1); }
|
|
655
660
|
if (args.json) { console.log(JSON.stringify(res, null, 2)); return; }
|
|
656
|
-
console.log(`
|
|
661
|
+
console.log(`thunder lift --all ${basename(file)}: ${res.count} mission(s) inferred`);
|
|
657
662
|
for (const m of res.missions) console.log(` ${m.mission} (${m.fn}, confidence ${m.confidence})`);
|
|
658
663
|
return;
|
|
659
664
|
}
|
|
@@ -665,7 +670,7 @@ test Example
|
|
|
665
670
|
if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
|
|
666
671
|
if (args.out) {
|
|
667
672
|
const p = writeText(args.out, `${slug(res.lifted.mission)}.intent`, res.intentText);
|
|
668
|
-
console.log(`
|
|
673
|
+
console.log(`thunder lift ${basename(file)} -> ${p.replace(process.cwd() + '/', '')}`);
|
|
669
674
|
} else {
|
|
670
675
|
console.log(res.intentText);
|
|
671
676
|
}
|
|
@@ -680,7 +685,7 @@ test Example
|
|
|
680
685
|
// args.out defaults to '.intent'; approve writes back to the file unless a real --out was given.
|
|
681
686
|
const target = args.out && args.out !== '.intent' ? args.out : file;
|
|
682
687
|
writeFileSync(target, res.text);
|
|
683
|
-
console.log(`
|
|
688
|
+
console.log(`thunder approve ${basename(file)} -> reviewed: true (${res.approval.source_hash.slice(0, 24)}...)`);
|
|
684
689
|
return;
|
|
685
690
|
}
|
|
686
691
|
|
|
@@ -691,7 +696,7 @@ test Example
|
|
|
691
696
|
const out = JSON.stringify(pack, null, 2);
|
|
692
697
|
if (args.out && args.out !== '.intent') {
|
|
693
698
|
const p = writeText(args.out, `${slug(pack.mission)}.drift-handoff.json`, out);
|
|
694
|
-
console.log(`
|
|
699
|
+
console.log(`thunder handoff ${basename(file)} -> ${p.replace(process.cwd() + '/', '')} (kind ${pack.kind}, approved ${pack.approved})`);
|
|
695
700
|
} else {
|
|
696
701
|
console.log(out);
|
|
697
702
|
}
|
|
@@ -707,7 +712,7 @@ test Example
|
|
|
707
712
|
const res = checkDrift(intentText, codeText, { language });
|
|
708
713
|
if (args.json) { console.log(JSON.stringify(res, null, 2)); }
|
|
709
714
|
else {
|
|
710
|
-
console.log(`
|
|
715
|
+
console.log(`thunder drift: ${res.status.toUpperCase()} (${res.summary.blocking} blocking)`);
|
|
711
716
|
for (const f of res.findings) console.log(` [${f.level}] ${f.code}: ${f.message}`);
|
|
712
717
|
}
|
|
713
718
|
process.exit(res.status === 'drift' ? 1 : 0);
|
|
@@ -725,7 +730,7 @@ test Example
|
|
|
725
730
|
});
|
|
726
731
|
const manifest = buildManifest(parsed, { projectId: args.product });
|
|
727
732
|
if (args.json) { console.log(JSON.stringify(manifest, null, 2)); return; }
|
|
728
|
-
console.log(`
|
|
733
|
+
console.log(`thunder ai list ${root}: ${manifest.summary.total} AI implementation(s)`);
|
|
729
734
|
for (const im of manifest.implementations) {
|
|
730
735
|
console.log(` ${im.id.padEnd(28)} ${im.status.padEnd(9)} risk:${im.risk} approval:${im.approval} scope:${im.scope}`);
|
|
731
736
|
}
|
|
@@ -738,7 +743,7 @@ test Example
|
|
|
738
743
|
const file = args._[1];
|
|
739
744
|
if (!file) { console.error('usage: intent ai generate <file.intent> [--from <lang>]'); process.exit(2); return; }
|
|
740
745
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
741
|
-
if (!ast.implementation) { console.error(`
|
|
746
|
+
if (!ast.implementation) { console.error(`thunder ai generate: ${basename(file)} has no "implement with ai" block.`); process.exit(2); return; }
|
|
742
747
|
const prompt = buildImplementationPrompt(ast, { language: args.from || 'typescript' });
|
|
743
748
|
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
749
|
else console.log(prompt);
|
|
@@ -765,7 +770,7 @@ test Example
|
|
|
765
770
|
});
|
|
766
771
|
const gate = productionGate(resolved, { allowPending: args.allowPending });
|
|
767
772
|
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(`
|
|
773
|
+
console.log(`thunder ai gate ${root} (${args.mode || 'production'}): ${gate.ok ? 'PASS' : 'BLOCKED'} , ${resolved.length} implementation(s)`);
|
|
769
774
|
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
775
|
if (!gate.ok) console.log(` ${gate.blocking.length} implementation(s) block production. Use --allow-pending for dev builds.`);
|
|
771
776
|
process.exit(gate.ok ? 0 : 1);
|
|
@@ -777,9 +782,9 @@ test Example
|
|
|
777
782
|
if (!file || !id) { console.error('usage: intent ai adopt <targetFile> <id> [--from <lang>]'); process.exit(2); return; }
|
|
778
783
|
const code = readFileSync(file, 'utf8');
|
|
779
784
|
const res = adoptRegion(code, id, args.from || languageForFile(file));
|
|
780
|
-
if (!res) { console.error(`
|
|
785
|
+
if (!res) { console.error(`thunder ai adopt: no AI-managed region "${id}" in ${basename(file)}.`); process.exit(2); return; }
|
|
781
786
|
writeFileSync(file, res.code);
|
|
782
|
-
console.log(`
|
|
787
|
+
console.log(`thunder ai adopt ${id} -> human-owned (origin="ai" ownership="human") in ${basename(file)}`);
|
|
783
788
|
return;
|
|
784
789
|
}
|
|
785
790
|
if (sub === 'approve' || sub === 'reject') {
|
|
@@ -790,7 +795,7 @@ test Example
|
|
|
790
795
|
const manifest = buildManifest(parsed.map((p) => ({ path: relative(root, p.file), source: '', ast: p.ast })), {});
|
|
791
796
|
const im = manifest.implementations.find((x) => x.id === id);
|
|
792
797
|
const ast = parsed.find((p) => (p.ast.implementation?.id || slug(p.ast.mission || '')) === id)?.ast;
|
|
793
|
-
if (!im || !ast) { console.error(`
|
|
798
|
+
if (!im || !ast) { console.error(`thunder ai ${sub}: no implementation "${id}" found under ${root}.`); process.exit(2); return; }
|
|
794
799
|
let region = null;
|
|
795
800
|
if (im.targetLocation && existsSync(join(root, im.targetLocation))) region = parseMarkers(readFileSync(join(root, im.targetLocation), 'utf8')).regions.find((r) => r.id === id) || null;
|
|
796
801
|
const pf = join(root, im.proofLocation);
|
|
@@ -809,7 +814,7 @@ test Example
|
|
|
809
814
|
by: args.by, role: args.role, note: args.note,
|
|
810
815
|
contractHash: contractHash(ast), implementationHash: implementationHash(region.code), at,
|
|
811
816
|
});
|
|
812
|
-
if (error) { console.error(`
|
|
817
|
+
if (error) { console.error(`thunder ai ${sub}: ${error}`); process.exit(2); return; }
|
|
813
818
|
writeJson(join(root, '.intent'), 'ai-approvals.json', next);
|
|
814
819
|
const event = makeEvent(sub === 'approve' ? 'IntentAiImplementationApproved' : 'IntentAiImplementationRejected', {
|
|
815
820
|
implementationId: id, missionId: ast.mission, contractHash: contractHash(ast), implementationHash: implementationHash(region.code),
|
|
@@ -817,18 +822,18 @@ test Example
|
|
|
817
822
|
newStatus: sub === 'approve' ? 'APPROVED' : 'REJECTED',
|
|
818
823
|
});
|
|
819
824
|
const logPath = sinkEvent(root, event);
|
|
820
|
-
console.log(`
|
|
825
|
+
console.log(`thunder ai ${sub} ${id} by ${args.by || '(anonymous)'}${args.role ? ` [${args.role}]` : ''} -> ${sub === 'approve' ? 'APPROVED' : 'REJECTED'} (bound to current hashes)`);
|
|
821
826
|
console.log(` logged ${event.type} to ${logPath.replace(process.cwd() + '/', '')}`);
|
|
822
827
|
if (args.json) console.log(JSON.stringify(event, null, 2));
|
|
823
828
|
return;
|
|
824
829
|
}
|
|
825
|
-
// `
|
|
830
|
+
// `thunder ai events [dir] [--subject <implId>] [--json]` , read the append-only audit log.
|
|
826
831
|
if (sub === 'events') {
|
|
827
832
|
const root = args._[1] || '.';
|
|
828
833
|
const log = readEventLog(root);
|
|
829
834
|
const events = args.subject ? log.events.filter((e) => e.implementationId === args.subject) : log.events;
|
|
830
835
|
if (args.json) { console.log(JSON.stringify({ ...log, events }, null, 2)); return; }
|
|
831
|
-
console.log(`
|
|
836
|
+
console.log(`thunder ai events ${root}: ${events.length} event${events.length === 1 ? '' : 's'}${args.subject ? ` for ${args.subject}` : ''}`);
|
|
832
837
|
for (const e of events) {
|
|
833
838
|
const who = e.actorId ? ` by ${e.actorId}` : '';
|
|
834
839
|
const move = e.previousStatus || e.newStatus ? ` (${e.previousStatus || '?'} -> ${e.newStatus || '?'})` : '';
|
|
@@ -845,7 +850,7 @@ test Example
|
|
|
845
850
|
.find((a) => (a.implementation?.id || slug(a.mission || '')) === id);
|
|
846
851
|
const policy = parseSelection(ast?.selection || []);
|
|
847
852
|
const cdir = join(root, '.intent', 'candidates', id);
|
|
848
|
-
if (!existsSync(cdir)) { console.error(`
|
|
853
|
+
if (!existsSync(cdir)) { console.error(`thunder ai select: no candidates at ${relative(process.cwd(), cdir)}.`); process.exit(2); return; }
|
|
849
854
|
const candidates = readdirSync(cdir).filter((n) => !n.endsWith('.proof.json') && !n.endsWith('.json')).map((name) => {
|
|
850
855
|
const code = readFileSync(join(cdir, name), 'utf8');
|
|
851
856
|
const region = parseMarkers(code).regions.find((r) => r.id === id);
|
|
@@ -856,12 +861,12 @@ test Example
|
|
|
856
861
|
});
|
|
857
862
|
const result = selectCandidate(candidates, policy);
|
|
858
863
|
if (args.json) { console.log(JSON.stringify({ ...result, policy }, null, 2)); return; }
|
|
859
|
-
console.log(`
|
|
864
|
+
console.log(`thunder ai select ${id}: ${result.winner ? result.winner.id : '(none eligible)'} wins (${result.eligibleCount}/${candidates.length} eligible)`);
|
|
860
865
|
console.log(` policy: prefer ${result.prefer.map((p) => `${p.direction} ${p.metric}`).join(', ')}${policy.requireAllChecks ? '; require all checks' : ''}`);
|
|
861
866
|
for (const c of result.ranking) console.log(` ${c.id.padEnd(20)} ${JSON.stringify(c.metrics)}${c.checksPassed === false ? ' [checks FAILED]' : ''}`);
|
|
862
867
|
return;
|
|
863
868
|
}
|
|
864
|
-
console.error(`
|
|
869
|
+
console.error(`thunder ai ${sub || ''}: IL supports list | generate | gate | adopt | approve | reject | select. OpenThunder runs verification.`);
|
|
865
870
|
process.exit(2);
|
|
866
871
|
return;
|
|
867
872
|
}
|
|
@@ -869,14 +874,14 @@ test Example
|
|
|
869
874
|
// Semantic diff: compare two snapshots (dirs or .intent files) by meaning.
|
|
870
875
|
if (cmd === 'diff') {
|
|
871
876
|
const b = args._[0]; const a = args._[1];
|
|
872
|
-
if (!b || !a) { console.error('usage:
|
|
877
|
+
if (!b || !a) { console.error('usage: thunder diff <before-dir|file> <after-dir|file> [--json]'); process.exit(2); return; }
|
|
873
878
|
const snap = (p) => {
|
|
874
879
|
if (statSync(p).isDirectory()) return buildAtlas(collectIntents(p).map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8')))));
|
|
875
880
|
return buildIntentGraph(parseIntent(readFileSync(p, 'utf8')));
|
|
876
881
|
};
|
|
877
882
|
const diff = diffGraphs(snap(b), snap(a));
|
|
878
883
|
if (args.json) { console.log(JSON.stringify(diff, null, 2)); return; }
|
|
879
|
-
console.log(`
|
|
884
|
+
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
885
|
if (Object.keys(diff.summary.addedByType).length) console.log(` added: ${JSON.stringify(diff.summary.addedByType)}`);
|
|
881
886
|
if (Object.keys(diff.summary.removedByType).length) console.log(` removed: ${JSON.stringify(diff.summary.removedByType)}`);
|
|
882
887
|
for (const c of diff.changedNodes.slice(0, 8)) console.log(` ~ ${c.type} ${c.id}`);
|
|
@@ -893,7 +898,7 @@ test Example
|
|
|
893
898
|
: buildIntentGraph(parseIntent(readFileSync(p, 'utf8')));
|
|
894
899
|
const res = mergeGraphs(snap(bp), snap(op), snap(tp));
|
|
895
900
|
if (args.json) { console.log(JSON.stringify(res, null, 2)); process.exit(res.clean ? 0 : 1); return; }
|
|
896
|
-
console.log(`
|
|
901
|
+
console.log(`thunder merge: ${res.clean ? 'CLEAN' : 'CONFLICTS'} , ${res.summary.nodes} node(s), ${res.summary.conflicts} conflict(s)`);
|
|
897
902
|
for (const c of res.conflicts) console.log(` conflict: ${c.type} ${c.id} (changed differently on both sides)`);
|
|
898
903
|
process.exit(res.clean ? 0 : 1);
|
|
899
904
|
return;
|
|
@@ -903,10 +908,10 @@ test Example
|
|
|
903
908
|
if (cmd === 'run') {
|
|
904
909
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
905
910
|
let inputs = {};
|
|
906
|
-
if (args.inputs) { try { inputs = JSON.parse(args.inputs); } catch { console.error('
|
|
911
|
+
if (args.inputs) { try { inputs = JSON.parse(args.inputs); } catch { console.error('thunder run: --inputs must be JSON'); process.exit(2); return; } }
|
|
907
912
|
let decisions = ast.decisions || [];
|
|
908
913
|
if (args.decision) decisions = decisions.filter((d) => slug(d.name) === slug(args.decision));
|
|
909
|
-
if (!decisions.length) { console.error('
|
|
914
|
+
if (!decisions.length) { console.error('thunder run: no decision to run' + (args.decision ? ` matching "${args.decision}"` : '')); process.exit(2); return; }
|
|
910
915
|
const runs = decisions.map((d) => evaluateDecision(d, inputs));
|
|
911
916
|
if (args.json) { console.log(JSON.stringify(runs.length === 1 ? runs[0] : runs, null, 2)); return; }
|
|
912
917
|
for (const r of runs) {
|
|
@@ -922,8 +927,8 @@ test Example
|
|
|
922
927
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
923
928
|
const r = evaluateOutcomes(ast);
|
|
924
929
|
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(`
|
|
926
|
-
console.log(`
|
|
930
|
+
if (r.total === 0) { console.log(`thunder outcomes ${basename(file)}: no outcome contracts found.`); return; }
|
|
931
|
+
console.log(`thunder outcomes ${basename(file)}: ${r.met} met, ${r.missed} missed, ${r.pending} pending`);
|
|
927
932
|
for (const e of r.evaluations) {
|
|
928
933
|
const tag = e.status === 'met' ? 'MET ' : e.status === 'missed' ? 'MISSED' : 'PENDING';
|
|
929
934
|
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 +943,8 @@ test Example
|
|
|
938
943
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
939
944
|
const r = analyzeStyle(ast);
|
|
940
945
|
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(`
|
|
942
|
-
console.log(`
|
|
946
|
+
if (r.styleIntents.length === 0) { console.log(`thunder style ${basename(file)}: no style intents found.`); return; }
|
|
947
|
+
console.log(`thunder style ${basename(file)}: ${r.styleIntents.length} style intent(s)`);
|
|
943
948
|
for (const s of r.styleIntents) {
|
|
944
949
|
const a11y = s.accessibility ? `${s.accessibility.target} (${s.accessibility.classification}, verified=${s.accessibility.verified})` : 'no target';
|
|
945
950
|
console.log(` ${s.name} , a11y ${a11y}, ${s.tokens.length} token(s)${s.appliesTo ? `, applies_to ${s.appliesTo}` : ''}`);
|
|
@@ -955,8 +960,8 @@ test Example
|
|
|
955
960
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
956
961
|
const r = runTests(ast);
|
|
957
962
|
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
|
|
958
|
-
if (r.total === 0) { console.log(`
|
|
959
|
-
console.log(`
|
|
963
|
+
if (r.total === 0) { console.log(`thunder test ${basename(file)}: no test blocks found.`); return; }
|
|
964
|
+
console.log(`thunder test ${basename(file)}: ${r.passed}/${r.total} passed`);
|
|
960
965
|
for (const c of r.results) {
|
|
961
966
|
const detail = c.error ? ` ${c.error}`
|
|
962
967
|
: c.kind === 'lifecycle' ? `expected ${c.expected ?? '(any)'}, got ${c.actual} (valid=${c.valid})`
|
|
@@ -993,7 +998,7 @@ test Example
|
|
|
993
998
|
if (!graph || !Array.isArray(graph.nodes)) { console.error('intent validate: not an Intent Graph (no nodes[])'); process.exit(2); return; }
|
|
994
999
|
const v = validateGraph(graph);
|
|
995
1000
|
if (args.json) { console.log(JSON.stringify(v, null, 2)); process.exit(v.valid ? 0 : 1); return; }
|
|
996
|
-
console.log(`
|
|
1001
|
+
console.log(`thunder validate ${basename(file)}: ${v.valid ? 'VALID' : `${v.issues.length} issue(s)`} (${v.version})`);
|
|
997
1002
|
for (const i of v.issues) console.log(` [${i.code}] ${i.message}${i.id ? ` (${i.id})` : ''}`);
|
|
998
1003
|
process.exit(v.valid ? 0 : 1);
|
|
999
1004
|
return;
|
|
@@ -1002,14 +1007,14 @@ test Example
|
|
|
1002
1007
|
if (cmd === 'migrate') {
|
|
1003
1008
|
const raw = readFileSync(file, 'utf8');
|
|
1004
1009
|
let graph;
|
|
1005
|
-
try { graph = JSON.parse(raw); } catch { console.error('
|
|
1006
|
-
if (!graph || !Array.isArray(graph.nodes)) { console.error('
|
|
1010
|
+
try { graph = JSON.parse(raw); } catch { console.error('thunder migrate: input is not valid JSON'); process.exit(2); return; }
|
|
1011
|
+
if (!graph || !Array.isArray(graph.nodes)) { console.error('thunder migrate: not an Intent Graph (no nodes[])'); process.exit(2); return; }
|
|
1007
1012
|
let result;
|
|
1008
1013
|
try { result = migrateGraph(graph, args.to ? { to: args.to } : {}); }
|
|
1009
|
-
catch (e) { console.error(`
|
|
1014
|
+
catch (e) { console.error(`thunder migrate: ${e instanceof Error ? e.message : e}`); process.exit(2); return; }
|
|
1010
1015
|
const v = validateGraph(result.graph);
|
|
1011
1016
|
if (args.json) { console.log(JSON.stringify({ ...result, validation: v }, null, 2)); process.exit(v.valid ? 0 : 1); return; }
|
|
1012
|
-
console.log(`
|
|
1017
|
+
console.log(`thunder migrate: ${result.from} -> ${result.to} (${result.migrated ? result.applied.length + ' step(s)' : 'already current'})`);
|
|
1013
1018
|
for (const a of result.applied) console.log(` applied ${a.from} -> ${a.to}: ${a.description}`);
|
|
1014
1019
|
console.log(` validation: ${v.valid ? 'OK' : `${v.issues.length} issue(s)`}`);
|
|
1015
1020
|
for (const i of v.issues.slice(0, 10)) console.log(` [${i.code}] ${i.message}`);
|
|
@@ -1032,7 +1037,7 @@ test Example
|
|
|
1032
1037
|
const src = graphToSource(graph);
|
|
1033
1038
|
if (args.out && args.out !== '.intent') {
|
|
1034
1039
|
const base = (graph.nodes.find((n) => n.type === 'Mission')?.title) || basename(file).replace(/\.[^.]+$/, '');
|
|
1035
|
-
console.log(`
|
|
1040
|
+
console.log(`thunder source: wrote ${writeText(args.out, `${slug(base)}.intent`, src)}`);
|
|
1036
1041
|
} else {
|
|
1037
1042
|
process.stdout.write(src);
|
|
1038
1043
|
}
|
|
@@ -1044,22 +1049,22 @@ test Example
|
|
|
1044
1049
|
const xml = readFileSync(file, 'utf8');
|
|
1045
1050
|
const fmt = args.format || detectFormat(xml);
|
|
1046
1051
|
if (!fmt || !IMPORT_FORMATS.includes(fmt)) {
|
|
1047
|
-
console.error(`
|
|
1052
|
+
console.error(`thunder import: could not detect format; pass --format <${IMPORT_FORMATS.join('|')}>`);
|
|
1048
1053
|
process.exit(2); return;
|
|
1049
1054
|
}
|
|
1050
1055
|
const report = importReport(xml, fmt);
|
|
1051
|
-
if (report == null) { console.error(`
|
|
1056
|
+
if (report == null) { console.error(`thunder import: unsupported format "${fmt}"`); process.exit(2); return; }
|
|
1052
1057
|
if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
1053
1058
|
const src = report.source;
|
|
1054
1059
|
if (args.out && args.out !== '.intent') {
|
|
1055
1060
|
const base = basename(file).replace(/\.[^.]+$/, '');
|
|
1056
1061
|
const p = writeText(args.out, `${slug(base)}.intent`, src);
|
|
1057
|
-
console.log(`
|
|
1062
|
+
console.log(`thunder import: wrote ${p}`);
|
|
1058
1063
|
} else {
|
|
1059
1064
|
process.stdout.write(src.endsWith('\n') ? src : src + '\n');
|
|
1060
1065
|
}
|
|
1061
1066
|
// Fidelity warnings go to stderr, so stdout stays clean for piping the source.
|
|
1062
|
-
for (const w of report.warnings) console.error(`
|
|
1067
|
+
for (const w of report.warnings) console.error(`thunder import: [${w.code}] ${w.message}`);
|
|
1063
1068
|
return;
|
|
1064
1069
|
}
|
|
1065
1070
|
|
|
@@ -1073,9 +1078,9 @@ test Example
|
|
|
1073
1078
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
1074
1079
|
const res = exportIntent(ast, fmt);
|
|
1075
1080
|
if (args.out && args.out !== '.intent') {
|
|
1076
|
-
const name = `${slug(ast.mission || basename(file
|
|
1081
|
+
const name = `${slug(ast.mission || stripSourceExt(basename(file)))}.${res.ext}`;
|
|
1077
1082
|
const p = writeText(args.out, name, res.content);
|
|
1078
|
-
console.log(`
|
|
1083
|
+
console.log(`thunder export: wrote ${p}`);
|
|
1079
1084
|
} else {
|
|
1080
1085
|
process.stdout.write(res.content);
|
|
1081
1086
|
}
|
|
@@ -1090,13 +1095,13 @@ test Example
|
|
|
1090
1095
|
if (args.search) {
|
|
1091
1096
|
const hits = searchAtlas(atlas, args.search, { type: args.from });
|
|
1092
1097
|
if (args.json) { console.log(JSON.stringify(hits, null, 2)); return; }
|
|
1093
|
-
console.log(`
|
|
1098
|
+
console.log(`thunder atlas search "${args.search}": ${hits.length} hit(s)`);
|
|
1094
1099
|
for (const h of hits) console.log(` ${h.type.padEnd(18)} ${h.id}${h.title ? ` , ${h.title}` : ''}`);
|
|
1095
1100
|
return;
|
|
1096
1101
|
}
|
|
1097
1102
|
if (args.expand) {
|
|
1098
1103
|
const ex = expandNode(atlas, args.expand);
|
|
1099
|
-
if (!ex) { console.error(`
|
|
1104
|
+
if (!ex) { console.error(`thunder atlas: no node "${args.expand}".`); process.exit(2); return; }
|
|
1100
1105
|
if (args.json) { console.log(JSON.stringify(ex, null, 2)); return; }
|
|
1101
1106
|
console.log(`${ex.node.type} ${ex.node.id}${ex.node.title ? ` , ${ex.node.title}` : ''}`);
|
|
1102
1107
|
for (const e of ex.out) console.log(` -> ${e.rel.padEnd(16)} ${e.node.id}`);
|
|
@@ -1104,10 +1109,10 @@ test Example
|
|
|
1104
1109
|
return;
|
|
1105
1110
|
}
|
|
1106
1111
|
if (args.json) { console.log(JSON.stringify(atlas, null, 2)); return; }
|
|
1107
|
-
console.log(`
|
|
1112
|
+
console.log(`thunder atlas ${root}: ${atlas.overview.missions} mission(s), ${atlas.overview.nodes} node(s), ${atlas.overview.relationships} edge(s)`);
|
|
1108
1113
|
console.log(` ${JSON.stringify(atlas.overview.byType)}`);
|
|
1109
1114
|
for (const m of atlas.missions) console.log(` mission ${m.id}${m.title ? ` , ${m.title}` : ''}`);
|
|
1110
|
-
console.log(' expand a node:
|
|
1115
|
+
console.log(' expand a node: thunder atlas . --expand <id> | search: --search <query>');
|
|
1111
1116
|
return;
|
|
1112
1117
|
}
|
|
1113
1118
|
|
|
@@ -1118,13 +1123,13 @@ test Example
|
|
|
1118
1123
|
try {
|
|
1119
1124
|
intentFiles = collectIntents(root).map((f) => ({ path: relative(root, f), source: readFileSync(f, 'utf8') }));
|
|
1120
1125
|
} catch (e) {
|
|
1121
|
-
console.error(`
|
|
1126
|
+
console.error(`thunder index: cannot read "${root}": ${e instanceof Error ? e.message : e}`);
|
|
1122
1127
|
process.exit(2);
|
|
1123
1128
|
return;
|
|
1124
1129
|
}
|
|
1125
1130
|
const index = buildMissionIndex(intentFiles, { product: args.product });
|
|
1126
1131
|
if (args.json) { console.log(JSON.stringify(index, null, 2)); return; }
|
|
1127
|
-
console.log(`
|
|
1132
|
+
console.log(`thunder index ${root}: ${index.summary.missions} mission(s)`);
|
|
1128
1133
|
console.log(` ${JSON.stringify(index.summary.byArea)}`);
|
|
1129
1134
|
for (const m of index.missions) {
|
|
1130
1135
|
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 +1139,17 @@ test Example
|
|
|
1134
1139
|
return;
|
|
1135
1140
|
}
|
|
1136
1141
|
|
|
1137
|
-
// `
|
|
1142
|
+
// `thunder verify-diff <intent> --after <code> [--before <code>]` , the AI-loop gate: prove
|
|
1138
1143
|
// deterministically which of the intent's guarantees/never-rules a code change upholds or breaks.
|
|
1139
1144
|
if (cmd === 'verify-diff') {
|
|
1140
1145
|
const intentText = readFileSync(file, 'utf8');
|
|
1141
|
-
if (!args.after) { console.error('usage:
|
|
1146
|
+
if (!args.after) { console.error('usage: thunder verify-diff <intent> --after <codeFile> [--before <codeFile>] [--from <lang>]'); process.exit(2); return; }
|
|
1142
1147
|
const after = readFileSync(args.after, 'utf8');
|
|
1143
1148
|
const before = args.before ? readFileSync(args.before, 'utf8') : null;
|
|
1144
1149
|
const language = args.from || languageForFile(args.after);
|
|
1145
1150
|
const r = verifyDiff(intentText, { before, after, language });
|
|
1146
1151
|
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
|
|
1147
|
-
console.log(`
|
|
1152
|
+
console.log(`thunder verify-diff ${basename(file)} vs ${basename(args.after)}: ${r.verdict} (${r.blocking} blocking, ${r.summary.regressions} regression(s))`);
|
|
1148
1153
|
for (const f of r.findings) {
|
|
1149
1154
|
const tag = f.code === 'INTENT_VERIFY_NEVER_VIOLATED' ? 'VIOLATION' : f.regression ? 'REGRESSION' : f.level.toUpperCase();
|
|
1150
1155
|
console.log(` [${tag}] ${f.message}${f.line ? ` (line ${f.line})` : ''}`);
|
|
@@ -1154,7 +1159,7 @@ test Example
|
|
|
1154
1159
|
return;
|
|
1155
1160
|
}
|
|
1156
1161
|
|
|
1157
|
-
// `
|
|
1162
|
+
// `thunder draft --brief <json|->` , scaffold a rigorous intent draft from a structured brief,
|
|
1158
1163
|
// plus a review checklist of what a human must still fill in. Prints the draft; --write saves it.
|
|
1159
1164
|
if (cmd === 'draft') {
|
|
1160
1165
|
const briefPath = args.brief || (cmd === 'draft' && file && file.endsWith('.json') ? file : null);
|
|
@@ -1164,19 +1169,19 @@ test Example
|
|
|
1164
1169
|
try { brief = JSON.parse(raw); } catch { console.error('intent draft: --brief is not valid JSON'); process.exit(2); return; }
|
|
1165
1170
|
const r = draftIntent(brief);
|
|
1166
1171
|
if (args.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
1167
|
-
if (args.write) { writeFileSync(args.write, r.source); console.error(`
|
|
1172
|
+
if (args.write) { writeFileSync(args.write, r.source); console.error(`thunder draft: wrote ${args.write}`); }
|
|
1168
1173
|
else process.stdout.write(r.source);
|
|
1169
1174
|
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
1175
|
return;
|
|
1171
1176
|
}
|
|
1172
1177
|
|
|
1173
|
-
// `
|
|
1178
|
+
// `thunder guard <file>` , preview what a runtime guard compiled from this intent enforces:
|
|
1174
1179
|
// which fields it redacts (secrets/PII) and which decisions it can gate at runtime.
|
|
1175
1180
|
if (cmd === 'guard') {
|
|
1176
1181
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
1177
1182
|
const g = guardSummary(ast);
|
|
1178
1183
|
if (args.json) { console.log(JSON.stringify(g, null, 2)); return; }
|
|
1179
|
-
console.log(`
|
|
1184
|
+
console.log(`thunder guard ${basename(file)}:`);
|
|
1180
1185
|
console.log(` redacts fields ${g.redactsFields.length ? g.redactsFields.join(', ') : '(none declared secret/PII)'}`);
|
|
1181
1186
|
console.log(` enforces decisions ${g.enforcesDecisions.length ? g.enforcesDecisions.join(', ') : '(none)'}`);
|
|
1182
1187
|
if (g.neverRules.length) { console.log(' never rules:'); for (const n of g.neverRules) console.log(` - ${n}`); }
|
|
@@ -1184,7 +1189,7 @@ test Example
|
|
|
1184
1189
|
return;
|
|
1185
1190
|
}
|
|
1186
1191
|
|
|
1187
|
-
// `
|
|
1192
|
+
// `thunder ledger <file.json>` , verify the tamper-evident chain, and explain a subject's history
|
|
1188
1193
|
// (why it was built, who approved it, what was assumed/corrected/verified, which risks accepted).
|
|
1189
1194
|
if (cmd === 'ledger') {
|
|
1190
1195
|
if (!file) { console.error('usage: intent ledger <ledger.json> [--subject <id>] [--json]'); process.exit(2); return; }
|
|
@@ -1194,7 +1199,7 @@ test Example
|
|
|
1194
1199
|
if (args.subject) {
|
|
1195
1200
|
const ex = ledgerExplain(ledger, args.subject);
|
|
1196
1201
|
if (args.json) { console.log(JSON.stringify({ chain, ...ex }, null, 2)); process.exit(chain.valid ? 0 : 1); return; }
|
|
1197
|
-
console.log(`
|
|
1202
|
+
console.log(`thunder ledger ${basename(file)} , ${args.subject} (chain ${chain.valid ? 'VALID' : 'BROKEN'})`);
|
|
1198
1203
|
if (ex.why.length) { console.log(' why built:'); for (const w of ex.why) console.log(` - ${w}`); }
|
|
1199
1204
|
if (ex.approvedBy.length) console.log(` approved by: ${ex.approvedBy.join(', ')}`);
|
|
1200
1205
|
if (ex.assumptions.length) console.log(` assumptions: ${ex.assumptions.length}`);
|
|
@@ -1207,12 +1212,12 @@ test Example
|
|
|
1207
1212
|
}
|
|
1208
1213
|
if (args.json) { console.log(JSON.stringify(chain, null, 2)); process.exit(chain.valid ? 0 : 1); return; }
|
|
1209
1214
|
const n = (ledger.entries || []).length;
|
|
1210
|
-
console.log(`
|
|
1215
|
+
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
1216
|
process.exit(chain.valid ? 0 : 1);
|
|
1212
1217
|
return;
|
|
1213
1218
|
}
|
|
1214
1219
|
|
|
1215
|
-
// `
|
|
1220
|
+
// `thunder impact <base> <proposed>` , the Simulator: estimate a change's impact BEFORE building it
|
|
1216
1221
|
// , the deterministic blast radius, the risk it would introduce, contradictions, release risk.
|
|
1217
1222
|
if (cmd === 'impact') {
|
|
1218
1223
|
const baseArg = args._[0]; const propArg = args._[1];
|
|
@@ -1220,7 +1225,7 @@ test Example
|
|
|
1220
1225
|
const collect = (p) => (existsSync(p) && statSync(p).isDirectory() ? collectIntents(p) : [p]).map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') }));
|
|
1221
1226
|
const r = simulateChange(collect(baseArg), collect(propArg));
|
|
1222
1227
|
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.summary.safe ? 0 : 1); return; }
|
|
1223
|
-
console.log(`
|
|
1228
|
+
console.log(`thunder impact: ${r.summary.safe ? 'SAFE' : 'REVIEW'} (${baseArg} -> ${propArg})`);
|
|
1224
1229
|
console.log(` change touches ${r.changedNodes} node(s); ripples to ${r.deterministicImpact.total} dependent(s)`);
|
|
1225
1230
|
const bt = Object.entries(r.deterministicImpact.byType);
|
|
1226
1231
|
if (bt.length) console.log(' deterministic impact by type: ' + bt.map(([t, ns]) => `${ns.length} ${t}`).join(', '));
|
|
@@ -1232,7 +1237,7 @@ test Example
|
|
|
1232
1237
|
return;
|
|
1233
1238
|
}
|
|
1234
1239
|
|
|
1235
|
-
// `
|
|
1240
|
+
// `thunder guardian <before> <after>` , drift detection: what a change did to the intent , which
|
|
1236
1241
|
// intent it affects, what risk it introduced, what must be reverified, what learning is stale.
|
|
1237
1242
|
if (cmd === 'guardian') {
|
|
1238
1243
|
const beforeArg = args._[0]; const afterArg = args._[1];
|
|
@@ -1241,7 +1246,7 @@ test Example
|
|
|
1241
1246
|
const r = guardianReport(collect(beforeArg), collect(afterArg));
|
|
1242
1247
|
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.verdict === 'needs-attention' ? 1 : 0); return; }
|
|
1243
1248
|
const c = r.changed;
|
|
1244
|
-
console.log(`
|
|
1249
|
+
console.log(`thunder guardian: ${r.verdict.toUpperCase()} (${beforeArg} -> ${afterArg})`);
|
|
1245
1250
|
console.log(` changed +${c.nodesAdded} / -${c.nodesRemoved} / ~${c.nodesChanged} nodes, +${c.relationshipsAdded} / -${c.relationshipsRemoved} relationships`);
|
|
1246
1251
|
if (r.affectedIntent.length) console.log(` affected ${r.affectedIntent.map((n) => n.title || n.id).join(', ')}`);
|
|
1247
1252
|
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 +1257,31 @@ test Example
|
|
|
1252
1257
|
return;
|
|
1253
1258
|
}
|
|
1254
1259
|
|
|
1255
|
-
// `
|
|
1260
|
+
// `thunder scan [dir]` , the Scanner spine: intent -> Intent IR -> explainable Fable findings ->
|
|
1256
1261
|
// risk themes. Deterministic, no AI. --json for the machine report; --ir writes the merged IR.
|
|
1257
1262
|
// Part 3 focused scan queries , one question each over the Intent IR + Fable findings:
|
|
1258
|
-
// `
|
|
1263
|
+
// `thunder risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json]`.
|
|
1259
1264
|
if (VIEWS[cmd]) {
|
|
1260
1265
|
const root = file || '.';
|
|
1261
1266
|
const targets = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : [root];
|
|
1262
|
-
if (!targets.length || !existsSync(targets[0])) { console.error(`
|
|
1267
|
+
if (!targets.length || !existsSync(targets[0])) { console.error(`thunder ${cmd}: no .intent files under ${root}`); process.exit(2); return; }
|
|
1263
1268
|
const scan = scanProject(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
|
|
1264
1269
|
const v = VIEWS[cmd](scan);
|
|
1265
1270
|
if (args.json) { console.log(JSON.stringify(v, null, 2)); return; }
|
|
1266
1271
|
if (cmd === 'coverage') {
|
|
1267
|
-
console.log(`
|
|
1272
|
+
console.log(`thunder coverage ${root}: ${v.verified}/${v.total} claims verified (${v.coverage}%)`);
|
|
1268
1273
|
for (const c of v.unverified) console.log(` unverified [${c.type}] ${c.title}`);
|
|
1269
1274
|
process.exit(v.coverage === 100 ? 0 : 1); return;
|
|
1270
1275
|
}
|
|
1271
1276
|
if (cmd === 'risks') {
|
|
1272
1277
|
const s = v.bySeverity;
|
|
1273
|
-
console.log(`
|
|
1278
|
+
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
1279
|
for (const t of v.themes) console.log(` ${String(t.count).padStart(3)} ${t.category}${t.blocker ? ` (${t.blocker} blocker)` : ''}`);
|
|
1275
1280
|
for (const r of v.remediationSequence.slice(0, 5)) console.log(` fix [${r.severity}] ${r.ruleId} (${r.count}x) , ${r.remediation}`);
|
|
1276
1281
|
process.exit((s.blocker || 0) + (s.error || 0) === 0 ? 0 : 1); return;
|
|
1277
1282
|
}
|
|
1278
1283
|
// gaps / unverified / unknowns / contradictions , a uniform list
|
|
1279
|
-
console.log(`
|
|
1284
|
+
console.log(`thunder ${cmd} ${root}: ${v.count}`);
|
|
1280
1285
|
for (const g of v.gaps || []) console.log(` [${g.severity}] ${g.ruleId} , ${g.detected}`);
|
|
1281
1286
|
for (const c of v.claims || []) console.log(` [${c.type}] ${c.title}`);
|
|
1282
1287
|
for (const u of v.unknowns || []) console.log(` [${u.type}] ${u.title}${u.confidence ? ` (${u.confidence})` : ''}`);
|
|
@@ -1291,10 +1296,10 @@ test Example
|
|
|
1291
1296
|
const root = file || '.';
|
|
1292
1297
|
const targets = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : [root];
|
|
1293
1298
|
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(`
|
|
1299
|
+
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
1300
|
if (args.json) { console.log(JSON.stringify(result, null, 2)); process.exit(result.ok ? 0 : 1); return; }
|
|
1296
1301
|
const s = result.bySeverity;
|
|
1297
|
-
console.log(`
|
|
1302
|
+
console.log(`thunder scan ${root}: ${result.totals.findings} finding(s) across ${result.totals.missions} mission(s) in ${result.totals.files} file(s)`);
|
|
1298
1303
|
console.log(` severity ${s.blocker} blocker, ${s.error} error, ${s.warning} warning, ${s.info} info , Intent IR: ${result.ir.nodes.length} nodes`);
|
|
1299
1304
|
if (result.risks.length) {
|
|
1300
1305
|
console.log(' risk themes:');
|
|
@@ -1308,7 +1313,7 @@ test Example
|
|
|
1308
1313
|
return;
|
|
1309
1314
|
}
|
|
1310
1315
|
|
|
1311
|
-
// `
|
|
1316
|
+
// `thunder report [dir]` , a repo-wide intent health summary (aggregates every .intent file).
|
|
1312
1317
|
// Distinct from `check` (pass/fail gate): counts by severity + area, top codes, and coverage.
|
|
1313
1318
|
if (cmd === 'report') {
|
|
1314
1319
|
const root = file || '.';
|
|
@@ -1316,7 +1321,7 @@ test Example
|
|
|
1316
1321
|
const rep = buildReport(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
|
|
1317
1322
|
if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(0); return; }
|
|
1318
1323
|
const c = rep.coverage;
|
|
1319
|
-
console.log(`
|
|
1324
|
+
console.log(`thunder report ${root}: ${rep.totals.missions} mission(s) in ${rep.totals.files} file(s), ${rep.totals.diagnostics} diagnostic(s)`);
|
|
1320
1325
|
console.log(` severity ${rep.bySeverity.blocker} blocker, ${rep.bySeverity.error} error, ${rep.bySeverity.warning} warning, ${rep.bySeverity.info} info`);
|
|
1321
1326
|
console.log(' coverage '
|
|
1322
1327
|
+ `guarantees verified ${c.guaranteesVerified}/${c.guarantees}${c.guaranteeVerifyRate != null ? ` (${c.guaranteeVerifyRate}%)` : ''}, `
|
|
@@ -1334,9 +1339,9 @@ test Example
|
|
|
1334
1339
|
return;
|
|
1335
1340
|
}
|
|
1336
1341
|
|
|
1337
|
-
// `
|
|
1342
|
+
// `thunder check <file|dir> --format sarif` emits a SARIF 2.1.0 log so ThunderLang
|
|
1338
1343
|
// 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 `
|
|
1344
|
+
// This is a REPORT (exit 0); gate the build with a plain `thunder check .` step.
|
|
1340
1345
|
if (cmd === 'check' && args.format === 'sarif') {
|
|
1341
1346
|
const targets = existsSync(file) && statSync(file).isDirectory() ? collectIntents(file) : [file];
|
|
1342
1347
|
const reports = targets.map((f) => {
|
|
@@ -1352,7 +1357,7 @@ test Example
|
|
|
1352
1357
|
return;
|
|
1353
1358
|
}
|
|
1354
1359
|
|
|
1355
|
-
// `
|
|
1360
|
+
// `thunder check <dir>` recurses and gates every .intent file (self-contained CI, no
|
|
1356
1361
|
// wrapper script needed). Errors fail the run; warnings do not.
|
|
1357
1362
|
if (cmd === 'check' && existsSync(file) && statSync(file).isDirectory()) {
|
|
1358
1363
|
const files = collectIntents(file);
|
|
@@ -1371,12 +1376,12 @@ test Example
|
|
|
1371
1376
|
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
1377
|
process.exit(failed.length ? 1 : 0);
|
|
1373
1378
|
}
|
|
1374
|
-
console.log(`
|
|
1379
|
+
console.log(`thunder check ${file}: ${reports.length - failed.length}/${reports.length} passed`);
|
|
1375
1380
|
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
1381
|
process.exit(failed.length ? 1 : 0);
|
|
1377
1382
|
}
|
|
1378
1383
|
|
|
1379
|
-
// `
|
|
1384
|
+
// `thunder edit <file>` , apply structural field edits (intent-patch-v1) to the source,
|
|
1380
1385
|
// preserving comments + formatting. Edits come from --edits <json|-> and/or convenience
|
|
1381
1386
|
// flags. Prints the result to stdout, or --write applies it in place.
|
|
1382
1387
|
if (cmd === 'edit') {
|
|
@@ -1401,7 +1406,7 @@ test Example
|
|
|
1401
1406
|
for (const s of result.skipped) console.error(` skipped: ${s.reason}`);
|
|
1402
1407
|
if (args.write) {
|
|
1403
1408
|
if (result.source !== src.replace(/\r\n?/g, '\n')) writeFileSync(file, result.source);
|
|
1404
|
-
console.error(`
|
|
1409
|
+
console.error(`thunder edit ${basename(file)}: applied ${result.applied.length}, skipped ${result.skipped.length}.`);
|
|
1405
1410
|
} else if (!args.json) {
|
|
1406
1411
|
process.stdout.write(result.source);
|
|
1407
1412
|
}
|
|
@@ -1409,7 +1414,7 @@ test Example
|
|
|
1409
1414
|
return;
|
|
1410
1415
|
}
|
|
1411
1416
|
|
|
1412
|
-
// `
|
|
1417
|
+
// `thunder fmt <file|dir>` , canonical formatting (whitespace only; content + comments
|
|
1413
1418
|
// preserved). Prints to stdout, or --write in place, or --check for CI (exit 1 if any
|
|
1414
1419
|
// file is not already formatted).
|
|
1415
1420
|
if (cmd === 'fmt') {
|
|
@@ -1425,18 +1430,18 @@ test Example
|
|
|
1425
1430
|
process.stdout.write(formatted);
|
|
1426
1431
|
}
|
|
1427
1432
|
if (args.check) {
|
|
1428
|
-
if (unformatted.length) { console.error(`
|
|
1429
|
-
console.log('
|
|
1433
|
+
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); }
|
|
1434
|
+
console.log('thunder fmt --check: all formatted.');
|
|
1430
1435
|
process.exit(0);
|
|
1431
1436
|
}
|
|
1432
|
-
if (args.write) console.log(`
|
|
1437
|
+
if (args.write) console.log(`thunder fmt: formatted ${changed} file(s), ${targets.length - changed} already clean.`);
|
|
1433
1438
|
return;
|
|
1434
1439
|
}
|
|
1435
1440
|
|
|
1436
1441
|
const { source, ast, sourceHash, sourceFile } = load(file);
|
|
1437
1442
|
const generatedAt = new Date().toISOString();
|
|
1438
1443
|
const diagnostics = semanticDiagnostics(ast);
|
|
1439
|
-
const outDir = join(args.out, slug(ast.mission || basename(file
|
|
1444
|
+
const outDir = join(args.out, slug(ast.mission || stripSourceExt(basename(file))));
|
|
1440
1445
|
|
|
1441
1446
|
// Production gate: a build --mode production refuses to proceed while an AI
|
|
1442
1447
|
// implementation is not shippable. --allow-pending is for dev builds only.
|
|
@@ -1497,7 +1502,7 @@ test Example
|
|
|
1497
1502
|
console.log(JSON.stringify(out, null, 2));
|
|
1498
1503
|
process.exit(errors > 0 ? 1 : 0);
|
|
1499
1504
|
}
|
|
1500
|
-
console.log(`
|
|
1505
|
+
console.log(`thunder check ${sourceFile} (mission: ${ast.mission})`);
|
|
1501
1506
|
process.exit(printDiagnostics(diags) > 0 ? 1 : 0);
|
|
1502
1507
|
}
|
|
1503
1508
|
|
|
@@ -1526,7 +1531,7 @@ test Example
|
|
|
1526
1531
|
console.error(`unknown command: ${cmd}`);
|
|
1527
1532
|
process.exit(2);
|
|
1528
1533
|
}
|
|
1529
|
-
console.log(`
|
|
1534
|
+
console.log(`thunder ${cmd} ${sourceFile} -> ${outDir}`);
|
|
1530
1535
|
for (const p of generated) console.log(` wrote ${p.replace(process.cwd() + '/', '')}`);
|
|
1531
1536
|
printDiagnostics(diagnostics);
|
|
1532
1537
|
}
|