@skillstech/thunderlang 0.2.0 → 0.4.1
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 +39 -0
- package/dist/core.cjs +217 -10
- package/dist/index.cjs +409 -11
- package/package.json +1 -1
- package/src/cli.mjs +77 -69
- 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/exporters.mjs +14 -5
- package/src/index.d.ts +1 -0
- package/src/index.mjs +1 -1
- package/src/lift.mjs +79 -3
- package/src/mcp.mjs +125 -1
package/src/cli.mjs
CHANGED
|
@@ -98,7 +98,13 @@ import {
|
|
|
98
98
|
import { parseEventLog, serializeEventLog, recordEvent, timeline } from './ai-events.mjs';
|
|
99
99
|
|
|
100
100
|
// Recursively collect supported source files, skipping vendored / build dirs.
|
|
101
|
-
|
|
101
|
+
// Kept in sync with lift.mjs languageForFile(): every language the lift adapters support,
|
|
102
|
+
// so repo-mode discovery matches the advertised multi-language lift surface.
|
|
103
|
+
const LIFT_EXTS = [
|
|
104
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.rs', '.pl', '.pm', '.t',
|
|
105
|
+
'.py', '.pyi', '.java', '.cs', '.go', '.cpp', '.cc', '.cxx', '.hpp', '.hh', '.c', '.h',
|
|
106
|
+
'.php', '.rb', '.kt', '.kts', '.scala', '.sc', '.ex', '.exs',
|
|
107
|
+
];
|
|
102
108
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build', '.intent', 'coverage', '.vercel']);
|
|
103
109
|
// ThunderLang source files. `.thunder` is the canonical public extension; `.tl` is an
|
|
104
110
|
// accepted shorthand; `.intent` stays supported so legacy IntentLang sources keep working.
|
|
@@ -321,81 +327,83 @@ function printDiagnostics(diags) {
|
|
|
321
327
|
|
|
322
328
|
const HELP = `thunder , the ThunderLang compiler + engine (deterministic, no AI required)
|
|
323
329
|
|
|
330
|
+
The flow: author intent, inspect it, run and prove it, then keep real code aligned with it.
|
|
331
|
+
|
|
324
332
|
usage: thunder <command> <file> [options]
|
|
325
333
|
thunder mission <Name> <command> run any command on a mission by name
|
|
326
334
|
|
|
327
|
-
|
|
328
|
-
new [Name]
|
|
329
|
-
mission <Name> [cmd]
|
|
330
|
-
|
|
331
|
-
|
|
335
|
+
Author
|
|
336
|
+
new [Name] scaffold a runnable starter mission (Name.thunder) [alias: init]
|
|
337
|
+
mission <Name> [cmd] resolve a mission by name and run a command on it (list | <Name> | <Name> <cmd>)
|
|
338
|
+
draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
|
|
339
|
+
edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
|
|
340
|
+
fmt <file|dir> [--write|--check] canonical formatting (whitespace only; comments kept)
|
|
341
|
+
source <file|graph.json> regenerate .thunder from a persisted graph
|
|
342
|
+
|
|
343
|
+
Inspect
|
|
344
|
+
check <file|dir> [--json|--format sarif] parse + lint + explainable diagnostics; gate a whole dir
|
|
345
|
+
report [dir] [--json] repo-wide intent health: severity + area counts, coverage
|
|
346
|
+
risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json] focused scan queries
|
|
347
|
+
focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json] Intent Lens: focused graph + brief
|
|
348
|
+
comprehension <file|dir> [--observed] [--learning] [--governed] [--json] the C0..C7 understanding level
|
|
349
|
+
twelve-factor <file> [--json] score against the 13 humanlayer/12-factor-agents principles [alias: 12factor]
|
|
350
|
+
explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
|
|
351
|
+
rules [--json] list the whole canonical diagnostic catalog
|
|
352
|
+
notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
|
|
353
|
+
docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
|
|
354
|
+
index <dir> the Mission Atlas inventory
|
|
355
|
+
atlas <dir> [--search q | --expand id] the whole-system Atlas
|
|
356
|
+
|
|
357
|
+
Run & test
|
|
358
|
+
run <file> --inputs '<json>' execute the decision(s) deterministically
|
|
332
359
|
test <file> [--contracts | --properties | --scenarios | --mutate | --evals | --changed | --coverage] [--strict]
|
|
333
360
|
test <file> --target typescript|python|csharp|java run the tests against the EXECUTED generated code
|
|
334
|
-
test <file> --all-targets
|
|
335
|
-
|
|
361
|
+
test <file> --all-targets run the tests against every AVAILABLE target in one pass
|
|
362
|
+
simulate <file> --events a,b,c walk the lifecycle(s) over events
|
|
363
|
+
outcomes <file> evaluate outcome contracts vs delivery results
|
|
364
|
+
style <file> resolve style intents vs the canonical token space
|
|
365
|
+
gen <file> [--target typescript|csharp|java|python] [--out <dir>] deterministic code scaffold (types + decision logic + TODOs)
|
|
366
|
+
build <file> generate docs, contract graph, test plan, and .thunder-proof.json
|
|
367
|
+
|
|
368
|
+
Prove & verify intent
|
|
369
|
+
prove <file> emit an intent-proof-v1 artifact (honest verdicts + freshness)
|
|
370
|
+
proof <file> the .thunder-proof.json artifact (see also: prove, which emits verdicts)
|
|
371
|
+
proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
|
|
336
372
|
verify <proof.json> [src] re-check a proof; reports STALE when impl/deps/compiler moved
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
mcp start the MCP server (for AI coding agents; stdio)
|
|
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
|
|
362
|
-
proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
|
|
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)
|
|
365
|
-
schema emit the canonical graph schema + diagnostic catalog
|
|
366
|
-
explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
|
|
367
|
-
rules [--json] list the whole canonical diagnostic catalog
|
|
368
|
-
notes <file> [--lens <lens>] [--json] ThunderLens: the compiled note blocks by lens (not verification)
|
|
369
|
-
docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
|
|
370
|
-
code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
|
|
371
|
-
apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
|
|
372
|
-
|
|
373
|
-
Execute (no AI, no generated code)
|
|
374
|
-
run <file> --inputs '<json>' evaluate the decision(s) against inputs
|
|
375
|
-
simulate <file> --events a,b,c walk the lifecycle(s) over events
|
|
376
|
-
test <file> run the in-file test blocks (case/scenario)
|
|
377
|
-
outcomes <file> evaluate outcome contracts vs delivery results
|
|
378
|
-
style <file> resolve style intents vs the canonical token space
|
|
373
|
+
conform <file> [--targets a,b] [--run typescript,python,csharp,java | --all-targets] [--results <json>] conformance matrix across targets
|
|
374
|
+
|
|
375
|
+
Real code vs intent (drift)
|
|
376
|
+
lift <file> [--from <lang>] lift source code into inferred intent
|
|
377
|
+
drift <codeFile> --intent <file.intent> [--from <lang>] check intent vs code drift
|
|
378
|
+
approve <file> --by <name> approve intent (drift baseline)
|
|
379
|
+
verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
|
|
380
|
+
changes [<base>..<head> | <base>] [--json] Change Lens: what a branch/PR changed by meaning
|
|
381
|
+
guardian <before> <after> drift: what changed, what risk, what to reverify, what learning is stale
|
|
382
|
+
impact <base> <proposed> Simulator: estimate a change's blast radius + risk BEFORE building it
|
|
383
|
+
diff <before> <after> semantic diff (by meaning)
|
|
384
|
+
merge <base> <ours> <theirs> deterministic 3-way semantic merge
|
|
385
|
+
handoff <file> the OpenThunder drift handoff
|
|
386
|
+
ledger <file.json> [--subject <id>] verify the tamper-evident history + explain why/who/what changed
|
|
387
|
+
guard <file> [--json] preview the runtime guard (redacted fields, enforced decisions)
|
|
388
|
+
|
|
389
|
+
Graph & IR
|
|
390
|
+
graph <file> [--safe] the canonical Intent Graph (intent-graph-v1); --safe = display-safe on stdout
|
|
391
|
+
migrate <graph.json> [--to <version>] upgrade a persisted graph
|
|
392
|
+
validate <graph.json> [--json] check a graph is canonical (anti-fork)
|
|
393
|
+
schema emit the canonical graph schema + diagnostic catalog
|
|
379
394
|
|
|
380
395
|
Interop
|
|
381
396
|
export <file> --format <dmn|bpmn|smv|jsonschema|openapi|tokens|mermaid|css|playwright> render to a standard format
|
|
382
|
-
import <file> [--format dmn|bpmn] [--json]
|
|
383
|
-
source <file|graph.json> regenerate .thunder from a graph
|
|
384
|
-
migrate <graph.json> [--to <version>] upgrade a persisted graph
|
|
385
|
-
validate <graph.json> [--json] check a graph is canonical (anti-fork)
|
|
397
|
+
import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
|
|
386
398
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
merge <base> <ours> <theirs> deterministic 3-way semantic merge
|
|
399
|
+
Fix
|
|
400
|
+
scan [dir] [--json] [--ir <path>] Scanner: intent -> Intent IR -> Fable findings -> risk themes
|
|
401
|
+
code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
|
|
402
|
+
apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
|
|
392
403
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
drift <file> --from <code> check intent vs code drift
|
|
397
|
-
verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
|
|
398
|
-
handoff <file> the OpenThunder drift handoff
|
|
404
|
+
Servers
|
|
405
|
+
lsp start the Language Server (LSP over stdio, for editors)
|
|
406
|
+
mcp start the MCP server (for AI coding agents; stdio)
|
|
399
407
|
|
|
400
408
|
Common options: --out <dir>, --json, --no-ai. See https://thunderlang.dev/docs`;
|
|
401
409
|
|
|
@@ -642,7 +650,7 @@ function main() {
|
|
|
642
650
|
return;
|
|
643
651
|
}
|
|
644
652
|
|
|
645
|
-
// `thunder gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
|
|
653
|
+
// `thunder gen <file> [--target typescript|csharp|java|python] [--out <dir>]` , deterministic code scaffold from
|
|
646
654
|
// intent. Generates the typed contract + the decision logic (already executable) and leaves
|
|
647
655
|
// honest TODO markers for business logic. No AI. Prints, or writes with --out.
|
|
648
656
|
if (cmd === 'gen') {
|
|
@@ -653,7 +661,7 @@ function main() {
|
|
|
653
661
|
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
654
662
|
const code = generate(ast);
|
|
655
663
|
if (args.outExplicit) {
|
|
656
|
-
const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target;
|
|
664
|
+
const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target === 'python' ? 'py' : target === 'csharp' ? 'cs' : target;
|
|
657
665
|
const p = writeText(args.out, `${slug(subjectName(ast) || 'intent')}.${ext}`, code.endsWith('\n') ? code : code + '\n');
|
|
658
666
|
console.log(`wrote ${p.replace(process.cwd() + '/', '')}`);
|
|
659
667
|
return;
|
|
@@ -846,9 +854,9 @@ test Example
|
|
|
846
854
|
return;
|
|
847
855
|
}
|
|
848
856
|
|
|
849
|
-
// Single-file mode.
|
|
857
|
+
// Single-file mode. Auto-detect language by extension (consistent with --all / repo modes).
|
|
850
858
|
const src = readFileSync(file, 'utf8');
|
|
851
|
-
const res = liftSource(src, { language: args.from ||
|
|
859
|
+
const res = liftSource(src, { language: args.from || languageForFile(file), file: basename(file) });
|
|
852
860
|
if (!res.ok) { console.error(res.error); process.exit(1); }
|
|
853
861
|
if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
|
|
854
862
|
if (args.out) {
|
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.1';
|
|
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/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
|
@@ -113,7 +113,7 @@ export { FOCUS_SCHEMA, SCOPE_TYPES, FOCUS_REASONS, makeScope, buildFocusGraph, i
|
|
|
113
113
|
// Comprehension Contract , the C0..C7 understanding level (intent-comprehension-v1)
|
|
114
114
|
export { COMPREHENSION_SCHEMA, LEVELS as COMPREHENSION_LEVELS, comprehensionLevel, comprehensionReport } from './comprehension.mjs';
|
|
115
115
|
// Code generation , deterministic scaffolds from intent (intent-codegen-v1)
|
|
116
|
-
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava } from './codegen.mjs';
|
|
116
|
+
export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava, toPython } from './codegen.mjs';
|
|
117
117
|
// Change Lens , what a branch/PR changed by meaning (intent-changes-v1)
|
|
118
118
|
export { CHANGES_SCHEMA, changeReport } from './changes.mjs';
|
|
119
119
|
export { exprToJs, exprToCSharp, exprToJava, exprToCode } from './expr.mjs';
|
package/src/lift.mjs
CHANGED
|
@@ -393,6 +393,74 @@ export function extractFactsRuby(source, file = 'input.rb') {
|
|
|
393
393
|
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'ruby', sourceRoot: file, functions, tests, errors };
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
// Params written "name: Type" (Kotlin, Scala). Top-level comma split so generics/tuples
|
|
397
|
+
// like Map<String, Int> or (A, B) don't split mid-type. Strips val/var/vararg/implicit and defaults.
|
|
398
|
+
function parseNameColonTypeParams(raw) {
|
|
399
|
+
return splitTopLevel(raw, ',').map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
400
|
+
const cleaned = p.replace(/@\w+(\([^)]*\))?/g, '').replace(/^(?:val|var|vararg|implicit|final|lazy)\s+/, '').replace(/=.*$/, '').trim();
|
|
401
|
+
const mm = cleaned.match(/^([A-Za-z_]\w*)\s*:\s*(.+)$/);
|
|
402
|
+
if (mm) return { name: mm[1], type: mm[2].trim() };
|
|
403
|
+
return { name: cleaned.replace(/[^\w].*$/, '') || cleaned, type: null };
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Kotlin adapter (JVM: fun name(p: Type): Ret, @Test, *Exception) ──────────
|
|
408
|
+
export function extractFactsKotlin(source, file = 'input.kt') {
|
|
409
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
410
|
+
const testMethods = new Set(); const ta = /@Test\b[\s\S]{0,120}?\bfun\s+(?:`([^`]+)`|(\w+))\s*\(/g;
|
|
411
|
+
while ((m = ta.exec(source))) testMethods.add(m[1] || m[2]);
|
|
412
|
+
const fnRe = /\bfun\s+(?:<[^>]*>\s*)?(?:[A-Za-z_][\w.]*\.)?(?:`([^`]+)`|([A-Za-z_]\w*))\s*\(([^)]*)\)\s*(?::\s*([^{=\n]+))?/g;
|
|
413
|
+
while ((m = fnRe.exec(source))) {
|
|
414
|
+
const name = m[1] || m[2];
|
|
415
|
+
if (testMethods.has(name)) { if (!tests.some((t) => t.name === name)) tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
|
|
416
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
417
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters: parseNameColonTypeParams(m[3] || ''), returnType: m[4] ? m[4].trim() : null, evidence: [{ kind: 'fun', file, line: lineOf(source, m.index) }] });
|
|
418
|
+
}
|
|
419
|
+
const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
|
|
420
|
+
let mm; const ce = /class\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
|
|
421
|
+
const th = /throw\s+(\w+)\s*\(/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index); // Kotlin: no `new`
|
|
422
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'kotlin', sourceRoot: file, functions, tests, errors };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Scala adapter (def name(p: Type): Ret, ScalaTest, *Exception) ─────────────
|
|
426
|
+
export function extractFactsScala(source, file = 'input.scala') {
|
|
427
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
428
|
+
const fnRe = /\bdef\s+([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\(([^)]*)\)\s*(?::\s*([^={\n]+))?/g;
|
|
429
|
+
while ((m = fnRe.exec(source))) {
|
|
430
|
+
const name = m[1];
|
|
431
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
432
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters: parseNameColonTypeParams(m[2] || ''), returnType: m[3] ? m[3].trim() : null, evidence: [{ kind: 'def', file, line: lineOf(source, m.index) }] });
|
|
433
|
+
}
|
|
434
|
+
// ScalaTest: `test("desc")` (FunSuite) and `"desc" in { }` / `"desc" should`/`must` (WordSpec/FlatSpec).
|
|
435
|
+
const seenT = new Set(); const addTest = (n, idx) => { const k = String(n).toLowerCase(); if (n && !seenT.has(k)) { seenT.add(k); tests.push({ name: n, file, line: lineOf(source, idx) }); } };
|
|
436
|
+
const tr = /\btest\s*\(\s*"([^"]+)"/g; while ((m = tr.exec(source))) addTest(m[1], m.index);
|
|
437
|
+
const inRe = /"([^"]+)"\s+(?:in|should|must)\b/g; while ((m = inRe.exec(source))) addTest(m[1], m.index);
|
|
438
|
+
const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
|
|
439
|
+
let mm; const ce = /class\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
|
|
440
|
+
const th = /throw\s+new\s+(\w+)\s*\(/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index);
|
|
441
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'scala', sourceRoot: file, functions, tests, errors };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Elixir adapter (dynamic: def/defp name(args), ExUnit test, raise/defexception)
|
|
445
|
+
export function extractFactsElixir(source, file = 'input.ex') {
|
|
446
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
447
|
+
const seenT = new Set(); const addTest = (n, idx) => { const k = String(n).toLowerCase(); if (n && !seenT.has(k)) { seenT.add(k); tests.push({ name: n, file, line: lineOf(source, idx) }); } };
|
|
448
|
+
const tr = /\btest\s+"([^"]+)"/g; while ((m = tr.exec(source))) addTest(m[1], m.index); // ExUnit: test "desc" do
|
|
449
|
+
const fnRe = /^[ \t]*defp?\s+([a-z_]\w*[!?]?)\s*(?:\(([^)]*)\))?/gm;
|
|
450
|
+
while ((m = fnRe.exec(source))) {
|
|
451
|
+
const name = m[1];
|
|
452
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
453
|
+
// Elixir params are pattern matches (conn, %{id: id}, x \\ default); take the leading binding name.
|
|
454
|
+
const parameters = splitTopLevel(m[2] || '', ',').map((p) => p.trim()).filter(Boolean)
|
|
455
|
+
.map((p) => ({ name: p.split(/\\\\/)[0].trim().replace(/^%\{?/, '').replace(/[^\w].*$/, '') || p, type: null }));
|
|
456
|
+
functions.push({ name, file, line: lineOf(source, m.index), indent: (m[0].match(/^[ \t]*/) || [''])[0].length, parameters, returnType: null, evidence: [{ kind: 'def', file, line: lineOf(source, m.index) }] });
|
|
457
|
+
}
|
|
458
|
+
const errors = []; const seenErr = new Set(); const addErr = (n, idx) => { if (n && !seenErr.has(n)) { seenErr.add(n); errors.push({ name: n, file, line: lineOf(source, idx) }); } };
|
|
459
|
+
const modErr = /defmodule\s+([\w.]*(?:Error|Exception))\b/g; while ((m = modErr.exec(source))) addErr(m[1].split('.').pop(), m.index);
|
|
460
|
+
const raiseRe = /raise\s+([A-Z][\w.]*)/g; while ((m = raiseRe.exec(source))) addErr(m[1].split('.').pop(), m.index);
|
|
461
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'elixir', sourceRoot: file, functions, tests, errors };
|
|
462
|
+
}
|
|
463
|
+
|
|
396
464
|
const ADAPTERS = {
|
|
397
465
|
typescript: extractFactsTypeScript, ts: extractFactsTypeScript,
|
|
398
466
|
javascript: extractFactsTypeScript, js: extractFactsTypeScript,
|
|
@@ -405,13 +473,17 @@ const ADAPTERS = {
|
|
|
405
473
|
cpp: extractFactsCpp, 'c++': extractFactsCpp, c: extractFactsCpp, cc: extractFactsCpp,
|
|
406
474
|
php: extractFactsPhp,
|
|
407
475
|
ruby: extractFactsRuby, rb: extractFactsRuby,
|
|
476
|
+
kotlin: extractFactsKotlin, kt: extractFactsKotlin, kts: extractFactsKotlin,
|
|
477
|
+
scala: extractFactsScala, sc: extractFactsScala,
|
|
478
|
+
elixir: extractFactsElixir, ex: extractFactsElixir, exs: extractFactsElixir,
|
|
408
479
|
};
|
|
409
|
-
export const SUPPORTED_LANGUAGES = ['typescript', 'javascript', 'python', 'java', 'csharp', 'go', 'rust', 'cpp', 'php', 'ruby', 'perl'];
|
|
410
|
-
const DYNAMIC_LANGUAGES = new Set(['perl', 'javascript', 'python', 'ruby', 'php']);
|
|
480
|
+
export const SUPPORTED_LANGUAGES = ['typescript', 'javascript', 'python', 'java', 'csharp', 'go', 'rust', 'cpp', 'php', 'ruby', 'perl', 'kotlin', 'scala', 'elixir'];
|
|
481
|
+
const DYNAMIC_LANGUAGES = new Set(['perl', 'javascript', 'python', 'ruby', 'php', 'elixir']);
|
|
411
482
|
|
|
412
483
|
const LANG_DISPLAY = {
|
|
413
484
|
typescript: 'TypeScript', javascript: 'JavaScript', python: 'Python', java: 'Java',
|
|
414
485
|
csharp: 'C#', go: 'Go', rust: 'Rust', cpp: 'C++', php: 'PHP', ruby: 'Ruby', perl: 'Perl',
|
|
486
|
+
kotlin: 'Kotlin', scala: 'Scala', elixir: 'Elixir',
|
|
415
487
|
};
|
|
416
488
|
|
|
417
489
|
// Unwrap Result<T, E> / Promise<T> / T -> { output, error }
|
|
@@ -558,6 +630,9 @@ export function languageForFile(file) {
|
|
|
558
630
|
if (/\.(cpp|cc|cxx|hpp|hh|c|h)$/i.test(file)) return 'cpp';
|
|
559
631
|
if (/\.php$/i.test(file)) return 'php';
|
|
560
632
|
if (/\.rb$/i.test(file)) return 'ruby';
|
|
633
|
+
if (/\.kts?$/i.test(file)) return 'kotlin';
|
|
634
|
+
if (/\.(scala|sc)$/i.test(file)) return 'scala';
|
|
635
|
+
if (/\.exs?$/i.test(file)) return 'elixir';
|
|
561
636
|
if (/\.(mjs|cjs|jsx?)$/i.test(file)) return 'javascript';
|
|
562
637
|
return 'typescript';
|
|
563
638
|
}
|
|
@@ -577,7 +652,7 @@ function isPublicFn(fn, language) {
|
|
|
577
652
|
// Common private-helper naming conventions, across languages.
|
|
578
653
|
if (/(?:Internal|Impl|_impl|_helper|_test|Helper)$/.test(name)) return false;
|
|
579
654
|
if (language === 'go' || language === 'golang') return /^[A-Z]/.test(name) && name !== 'Test';
|
|
580
|
-
if (language === 'python' || language === 'ruby') return !name.startsWith('_') && (fn.indent == null || fn.indent <= 4);
|
|
655
|
+
if (language === 'python' || language === 'ruby' || language === 'elixir') return !name.startsWith('_') && (fn.indent == null || fn.indent <= 4);
|
|
581
656
|
return !name.startsWith('_') && name !== 'init' && name !== 'constructor';
|
|
582
657
|
}
|
|
583
658
|
|
|
@@ -643,6 +718,7 @@ export function liftRepo(files, { language } = {}) {
|
|
|
643
718
|
const LANG_EXT = {
|
|
644
719
|
typescript: 'ts', javascript: 'js', python: 'py', java: 'java', csharp: 'cs',
|
|
645
720
|
go: 'go', rust: 'rs', cpp: 'cpp', php: 'php', ruby: 'rb', perl: 'pl',
|
|
721
|
+
kotlin: 'kt', scala: 'scala', elixir: 'ex',
|
|
646
722
|
};
|
|
647
723
|
|
|
648
724
|
/**
|