@skillstech/thunderlang 0.3.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +25 -25
- package/dist/core.cjs +89 -5
- package/dist/index.cjs +388 -8
- package/package.json +3 -3
- package/src/cli.mjs +116 -67
- package/src/codegen.mjs +73 -4
- package/src/core.d.ts +1 -1
- package/src/core.mjs +1 -1
- package/src/emit.mjs +1 -1
- package/src/evidence.mjs +116 -0
- package/src/exporters.mjs +14 -5
- package/src/index.d.ts +1 -0
- package/src/index.mjs +2 -1
- package/src/lift.mjs +1 -1
- package/src/mcp.mjs +125 -1
package/src/cli.mjs
CHANGED
|
@@ -84,6 +84,7 @@ function runLiveTarget(ast, target) {
|
|
|
84
84
|
}
|
|
85
85
|
import { importIntent, importReport, detectFormat, IMPORT_FORMATS } from './importers.mjs';
|
|
86
86
|
import { runTests } from './testing.mjs';
|
|
87
|
+
import { toEvidenceEvents, verifyDiffToEvidence, conformToEvidence, driftToEvidence } from './evidence.mjs';
|
|
87
88
|
import { evaluateOutcomes } from './outcome.mjs';
|
|
88
89
|
import { analyzeStyle } from './style.mjs';
|
|
89
90
|
import { intentProofJsonSchema, validateProof } from './proof-schema.mjs';
|
|
@@ -261,6 +262,7 @@ function parseArgs(argv) {
|
|
|
261
262
|
else if (a === '--governed') args.governed = true;
|
|
262
263
|
else if (a === '--target') args.target = argv[++i];
|
|
263
264
|
else if (a === '--all-targets') args.allTargets = true;
|
|
265
|
+
else if (a === '--evidence') args.evidence = true;
|
|
264
266
|
else if (a === '--env') args.env = argv[++i];
|
|
265
267
|
else if (a === '--brief') args.brief = argv[++i];
|
|
266
268
|
else if (a === '--after') args.after = argv[++i];
|
|
@@ -327,81 +329,85 @@ function printDiagnostics(diags) {
|
|
|
327
329
|
|
|
328
330
|
const HELP = `thunder , the ThunderLang compiler + engine (deterministic, no AI required)
|
|
329
331
|
|
|
332
|
+
The flow: author intent, inspect it, run and prove it, then keep real code aligned with it.
|
|
333
|
+
|
|
330
334
|
usage: thunder <command> <file> [options]
|
|
331
335
|
thunder mission <Name> <command> run any command on a mission by name
|
|
332
336
|
|
|
333
|
-
|
|
334
|
-
new [Name]
|
|
335
|
-
mission <Name> [cmd]
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
Author
|
|
338
|
+
new [Name] scaffold a runnable starter mission (Name.thunder) [alias: init]
|
|
339
|
+
mission <Name> [cmd] resolve a mission by name and run a command on it (list | <Name> | <Name> <cmd>)
|
|
340
|
+
draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
|
|
341
|
+
edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
|
|
342
|
+
fmt <file|dir> [--write|--check] canonical formatting (whitespace only; comments kept)
|
|
343
|
+
source <file|graph.json> regenerate .thunder from a persisted graph
|
|
344
|
+
|
|
345
|
+
Inspect
|
|
346
|
+
check <file|dir> [--json|--format sarif] parse + lint + explainable diagnostics; gate a whole dir
|
|
347
|
+
report [dir] [--json] repo-wide intent health: severity + area counts, coverage
|
|
348
|
+
risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json] focused scan queries
|
|
349
|
+
focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json] Intent Lens: focused graph + brief
|
|
350
|
+
comprehension <file|dir> [--observed] [--learning] [--governed] [--json] the C0..C7 understanding level
|
|
351
|
+
twelve-factor <file> [--json] score against the 13 humanlayer/12-factor-agents principles [alias: 12factor]
|
|
352
|
+
explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
|
|
353
|
+
rules [--json] list the whole canonical diagnostic catalog
|
|
354
|
+
notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
|
|
355
|
+
docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
|
|
356
|
+
index <dir> the Mission Atlas inventory
|
|
357
|
+
atlas <dir> [--search q | --expand id] the whole-system Atlas
|
|
358
|
+
|
|
359
|
+
Run & test
|
|
360
|
+
run <file> --inputs '<json>' execute the decision(s) deterministically
|
|
338
361
|
test <file> [--contracts | --properties | --scenarios | --mutate | --evals | --changed | --coverage] [--strict]
|
|
339
362
|
test <file> --target typescript|python|csharp|java run the tests against the EXECUTED generated code
|
|
340
|
-
test <file> --all-targets
|
|
341
|
-
|
|
363
|
+
test <file> --all-targets run the tests against every AVAILABLE target in one pass
|
|
364
|
+
simulate <file> --events a,b,c walk the lifecycle(s) over events
|
|
365
|
+
outcomes <file> evaluate outcome contracts vs delivery results
|
|
366
|
+
style <file> resolve style intents vs the canonical token space
|
|
367
|
+
gen <file> [--target typescript|csharp|java|python] [--out <dir>] deterministic code scaffold (types + decision logic + TODOs)
|
|
368
|
+
build <file> generate docs, contract graph, test plan, and .thunder-proof.json
|
|
369
|
+
|
|
370
|
+
Prove & verify intent
|
|
371
|
+
prove <file> emit an intent-proof-v1 artifact (honest verdicts + freshness)
|
|
372
|
+
proof <file> the .thunder-proof.json artifact (see also: prove, which emits verdicts)
|
|
373
|
+
proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
|
|
342
374
|
verify <proof.json> [src] re-check a proof; reports STALE when impl/deps/compiler moved
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
|
|
367
|
-
proof <file> the .thunder-proof.json artifact
|
|
368
|
-
proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
|
|
369
|
-
verify <proof.json> [src] confirm a proof is well-formed and still matches its source
|
|
370
|
-
conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] run the same cases against every target (conformance matrix)
|
|
371
|
-
schema emit the canonical graph schema + diagnostic catalog
|
|
372
|
-
explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
|
|
373
|
-
rules [--json] list the whole canonical diagnostic catalog
|
|
374
|
-
notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
|
|
375
|
-
docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
|
|
376
|
-
code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
|
|
377
|
-
apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
|
|
378
|
-
|
|
379
|
-
Execute (no AI, no generated code)
|
|
380
|
-
run <file> --inputs '<json>' evaluate the decision(s) against inputs
|
|
381
|
-
simulate <file> --events a,b,c walk the lifecycle(s) over events
|
|
382
|
-
test <file> run the in-file test blocks (case/scenario)
|
|
383
|
-
outcomes <file> evaluate outcome contracts vs delivery results
|
|
384
|
-
style <file> resolve style intents vs the canonical token space
|
|
375
|
+
evidence <file> emit evidence-event-v1 JSON (tool_verified) for the shared proof spine
|
|
376
|
+
(also: prove/verify-diff/conform/drift accept --evidence to emit their own event)
|
|
377
|
+
conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] conformance matrix across targets
|
|
378
|
+
|
|
379
|
+
Real code vs intent (drift)
|
|
380
|
+
lift <file> [--from <lang>] lift source code into inferred intent
|
|
381
|
+
drift <codeFile> --intent <file.intent> [--from <lang>] check intent vs code drift
|
|
382
|
+
approve <file> --by <name> approve intent (drift baseline)
|
|
383
|
+
verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
|
|
384
|
+
changes [<base>..<head> | <base>] [--json] Change Lens: what a branch/PR changed by meaning
|
|
385
|
+
guardian <before> <after> drift: what changed, what risk, what to reverify, what learning is stale
|
|
386
|
+
impact <base> <proposed> Simulator: estimate a change's blast radius + risk BEFORE building it
|
|
387
|
+
diff <before> <after> semantic diff (by meaning)
|
|
388
|
+
merge <base> <ours> <theirs> deterministic 3-way semantic merge
|
|
389
|
+
handoff <file> the OpenThunder drift handoff
|
|
390
|
+
ledger <file.json> [--subject <id>] verify the tamper-evident history + explain why/who/what changed
|
|
391
|
+
guard <file> [--json] preview the runtime guard (redacted fields, enforced decisions)
|
|
392
|
+
|
|
393
|
+
Graph & IR
|
|
394
|
+
graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
|
|
395
|
+
migrate <graph.json> [--to <version>] upgrade a persisted graph
|
|
396
|
+
validate <graph.json> [--json] check a graph is canonical (anti-fork)
|
|
397
|
+
schema emit the canonical graph schema + diagnostic catalog
|
|
385
398
|
|
|
386
399
|
Interop
|
|
387
400
|
export <file> --format <dmn|bpmn|smv|jsonschema|openapi|tokens|mermaid|css|playwright> render to a standard format
|
|
388
|
-
import <file> [--format dmn|bpmn] [--json]
|
|
389
|
-
source <file|graph.json> regenerate .thunder from a graph
|
|
390
|
-
migrate <graph.json> [--to <version>] upgrade a persisted graph
|
|
391
|
-
validate <graph.json> [--json] check a graph is canonical (anti-fork)
|
|
401
|
+
import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
|
|
392
402
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
merge <base> <ours> <theirs> deterministic 3-way semantic merge
|
|
403
|
+
Fix
|
|
404
|
+
scan [dir] [--json] [--ir <path>] Scanner: intent -> Intent IR -> Fable findings -> risk themes
|
|
405
|
+
code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
|
|
406
|
+
apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
|
|
398
407
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
drift <file> --from <code> check intent vs code drift
|
|
403
|
-
verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
|
|
404
|
-
handoff <file> the OpenThunder drift handoff
|
|
408
|
+
Servers
|
|
409
|
+
lsp start the Language Server (LSP over stdio, for editors)
|
|
410
|
+
mcp start the MCP server (for AI coding agents; stdio)
|
|
405
411
|
|
|
406
412
|
Common options: --out <dir>, --json, --no-ai. See https://thunderlang.dev/docs`;
|
|
407
413
|
|
|
@@ -648,7 +654,7 @@ function main() {
|
|
|
648
654
|
return;
|
|
649
655
|
}
|
|
650
656
|
|
|
651
|
-
// `thunder gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
|
|
657
|
+
// `thunder gen <file> [--target typescript|csharp|java|python] [--out <dir>]` , deterministic code scaffold from
|
|
652
658
|
// intent. Generates the typed contract + the decision logic (already executable) and leaves
|
|
653
659
|
// honest TODO markers for business logic. No AI. Prints, or writes with --out.
|
|
654
660
|
if (cmd === 'gen') {
|
|
@@ -659,7 +665,7 @@ function main() {
|
|
|
659
665
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
660
666
|
const code = generate(ast);
|
|
661
667
|
if (args.outExplicit) {
|
|
662
|
-
const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target;
|
|
668
|
+
const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target === 'python' ? 'py' : target === 'csharp' ? 'cs' : target;
|
|
663
669
|
const p = writeText(args.out, `${slug(subjectName(ast) || 'intent')}.${ext}`, code.endsWith('\n') ? code : code + '\n');
|
|
664
670
|
console.log(`wrote ${p.replace(process.cwd() + '/', '')}`);
|
|
665
671
|
return;
|
|
@@ -858,7 +864,7 @@ test Example
|
|
|
858
864
|
if (!res.ok) { console.error(res.error); process.exit(1); }
|
|
859
865
|
if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
|
|
860
866
|
if (args.out) {
|
|
861
|
-
const p = writeText(args.out, `${slug(res.lifted.mission)}.
|
|
867
|
+
const p = writeText(args.out, `${slug(res.lifted.mission)}.thunder`, res.intentText);
|
|
862
868
|
console.log(`thunder lift ${basename(file)} -> ${p.replace(process.cwd() + '/', '')}`);
|
|
863
869
|
} else {
|
|
864
870
|
console.log(res.intentText);
|
|
@@ -899,6 +905,11 @@ test Example
|
|
|
899
905
|
const codeText = readFileSync(file, 'utf8');
|
|
900
906
|
const language = args.from && args.from !== 'repo' ? args.from : languageForFile(file);
|
|
901
907
|
const res = checkDrift(intentText, codeText, { language });
|
|
908
|
+
if (args.evidence) {
|
|
909
|
+
const ctx = { compilerVersion: COMPILER_VERSION, occurredAt: new Date().toISOString(), missionName: parseIntent(intentText).mission || null, intentHash: sha256(intentText), codeHash: sha256(codeText) };
|
|
910
|
+
console.log(JSON.stringify(driftToEvidence(res, ctx), null, 2));
|
|
911
|
+
process.exit(res.status === 'drift' ? 1 : 0); return;
|
|
912
|
+
}
|
|
902
913
|
if (args.json) { console.log(JSON.stringify(res, null, 2)); }
|
|
903
914
|
else {
|
|
904
915
|
console.log(`thunder drift: ${res.status.toUpperCase()} (${res.summary.blocking} blocking)`);
|
|
@@ -1503,6 +1514,34 @@ test Example
|
|
|
1503
1514
|
return;
|
|
1504
1515
|
}
|
|
1505
1516
|
|
|
1517
|
+
// `thunder evidence <file>` , project the intent's proof into evidence-event-v1 events for the
|
|
1518
|
+
// shared SkillsTech evidence graph (STW spine). Deterministic, offline, safe-derived; prints JSON.
|
|
1519
|
+
// The network push to the spine belongs to the org-layer/CI, not this CLI.
|
|
1520
|
+
if (cmd === 'evidence') {
|
|
1521
|
+
if (!file || !existsSync(file)) { console.error('usage: thunder evidence <file> # emit evidence-event-v1 JSON for the shared proof spine'); process.exit(2); return; }
|
|
1522
|
+
const source = readFileSync(file, 'utf8');
|
|
1523
|
+
const ast = parseIntent(source);
|
|
1524
|
+
const diagnostics = semanticDiagnostics(ast);
|
|
1525
|
+
const resolved = resolveObligations(ast);
|
|
1526
|
+
const proof = buildProof(ast, {
|
|
1527
|
+
sourceFile: basename(file),
|
|
1528
|
+
sourceHash: sha256(source),
|
|
1529
|
+
targetsRequested: ast.targets || [],
|
|
1530
|
+
targetsGenerated: [],
|
|
1531
|
+
diagnostics,
|
|
1532
|
+
generatedAt: new Date().toISOString(),
|
|
1533
|
+
});
|
|
1534
|
+
const CLAIM_MAP = { verified: 'verified', failed: 'failed', declared: 'planned', unverified: 'needs_verification' };
|
|
1535
|
+
const byId = new Map(resolved.obligations.map((o) => [o.id, o]));
|
|
1536
|
+
const applyStatus = (claim) => { const o = byId.get(claim.id); return o ? { ...claim, status: CLAIM_MAP[o.status], provenBy: o.provenBy || null } : claim; };
|
|
1537
|
+
proof.guarantees = proof.guarantees.map(applyStatus);
|
|
1538
|
+
proof.neverRules = proof.neverRules.map(applyStatus);
|
|
1539
|
+
proof.freshness = freshnessFor(file, proof, args.env);
|
|
1540
|
+
const events = toEvidenceEvents(proof);
|
|
1541
|
+
console.log(JSON.stringify(events, null, 2));
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1506
1545
|
// CONFORMANCE: the same test cases run against every target. The engine defines the canonical
|
|
1507
1546
|
// result each case must produce; target outputs fed via --results are graded against it.
|
|
1508
1547
|
// `thunder conform <file> [--targets ts,py] [--results <json|path>] [--json]`.
|
|
@@ -1540,6 +1579,11 @@ test Example
|
|
|
1540
1579
|
const rep = buildConformance(ast, { targets, results });
|
|
1541
1580
|
if (skippedTargets.length) rep.skipped = Array.from(new Set(skippedTargets));
|
|
1542
1581
|
const bad = rep.semanticFailures > 0 || rep.failures.length > 0;
|
|
1582
|
+
if (args.evidence) {
|
|
1583
|
+
const ctx = { compilerVersion: COMPILER_VERSION, occurredAt: new Date().toISOString(), missionName: ast.mission || null, intentHash: sha256(readFileSync(file, 'utf8')) };
|
|
1584
|
+
console.log(JSON.stringify(conformToEvidence(rep, ctx), null, 2));
|
|
1585
|
+
process.exit(bad ? 1 : 0); return;
|
|
1586
|
+
}
|
|
1543
1587
|
if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(bad ? 1 : 0); return; }
|
|
1544
1588
|
if (!rep.total) { console.log(`thunder conform ${basename(file)}: no test cases to conform.`); return; }
|
|
1545
1589
|
|
|
@@ -1750,6 +1794,11 @@ test Example
|
|
|
1750
1794
|
const before = args.before ? readFileSync(args.before, 'utf8') : null;
|
|
1751
1795
|
const language = args.from || languageForFile(args.after);
|
|
1752
1796
|
const r = verifyDiff(intentText, { before, after, language });
|
|
1797
|
+
if (args.evidence) {
|
|
1798
|
+
const ctx = { compilerVersion: COMPILER_VERSION, occurredAt: new Date().toISOString(), missionName: parseIntent(intentText).mission || null, intentHash: sha256(intentText), changeHash: sha256(after) };
|
|
1799
|
+
console.log(JSON.stringify(verifyDiffToEvidence(r, ctx), null, 2));
|
|
1800
|
+
process.exit(r.ok ? 0 : 1); return;
|
|
1801
|
+
}
|
|
1753
1802
|
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
|
|
1754
1803
|
console.log(`thunder verify-diff ${basename(file)} vs ${basename(args.after)}: ${r.verdict} (${r.blocking} blocking, ${r.summary.regressions} regression(s))`);
|
|
1755
1804
|
for (const f of r.findings) {
|
package/src/codegen.mjs
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// the "see how it works, then change it" surface for the playground and `thunder gen`.
|
|
6
6
|
//
|
|
7
7
|
// Pure and browser-safe so the playground can render it. TypeScript first; the same adapter
|
|
8
|
-
// shape (a type map + a body walk) extends to C# / Java.
|
|
8
|
+
// shape (a type map + a body walk) extends to C# / Java / Python.
|
|
9
9
|
|
|
10
|
-
import { exprToJs, exprToCSharp, exprToJava } from './expr.mjs';
|
|
10
|
+
import { exprToJs, exprToCSharp, exprToJava, exprToPython } from './expr.mjs';
|
|
11
11
|
import { subjectName } from './parse.mjs';
|
|
12
12
|
|
|
13
13
|
export const CODEGEN_SCHEMA = 'intent-codegen-v1';
|
|
@@ -138,7 +138,7 @@ export function toCSharp(ast) {
|
|
|
138
138
|
L.push(` public static string ${pascal(d.name)}(${inputs.map((i) => `dynamic ${i}`).join(', ')})`);
|
|
139
139
|
L.push(' {');
|
|
140
140
|
for (const r of d.rules || []) {
|
|
141
|
-
let cond; try { cond = exprToCSharp(r.when, { inputs }); } catch { cond = `false /* TODO: "${r.when}" */`; }
|
|
141
|
+
let cond; try { cond = exprToCSharp(r.when, { inputs }); } catch { cond = `false /* TODO: could not translate "${r.when}" */`; }
|
|
142
142
|
L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
|
|
143
143
|
}
|
|
144
144
|
L.push(` return ${JSON.stringify(d.default ?? 'Undecided')}; // default`, ' }');
|
|
@@ -146,6 +146,7 @@ export function toCSharp(ast) {
|
|
|
146
146
|
const inName = (ast.inputs || []).length ? `${subject}Input` : null;
|
|
147
147
|
const outName = (ast.outputs || []).length ? `${subject}Output` : 'void';
|
|
148
148
|
L.push(` public static ${outName} Run(${inName ? `${inName} input` : ''})`, ' {');
|
|
149
|
+
for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
|
|
149
150
|
for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
|
|
150
151
|
for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
|
|
151
152
|
L.push(' throw new NotImplementedException("TODO: implement , the intent above defines what this must do.");', ' }');
|
|
@@ -192,7 +193,7 @@ export function toJava(ast) {
|
|
|
192
193
|
L.push(` // decision ${d.name} , first matching rule wins.`);
|
|
193
194
|
L.push(` public static String ${lower(pascal(d.name))}(${inputs.map((i) => `Object ${i}`).join(', ')}) {`);
|
|
194
195
|
for (const r of d.rules || []) {
|
|
195
|
-
let cond; try { cond = exprToJava(r.when, { inputs }); } catch { cond = `false /* TODO: "${r.when}" */`; }
|
|
196
|
+
let cond; try { cond = exprToJava(r.when, { inputs }); } catch { cond = `false /* TODO: could not translate "${r.when}" */`; }
|
|
196
197
|
L.push(` if (${cond}) return ${JSON.stringify(r.result)}; // rule ${r.name}`);
|
|
197
198
|
}
|
|
198
199
|
L.push(` return ${JSON.stringify(d.default ?? 'Undecided')}; // default`, ' }');
|
|
@@ -200,6 +201,7 @@ export function toJava(ast) {
|
|
|
200
201
|
const inName = (ast.inputs || []).length ? `${subject}Input` : null;
|
|
201
202
|
const outName = (ast.outputs || []).length ? `${subject}Output` : 'void';
|
|
202
203
|
L.push(` public static ${outName} run(${inName ? `${inName} input` : ''}) {`);
|
|
204
|
+
for (const r of ast.requires || []) L.push(` // precondition: ${r} , TODO: validate`);
|
|
203
205
|
for (const n of ast.neverRules || []) L.push(` // NEVER: ${n.statement}`);
|
|
204
206
|
for (const g of ast.guarantees || []) L.push(` // GUARANTEE: ${g.statement}`);
|
|
205
207
|
L.push(' throw new UnsupportedOperationException("TODO: implement , the intent above defines what this must do.");', ' }');
|
|
@@ -207,8 +209,75 @@ export function toJava(ast) {
|
|
|
207
209
|
return L.join('\n');
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
// ── Python ───────────────────────────────────────────────────────────────────
|
|
213
|
+
const PY_TYPES = {
|
|
214
|
+
Email: 'str', Url: 'str', UserId: 'str', AccountId: 'str', OrderId: 'str',
|
|
215
|
+
InvoiceId: 'str', CustomerId: 'str', Secret: 'str', Token: 'str', Jwt: 'str',
|
|
216
|
+
IdempotencyKey: 'str', Currency: 'str', Date: 'str', DateTime: 'str',
|
|
217
|
+
Money: 'float', Percentage: 'float', Count: 'int', Duration: 'int', Flag: 'bool',
|
|
218
|
+
};
|
|
219
|
+
function pyType(type, domain) {
|
|
220
|
+
if (!type) return 'object';
|
|
221
|
+
const list = /^List<(.+)>$/.exec(type);
|
|
222
|
+
if (list) return `list[${pyType(list[1], domain)}]`;
|
|
223
|
+
const base = type.replace(/\(.*\)/, '');
|
|
224
|
+
if (PY_TYPES[base]) return PY_TYPES[base];
|
|
225
|
+
domain.add(base); return base;
|
|
226
|
+
}
|
|
227
|
+
// PascalCase/camelCase -> snake_case, for idiomatic Python function names. Field and decision
|
|
228
|
+
// input names stay verbatim: the translated conditions reference them by their intent names.
|
|
229
|
+
const snake = (s) => pascal(s).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
230
|
+
|
|
231
|
+
/** Generate a deterministic Python scaffold from an intent AST. */
|
|
232
|
+
export function toPython(ast) {
|
|
233
|
+
const subject = pascal(subjectName(ast) || 'Intent');
|
|
234
|
+
const domain = new Set();
|
|
235
|
+
const L = [
|
|
236
|
+
`# ${subject} , generated from ThunderLang (intent-codegen-v1). Deterministic, no AI.`,
|
|
237
|
+
'# The dataclass contract and the decision logic are fully determined by the intent;',
|
|
238
|
+
'# business logic marked TODO is yours, bound by the guarantees and never-rules below.',
|
|
239
|
+
'',
|
|
240
|
+
];
|
|
241
|
+
const hasData = (ast.inputs || []).length || (ast.outputs || []).length;
|
|
242
|
+
// Postponed annotations so a dataclass may reference a domain stub declared below it.
|
|
243
|
+
if (hasData) L.push('from __future__ import annotations', 'from dataclasses import dataclass', '');
|
|
244
|
+
const cls = (name, fields) => fields.length
|
|
245
|
+
? ['@dataclass', `class ${name}:`, ...fields.map((f) => ` ${f.name}: ${pyType(f.type, domain)}`), '']
|
|
246
|
+
: [];
|
|
247
|
+
if ((ast.inputs || []).length) L.push(...cls(`${subject}Input`, ast.inputs));
|
|
248
|
+
if ((ast.outputs || []).length) L.push(...cls(`${subject}Output`, ast.outputs));
|
|
249
|
+
for (const d of ast.decisions || []) {
|
|
250
|
+
const inputs = d.inputs || [];
|
|
251
|
+
L.push(`# decision ${d.name} , first matching rule wins.`);
|
|
252
|
+
L.push(`def ${snake(d.name)}(${inputs.join(', ')}) -> str:`);
|
|
253
|
+
for (const r of d.rules || []) {
|
|
254
|
+
let cond, note = '';
|
|
255
|
+
try { cond = exprToPython(r.when, { inputs }); }
|
|
256
|
+
catch { cond = 'False'; note = ` # TODO: could not translate "${r.when}"`; }
|
|
257
|
+
L.push(` if ${cond}:${note}`);
|
|
258
|
+
L.push(` return ${JSON.stringify(r.result)} # rule ${r.name}`);
|
|
259
|
+
}
|
|
260
|
+
L.push(` return ${JSON.stringify(d.default ?? 'Undecided')} # default`, '');
|
|
261
|
+
}
|
|
262
|
+
const inName = (ast.inputs || []).length ? `${subject}Input` : null;
|
|
263
|
+
const outName = (ast.outputs || []).length ? `${subject}Output` : 'None';
|
|
264
|
+
L.push(`def run(${inName ? `input: ${inName}` : ''}) -> ${outName}:`);
|
|
265
|
+
for (const r of ast.requires || []) L.push(` # precondition: ${r} , TODO: validate`);
|
|
266
|
+
for (const n of ast.neverRules || []) L.push(` # NEVER: ${n.statement}`);
|
|
267
|
+
for (const g of ast.guarantees || []) L.push(` # GUARANTEE: ${g.statement}`);
|
|
268
|
+
L.push(' raise NotImplementedError("TODO: implement , the intent above defines what this must do.")', '');
|
|
269
|
+
const stubs = [...domain].filter((t) => !PY_TYPES[t] && /^[A-Z]/.test(t)).sort();
|
|
270
|
+
if (stubs.length) {
|
|
271
|
+
L.push('# Domain types referenced by the intent , complete these.');
|
|
272
|
+
for (const t of stubs) L.push(`class ${t}: pass # TODO: fields`);
|
|
273
|
+
L.push('');
|
|
274
|
+
}
|
|
275
|
+
return L.join('\n');
|
|
276
|
+
}
|
|
277
|
+
|
|
210
278
|
export const GENERATORS = {
|
|
211
279
|
typescript: toTypeScript, ts: toTypeScript,
|
|
212
280
|
csharp: toCSharp, cs: toCSharp,
|
|
213
281
|
java: toJava,
|
|
282
|
+
python: toPython, py: toPython,
|
|
214
283
|
};
|
package/src/core.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export {
|
|
|
50
50
|
} from './index';
|
|
51
51
|
|
|
52
52
|
// Code generation , deterministic scaffolds from intent.
|
|
53
|
-
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, subjectName, intentRefId, skillRefId } from './index';
|
|
53
|
+
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython, subjectName, intentRefId, skillRefId } from './index';
|
|
54
54
|
|
|
55
55
|
// 12-Factor Agents conformance lens (twelve-factor-v1).
|
|
56
56
|
export { TWELVE_FACTOR_SCHEMA, TwelveFactorResult, twelveFactorReport, twelveFactorSummary } from './index';
|
package/src/core.mjs
CHANGED
|
@@ -80,7 +80,7 @@ export { FOCUS_SCHEMA, SCOPE_TYPES, FOCUS_REASONS, makeScope, buildFocusGraph, i
|
|
|
80
80
|
// Comprehension Contract: the C0..C7 understanding level (browser-safe; every product reads it).
|
|
81
81
|
export { COMPREHENSION_SCHEMA, LEVELS as COMPREHENSION_LEVELS, comprehensionLevel, comprehensionReport } from './comprehension.mjs';
|
|
82
82
|
// Code generation: deterministic scaffolds from intent (browser-safe, so the playground renders it).
|
|
83
|
-
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava } from './codegen.mjs';
|
|
83
|
+
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython } from './codegen.mjs';
|
|
84
84
|
// Change Lens: what a branch/PR changed by meaning (pure; the CLI supplies the git-diffed graphs).
|
|
85
85
|
export { CHANGES_SCHEMA, changeReport } from './changes.mjs';
|
|
86
86
|
export { subjectName, intentRefId, skillRefId } from './parse.mjs';
|
package/src/emit.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function notesSummary(ast) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export const COMPILER_VERSION = '0.
|
|
33
|
+
export const COMPILER_VERSION = '0.4.2';
|
|
34
34
|
export const PROOF_SCHEMA_VERSION = '0.1.0';
|
|
35
35
|
// Identifies which ecosystem product emitted this proof. Consumed by SkillsTech
|
|
36
36
|
// Certified to key cert proofs to the compiler. Stable slug per the coordination bus.
|
package/src/evidence.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// ThunderLang -> shared evidence graph projection.
|
|
2
|
+
//
|
|
3
|
+
// Maps ThunderLang's verification artifacts into evidence-event-v1 events for the SkillsTech proof
|
|
4
|
+
// spine (owned by STW: siblings emit evidence-event-v1, STW composes proof-bundle-v1). ThunderLang is
|
|
5
|
+
// the deterministic, machine-checked layer, so its evidence is evidenceType `tool_verified`.
|
|
6
|
+
//
|
|
7
|
+
// These projections are PURE and OFFLINE. They perform no network I/O: the transport (POST to the
|
|
8
|
+
// spine) belongs to the hosted org-layer / CI, never the end-user CLI, so the compiler stays
|
|
9
|
+
// deterministic and phone-home-free. They are also SAFE-DERIVED: events carry only ids, hashes,
|
|
10
|
+
// verdicts, diagnostic codes, and counts , never source code, never secret values, never claim
|
|
11
|
+
// statement text, never data-element contents (no finding `message`, no expected/actual values).
|
|
12
|
+
//
|
|
13
|
+
// NOTE: the evidence-event-v1 envelope below is ThunderLang's PROPOSED shape, pending STW's canonical
|
|
14
|
+
// schema (see the TL->STW proposal on the comms bus). Field names may adjust once STW confirms.
|
|
15
|
+
|
|
16
|
+
const COUNTED_STATUSES = ['verified', 'failed', 'planned', 'needs_verification'];
|
|
17
|
+
const stripHash = (h) => String(h || '').replace(/^sha256:/, '');
|
|
18
|
+
const short = (h, n = 16) => stripHash(h).slice(0, n);
|
|
19
|
+
|
|
20
|
+
// The shared evidence-event-v1 envelope. `ctx` carries the cross-artifact context the individual
|
|
21
|
+
// artifacts do not all include (missionName, intentHash, compilerVersion, occurredAt).
|
|
22
|
+
function envelope(eventType, evidenceId, ctx = {}, payload = {}) {
|
|
23
|
+
return {
|
|
24
|
+
schema: 'evidence-event-v1',
|
|
25
|
+
sourceProduct: 'thunderlang',
|
|
26
|
+
producer: { name: 'thunderlang', version: ctx.compilerVersion || null },
|
|
27
|
+
eventType,
|
|
28
|
+
evidenceType: 'tool_verified',
|
|
29
|
+
evidenceId,
|
|
30
|
+
occurredAt: ctx.occurredAt || null,
|
|
31
|
+
visibility: ctx.visibility || 'private',
|
|
32
|
+
subject: { missionName: ctx.missionName || null, intentHash: ctx.intentHash || null },
|
|
33
|
+
payload,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const safeClaim = (kind, c) => ({ id: c.id, kind, status: c.status, provenBy: c.provenBy || null });
|
|
38
|
+
function countByStatus(claims) {
|
|
39
|
+
const counts = Object.fromEntries(COUNTED_STATUSES.map((s) => [s, 0]));
|
|
40
|
+
for (const c of claims) if (c && Object.prototype.hasOwnProperty.call(counts, c.status)) counts[c.status]++;
|
|
41
|
+
return counts;
|
|
42
|
+
}
|
|
43
|
+
// Safe-derived finding: diagnostic code + level (+ flags), never the human message (which can quote code).
|
|
44
|
+
const safeFinding = (f) => ({ code: f.code, level: f.level, ...(f.regression !== undefined ? { regression: !!f.regression } : {}), ...(f.line !== undefined ? { line: f.line } : {}) });
|
|
45
|
+
|
|
46
|
+
// intent.proven , from an intent-proof-v1 artifact.
|
|
47
|
+
export function toEvidenceEvents(proof) {
|
|
48
|
+
if (!proof || typeof proof !== 'object') return [];
|
|
49
|
+
const guarantees = Array.isArray(proof.guarantees) ? proof.guarantees : [];
|
|
50
|
+
const neverRules = Array.isArray(proof.neverRules) ? proof.neverRules : [];
|
|
51
|
+
const claims = [
|
|
52
|
+
...guarantees.map((c) => safeClaim('guarantee', c)),
|
|
53
|
+
...neverRules.map((c) => safeClaim('prohibition', c)),
|
|
54
|
+
];
|
|
55
|
+
const fr = proof.freshness || {};
|
|
56
|
+
const intentHash = proof.freshness?.intentHash || proof.sourceHash || null;
|
|
57
|
+
return [envelope('intent.proven', `tl-proof-${short(intentHash)}`, {
|
|
58
|
+
compilerVersion: proof.compilerVersion, occurredAt: proof.generatedAt, missionName: proof.missionName, intentHash,
|
|
59
|
+
}, {
|
|
60
|
+
proofStatus: proof.proofStatus || null,
|
|
61
|
+
total: claims.length,
|
|
62
|
+
counts: countByStatus(claims),
|
|
63
|
+
freshness: { implementation: fr.implementation || null, dependencies: fr.dependencies ? fr.dependencies.hash || null : null, environment: fr.environment || null },
|
|
64
|
+
claims,
|
|
65
|
+
})];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// change.gated , from a verify-diff verdict ({ verdict, ok, findings, blocking, summary }).
|
|
69
|
+
export function verifyDiffToEvidence(verdict, ctx = {}) {
|
|
70
|
+
if (!verdict || typeof verdict !== 'object') return [];
|
|
71
|
+
const findings = Array.isArray(verdict.findings) ? verdict.findings : [];
|
|
72
|
+
const id = `tl-verifydiff-${short(ctx.intentHash)}${ctx.changeHash ? `-${short(ctx.changeHash, 8)}` : ''}`;
|
|
73
|
+
return [envelope('change.gated', id, ctx, {
|
|
74
|
+
verdict: verdict.verdict || null,
|
|
75
|
+
ok: !!verdict.ok,
|
|
76
|
+
blocking: verdict.blocking || 0,
|
|
77
|
+
regressions: verdict.summary?.regressions ?? findings.filter((f) => f.regression).length,
|
|
78
|
+
findings: findings.map(safeFinding),
|
|
79
|
+
})];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// conformance.verified , from a thunder-conformance-v1 report.
|
|
83
|
+
export function conformToEvidence(report, ctx = {}) {
|
|
84
|
+
if (!report || typeof report !== 'object') return [];
|
|
85
|
+
const cases = Array.isArray(report.cases) ? report.cases : [];
|
|
86
|
+
const columns = Array.isArray(report.columns) ? report.columns : [];
|
|
87
|
+
// Per-target pass/fail/declared counts, derived without leaking any expected/actual values.
|
|
88
|
+
const targets = {};
|
|
89
|
+
for (const col of columns) {
|
|
90
|
+
const t = { pass: 0, fail: 0, declared: 0 };
|
|
91
|
+
for (const c of cases) { const s = c.targets?.[col]?.status; if (s === 'pass') t.pass++; else if (s === 'fail') t.fail++; else t.declared++; }
|
|
92
|
+
targets[col] = t;
|
|
93
|
+
}
|
|
94
|
+
return [envelope('conformance.verified', `tl-conform-${short(ctx.intentHash)}`, ctx, {
|
|
95
|
+
total: report.total || cases.length,
|
|
96
|
+
columns,
|
|
97
|
+
semanticFailures: report.semanticFailures || 0,
|
|
98
|
+
graded: !!report.graded,
|
|
99
|
+
targets,
|
|
100
|
+
// failure locations by name only (case + target), never expected/actual values.
|
|
101
|
+
failures: (Array.isArray(report.failures) ? report.failures : []).map((f) => ({ target: f.target, case: f.case })),
|
|
102
|
+
})];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// intent.drift , from a checkDrift report ({ status, findings, summary }).
|
|
106
|
+
export function driftToEvidence(report, ctx = {}) {
|
|
107
|
+
if (!report || typeof report !== 'object') return [];
|
|
108
|
+
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
109
|
+
const id = `tl-drift-${short(ctx.intentHash)}${ctx.codeHash ? `-${short(ctx.codeHash, 8)}` : ''}`;
|
|
110
|
+
return [envelope('intent.drift', id, ctx, {
|
|
111
|
+
status: report.status || null,
|
|
112
|
+
total: findings.length,
|
|
113
|
+
blocking: report.summary?.blocking ?? 0,
|
|
114
|
+
findings: findings.map(safeFinding),
|
|
115
|
+
})];
|
|
116
|
+
}
|
package/src/exporters.mjs
CHANGED
|
@@ -180,17 +180,23 @@ export function toMermaid(ast) {
|
|
|
180
180
|
|
|
181
181
|
// ── Playwright (experience -> E2E test scaffold) ─────────────────────────────
|
|
182
182
|
// A SKELETON, not a passing test: declared experiences/journeys/states become structured
|
|
183
|
-
// Playwright stubs (describe/test/test.step) with TODOs for selectors + assertions.
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// (
|
|
183
|
+
// Playwright stubs (describe/test/test.step) with TODOs for selectors + assertions. The
|
|
184
|
+
// TODOs are intentional and stay: a deterministic compiler cannot know the app's selectors,
|
|
185
|
+
// so it must not fabricate them. To keep the scaffold honest, every generated test opens
|
|
186
|
+
// with a test.fixme(true, ...) guard: Playwright reports it as "fixme" (not a vacuous pass)
|
|
187
|
+
// until a human fills in the body and removes the guard. This is the test-plan target for
|
|
188
|
+
// the experience profile , it turns "what the UI must do" into the shape of the test that
|
|
189
|
+
// proves it, deterministically. Consistent with the compiler's scope (test scaffolds, not
|
|
190
|
+
// production code).
|
|
187
191
|
const jsStr = (s) => `'${String(s ?? '').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\r?\n/g, ' ')}'`;
|
|
188
192
|
|
|
189
193
|
export function toPlaywright(ast) {
|
|
190
194
|
const exps = ast.experiences || [];
|
|
191
195
|
const L = [];
|
|
192
196
|
L.push('// Playwright test scaffold generated from experience intent by @skillstech/thunderlang.');
|
|
193
|
-
L.push('// SKELETON: fill in selectors and assertions.
|
|
197
|
+
L.push('// SKELETON: fill in selectors and assertions. Every test starts with a test.fixme');
|
|
198
|
+
L.push('// guard so an unimplemented scaffold reports "fixme" instead of passing vacuously;');
|
|
199
|
+
L.push('// remove the guard from each test once its body is real.');
|
|
194
200
|
L.push("import { test, expect } from '@playwright/test';");
|
|
195
201
|
L.push('');
|
|
196
202
|
if (!exps.length) {
|
|
@@ -204,6 +210,7 @@ export function toPlaywright(ast) {
|
|
|
204
210
|
}
|
|
205
211
|
for (const j of exp.journeys || []) {
|
|
206
212
|
L.push(` test(${jsStr(j.name || 'journey')}, async ({ page }) => {`);
|
|
213
|
+
L.push(` test.fixme(true, ${jsStr('scaffold: implement the steps, then remove this guard')});`);
|
|
207
214
|
if (!(j.steps || []).length) L.push(' // TODO: no steps declared for this journey');
|
|
208
215
|
for (const step of j.steps || []) {
|
|
209
216
|
L.push(` await test.step(${jsStr(step)}, async () => {`);
|
|
@@ -215,10 +222,12 @@ export function toPlaywright(ast) {
|
|
|
215
222
|
for (const st of exp.states || []) {
|
|
216
223
|
if (st.hasRecovery) {
|
|
217
224
|
L.push(` test(${jsStr(`failure state "${st.name}" offers a recovery path`)}, async ({ page }) => {`);
|
|
225
|
+
L.push(` test.fixme(true, ${jsStr('scaffold: implement this state check, then remove this guard')});`);
|
|
218
226
|
L.push(` // TODO: drive the UI into "${st.name}", assert a recovery action is available`);
|
|
219
227
|
L.push(' });');
|
|
220
228
|
} else {
|
|
221
229
|
L.push(` test(${jsStr(`reaches state: ${st.name}`)}, async ({ page }) => {`);
|
|
230
|
+
L.push(` test.fixme(true, ${jsStr('scaffold: implement this state check, then remove this guard')});`);
|
|
222
231
|
L.push(` // TODO: drive the UI to "${st.name}" and assert it is shown`);
|
|
223
232
|
L.push(' });');
|
|
224
233
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -691,6 +691,7 @@ export const CODEGEN_SCHEMA: string;
|
|
|
691
691
|
export function toTypeScript(ast: IntentAst): string;
|
|
692
692
|
export function toCSharp(ast: IntentAst): string;
|
|
693
693
|
export function toJava(ast: IntentAst): string;
|
|
694
|
+
export function toPython(ast: IntentAst): string;
|
|
694
695
|
export const GENERATORS: Record<string, (ast: IntentAst) => string>;
|
|
695
696
|
export function exprToJs(src: string, opts?: { inputs?: string[] }): string;
|
|
696
697
|
export function subjectName(ast: IntentAst): string | null;
|
package/src/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
// Parsing
|
|
10
10
|
export { parseIntent, slug, KNOWN_LENSES } from './parse.mjs';
|
|
11
|
+
export { toEvidenceEvents, verifyDiffToEvidence, conformToEvidence, driftToEvidence } from './evidence.mjs';
|
|
11
12
|
|
|
12
13
|
// Semantic analysis, graphs, and proof
|
|
13
14
|
export {
|
|
@@ -113,7 +114,7 @@ export { FOCUS_SCHEMA, SCOPE_TYPES, FOCUS_REASONS, makeScope, buildFocusGraph, i
|
|
|
113
114
|
// Comprehension Contract , the C0..C7 understanding level (intent-comprehension-v1)
|
|
114
115
|
export { COMPREHENSION_SCHEMA, LEVELS as COMPREHENSION_LEVELS, comprehensionLevel, comprehensionReport } from './comprehension.mjs';
|
|
115
116
|
// Code generation , deterministic scaffolds from intent (intent-codegen-v1)
|
|
116
|
-
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava } from './codegen.mjs';
|
|
117
|
+
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython } from './codegen.mjs';
|
|
117
118
|
// Change Lens , what a branch/PR changed by meaning (intent-changes-v1)
|
|
118
119
|
export { CHANGES_SCHEMA, changeReport } from './changes.mjs';
|
|
119
120
|
export { exprToJs, exprToCSharp, exprToJava, exprToCode } from './expr.mjs';
|
package/src/lift.mjs
CHANGED
|
@@ -696,7 +696,7 @@ export function liftRepo(files, { language } = {}) {
|
|
|
696
696
|
const base = slug(r.lifted.mission);
|
|
697
697
|
const n = (usedNames.get(base) || 0) + 1;
|
|
698
698
|
usedNames.set(base, n);
|
|
699
|
-
const outName = n === 1 ? `${base}.
|
|
699
|
+
const outName = n === 1 ? `${base}.thunder` : `${base}-${n}.thunder`;
|
|
700
700
|
missions.push({
|
|
701
701
|
mission: r.lifted.mission, sourceFile: file, outName,
|
|
702
702
|
intentText: r.intentText, summary: r.summary, diagnostics: r.diagnostics,
|