@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/CHANGELOG.md +87 -47
- package/LICENSE +201 -21
- package/NOTICE +15 -0
- package/README.md +5 -8
- package/dist/core.cjs +123 -26
- package/dist/index.cjs +128 -31
- package/intent-graph.schema.json +227 -4
- package/package.json +12 -12
- package/src/ai-events.mjs +1 -1
- package/src/cli.mjs +759 -151
- package/src/codegen.mjs +1 -1
- package/src/compile.mjs +3 -3
- package/src/conformance.mjs +35 -0
- package/src/coverage.mjs +56 -0
- package/src/drift.mjs +3 -3
- package/src/emit.mjs +5 -3
- package/src/expr.mjs +11 -7
- package/src/focus.mjs +1 -1
- package/src/guard.mjs +3 -3
- package/src/intellisense.mjs +1 -1
- package/src/intent-graph.mjs +29 -0
- 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/mutate.mjs +46 -0
- package/src/parse.mjs +85 -6
- package/src/proof-schema.mjs +2 -2
- package/src/property.mjs +108 -0
- 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/target-cs.mjs +98 -0
- package/src/target-java.mjs +87 -0
- package/src/target-py.mjs +90 -0
- package/src/target-ts.mjs +66 -0
- package/src/target-util.mjs +95 -0
- package/src/testing.mjs +1 -1
- package/src/twelve-factor.mjs +2 -2
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// intent proof <file> [--out .intent] .
|
|
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
|
|
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
|
|
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
|
|
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 = `
|
|
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
|
-
|
|
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.
|
|
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 .
|
|
225
|
-
graph <file>
|
|
226
|
-
proof <file> the .
|
|
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]
|
|
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 .
|
|
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
|
-
// `
|
|
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 .
|
|
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:
|
|
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('
|
|
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(`
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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 (
|
|
319
|
-
|
|
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. `
|
|
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:
|
|
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(`
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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(`
|
|
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
|
-
// `
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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('
|
|
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(`
|
|
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(`
|
|
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 `
|
|
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). `
|
|
569
|
-
if (cmd === 'init') {
|
|
570
|
-
const name = (file || 'Mission')
|
|
571
|
-
const target = join(args.out && args.out !== '.intent' ? args.out : '.', `${name}.
|
|
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(`
|
|
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:
|
|
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:
|
|
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(`
|
|
611
|
-
console.log(` next:
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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:
|
|
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(`
|
|
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(`
|
|
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('
|
|
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('
|
|
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(`
|
|
926
|
-
console.log(`
|
|
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(`
|
|
942
|
-
console.log(`
|
|
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(`
|
|
959
|
-
console.log(`
|
|
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(`
|
|
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('
|
|
1006
|
-
if (!graph || !Array.isArray(graph.nodes)) { console.error('
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
|
1677
|
+
const name = `${slug(ast.mission || stripSourceExt(basename(file)))}.${res.ext}`;
|
|
1077
1678
|
const p = writeText(args.out, name, res.content);
|
|
1078
|
-
console.log(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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:
|
|
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(`
|
|
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(`
|
|
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
|
-
// `
|
|
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:
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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
|
-
// `
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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 `
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
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
|
-
// `
|
|
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(`
|
|
1429
|
-
console.log('
|
|
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(`
|
|
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
|
|
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(`
|
|
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, '.
|
|
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(`
|
|
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
|
}
|