@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/codegen.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// intent always produces the same code. It generates what the intent fully determines (typed
|
|
3
3
|
// interfaces, and the decision logic, which is already executable) and leaves honest TODO
|
|
4
4
|
// markers where a human must supply the business logic , never a fake implementation. This is
|
|
5
|
-
// the "see how it works, then change it" surface for the playground and `
|
|
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
8
|
// shape (a type map + a body walk) extends to C# / Java.
|
package/src/compile.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function renderMarkdown(ast) {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Render a mission as Markdown documentation for ONE audience, weaving that lens's
|
|
33
|
-
*
|
|
33
|
+
* ThunderLens notes inline next to the mission element they annotate. Notes explain
|
|
34
34
|
* meaning; they are never verification, and the doc says so. Pure and browser-safe.
|
|
35
35
|
*/
|
|
36
36
|
export function renderLensDoc(ast, lens) {
|
|
@@ -40,7 +40,7 @@ export function renderLensDoc(ast, lens) {
|
|
|
40
40
|
const notesFor = (path) => (ast.notes || []).filter((n) => n.lens === lens && n.targetPath === path);
|
|
41
41
|
const L = [];
|
|
42
42
|
L.push(`# ${m} , for the ${lens} reader`, '');
|
|
43
|
-
L.push(`>
|
|
43
|
+
L.push(`> ThunderLens \`${lens}\` notes are woven in below. They explain meaning for this`,
|
|
44
44
|
`> audience; they are documentation, not verification.`, '');
|
|
45
45
|
for (const nt of notesFor(prefix)) L.push(`_${flat(nt.text)}_`, '');
|
|
46
46
|
if (ast.goal) L.push(`**Goal.** ${ast.goal}`, '');
|
|
@@ -93,7 +93,7 @@ export function renderTestplan(ast) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Compile ThunderLang source in memory and return every artifact `
|
|
96
|
+
* Compile ThunderLang source in memory and return every artifact `thunder build`
|
|
97
97
|
* would emit, without touching the filesystem.
|
|
98
98
|
*/
|
|
99
99
|
export function compileSource(source, { sourceFile = 'playground.intent', generatedAt, origin = 'authored' } = {}) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Cross-target conformance. ThunderLang can't execute generated TypeScript/Python, so it does
|
|
2
|
+
// the honest thing: the deterministic engine defines the canonical contract (what every target
|
|
3
|
+
// must produce for each test case), and target outputs fed via results are graded against it.
|
|
4
|
+
// A target case that diverges is a CONFORMANCE FAILURE. Same tests, every implementation.
|
|
5
|
+
import { runTests } from './testing.mjs';
|
|
6
|
+
|
|
7
|
+
export function buildConformance(ast, { targets = [], results = null } = {}) {
|
|
8
|
+
const sem = runTests(ast);
|
|
9
|
+
const cases = (sem.results || []).map((r) => ({
|
|
10
|
+
key: `${r.target} / ${r.case}`, test: r.target, case: r.case,
|
|
11
|
+
inputs: r.inputs || {}, expected: r.expected, semantic: r.actual, semanticPass: !!r.pass,
|
|
12
|
+
}));
|
|
13
|
+
const columns = (targets.length ? targets : (ast.targets || [])).map((t) => String(t).toLowerCase());
|
|
14
|
+
const rows = cases.map((c) => {
|
|
15
|
+
const t = {};
|
|
16
|
+
for (const col of columns) {
|
|
17
|
+
const tr = results && results[col];
|
|
18
|
+
if (!tr || !(c.key in tr)) { t[col] = { status: 'declared' }; continue; }
|
|
19
|
+
const actual = tr[c.key];
|
|
20
|
+
t[col] = { status: String(actual) === String(c.expected) ? 'pass' : 'fail', actual };
|
|
21
|
+
}
|
|
22
|
+
return { ...c, targets: t };
|
|
23
|
+
});
|
|
24
|
+
const failures = [];
|
|
25
|
+
for (const row of rows) for (const col of columns) {
|
|
26
|
+
const tr = row.targets[col];
|
|
27
|
+
if (tr.status === 'fail') failures.push({ target: col, case: row.key, expected: row.expected, actual: tr.actual });
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
schema: 'thunder-conformance-v1', mission: ast.mission,
|
|
31
|
+
total: cases.length, columns,
|
|
32
|
+
semanticFailures: cases.filter((c) => !c.semanticPass).length,
|
|
33
|
+
graded: !!results, failures, cases: rows,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/coverage.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Semantic coverage , meaning-level metrics, not line coverage. Answers "which goals, decision
|
|
2
|
+
// rules, guarantees, prohibitions, scenarios, and targets are actually exercised by the tests?"
|
|
3
|
+
// so "12 of 15 prohibitions challenged" is far more useful than "82% line coverage".
|
|
4
|
+
import { runTests } from './testing.mjs';
|
|
5
|
+
|
|
6
|
+
const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
7
|
+
|
|
8
|
+
export function semanticCoverage(ast) {
|
|
9
|
+
const t = runTests(ast);
|
|
10
|
+
const metrics = [];
|
|
11
|
+
const unverified = [];
|
|
12
|
+
const add = (name, covered, total, missing = []) => {
|
|
13
|
+
metrics.push({ name, covered, total, pct: total ? Math.round((covered / total) * 100) : 100 });
|
|
14
|
+
for (const m of missing) unverified.push(m);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Goal
|
|
18
|
+
if (ast.mission) add('Goals', ast.goal ? 1 : 0, 1, ast.goal ? [] : ['goal , the mission declares no goal']);
|
|
19
|
+
|
|
20
|
+
// Decision rules matched by at least one test case (branch coverage for decisions)
|
|
21
|
+
const matched = new Set((t.results || []).map((r) => r.matched).filter(Boolean));
|
|
22
|
+
const rules = (ast.decisions || []).flatMap((d) => (d.rules || []).filter((r) => r.name).map((r) => ({ dec: d.name, name: r.name })));
|
|
23
|
+
add('Decision rules', rules.filter((r) => matched.has(r.name)).length, rules.length,
|
|
24
|
+
rules.filter((r) => !matched.has(r.name)).map((r) => `rule ${r.dec}/${r.name} , never matched by a test`));
|
|
25
|
+
|
|
26
|
+
// Guarantees that carry a verification
|
|
27
|
+
const g = ast.guarantees || [];
|
|
28
|
+
add('Guarantees', g.filter((x) => (x.verify || []).length).length, g.length,
|
|
29
|
+
g.filter((x) => !(x.verify || []).length).map((x) => `guarantee ${x.id} , no verification`));
|
|
30
|
+
|
|
31
|
+
// Prohibitions challenged (a `never` with a verification)
|
|
32
|
+
const n = ast.neverRules || [];
|
|
33
|
+
add('Prohibitions', n.filter((x) => (x.verify || []).length).length, n.length,
|
|
34
|
+
n.filter((x) => !(x.verify || []).length).map((x) => `never ${x.id} , not challenged`));
|
|
35
|
+
|
|
36
|
+
// Scenarios that are self-consistent (not both expected and prohibited)
|
|
37
|
+
const sc = ast.scenarios || [];
|
|
38
|
+
if (sc.length) {
|
|
39
|
+
const consistent = sc.filter((s) => {
|
|
40
|
+
const nv = new Set((s.never || []).map(norm));
|
|
41
|
+
return ![...(s.then || []), ...(s.eventually || []).flatMap((e) => e.clauses)].some((p) => nv.has(norm(p)));
|
|
42
|
+
});
|
|
43
|
+
add('Scenarios', consistent.length, sc.length, sc.filter((s) => !consistent.includes(s)).map((s) => `scenario ${s.name} , self-contradictory`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Targets exercised by conformance (needs `thunder conform --results`; 0 until then)
|
|
47
|
+
const tg = ast.targets || [];
|
|
48
|
+
if (tg.length) add('Targets tested', 0, tg.length, tg.map((x) => `target ${x} , not conformance-tested`));
|
|
49
|
+
|
|
50
|
+
const totalCovered = metrics.reduce((a, m) => a + m.covered, 0);
|
|
51
|
+
const totalAll = metrics.reduce((a, m) => a + m.total, 0);
|
|
52
|
+
return {
|
|
53
|
+
schema: 'thunder-coverage-v1', mission: ast.mission, metrics, unverified,
|
|
54
|
+
overall: totalAll ? Math.round((totalCovered / totalAll) * 100) : 100,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/drift.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// still matches the approved intent. Deterministic, no AI. This is the
|
|
3
3
|
// compiler-side drift check; OpenThunder does the deeper repo-wide version later.
|
|
4
4
|
//
|
|
5
|
-
// lift code -> .intent draft ->
|
|
5
|
+
// lift code -> .intent draft -> thunder approve -> (code changes) -> intent drift
|
|
6
6
|
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { parseIntent, slug } from './parse.mjs';
|
|
@@ -45,7 +45,7 @@ export function approveIntent(intentText, { approvedBy, approvedAt } = {}) {
|
|
|
45
45
|
.replace(/^(\s*)reviewed\s+false\b/m, '$1reviewed true')
|
|
46
46
|
.replace(/\s+$/, '');
|
|
47
47
|
// Hash the exact content that gets written (post reviewed-flip), so a later
|
|
48
|
-
// `
|
|
48
|
+
// `thunder drift` recomputes the same hash when nothing has changed.
|
|
49
49
|
const hash = sha256(base.trim());
|
|
50
50
|
const lines = ['approval', ' reviewed true'];
|
|
51
51
|
if (approvedBy) lines.push(` approved_by ${approvedBy}`);
|
|
@@ -137,7 +137,7 @@ export function checkDrift(intentText, codeSource, { language = 'typescript' } =
|
|
|
137
137
|
add('warning', 'INTENT_DRIFT_STALE_PROOF', 'The approved intent was edited after approval (hash mismatch). Re-approve it.');
|
|
138
138
|
}
|
|
139
139
|
} else {
|
|
140
|
-
add('info', 'INTENT_DRIFT_NOT_APPROVED', 'This intent has no approval block. Approve it first:
|
|
140
|
+
add('info', 'INTENT_DRIFT_NOT_APPROVED', 'This intent has no approval block. Approve it first: thunder approve <file>.');
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
const lift = liftSource(codeSource, { language });
|
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.2.0';
|
|
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.
|
|
@@ -188,7 +188,7 @@ export function semanticDiagnostics(ast) {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// ──
|
|
191
|
+
// ── ThunderLens note checks (understanding, never verification) ──
|
|
192
192
|
for (const note of ast.notes || []) {
|
|
193
193
|
if (!KNOWN_LENSES.includes(note.lens)) {
|
|
194
194
|
warn('INTENT_NOTE_UNKNOWN_LENS',
|
|
@@ -245,7 +245,7 @@ export function semanticDiagnostics(ast) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
// ── Product / intent-graph diagnostics (intent-graph-v1) ──
|
|
248
|
-
// Role-aware. Kept at warning/info so `
|
|
248
|
+
// Role-aware. Kept at warning/info so `thunder check` stays valid; `severity` +
|
|
249
249
|
// `blocks` drive phase gates (a valid spec can still be not-ready-to-proceed).
|
|
250
250
|
for (const m of ast.metrics || []) {
|
|
251
251
|
if (!m.window) d.push({
|
|
@@ -494,11 +494,13 @@ export function buildProof(ast, { sourceFile, sourceHash, targetsRequested, targ
|
|
|
494
494
|
id: g.id, text: g.statement,
|
|
495
495
|
status: g.verify.length > 0 || verifiedText ? 'planned' : 'needs_verification',
|
|
496
496
|
evidence: g.verify,
|
|
497
|
+
verifications: g.verifications || [],
|
|
497
498
|
})),
|
|
498
499
|
neverRules: ast.neverRules.map((n) => ({
|
|
499
500
|
id: n.id, text: n.statement,
|
|
500
501
|
status: n.verify.length > 0 ? 'planned' : 'needs_verification',
|
|
501
502
|
evidence: n.verify,
|
|
503
|
+
verifications: n.verifications || [],
|
|
502
504
|
})),
|
|
503
505
|
errors: (ast.errors || []).map((e) => ({ name: e.name })),
|
|
504
506
|
examples: (ast.examples || []).map((ex) => ({ given: ex.given, expect: ex.expect })),
|
package/src/expr.mjs
CHANGED
|
@@ -203,19 +203,22 @@ export function evalExpr(src, inputs = {}) {
|
|
|
203
203
|
|
|
204
204
|
// Per-language rendering of the `when` grammar. `eq`/`neq` build an equality expression (JS uses
|
|
205
205
|
// ===, C# ==, Java Objects.equals for correct string value equality); `inList` builds membership.
|
|
206
|
+
// C-family boolean/logic defaults (js, csharp, java share these).
|
|
207
|
+
const C_LOGIC = { and: (a, b) => `(${a} && ${b})`, or: (a, b) => `(${a} || ${b})`, not: (a) => `!${a}`, bool: (v) => String(v) };
|
|
206
208
|
const DIALECTS = {
|
|
207
|
-
js: { eq: (a, b) => `${a} === ${b}`, neq: (a, b) => `${a} !== ${b}`, inList: (list, x) => `[${list.join(', ')}].includes(${x})`, nil: 'undefined' },
|
|
208
|
-
csharp: { eq: (a, b) => `${a} == ${b}`, neq: (a, b) => `${a} != ${b}`, inList: (list, x) => `new[]{${list.join(', ')}}.Contains(${x})`, nil: 'null' },
|
|
209
|
-
java: { eq: (a, b) => `java.util.Objects.equals(${a}, ${b})`, neq: (a, b) => `!java.util.Objects.equals(${a}, ${b})`, inList: (list, x) => `java.util.List.of(${list.join(', ')}).contains(${x})`, nil: 'null' },
|
|
209
|
+
js: { ...C_LOGIC, eq: (a, b) => `${a} === ${b}`, neq: (a, b) => `${a} !== ${b}`, inList: (list, x) => `[${list.join(', ')}].includes(${x})`, nil: 'undefined' },
|
|
210
|
+
csharp: { ...C_LOGIC, eq: (a, b) => `${a} == ${b}`, neq: (a, b) => `${a} != ${b}`, inList: (list, x) => `new[]{${list.join(', ')}}.Contains(${x})`, nil: 'null' },
|
|
211
|
+
java: { ...C_LOGIC, eq: (a, b) => `java.util.Objects.equals(${a}, ${b})`, neq: (a, b) => `!java.util.Objects.equals(${a}, ${b})`, inList: (list, x) => `java.util.List.of(${list.join(', ')}).contains(${x})`, nil: 'null' },
|
|
212
|
+
python: { and: (a, b) => `(${a} and ${b})`, or: (a, b) => `(${a} or ${b})`, not: (a) => `(not ${a})`, bool: (v) => (v ? 'True' : 'False'), eq: (a, b) => `${a} == ${b}`, neq: (a, b) => `${a} != ${b}`, inList: (list, x) => `${x} in [${list.join(', ')}]`, nil: 'None' },
|
|
210
213
|
};
|
|
211
214
|
|
|
212
215
|
function renderExpr(src, inputs, D) {
|
|
213
216
|
const known = new Set(inputs);
|
|
214
217
|
const r = (n) => {
|
|
215
218
|
switch (n.k) {
|
|
216
|
-
case 'or': return
|
|
217
|
-
case 'and': return
|
|
218
|
-
case 'not': return
|
|
219
|
+
case 'or': return D.or(r(n.a), r(n.b));
|
|
220
|
+
case 'and': return D.and(r(n.a), r(n.b));
|
|
221
|
+
case 'not': return D.not(r(n.a));
|
|
219
222
|
case 'neg': return `-${r(n.a)}`;
|
|
220
223
|
case 'arith': return `(${r(n.a)} ${n.op} ${r(n.b)})`;
|
|
221
224
|
case 'cmp':
|
|
@@ -224,7 +227,7 @@ function renderExpr(src, inputs, D) {
|
|
|
224
227
|
return `(${r(n.a)} ${n.op} ${r(n.b)})`;
|
|
225
228
|
case 'in': return D.inList(n.list.map(r), r(n.a));
|
|
226
229
|
case 'list': return `[${n.items.map(r).join(', ')}]`;
|
|
227
|
-
case 'lit': return typeof n.v === 'string' ? JSON.stringify(n.v) : String(n.v);
|
|
230
|
+
case 'lit': return typeof n.v === 'string' ? JSON.stringify(n.v) : typeof n.v === 'boolean' ? D.bool(n.v) : String(n.v);
|
|
228
231
|
case 'ref': return (known.has(n.path) || known.has(n.path.split('.')[0])) ? n.path : JSON.stringify(n.path);
|
|
229
232
|
default: return D.nil;
|
|
230
233
|
}
|
|
@@ -243,3 +246,4 @@ export function exprToCode(src, { inputs = [], dialect = 'js' } = {}) {
|
|
|
243
246
|
export const exprToJs = (src, opts = {}) => exprToCode(src, { ...opts, dialect: 'js' });
|
|
244
247
|
export const exprToCSharp = (src, opts = {}) => exprToCode(src, { ...opts, dialect: 'csharp' });
|
|
245
248
|
export const exprToJava = (src, opts = {}) => exprToCode(src, { ...opts, dialect: 'java' });
|
|
249
|
+
export const exprToPython = (src, opts = {}) => exprToCode(src, { ...opts, dialect: 'python' });
|
package/src/focus.mjs
CHANGED
|
@@ -33,7 +33,7 @@ function fingerprint(parts) {
|
|
|
33
33
|
|
|
34
34
|
/** Build a typed Intent Scope. `createdAt` is supplied by the caller (deterministic/testable). */
|
|
35
35
|
export function makeScope({ type = 'custom', title = null, seeds = [], projectId = null, createdBy = null, createdAt = null, ...rest } = {}) {
|
|
36
|
-
if (!SCOPE_TYPES.includes(type)) throw new Error(`
|
|
36
|
+
if (!SCOPE_TYPES.includes(type)) throw new Error(`thunder focus: unknown scope type "${type}"`);
|
|
37
37
|
return {
|
|
38
38
|
schema: FOCUS_SCHEMA,
|
|
39
39
|
scopeId: `scope.${type}.${fingerprint([type, ...seeds].map(String))}`,
|
package/src/guard.mjs
CHANGED
|
@@ -65,7 +65,7 @@ export function buildGuard(ast, { denyResults, mask = '[redacted]' } = {}) {
|
|
|
65
65
|
|
|
66
66
|
function decide(name, inputs) {
|
|
67
67
|
const d = decisions.get(name);
|
|
68
|
-
if (!d) throw new Error(`
|
|
68
|
+
if (!d) throw new Error(`thunder guard: no decision "${name}"`);
|
|
69
69
|
const r = evaluateDecision(d, inputs || {});
|
|
70
70
|
return { ...r, allowed: !isDenied(r.result) };
|
|
71
71
|
}
|
|
@@ -73,7 +73,7 @@ export function buildGuard(ast, { denyResults, mask = '[redacted]' } = {}) {
|
|
|
73
73
|
function assertAllowed(name, inputs) {
|
|
74
74
|
const r = decide(name, inputs);
|
|
75
75
|
if (!r.allowed) {
|
|
76
|
-
const e = new Error(`
|
|
76
|
+
const e = new Error(`thunder guard: decision "${name}" denied the action (result: ${r.result})`);
|
|
77
77
|
e.code = 'INTENT_GUARD_DENIED';
|
|
78
78
|
e.decision = name;
|
|
79
79
|
e.result = r.result;
|
|
@@ -98,7 +98,7 @@ export function compileGuard(intentText, opts) {
|
|
|
98
98
|
return buildGuard(parseIntent(String(intentText ?? '')), opts);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/** A JSON-able summary of what a guard would enforce (for `
|
|
101
|
+
/** A JSON-able summary of what a guard would enforce (for `thunder guard` / audits). */
|
|
102
102
|
export function guardSummary(ast) {
|
|
103
103
|
const g = buildGuard(ast);
|
|
104
104
|
return { schema: GUARD_SCHEMA, redactsFields: g.secretFields, enforcesDecisions: g.decisions, neverRules: g.neverRules };
|
package/src/intellisense.mjs
CHANGED
|
@@ -129,7 +129,7 @@ export function getHover(source, position = {}) {
|
|
|
129
129
|
return {
|
|
130
130
|
hover: {
|
|
131
131
|
target: lens, kind: 'note_lens', title: `note ${lens}`,
|
|
132
|
-
description: LENS_INFO[lens] || 'An
|
|
132
|
+
description: LENS_INFO[lens] || 'An ThunderLens reader lens.',
|
|
133
133
|
examples: [], relatedSuggestions: [],
|
|
134
134
|
},
|
|
135
135
|
};
|
package/src/intent-graph.mjs
CHANGED
|
@@ -344,3 +344,32 @@ export function buildIntentGraph(ast) {
|
|
|
344
344
|
},
|
|
345
345
|
};
|
|
346
346
|
}
|
|
347
|
+
|
|
348
|
+
// Data classifications that must never be shown in an external / display context.
|
|
349
|
+
export const SENSITIVE_CLASSIFICATIONS = new Set([
|
|
350
|
+
'confidential', 'pii', 'sensitive', 'phi', 'restricted', 'personal', 'secret',
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
// A display-safe projection of an intent graph for external rendering (e.g. STT's viewer).
|
|
354
|
+
// Keeps the structural intent (ids, types, titles, statuses, relationships, classification LABEL)
|
|
355
|
+
// but drops owner/source provenance and redacts free text on any node with a sensitive
|
|
356
|
+
// classification. The intent graph carries no source code; data elements are governed separately
|
|
357
|
+
// and are not emitted as graph nodes, so they cannot leak through this projection.
|
|
358
|
+
export function safeGraph(graph) {
|
|
359
|
+
let redactedNodes = 0;
|
|
360
|
+
const nodes = (graph.nodes || []).map((n) => {
|
|
361
|
+
const sensitive = SENSITIVE_CLASSIFICATIONS.has(String(n.classification || '').toLowerCase());
|
|
362
|
+
const out = {
|
|
363
|
+
id: n.id, type: n.type, title: n.title ?? null, status: n.status ?? null,
|
|
364
|
+
classification: n.classification ?? null, tags: n.tags ?? [],
|
|
365
|
+
description: sensitive ? null : (n.description ?? null),
|
|
366
|
+
};
|
|
367
|
+
if (sensitive) { out.redacted = true; redactedNodes++; }
|
|
368
|
+
return out; // owner + source intentionally omitted
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
schema: graph.schema, missionId: graph.missionId,
|
|
372
|
+
safe: true, redactedNodes,
|
|
373
|
+
nodes, relationships: graph.relationships || [], summary: graph.summary,
|
|
374
|
+
};
|
|
375
|
+
}
|
package/src/intent-schema.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// enums / diagnostics (gap-closure program non-negotiable #1). Pure (no Node deps).
|
|
4
4
|
//
|
|
5
5
|
// IL owns + versions this. OT/RM/ST/STW import these enums + the JSON Schema instead
|
|
6
|
-
// of maintaining their own copies. `
|
|
6
|
+
// of maintaining their own copies. `thunder schema` emits the JSON Schema.
|
|
7
7
|
|
|
8
8
|
import { CLASSIFICATIONS, CONFIDENCE } from './classification.mjs';
|
|
9
9
|
|
|
@@ -134,7 +134,7 @@ const IL_AUTHOR_RULE_ROWS = [
|
|
|
134
134
|
{ ruleId: 'IL-SEC-002', area: 'security', severity: 'blocker', blocks: ['release'], summary: 'API returns a secret with no auth requirement.' },
|
|
135
135
|
{ ruleId: 'IL-TYPE-001', area: 'type', severity: 'info', blocks: [], summary: 'Field uses an unrecognized (likely mistyped) type.' },
|
|
136
136
|
// 12-Factor Agents conformance (twelve-factor-v1). Advisory: an intent's alignment with the
|
|
137
|
-
// humanlayer/12-factor-agents principles. `
|
|
137
|
+
// humanlayer/12-factor-agents principles. `thunder twelve-factor` scores these.
|
|
138
138
|
{ ruleId: 'IL-12F-01', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F1 Natural language to tool calls: model dispatch as a decision/command.' },
|
|
139
139
|
{ ruleId: 'IL-12F-02', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F2 Own your prompts: behavior is an owned, guaranteed contract, not a black box.' },
|
|
140
140
|
{ ruleId: 'IL-12F-03', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F3 Own your context window: declare a scope boundary.' },
|
|
@@ -165,8 +165,8 @@ const IL_CORE_RULE_ROWS = [
|
|
|
165
165
|
{ ruleId: 'unknown-block', area: 'core', severity: 'info', blocks: [], summary: 'Unrecognized top-level block.' },
|
|
166
166
|
{ ruleId: 'INTENT-ARCH-001', area: 'architecture', severity: 'warning', blocks: [], summary: 'Architecture rule not understood.' },
|
|
167
167
|
{ ruleId: 'INTENT-AI-010', area: 'ai', severity: 'warning', blocks: [], summary: 'Unsupported implementation scope.' },
|
|
168
|
-
{ ruleId: 'INTENT_NOTE_UNKNOWN_LENS', area: 'note', severity: 'info', blocks: [], summary: '
|
|
169
|
-
{ ruleId: 'INTENT_NOTE_EMPTY', area: 'note', severity: 'info', blocks: [], summary: '
|
|
168
|
+
{ ruleId: 'INTENT_NOTE_UNKNOWN_LENS', area: 'note', severity: 'info', blocks: [], summary: 'ThunderLens note uses an unknown lens.' },
|
|
169
|
+
{ ruleId: 'INTENT_NOTE_EMPTY', area: 'note', severity: 'info', blocks: [], summary: 'ThunderLens note is empty.' },
|
|
170
170
|
];
|
|
171
171
|
|
|
172
172
|
// ── Rule namespaces: ONE id space across two PHASES ──────────────────────────
|
package/src/ledger.mjs
CHANGED
|
@@ -40,7 +40,7 @@ function payloadOf(seq, entry, prev) {
|
|
|
40
40
|
|
|
41
41
|
/** Append an entry to the ledger, hash-chained over the previous head. Returns a NEW ledger. */
|
|
42
42
|
export function record(ledger, entry) {
|
|
43
|
-
if (!entry || !ENTRY_TYPES.includes(entry.type)) throw new Error(`
|
|
43
|
+
if (!entry || !ENTRY_TYPES.includes(entry.type)) throw new Error(`thunder ledger: unknown entry type "${entry?.type}"`);
|
|
44
44
|
const base = ledger && Array.isArray(ledger.entries) ? ledger : emptyLedger();
|
|
45
45
|
const payload = payloadOf(base.entries.length, entry, base.head);
|
|
46
46
|
const hash = sha256(JSON.stringify(payload));
|
package/src/lift.mjs
CHANGED
|
@@ -546,7 +546,7 @@ function liftDiagnostics(lift, facts) {
|
|
|
546
546
|
* Lift a set of source files (a repo) into inferred ThunderLang drafts, one per
|
|
547
547
|
* file that yields a mission. `files` is [{ file, source }] (the CLI reads the
|
|
548
548
|
* filesystem; this core function stays pure). Returns per-mission drafts + a
|
|
549
|
-
* repo-level summary matching the `
|
|
549
|
+
* repo-level summary matching the `thunder lift --from repo --json` contract.
|
|
550
550
|
*/
|
|
551
551
|
export function languageForFile(file) {
|
|
552
552
|
if (/\.rs$/i.test(file)) return 'rust';
|
|
@@ -651,7 +651,7 @@ const LANG_EXT = {
|
|
|
651
651
|
* EXACT node ids instead of lift's own function refs, so there is no divergent second reading.
|
|
652
652
|
* Additive: with no seeds the output is byte-identical to before.
|
|
653
653
|
*/
|
|
654
|
-
export function liftSource(source, { language = 'typescript', file = '', seeds } = {}) {
|
|
654
|
+
export function liftSource(source, { language = 'typescript', file = '', seeds = undefined } = {}) {
|
|
655
655
|
const key = String(language).toLowerCase();
|
|
656
656
|
const adapter = ADAPTERS[key];
|
|
657
657
|
if (!adapter) {
|
package/src/lsp.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A minimal Language Server for ThunderLang (LSP over stdio). Wraps the compiler's existing
|
|
2
2
|
// diagnostics + IntelliSense so ANY LSP-capable editor (VS Code, Neovim, Helix, ...) gets
|
|
3
3
|
// live ThunderLang intelligence: diagnostics on open/change, keyword/type completions, and
|
|
4
|
-
// hover docs for semantic types and note lenses. Deterministic; no AI. `
|
|
4
|
+
// hover docs for semantic types and note lenses. Deterministic; no AI. `thunder lsp` starts it.
|
|
5
5
|
|
|
6
6
|
import { parseIntent } from './parse.mjs';
|
|
7
7
|
import { semanticDiagnostics } from './emit.mjs';
|
package/src/mcp.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// check its own output, and , the keystone , verify a proposed code change against the intent
|
|
5
5
|
// before it ships. No AI runs here; every tool is the pure, deterministic compiler.
|
|
6
6
|
//
|
|
7
|
-
// Start it with `
|
|
7
|
+
// Start it with `thunder mcp`. Point an MCP client at that command.
|
|
8
8
|
|
|
9
9
|
import { parseIntent } from './parse.mjs';
|
|
10
10
|
import { semanticDiagnostics, COMPILER_VERSION } from './emit.mjs';
|
package/src/mutate.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Mutation testing for ThunderLang decisions. Inject small, deterministic faults into a
|
|
2
|
+
// decision's rules, re-run the in-file tests, and check whether they catch the change. A mutant
|
|
3
|
+
// that no test detects (SURVIVED) marks a weak spot , tests that pass but do not protect the
|
|
4
|
+
// system, exactly the failure mode of AI-authored tests.
|
|
5
|
+
import { runTests } from './testing.mjs';
|
|
6
|
+
|
|
7
|
+
const FLIP = { '>=': '<', '<=': '>', '>': '<=', '<': '>=', '==': '!=', '!=': '==' };
|
|
8
|
+
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
9
|
+
const withDecisions = (ast, decisions) => ({ ...ast, decisions });
|
|
10
|
+
const passingSet = (ast) => new Set(runTests(ast).results.filter((x) => x.pass).map((x) => `${x.target}/${x.case}`));
|
|
11
|
+
|
|
12
|
+
// Generate one mutant per (rule, operator). Each returns a mutated AST plus a description.
|
|
13
|
+
function mutants(ast) {
|
|
14
|
+
const out = [];
|
|
15
|
+
const decs = ast.decisions || [];
|
|
16
|
+
decs.forEach((dec, di) => {
|
|
17
|
+
(dec.rules || []).forEach((rule, ri) => {
|
|
18
|
+
const tag = rule.name || `rule ${ri + 1}`;
|
|
19
|
+
if (rule.when) {
|
|
20
|
+
const m = rule.when.match(/(>=|<=|==|!=|>|<)/);
|
|
21
|
+
if (m) { const md = clone(decs); md[di].rules[ri].when = rule.when.replace(m[1], FLIP[m[1]]); out.push({ id: `${dec.name}/${tag}/flip`, describe: `flip ${m[1]} in ${tag} of ${dec.name}`, ast: withDecisions(ast, md) }); }
|
|
22
|
+
if (/\band\b/.test(rule.when)) { const md = clone(decs); md[di].rules[ri].when = rule.when.replace(/\band\b/, 'or'); out.push({ id: `${dec.name}/${tag}/and-or`, describe: `and -> or in ${tag} of ${dec.name}`, ast: withDecisions(ast, md) }); }
|
|
23
|
+
}
|
|
24
|
+
if (rule.result != null && dec.default != null && rule.result !== dec.default) {
|
|
25
|
+
const md = clone(decs); md[di].rules[ri].result = dec.default;
|
|
26
|
+
out.push({ id: `${dec.name}/${tag}/swap-return`, describe: `${tag} returns the default instead of ${rule.result}`, ast: withDecisions(ast, md) });
|
|
27
|
+
}
|
|
28
|
+
{ const md = clone(decs); md[di].rules.splice(ri, 1); out.push({ id: `${dec.name}/${tag}/remove`, describe: `remove ${tag} from ${dec.name}`, ast: withDecisions(ast, md) }); }
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runMutations(ast) {
|
|
35
|
+
const base = passingSet(ast);
|
|
36
|
+
if (base.size === 0) return { schema: 'thunder-mutation-v1', total: 0, killed: 0, survived: 0, score: null, note: 'no passing decision tests to mutate against', results: [] };
|
|
37
|
+
const results = mutants(ast).map((mu) => {
|
|
38
|
+
const now = passingSet(mu.ast);
|
|
39
|
+
// Killed if any test that passed on the original now fails on the mutant.
|
|
40
|
+
const killed = [...base].some((k) => !now.has(k));
|
|
41
|
+
return { id: mu.id, describe: mu.describe, killed };
|
|
42
|
+
});
|
|
43
|
+
const killed = results.filter((r) => r.killed).length;
|
|
44
|
+
const survived = results.length - killed;
|
|
45
|
+
return { schema: 'thunder-mutation-v1', total: results.length, killed, survived, score: results.length ? Math.round((killed / results.length) * 100) : null, results };
|
|
46
|
+
}
|
package/src/parse.mjs
CHANGED
|
@@ -86,7 +86,7 @@ function buildTree(rows) {
|
|
|
86
86
|
return root.children;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// Known
|
|
89
|
+
// Known ThunderLens reader lenses. Unknown lenses warn (INTENT_NOTE_UNKNOWN_LENS).
|
|
90
90
|
export const KNOWN_LENSES = [
|
|
91
91
|
'pm', 'beginner', 'qa', 'risk', 'security', 'support', 'reviewer', 'ops', 'non_goal', 'term',
|
|
92
92
|
];
|
|
@@ -114,7 +114,7 @@ const kvChildren = (node) => {
|
|
|
114
114
|
const childBlock = (node, kw) => node.children.find((c) => firstWord(c.text) === kw);
|
|
115
115
|
|
|
116
116
|
function parseFields(node) {
|
|
117
|
-
// "key: Type" lines. A field may carry indented modifiers and
|
|
117
|
+
// "key: Type" lines. A field may carry indented modifiers and ThunderLens notes.
|
|
118
118
|
return node.children.map((c) => {
|
|
119
119
|
const m = c.text.match(/^([A-Za-z_][\w]*)\s*:\s*(.+)$/);
|
|
120
120
|
const modifiers = [];
|
|
@@ -181,6 +181,59 @@ function parseDecision(name, node) {
|
|
|
181
181
|
return dec;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Property-based test: `property Name` + forAll (typed vars with `where` constraints) +
|
|
185
|
+
// `decide <Decision>` + expect clauses. The runner generates many cases and shrinks failures.
|
|
186
|
+
function parseProperty(name, node) {
|
|
187
|
+
const prop = { name, vars: [], decide: null, expects: [], line: node.line };
|
|
188
|
+
for (const c of node.children.filter((x) => !isNote(x))) {
|
|
189
|
+
const k = firstWord(c.text); const a = rest(c.text);
|
|
190
|
+
if (k === 'forAll' || k === 'forall') {
|
|
191
|
+
for (const v of c.children.filter((x) => !isNote(x))) {
|
|
192
|
+
const m = v.text.trim().match(/^([A-Za-z_]\w*)\s*:\s*(\w+)(?:\s+where\s+(.+))?$/);
|
|
193
|
+
if (!m) continue;
|
|
194
|
+
const whereChild = (v.children || []).find((x) => firstWord(x.text) === 'where');
|
|
195
|
+
prop.vars.push({ name: m[1], type: m[2], where: m[3] || (whereChild ? rest(whereChild.text) : null) });
|
|
196
|
+
}
|
|
197
|
+
} else if (k === 'decide') prop.decide = a || (c.children[0] && c.children[0].text.trim()) || null;
|
|
198
|
+
else if (k === 'when') { const dm = a.match(/(?:decide|execute)\s+(\w+)/); if (dm) prop.decide = dm[1]; }
|
|
199
|
+
else if (k === 'expect') for (const e of c.children.filter((x) => !isNote(x))) prop.expects.push(e.text.trim());
|
|
200
|
+
}
|
|
201
|
+
return prop;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// AI evaluation: probabilistic behavior graded against a dataset by metric thresholds,
|
|
205
|
+
// not a binary run. `evaluation <Name>` + `dataset <id>` + `require` (metric op threshold).
|
|
206
|
+
function parseEvaluation(name, node) {
|
|
207
|
+
const ev = { name, dataset: null, requires: [], line: node.line };
|
|
208
|
+
for (const c of node.children.filter((x) => !isNote(x))) {
|
|
209
|
+
const k = firstWord(c.text); const a = rest(c.text);
|
|
210
|
+
if (k === 'dataset') ev.dataset = a || (c.children[0] && c.children[0].text.trim()) || null;
|
|
211
|
+
else if (k === 'require') {
|
|
212
|
+
for (const r of c.children.filter((x) => !isNote(x))) {
|
|
213
|
+
const m = r.text.trim().match(/^([A-Za-z_][\w.]*)\s*(>=|<=|==|!=|>|<)\s*(-?[\d.]+)$/);
|
|
214
|
+
if (m) ev.requires.push({ metric: m[1], op: m[2], threshold: parseFloat(m[3]) });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return ev;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Scenario: a workflow-level test , given / when / then / never (+ eventually within <t>).
|
|
222
|
+
// Deterministically checkable for self-contradiction (a `then` also listed under `never`).
|
|
223
|
+
function parseScenario(name, node) {
|
|
224
|
+
const sc = { name, given: [], when: [], then: [], never: [], eventually: [], line: node.line };
|
|
225
|
+
const clausesOf = (c) => { const items = leafItems(c); return items.length ? items : (rest(c.text) ? [rest(c.text)] : []); };
|
|
226
|
+
for (const c of node.children.filter((x) => !isNote(x))) {
|
|
227
|
+
const k = firstWord(c.text);
|
|
228
|
+
if (k === 'given') sc.given.push(...clausesOf(c));
|
|
229
|
+
else if (k === 'when') sc.when.push(...clausesOf(c));
|
|
230
|
+
else if (k === 'then') sc.then.push(...clausesOf(c));
|
|
231
|
+
else if (k === 'never') sc.never.push(...clausesOf(c));
|
|
232
|
+
else if (k === 'eventually') { const within = (rest(c.text).match(/within\s+(.+)/) || [])[1] || null; sc.eventually.push({ within, clauses: leafItems(c) }); }
|
|
233
|
+
}
|
|
234
|
+
return sc;
|
|
235
|
+
}
|
|
236
|
+
|
|
184
237
|
// Lifecycle state machine (intent-graph-v1 Gap 2).
|
|
185
238
|
function parseLifecycle(name, node) {
|
|
186
239
|
const lc = { name, states: [], transitions: [], terminals: [], line: node.line };
|
|
@@ -215,6 +268,7 @@ function parseInvariant(name, node) {
|
|
|
215
268
|
for (const c of kids(node)) {
|
|
216
269
|
const k = firstWord(c.text);
|
|
217
270
|
if (k === 'verify') inv.verify.push(rest(c.text));
|
|
271
|
+
else if (k === 'id') { const v = rest(c.text); if (v) { inv.id = v; inv.stableId = v; } }
|
|
218
272
|
else if (k === 'because') inv.because = rest(c.text);
|
|
219
273
|
else if (k === 'statement') inv.statement = rest(c.text) || leafItems(c).join(' ');
|
|
220
274
|
else if (k === 'scope') inv.scope = (rest(c.text) || leafItems(c).join(' ')).toLowerCase();
|
|
@@ -288,10 +342,15 @@ function parseHandler(trigger, node) {
|
|
|
288
342
|
};
|
|
289
343
|
}
|
|
290
344
|
|
|
345
|
+
// How a claim is verified (the six kinds of verification). Assertion = executable check;
|
|
346
|
+
// static = types/deps/architecture; runtime = logs/events/traces; evidence = recorded proof;
|
|
347
|
+
// tool = external scanner/CI; approval = human judgment; formal = mathematical proof.
|
|
348
|
+
export const VERIFY_KINDS = new Set(['assertion', 'static', 'runtime', 'evidence', 'tool', 'approval', 'formal']);
|
|
349
|
+
|
|
291
350
|
function upsertRule(list, statement, line) {
|
|
292
351
|
const id = slug(statement);
|
|
293
352
|
let r = list.find((x) => x.id === id);
|
|
294
|
-
if (!r) { r = { id, statement, because: null, verify: [], notes: [], line }; list.push(r); }
|
|
353
|
+
if (!r) { r = { id, statement, because: null, verify: [], verifications: [], notes: [], line }; list.push(r); }
|
|
295
354
|
return r;
|
|
296
355
|
}
|
|
297
356
|
|
|
@@ -299,7 +358,24 @@ function applyDetail(rule, node) {
|
|
|
299
358
|
for (const c of node.children) {
|
|
300
359
|
const kw = firstWord(c.text);
|
|
301
360
|
if (kw === 'because') rule.because = rest(c.text) || (c.children[0] && c.children[0].text) || null;
|
|
302
|
-
else if (kw === 'verify')
|
|
361
|
+
else if (kw === 'verify') {
|
|
362
|
+
const a = rest(c.text);
|
|
363
|
+
const m = a && a.match(/^by\s+(\w+)/i);
|
|
364
|
+
if (m) {
|
|
365
|
+
// Classified: `verify by <kind>` with the details as child lines.
|
|
366
|
+
const kind = m[1].toLowerCase();
|
|
367
|
+
const details = (c.children || []).filter((x) => !isNote(x)).map((x) => x.text.trim()).filter(Boolean);
|
|
368
|
+
rule.verifications.push({ kind: VERIFY_KINDS.has(kind) ? kind : 'unknown', declared: kind, details });
|
|
369
|
+
rule.verify.push(details.length ? `${kind}: ${details.join('; ')}` : `by ${kind}`);
|
|
370
|
+
} else {
|
|
371
|
+
// Legacy: `verify <text>` , an unclassified reference to a check.
|
|
372
|
+
const v = a || (c.children[0] && c.children[0].text) || '';
|
|
373
|
+
if (v) { rule.verify.push(v); rule.verifications.push({ kind: 'unclassified', declared: null, details: [v] }); }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Explicit stable id (e.g. `id INV-G-001`): survives renames and moved files so semantic
|
|
377
|
+
// diffs, traceability, and durable proof stay stable. Falls back to the slug when absent.
|
|
378
|
+
else if (kw === 'id') { const v = rest(c.text) || (c.children[0] && c.children[0].text); if (v) { rule.id = v; rule.stableId = v; } }
|
|
303
379
|
else if (isNote(c)) rule.notes.push(parseNoteNode(c));
|
|
304
380
|
}
|
|
305
381
|
rule.verify = rule.verify.filter(Boolean);
|
|
@@ -329,7 +405,7 @@ export function parseIntent(source) {
|
|
|
329
405
|
// Distributed + failure semantics (Gap 3)
|
|
330
406
|
commands: [], handlers: [],
|
|
331
407
|
// Decisions, rules, process (Gap 4)
|
|
332
|
-
decisions: [],
|
|
408
|
+
decisions: [], properties: [], scenarios: [], evaluations: [],
|
|
333
409
|
// Governance: waivers , governed exceptions to blocking diagnostics (Gap 5)
|
|
334
410
|
waivers: [],
|
|
335
411
|
// Data purpose + privacy , governed data elements (Gap 6)
|
|
@@ -481,6 +557,9 @@ export function parseIntent(source) {
|
|
|
481
557
|
break;
|
|
482
558
|
}
|
|
483
559
|
case 'decision': ast.decisions.push(parseDecision(arg, node)); break;
|
|
560
|
+
case 'property': ast.properties.push(parseProperty(arg, node)); break;
|
|
561
|
+
case 'scenario': ast.scenarios.push(parseScenario(arg, node)); break;
|
|
562
|
+
case 'evaluation': ast.evaluations.push(parseEvaluation(arg, node)); break;
|
|
484
563
|
// ── System profile ──
|
|
485
564
|
case 'capability': {
|
|
486
565
|
const cap = { name: arg, description: null, implements: [], line: node.line };
|
|
@@ -712,7 +791,7 @@ export function parseIntent(source) {
|
|
|
712
791
|
}
|
|
713
792
|
}
|
|
714
793
|
|
|
715
|
-
// ── Assemble
|
|
794
|
+
// ── Assemble ThunderLens notes with stable ids, target kinds, paths, spans ──
|
|
716
795
|
const mprefix = `mission.${ast.mission || 'unnamed'}`;
|
|
717
796
|
const pushNote = (raw, targetKind, targetPath) => {
|
|
718
797
|
if (!raw) return;
|
package/src/proof-schema.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Canonical proof envelope schema (intent-proof-v1) , the shape of a `.intent-proof.json`
|
|
2
|
-
// document. IL owns this: it is what the compiler emits (`buildProof`), what `
|
|
2
|
+
// document. IL owns this: it is what the compiler emits (`buildProof`), what `thunder verify`
|
|
3
3
|
// re-derives against, and the "shared envelope" siblings sign (STW) and re-verify (RM/OT)
|
|
4
4
|
// rather than each re-describing the proof shape. Pure ESM, ZERO Node deps , browser-safe so
|
|
5
5
|
// a signing service or a cert renderer can validate an envelope without a Node build.
|
|
@@ -41,7 +41,7 @@ export function intentProofJsonSchema() {
|
|
|
41
41
|
sourceProduct: { type: 'string' },
|
|
42
42
|
missionName: { type: ['string', 'null'] },
|
|
43
43
|
sourceFile: { type: 'string' },
|
|
44
|
-
// sha256:<hex> , the source fingerprint `
|
|
44
|
+
// sha256:<hex> , the source fingerprint `thunder verify` re-checks for drift/tampering.
|
|
45
45
|
sourceHash: { type: 'string', pattern: '^sha256:[0-9a-f]{64}$' },
|
|
46
46
|
compilerVersion: { type: 'string' },
|
|
47
47
|
generatedAt: { type: ['string', 'null'] },
|