@skillstech/thunderlang 0.1.6

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -0
  4. package/dist/core.cjs +6768 -0
  5. package/dist/index.cjs +9308 -0
  6. package/intent-graph.schema.json +687 -0
  7. package/package.json +84 -0
  8. package/src/ai-core.mjs +67 -0
  9. package/src/ai-events.mjs +56 -0
  10. package/src/ai.mjs +324 -0
  11. package/src/arch.mjs +55 -0
  12. package/src/atlas.mjs +118 -0
  13. package/src/changes.mjs +74 -0
  14. package/src/classification.mjs +36 -0
  15. package/src/cli.mjs +1534 -0
  16. package/src/codegen.mjs +214 -0
  17. package/src/compile.mjs +142 -0
  18. package/src/comprehension.mjs +130 -0
  19. package/src/conflict.mjs +0 -0
  20. package/src/core.d.ts +99 -0
  21. package/src/core.mjs +92 -0
  22. package/src/data-schema.mjs +137 -0
  23. package/src/decision.mjs +38 -0
  24. package/src/distributed.mjs +48 -0
  25. package/src/draft.mjs +101 -0
  26. package/src/drift.mjs +177 -0
  27. package/src/emit.mjs +519 -0
  28. package/src/exporters.mjs +245 -0
  29. package/src/expr.mjs +245 -0
  30. package/src/fable.mjs +110 -0
  31. package/src/focus.mjs +151 -0
  32. package/src/format.mjs +55 -0
  33. package/src/governance.mjs +100 -0
  34. package/src/graph-source.mjs +292 -0
  35. package/src/guard.mjs +105 -0
  36. package/src/guardian.mjs +98 -0
  37. package/src/hash.mjs +89 -0
  38. package/src/importers.mjs +194 -0
  39. package/src/index.d.ts +725 -0
  40. package/src/index.mjs +185 -0
  41. package/src/intellisense.mjs +210 -0
  42. package/src/intent-atlas.mjs +77 -0
  43. package/src/intent-graph.mjs +346 -0
  44. package/src/intent-ir.mjs +144 -0
  45. package/src/intent-schema.mjs +215 -0
  46. package/src/ledger.mjs +109 -0
  47. package/src/lifecycle.mjs +56 -0
  48. package/src/lift.mjs +693 -0
  49. package/src/lsp.mjs +152 -0
  50. package/src/mcp.mjs +158 -0
  51. package/src/migrate.mjs +118 -0
  52. package/src/outcome.mjs +93 -0
  53. package/src/parse.mjs +733 -0
  54. package/src/patch.mjs +364 -0
  55. package/src/privacy.mjs +61 -0
  56. package/src/proof-schema.mjs +129 -0
  57. package/src/report.mjs +84 -0
  58. package/src/runtime.mjs +96 -0
  59. package/src/sarif.mjs +88 -0
  60. package/src/scan-queries.mjs +97 -0
  61. package/src/scan.mjs +87 -0
  62. package/src/security.mjs +73 -0
  63. package/src/select.mjs +80 -0
  64. package/src/semantic-diff.mjs +125 -0
  65. package/src/simulate.mjs +106 -0
  66. package/src/style.mjs +250 -0
  67. package/src/sync.mjs +103 -0
  68. package/src/testing.mjs +59 -0
  69. package/src/twelve-factor.mjs +173 -0
  70. package/src/verify-diff.mjs +105 -0
  71. package/src/xml.mjs +87 -0
  72. package/syntaxes/intent.tmLanguage.json +55 -0
@@ -0,0 +1,96 @@
1
+ // The Intent Runtime , EXECUTABLE intent with zero AI and zero code generation. Decisions
2
+ // evaluate against concrete inputs; lifecycles simulate against a sequence of events. This
3
+ // is the step beyond prompt engineering: you write intent and you can immediately RUN it,
4
+ // TEST it, and PROVE how it behaves, before any implementation exists. Deterministic and
5
+ // pure , the same intent and inputs always produce the same result and the same trace.
6
+
7
+ import { compileExpr, ExprError } from './expr.mjs';
8
+ import { buildLifecycle } from './lifecycle.mjs';
9
+
10
+ export const RUNTIME_SCHEMA = 'intent-runtime-v1';
11
+
12
+ /**
13
+ * Evaluate a declared decision (Gap 4) against a concrete inputs object. FIRST-hit: rules
14
+ * are tried in order, the first whose `when` is true wins; the mission `default` is the
15
+ * catch-all. Returns the result plus a full per-rule trace, so the evaluation is auditable.
16
+ * A malformed `when` does not throw , it is recorded as an error in the trace and skipped.
17
+ *
18
+ * @returns {{schema, decision, result, matched, trace, ok}}
19
+ */
20
+ export function evaluateDecision(dec, inputs = {}) {
21
+ const trace = [];
22
+ let result = null;
23
+ let matched = null;
24
+ for (const rule of dec.rules || []) {
25
+ if (!rule.when) { trace.push({ rule: rule.name, when: null, matched: false, note: 'no condition' }); continue; }
26
+ let ok = false; let error = null;
27
+ try { ok = !!compileExpr(rule.when)(inputs); }
28
+ catch (e) { error = e instanceof ExprError ? e.message : String(e); }
29
+ trace.push({ rule: rule.name, when: rule.when, matched: ok, ...(error ? { error } : {}) });
30
+ if (ok && matched === null) { result = rule.result; matched = rule.name; }
31
+ }
32
+ if (matched === null && dec.default != null) { result = dec.default; matched = 'default'; }
33
+ return {
34
+ schema: RUNTIME_SCHEMA,
35
+ decision: dec.name,
36
+ result,
37
+ matched,
38
+ undecided: matched === null,
39
+ explanationRequired: !!dec.explanationRequired,
40
+ trace,
41
+ ok: trace.every((t) => !t.error),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Simulate a declared lifecycle (Gap 2) against a sequence of events. Each event is a
47
+ * transition name OR a target-state name; from the current state we take the matching
48
+ * transition. An event with no valid transition is recorded as rejected (the state does
49
+ * not change). Returns the walked path, a per-step trace, and whether every step was valid.
50
+ *
51
+ * @returns {{schema, lifecycle, path, steps, finalState, valid, endedTerminal}}
52
+ */
53
+ export function simulateLifecycle(lc, events = []) {
54
+ const ir = buildLifecycle(lc);
55
+ const terminals = new Set(ir.terminals.length ? ir.terminals : ir.states.filter((s) => (ir.out[s] || []).length === 0));
56
+ let state = ir.initial;
57
+ const path = [state];
58
+ const steps = [];
59
+ let valid = true;
60
+ for (const ev of events) {
61
+ const candidates = (ir.transitions || []).filter((t) => t.from === state && (t.name === ev || t.to === ev));
62
+ if (candidates.length === 0) {
63
+ valid = false;
64
+ const reason = terminals.has(state) ? `state "${state}" is terminal` : `no transition "${ev}" from "${state}"`;
65
+ steps.push({ event: ev, from: state, to: state, ok: false, reason });
66
+ continue;
67
+ }
68
+ const t = candidates[0];
69
+ steps.push({ event: ev, from: state, to: t.to, ok: true, transition: t.name });
70
+ state = t.to;
71
+ path.push(state);
72
+ }
73
+ return {
74
+ schema: RUNTIME_SCHEMA,
75
+ lifecycle: lc.name,
76
+ path,
77
+ steps,
78
+ finalState: state,
79
+ valid,
80
+ endedTerminal: terminals.has(state),
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Run a decision against a table of test cases (each {inputs, expect}). Returns pass/fail
86
+ * per case , this turns a decision into a self-checking specification you can regression-test
87
+ * with no code and no AI.
88
+ */
89
+ export function checkDecisionCases(dec, cases = []) {
90
+ const results = cases.map((c, i) => {
91
+ const run = evaluateDecision(dec, c.inputs || {});
92
+ const pass = c.expect === undefined || String(run.result) === String(c.expect);
93
+ return { case: c.name || `case ${i + 1}`, inputs: c.inputs || {}, expected: c.expect, actual: run.result, matched: run.matched, pass };
94
+ });
95
+ return { schema: RUNTIME_SCHEMA, decision: dec.name, total: results.length, passed: results.filter((r) => r.pass).length, results };
96
+ }
package/src/sarif.mjs ADDED
@@ -0,0 +1,88 @@
1
+ // SARIF 2.1.0 output for `intent check` , so ThunderLang diagnostics show up natively in the
2
+ // surfaces teams already use: GitHub / GitLab code scanning (inline PR annotations + the
3
+ // Security tab) and any SARIF-aware IDE. Pure: reports in, one SARIF log object out.
4
+ //
5
+ // Feed it per-file reports: [{ file, diagnostics: [{ code, level, severity, message, line? }] }].
6
+ // `level`/`severity` map to SARIF levels (blocker/error -> error, warning -> warning,
7
+ // info -> note). A diagnostic with a `line` gets a precise region; otherwise it lands at the
8
+ // file level (valid SARIF; the surface shows it at the top of the file).
9
+
10
+ import { ALL_DIAGNOSTICS } from './intent-schema.mjs';
11
+
12
+ export const SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json';
13
+
14
+ const META = new Map(ALL_DIAGNOSTICS.map((r) => [r.ruleId, r]));
15
+
16
+ // SARIF result level for one diagnostic.
17
+ export function sarifLevel(diag) {
18
+ if (diag.severity === 'blocker' || diag.level === 'error') return 'error';
19
+ if (diag.level === 'warning') return 'warning';
20
+ return 'note';
21
+ }
22
+
23
+ /**
24
+ * Build a SARIF 2.1.0 log from per-file diagnostic reports.
25
+ * @param {Array<{file: string, diagnostics: Array}>} reports
26
+ * @param {{version?: string, toolName?: string}} [opts]
27
+ */
28
+ export function toSarif(reports, opts = {}) {
29
+ const version = opts.version || '0.0.0';
30
+ const toolName = opts.toolName || 'ThunderLang';
31
+
32
+ // Rules referenced by results, in first-seen order, with catalog metadata when available.
33
+ const ruleIndex = new Map();
34
+ const rules = [];
35
+ const ruleIndexOf = (code) => {
36
+ if (ruleIndex.has(code)) return ruleIndex.get(code);
37
+ const meta = META.get(code);
38
+ const idx = rules.length;
39
+ rules.push({
40
+ id: code,
41
+ name: code,
42
+ shortDescription: { text: meta ? meta.summary : code },
43
+ defaultConfiguration: { level: meta ? (meta.severity === 'blocker' ? 'error' : meta.severity === 'error' ? 'error' : meta.severity === 'warning' ? 'warning' : 'note') : 'warning' },
44
+ properties: meta ? { area: meta.area, severity: meta.severity, blocks: meta.blocks } : {},
45
+ ...(code.startsWith('IL-') || META.has(code)
46
+ ? { helpUri: `https://thunderlang.dev/docs/diagnostics#${code.toLowerCase()}` }
47
+ : {}),
48
+ });
49
+ ruleIndex.set(code, idx);
50
+ return idx;
51
+ };
52
+
53
+ const results = [];
54
+ for (const rep of reports) {
55
+ for (const d of rep.diagnostics || []) {
56
+ if (!d.code) continue;
57
+ const region = Number.isInteger(d.line) && d.line > 0 ? { region: { startLine: d.line } } : {};
58
+ results.push({
59
+ ruleId: d.code,
60
+ ruleIndex: ruleIndexOf(d.code),
61
+ level: sarifLevel(d),
62
+ message: { text: d.message || d.code },
63
+ locations: [{
64
+ physicalLocation: {
65
+ artifactLocation: { uri: rep.file },
66
+ ...region,
67
+ },
68
+ }],
69
+ });
70
+ }
71
+ }
72
+
73
+ return {
74
+ $schema: SARIF_SCHEMA,
75
+ version: '2.1.0',
76
+ runs: [{
77
+ tool: {
78
+ driver: {
79
+ name: toolName,
80
+ informationUri: 'https://thunderlang.dev',
81
+ version,
82
+ rules,
83
+ },
84
+ },
85
+ results,
86
+ }],
87
+ };
88
+ }
@@ -0,0 +1,97 @@
1
+ // Scanner query views (intent-scan-view-v1) , the deterministic answers behind the Part 3
2
+ // CLI verbs: `intent risks | gaps | unverified | coverage | unknowns | contradictions`.
3
+ // These are pure derivations over a scanProject() result (its Intent IR + Fable findings) ,
4
+ // no new analysis, no AI. They exist so a person can ask one focused question ("what is
5
+ // unverified?", "what contradicts?") instead of reading the whole scan report.
6
+ //
7
+ // ThunderLang owns this because it owns the Scanner spine + Intent IR. It deliberately does
8
+ // NOT own learning/mastery (RepoMastery) or code verification (OpenThunder); those consume
9
+ // these same artifacts.
10
+
11
+ export const VIEW_SCHEMA = 'intent-scan-view-v1';
12
+
13
+ const nodes = (scan) => scan?.ir?.nodes || [];
14
+ const rels = (scan) => scan?.ir?.relationships || [];
15
+ const findings = (scan) => scan?.findings || [];
16
+ const claimNodes = (scan) => nodes(scan).filter((n) => n.type === 'Guarantee' || n.type === 'Never');
17
+
18
+ // Node ids that a `verified_by` edge touches (robust to edge direction).
19
+ function verifiedIds(scan) {
20
+ const s = new Set();
21
+ for (const e of rels(scan)) if (e.type === 'verified_by') { s.add(e.from); s.add(e.to); }
22
+ return s;
23
+ }
24
+
25
+ /** Risk themes + the highest-impact remediation order (the scan report's risk half, focused). */
26
+ export function risksView(scan) {
27
+ return {
28
+ schema: VIEW_SCHEMA, view: 'risks',
29
+ bySeverity: scan?.bySeverity || {},
30
+ themes: scan?.risks || [],
31
+ remediationSequence: scan?.remediationSequence || [],
32
+ count: (scan?.risks || []).length,
33
+ };
34
+ }
35
+
36
+ /** Intent gaps: missing goal, unverified guarantees, and other "something required is absent" findings. */
37
+ export function gapsView(scan) {
38
+ const GAP = /(^missing-|-without-|-missing$|^no-)/;
39
+ const gaps = findings(scan).filter((f) => f.category === 'Intent risk' || GAP.test(f.ruleId));
40
+ return {
41
+ schema: VIEW_SCHEMA, view: 'gaps', count: gaps.length,
42
+ gaps: gaps.map((f) => ({ ruleId: f.ruleId, severity: f.severity, detected: f.detected, why: f.why, remediation: f.remediation })),
43
+ };
44
+ }
45
+
46
+ /** Unverified claims: guarantees / never-rules with no verification behind them (IR + findings). */
47
+ export function unverifiedView(scan) {
48
+ const verified = verifiedIds(scan);
49
+ const claims = claimNodes(scan).filter((c) => !verified.has(c.id));
50
+ return {
51
+ schema: VIEW_SCHEMA, view: 'unverified', count: claims.length,
52
+ claims: claims.map((c) => ({ id: c.id, type: c.type, title: c.title })),
53
+ findings: findings(scan).filter((f) => /-without-verification$/.test(f.ruleId)).map((f) => ({ ruleId: f.ruleId, detected: f.detected })),
54
+ };
55
+ }
56
+
57
+ /** Verification coverage: share of guarantees + never-rules that have a verification. */
58
+ export function coverageView(scan) {
59
+ const claims = claimNodes(scan);
60
+ const verified = verifiedIds(scan);
61
+ const covered = claims.filter((c) => verified.has(c.id));
62
+ const coverage = claims.length ? Math.round((covered.length / claims.length) * 100) : 100;
63
+ return {
64
+ schema: VIEW_SCHEMA, view: 'coverage',
65
+ total: claims.length, verified: covered.length, coverage,
66
+ unverified: claims.filter((c) => !verified.has(c.id)).map((c) => ({ id: c.id, type: c.type, title: c.title })),
67
+ };
68
+ }
69
+
70
+ /** Unknowns: nodes that are open questions, assumptions, or low-confidence (never shown as fact). */
71
+ export function unknownsView(scan) {
72
+ const LOW = new Set(['Inferred', 'Speculative', 'Conflicted']);
73
+ const open = nodes(scan).filter((n) => ['Unknown', 'Question', 'Assumption'].includes(n.type) || LOW.has(n.confidence));
74
+ return {
75
+ schema: VIEW_SCHEMA, view: 'unknowns', count: open.length,
76
+ unknowns: open.map((n) => ({ id: n.id, type: n.type, title: n.title, confidence: n.confidence || null, provenance: n.provenance || null })),
77
+ };
78
+ }
79
+
80
+ /** Contradictions: explicit Conflict nodes, contradicts/conflicts edges, and conflict-shaped findings. */
81
+ export function contradictionsView(scan) {
82
+ const conflictNodes = nodes(scan).filter((n) => n.type === 'Conflict');
83
+ const conflictRels = rels(scan).filter((e) => e.type === 'contradicts' || e.type === 'conflicts_with');
84
+ const conflictFindings = findings(scan).filter((f) => /(contradict|conflict)/.test(f.ruleId));
85
+ return {
86
+ schema: VIEW_SCHEMA, view: 'contradictions',
87
+ count: conflictNodes.length + conflictRels.length + conflictFindings.length,
88
+ conflicts: conflictNodes.map((n) => ({ id: n.id, title: n.title })),
89
+ links: conflictRels.map((e) => ({ from: e.from, to: e.to, type: e.type })),
90
+ findings: conflictFindings.map((f) => ({ ruleId: f.ruleId, detected: f.detected })),
91
+ };
92
+ }
93
+
94
+ export const VIEWS = {
95
+ risks: risksView, gaps: gapsView, unverified: unverifiedView,
96
+ coverage: coverageView, unknowns: unknownsView, contradictions: contradictionsView,
97
+ };
package/src/scan.mjs ADDED
@@ -0,0 +1,87 @@
1
+ // Intent Scanner (intent-scan-v1) , the staged pipeline that turns a project into Intent IR +
2
+ // explainable Fable findings + a risk view. This is the Scanner SPINE: discover -> normalize into
3
+ // Intent IR -> run deterministic Fable rules -> produce findings -> group into risk themes ->
4
+ // report. Deterministic (no AI, no key required); the whole thing runs locally. Pure ESM.
5
+ //
6
+ // scanIntent(source, { file }) -> per-file scan result (IR + findings + risks)
7
+ // scanProject([{ file, source }]) -> whole-project scan (merged IR + findings + executive summary)
8
+
9
+ import { parseIntent } from './parse.mjs';
10
+ import { buildIntentGraph } from './intent-graph.mjs';
11
+ import { semanticDiagnostics } from './emit.mjs';
12
+ import { graphToIR } from './intent-ir.mjs';
13
+ import { toFinding, RISK_CATEGORIES } from './fable.mjs';
14
+
15
+ export const SCAN_SCHEMA = 'intent-scan-v1';
16
+
17
+ const severityRank = { blocker: 0, error: 1, warning: 2, info: 3 };
18
+
19
+ // Group findings into risk themes: one entry per risk category that has findings.
20
+ function groupRisks(findings) {
21
+ const byCat = new Map();
22
+ for (const f of findings) {
23
+ const c = byCat.get(f.category) || { category: f.category, count: 0, blocker: 0, error: 0, warning: 0, info: 0, findingIds: [] };
24
+ c.count += 1;
25
+ c[f.severity] = (c[f.severity] || 0) + 1;
26
+ c.findingIds.push(f.findingId);
27
+ byCat.set(f.category, c);
28
+ }
29
+ // ordered by the canonical risk taxonomy, then severity weight
30
+ return [...byCat.values()].sort((a, b) => RISK_CATEGORIES.indexOf(a.category) - RISK_CATEGORIES.indexOf(b.category));
31
+ }
32
+
33
+ const countBySeverity = (findings) => findings.reduce((m, f) => ((m[f.severity] = (m[f.severity] || 0) + 1), m), { blocker: 0, error: 0, warning: 0, info: 0 });
34
+
35
+ /** Scan one intent source into Intent IR + Fable findings + risk themes. */
36
+ export function scanIntent(source, { file = null } = {}) {
37
+ const ast = parseIntent(String(source ?? ''));
38
+ const graph = buildIntentGraph(ast);
39
+ const ir = graphToIR(graph);
40
+ const missionNodeId = graph.nodes.find((n) => n.type === 'Mission')?.id;
41
+ const diags = semanticDiagnostics(ast);
42
+ const findings = diags.map((d, i) => toFinding(d, { file, index: i, affectedNodes: missionNodeId ? [missionNodeId] : [] }));
43
+ const risks = groupRisks(findings);
44
+ const bySeverity = countBySeverity(findings);
45
+ return {
46
+ schema: SCAN_SCHEMA,
47
+ file,
48
+ mission: ast.mission || null,
49
+ ir,
50
+ findings,
51
+ risks,
52
+ summary: { file, mission: ast.mission || null, findings: findings.length, bySeverity, ok: bySeverity.error === 0 && bySeverity.blocker === 0 },
53
+ };
54
+ }
55
+
56
+ /** Scan a whole project: merge each file's IR + findings, plus an executive summary. */
57
+ export function scanProject(files) {
58
+ const perFile = [];
59
+ const allFindings = [];
60
+ const irNodes = [];
61
+ const irRels = [];
62
+ const seenNodeIds = new Set();
63
+ for (const { file, source } of files || []) {
64
+ const r = scanIntent(source, { file });
65
+ perFile.push(r.summary);
66
+ allFindings.push(...r.findings);
67
+ for (const n of r.ir.nodes) { const key = `${file}:${n.id}`; if (!seenNodeIds.has(key)) { seenNodeIds.add(key); irNodes.push({ ...n, id: `${file ? `${file}#` : ''}${n.id}` }); } }
68
+ for (const e of r.ir.relationships) irRels.push({ ...e, from: `${file ? `${file}#` : ''}${e.from}`, to: `${file ? `${file}#` : ''}${e.to}` });
69
+ }
70
+ const risks = groupRisks(allFindings);
71
+ const bySeverity = countBySeverity(allFindings);
72
+ // Highest-impact remediation sequence: blocker > error > warning, most-common rule first.
73
+ const byRule = new Map();
74
+ for (const f of allFindings) { const k = f.ruleId; const e = byRule.get(k) || { ruleId: k, category: f.category, severity: f.severity, count: 0, remediation: f.remediation }; e.count += 1; byRule.set(k, e); }
75
+ const remediationSequence = [...byRule.values()].sort((a, b) => (severityRank[a.severity] - severityRank[b.severity]) || (b.count - a.count)).slice(0, 10);
76
+ return {
77
+ schema: SCAN_SCHEMA,
78
+ totals: { files: perFile.length, missions: perFile.filter((f) => f.mission).length, findings: allFindings.length },
79
+ bySeverity,
80
+ risks,
81
+ remediationSequence,
82
+ ir: { schema: 'intent-ir-v1', embeds: 'intent-graph-v1', nodes: irNodes, relationships: irRels },
83
+ files: perFile,
84
+ findings: allFindings,
85
+ ok: bySeverity.error === 0 && bySeverity.blocker === 0,
86
+ };
87
+ }
@@ -0,0 +1,73 @@
1
+ // Security + type semantic pass , the deterministic checks that catch the mistakes prompt
2
+ // engineering routinely ships: secrets travelling over the event bus, sensitive data returned
3
+ // from an unauthenticated API, and mistyped fields. No AI; pure functions over the AST.
4
+ //
5
+ // These sit alongside privacy.mjs (which governs declared `data` blocks): this pass looks at
6
+ // TYPED FIELDS (input/output/event payload/api) rather than governed data elements.
7
+
8
+ import { isRecognizedType } from './data-schema.mjs';
9
+
10
+ export const SECURITY_SCHEMA = 'intent-security-v1';
11
+
12
+ // Types that are secret by construction: transporting or exposing them is a leak. Kept tight
13
+ // (secret/password/jwt) so the checks never cry wolf on ambiguous names like "token".
14
+ const SECRET_TYPES = new Set(['secret', 'password', 'jwt']);
15
+
16
+ const baseType = (t) => {
17
+ const m = String(t || '').trim().match(/^(?:List|Array)<(.+)>$/i);
18
+ return (m ? m[1] : String(t || '')).trim();
19
+ };
20
+ const isSecretType = (t) => SECRET_TYPES.has(baseType(t).toLowerCase());
21
+
22
+ /**
23
+ * Deterministic security + type findings. Returns { code, severity, message, where, line }.
24
+ * IL-SEC-001 a secret-typed field rides an event payload (secret over the bus) [blocker]
25
+ * IL-SEC-002 an API returns a secret-typed output with no auth requirement [blocker]
26
+ * IL-TYPE-001 a field uses an unrecognized (lowercase, likely mistyped) type [info]
27
+ */
28
+ export function securityDiagnostics(ast) {
29
+ const out = [];
30
+
31
+ // IL-SEC-001 , secrets on the event bus.
32
+ for (const ev of ast.events || []) {
33
+ for (const f of ev.payload || []) {
34
+ if (isSecretType(f.type)) {
35
+ out.push({
36
+ code: 'IL-SEC-001', severity: 'blocker',
37
+ message: `Event "${ev.name}" payload field "${f.name}" is a ${baseType(f.type)}; secrets must not travel over the event bus.`,
38
+ where: ev.name, line: f.line ?? null,
39
+ });
40
+ }
41
+ }
42
+ }
43
+
44
+ // IL-SEC-002 , sensitive output from an unauthenticated API. `requires` is the auth gate.
45
+ for (const api of ast.apis || []) {
46
+ const hasAuth = (api.requires || []).length > 0;
47
+ if (isSecretType(api.output) && !hasAuth) {
48
+ out.push({
49
+ code: 'IL-SEC-002', severity: 'blocker',
50
+ message: `API "${api.name}" returns a ${baseType(api.output)} but declares no auth requirement; sensitive output must be authenticated.`,
51
+ where: api.name, line: api.line ?? null,
52
+ });
53
+ }
54
+ }
55
+
56
+ // IL-TYPE-001 , unrecognized field type (almost always a typo). Info, gate-safe.
57
+ const checkFields = (fields, where) => {
58
+ for (const f of fields || []) {
59
+ if (f.type && !isRecognizedType(f.type)) {
60
+ out.push({
61
+ code: 'IL-TYPE-001', severity: 'info',
62
+ message: `Field "${f.name}" has an unrecognized type "${f.type}". Use a known semantic type/primitive, or a PascalCase entity name.`,
63
+ where, line: f.line ?? null,
64
+ });
65
+ }
66
+ }
67
+ };
68
+ checkFields(ast.inputs, 'input');
69
+ checkFields(ast.outputs, 'output');
70
+ for (const ev of ast.events || []) checkFields(ev.payload, `event ${ev.name}`);
71
+
72
+ return out;
73
+ }
package/src/select.mjs ADDED
@@ -0,0 +1,80 @@
1
+ // Deterministic candidate selection , the AI generates N candidates; ThunderLang and
2
+ // OpenThunder pick the winner by MEASURABLE rules. An LLM never decides which
3
+ // candidate is best. Pure (no Node deps): browser-safe.
4
+ //
5
+ // selection
6
+ // require all verification checks
7
+ // prefer lower complexity
8
+ // prefer fewer dependencies
9
+ // prefer smaller implementation
10
+ // prefer better mutation score
11
+
12
+ const MIN_WORDS = new Set(['lower', 'fewer', 'smaller', 'less', 'least', 'lowest', 'minimal']);
13
+ const MAX_WORDS = new Set(['higher', 'more', 'better', 'greater', 'most', 'highest', 'larger', 'best']);
14
+
15
+ function normalizeMetric(s) {
16
+ const t = String(s).toLowerCase();
17
+ if (/complex/.test(t)) return 'complexity';
18
+ if (/depend/.test(t)) return 'dependencies';
19
+ if (/implement|size|line/.test(t)) return 'size';
20
+ if (/alloc/.test(t)) return 'allocation';
21
+ if (/mutation/.test(t)) return 'mutationScore';
22
+ return t.trim().replace(/\s+/g, '_');
23
+ }
24
+
25
+ /** Parse selection-block lines into a structured, deterministic policy. */
26
+ export function parseSelection(lines) {
27
+ const require = [];
28
+ const prefer = [];
29
+ for (const raw of lines || []) {
30
+ const t = String(raw).trim().toLowerCase();
31
+ if (/^require\b/.test(t)) {
32
+ require.push(t.replace(/^require\s+/, ''));
33
+ } else if (/^prefer\b/.test(t)) {
34
+ const words = t.replace(/^prefer\s+/, '').split(/\s+/);
35
+ const direction = MAX_WORDS.has(words[0]) ? 'max' : MIN_WORDS.has(words[0]) ? 'min' : 'min';
36
+ prefer.push({ metric: normalizeMetric(words.slice(1).join(' ')), direction });
37
+ }
38
+ }
39
+ return { require, prefer, requireAllChecks: require.some((r) => /verification|all/.test(r)) };
40
+ }
41
+
42
+ /** Deterministic metrics derivable from a code region (size / complexity / deps). */
43
+ export function regionMetrics(code) {
44
+ const text = String(code || '');
45
+ const size = text.split('\n').filter((l) => l.trim() && !l.trim().startsWith('//') && !l.includes('intent:')).length;
46
+ const complexity = (text.match(/\b(if|else|for|while|switch|case|catch|return)\b|&&|\|\||\?/g) || []).length;
47
+ const dependencies = (text.match(/\b(import|require)\b/g) || []).length;
48
+ return { size, complexity, dependencies };
49
+ }
50
+
51
+ const DEFAULT_PREFER = [
52
+ { metric: 'complexity', direction: 'min' },
53
+ { metric: 'dependencies', direction: 'min' },
54
+ { metric: 'size', direction: 'min' },
55
+ ];
56
+
57
+ /**
58
+ * Select the winning candidate by measurable rules. Deterministic:
59
+ * lexicographic by the `prefer` list, stable tiebreak by id. An unavailable metric
60
+ * ranks last for that comparison. Candidates failing `require` are ineligible.
61
+ * @param {{id:string, metrics:object, checksPassed?:boolean}[]} candidates
62
+ * @param {{prefer?:{metric,direction}[], requireAllChecks?:boolean}} policy
63
+ */
64
+ export function selectCandidate(candidates, policy = {}) {
65
+ const prefer = policy.prefer && policy.prefer.length ? policy.prefer : DEFAULT_PREFER;
66
+ const requireAllChecks = !!policy.requireAllChecks;
67
+ const eligible = (candidates || []).filter((c) => (requireAllChecks ? c.checksPassed !== false : true));
68
+ const ranked = [...eligible].sort((a, b) => {
69
+ for (const p of prefer) {
70
+ const av = a.metrics ? a.metrics[p.metric] : undefined;
71
+ const bv = b.metrics ? b.metrics[p.metric] : undefined;
72
+ if (av == null && bv == null) continue;
73
+ if (av == null) return 1;
74
+ if (bv == null) return -1;
75
+ if (av !== bv) return p.direction === 'max' ? bv - av : av - bv;
76
+ }
77
+ return String(a.id).localeCompare(String(b.id)); // stable tiebreak
78
+ });
79
+ return { winner: ranked[0] || null, ranking: ranked, eligibleCount: eligible.length, rejected: (candidates || []).length - eligible.length, prefer };
80
+ }
@@ -0,0 +1,125 @@
1
+ // Semantic diff over the Intent Graph (founder directive #4, IL owns diff/merge). Diff
2
+ // two versions of a graph/atlas BY MEANING: nodes added/removed/changed, relationships
3
+ // added/removed, and , the load-bearing feature , which approvals a change invalidates.
4
+ // Deterministic; pure (no Node deps).
5
+
6
+ // Content identity of a node (ignores volatile timestamps).
7
+ const contentKey = (n) => JSON.stringify({
8
+ type: n.type, title: n.title ?? null, description: n.description ?? null,
9
+ status: n.status ?? null, owner: n.owner ?? null,
10
+ classification: n.classification ?? null, confidence: n.confidence ?? null,
11
+ });
12
+ const relKey = (r) => `${r.from}|${r.type}|${r.to}`;
13
+ const CONTRACT_RELS = new Set(['requires', 'constrained_by', 'targets', 'measured_by']);
14
+
15
+ /**
16
+ * Semantic diff of two Intent Graphs (or Atlases). Deterministic.
17
+ * @param {{nodes, relationships}} before
18
+ * @param {{nodes, relationships}} after
19
+ */
20
+ export function diffGraphs(before, after) {
21
+ const bNodes = new Map((before.nodes || []).map((n) => [n.id, n]));
22
+ const aNodes = new Map((after.nodes || []).map((n) => [n.id, n]));
23
+
24
+ const addedNodes = [];
25
+ const removedNodes = [];
26
+ const changedNodes = [];
27
+ for (const [id, n] of aNodes) if (!bNodes.has(id)) addedNodes.push(n);
28
+ for (const [id, n] of bNodes) if (!aNodes.has(id)) removedNodes.push(n);
29
+ for (const [id, an] of aNodes) {
30
+ const bn = bNodes.get(id);
31
+ if (bn && contentKey(bn) !== contentKey(an)) changedNodes.push({ id, type: an.type, before: bn, after: an });
32
+ }
33
+ addedNodes.sort((a, b) => a.id.localeCompare(b.id));
34
+ removedNodes.sort((a, b) => a.id.localeCompare(b.id));
35
+ changedNodes.sort((a, b) => a.id.localeCompare(b.id));
36
+
37
+ const bRel = new Set((before.relationships || []).map(relKey));
38
+ const aRel = new Set((after.relationships || []).map(relKey));
39
+ const addedRelationships = (after.relationships || []).filter((r) => !bRel.has(relKey(r)));
40
+ const removedRelationships = (before.relationships || []).filter((r) => !aRel.has(relKey(r)));
41
+
42
+ // Which approvals does this change invalidate? An approval is invalidated when its
43
+ // mission's contract (requires / constrained_by / targets / measured_by nodes) changed.
44
+ const changedIds = new Set([...addedNodes.map((n) => n.id), ...removedNodes.map((n) => n.id), ...changedNodes.map((c) => c.id)]);
45
+ const missionContract = {};
46
+ const missionApprovals = {};
47
+ for (const r of after.relationships || []) {
48
+ if (!r.from.startsWith('mission.')) continue;
49
+ if (CONTRACT_RELS.has(r.type)) (missionContract[r.from] ||= new Set()).add(r.to);
50
+ if (r.type === 'approved_by') (missionApprovals[r.from] ||= []).push(r.to);
51
+ }
52
+ const invalidatedApprovals = [];
53
+ for (const [mission, contractIds] of Object.entries(missionContract)) {
54
+ const contractChanged = changedIds.has(mission) || [...contractIds].some((id) => changedIds.has(id));
55
+ if (contractChanged) for (const ap of missionApprovals[mission] || []) invalidatedApprovals.push(ap);
56
+ }
57
+
58
+ const byType = (arr) => arr.reduce((m, n) => ((m[n.type] = (m[n.type] || 0) + 1), m), {});
59
+ void relKey; // (used by mergeGraphs below)
60
+ return {
61
+ schema: 'intent-diff-v1',
62
+ addedNodes, removedNodes, changedNodes,
63
+ addedRelationships, removedRelationships,
64
+ invalidatedApprovals: [...new Set(invalidatedApprovals)].sort(),
65
+ summary: {
66
+ added: addedNodes.length, removed: removedNodes.length, changed: changedNodes.length,
67
+ addedByType: byType(addedNodes), removedByType: byType(removedNodes),
68
+ relationshipsAdded: addedRelationships.length, relationshipsRemoved: removedRelationships.length,
69
+ approvalsInvalidated: [...new Set(invalidatedApprovals)].length,
70
+ },
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Three-way semantic merge over the Intent Graph. Given a common `base` and two
76
+ * concurrent versions `ours` / `theirs`, produce the merged graph and the list of
77
+ * CONFLICTS (the same node changed differently on both sides). Deterministic; the side
78
+ * that changed a node from base wins; if both changed it differently, it is a conflict
79
+ * (merged keeps `ours` provisionally). Node identity is the stable id; equality is content.
80
+ */
81
+ export function mergeGraphs(base, ours, theirs) {
82
+ const key = (n) => (n ? contentKey(n) : 'MISSING');
83
+ const bN = new Map((base?.nodes || []).map((n) => [n.id, n]));
84
+ const oN = new Map((ours?.nodes || []).map((n) => [n.id, n]));
85
+ const tN = new Map((theirs?.nodes || []).map((n) => [n.id, n]));
86
+
87
+ const ids = [...new Set([...bN.keys(), ...oN.keys(), ...tN.keys()])].sort();
88
+ const merged = new Map();
89
+ const conflicts = [];
90
+ for (const id of ids) {
91
+ const b = bN.get(id) || null;
92
+ const o = oN.get(id) || null;
93
+ const t = tN.get(id) || null;
94
+ const oChanged = key(o) !== key(b);
95
+ const tChanged = key(t) !== key(b);
96
+ if (!oChanged && !tChanged) { if (b) merged.set(id, b); continue; } // untouched (or both removed)
97
+ if (oChanged && !tChanged) { if (o) merged.set(id, o); continue; } // only ours
98
+ if (!oChanged && tChanged) { if (t) merged.set(id, t); continue; } // only theirs
99
+ if (key(o) === key(t)) { if (o) merged.set(id, o); continue; } // both made the same change
100
+ conflicts.push({ id, type: (o || t || b).type, base: b, ours: o, theirs: t }); // both changed differently
101
+ if (o) merged.set(id, o); else if (t) merged.set(id, t); // keep ours provisionally
102
+ }
103
+
104
+ // Relationships (presence booleans): whoever changed presence from base wins.
105
+ const bR = new Set((base?.relationships || []).map(relKey));
106
+ const oR = new Set((ours?.relationships || []).map(relKey));
107
+ const tR = new Set((theirs?.relationships || []).map(relKey));
108
+ const byKey = new Map();
109
+ for (const r of [...(ours?.relationships || []), ...(theirs?.relationships || []), ...(base?.relationships || [])]) byKey.set(relKey(r), r);
110
+ const mergedRels = [];
111
+ for (const [k, r] of byKey) {
112
+ const inB = bR.has(k), inO = oR.has(k), inT = tR.has(k);
113
+ const present = inO !== inB ? inO : inT; // ours changed presence -> ours; else theirs
114
+ if (present) mergedRels.push(r);
115
+ }
116
+ mergedRels.sort((a, b2) => relKey(a).localeCompare(relKey(b2)));
117
+
118
+ return {
119
+ schema: 'intent-merge-v1',
120
+ merged: { nodes: [...merged.values()].sort((a, b2) => a.id.localeCompare(b2.id)), relationships: mergedRels },
121
+ conflicts: conflicts.sort((a, b2) => a.id.localeCompare(b2.id)),
122
+ clean: conflicts.length === 0,
123
+ summary: { nodes: merged.size, relationships: mergedRels.length, conflicts: conflicts.length },
124
+ };
125
+ }