@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,173 @@
1
+ // 12-Factor Agents conformance lens (twelve-factor-v1). Deterministic, browser-safe.
2
+ //
3
+ // Scores an intent against the 13 principles from humanlayer/12-factor-agents
4
+ // (https://github.com/humanlayer/12-factor-agents). Their thesis , "production agents are
5
+ // mostly deterministic code with LLM steps sprinkled in" , is ThunderLang's thesis, so most
6
+ // factors map to structure IL already models (decisions, lifecycles, typed I/O, approvals,
7
+ // errors/handlers, events, a pure runtime). This turns "12-factor compliant" into a
8
+ // human-owned, verifiable claim instead of a marketing checkbox.
9
+ //
10
+ // Verdicts: 'satisfied' | 'partial' | 'absent'. Score = (satisfied + 0.5*partial) / 13.
11
+ // Each factor cites the IL signal it inspects, the evidence found, and a concrete fix. The
12
+ // finding ids (IL-12F-01..13) are in the canonical rule catalog for `intent explain`.
13
+
14
+ export const TWELVE_FACTOR_SCHEMA = 'twelve-factor-v1';
15
+
16
+ const VERDICTS = ['satisfied', 'partial', 'absent'];
17
+ const WEIGHT = { satisfied: 1, partial: 0.5, absent: 0 };
18
+
19
+ const len = (x) => (Array.isArray(x) ? x.length : 0);
20
+ const typedFields = (fields) => (fields || []).filter((f) => f.type && f.type !== 'Unknown');
21
+
22
+ // Each factor: id, number, name, and a check(ast) -> { verdict, evidence, fix }.
23
+ const FACTORS = [
24
+ {
25
+ id: 'IL-12F-01', num: 1, name: 'Natural language to tool calls',
26
+ check(ast) {
27
+ const ops = len(ast.decisions) + len(ast.commands);
28
+ if (ops) return { verdict: 'satisfied', evidence: `${ops} structured operation(s): ${[...ast.decisions.map((d) => d.name), ...ast.commands.map((c) => c.name)].filter(Boolean).join(', ')}` };
29
+ return { verdict: 'absent', evidence: 'no decisions or commands', fix: 'Model the tool dispatch as a `decision` (rules -> result) or a `command`, so intent becomes a structured, whitelisted call.' };
30
+ },
31
+ },
32
+ {
33
+ id: 'IL-12F-02', num: 2, name: 'Own your prompts',
34
+ check(ast) {
35
+ const g = len(ast.guarantees);
36
+ if (g) return { verdict: 'satisfied', evidence: `behavior is an owned, specified contract (${g} guarantee(s))` };
37
+ if (ast.goal) return { verdict: 'partial', evidence: 'a goal is stated but no guarantees pin the behavior', fix: 'Add `guarantee` statements so the behavior is owned + testable, not a black box.' };
38
+ return { verdict: 'absent', evidence: 'no goal or guarantees', fix: 'Author the intent explicitly (goal + guarantees) rather than delegating behavior to a framework.' };
39
+ },
40
+ },
41
+ {
42
+ id: 'IL-12F-03', num: 3, name: 'Own your context window',
43
+ check(ast) {
44
+ const scoped = len(ast.scope?.include) + len(ast.scope?.exclude);
45
+ if (scoped) return { verdict: 'satisfied', evidence: `context boundary declared (scope: ${len(ast.scope.include)} include / ${len(ast.scope.exclude)} exclude)` };
46
+ if (len(ast.requires)) return { verdict: 'partial', evidence: 'dependencies declared but no explicit scope boundary', fix: 'Declare `scope include/exclude` so the context window is curated, not unbounded.' };
47
+ return { verdict: 'absent', evidence: 'no scope declared', fix: 'Add a `scope` block to bound what belongs in context (IntentLens/Focus narrows it further).' };
48
+ },
49
+ },
50
+ {
51
+ id: 'IL-12F-04', num: 4, name: 'Tools are structured outputs',
52
+ check(ast) {
53
+ const inT = typedFields(ast.inputs).length, outT = typedFields(ast.outputs).length;
54
+ const discriminated = (ast.decisions || []).some((d) => (d.rules || []).some((r) => r.result));
55
+ if ((inT || outT) && (discriminated || len(ast.outputs))) return { verdict: 'satisfied', evidence: `typed I/O (${inT} in / ${outT} out)${discriminated ? ' + discriminated decision results' : ''}` };
56
+ if (len(ast.inputs) || len(ast.outputs)) return { verdict: 'partial', evidence: 'inputs/outputs present but under-typed or no discriminated result', fix: 'Type the inputs/outputs (semantic types) and give decision rules explicit `result`s so outputs are a structured, parseable contract (export via JSON Schema/OpenAPI).' };
57
+ return { verdict: 'absent', evidence: 'no typed inputs/outputs', fix: 'Declare typed `input`/`output` fields; tools are structured outputs, not prose.' };
58
+ },
59
+ },
60
+ {
61
+ id: 'IL-12F-05', num: 5, name: 'Unify execution and business state',
62
+ check(ast) {
63
+ if (len(ast.lifecycles)) return { verdict: 'satisfied', evidence: `single state model (${ast.lifecycles.map((l) => l.name).join(', ')})` };
64
+ if (len(ast.decisions) || len(ast.commands)) return { verdict: 'partial', evidence: 'operations exist but no lifecycle unifies their state', fix: 'Model a `lifecycle` so execution state is inferable from one thread instead of tracked in parallel.' };
65
+ return { verdict: 'absent', evidence: 'no lifecycle / state model', fix: 'Add a `lifecycle` (states + transitions) so execution and business state live in one serializable model.' };
66
+ },
67
+ },
68
+ {
69
+ id: 'IL-12F-06', num: 6, name: 'Launch / pause / resume',
70
+ check(ast) {
71
+ const lc = (ast.lifecycles || [])[0];
72
+ if (lc && len(lc.states) && len(lc.terminals) && len(lc.states) > len(lc.terminals)) return { verdict: 'satisfied', evidence: `resumable lifecycle: ${len(lc.states)} states, ${len(lc.terminals)} terminal , non-terminal (pausable) states exist` };
73
+ if (lc) return { verdict: 'partial', evidence: 'lifecycle present but no clear pausable/terminal structure', fix: 'Give the lifecycle explicit non-terminal (waiting) states + terminals so it can pause and resume.' };
74
+ return { verdict: 'absent', evidence: 'no lifecycle to pause/resume', fix: 'Model a `lifecycle` with waiting states so the agent can pause on long ops and resume from a saved thread.' };
75
+ },
76
+ },
77
+ {
78
+ id: 'IL-12F-07', num: 7, name: 'Contact humans with tool calls',
79
+ check(ast) {
80
+ if (len(ast.approvals) || ast.approval?.reviewed != null) return { verdict: 'satisfied', evidence: `human gate declared (${len(ast.approvals) ? ast.approvals.join(', ') : 'approval block'})` };
81
+ if (ast.owner) return { verdict: 'partial', evidence: `an owner is named (${ast.owner}) but no structured approval/human-input step`, fix: 'Declare an `approval required from <role>` gate so contacting a human is a structured, resumable step.' };
82
+ return { verdict: 'absent', evidence: 'no human-in-the-loop gate', fix: 'Add an `approval required from <role>` (or a human-input decision result) so high-stakes steps pause for a human.' };
83
+ },
84
+ },
85
+ {
86
+ id: 'IL-12F-08', num: 8, name: 'Own your control flow',
87
+ check(ast) {
88
+ const decs = ast.decisions || [];
89
+ if (decs.length && decs.every((d) => d.default != null)) return { verdict: 'satisfied', evidence: `${decs.length} decision(s), all with an explicit default (total, deterministic control flow)` };
90
+ if (decs.length) return { verdict: 'partial', evidence: `${decs.length} decision(s) but some have no default (undefined when no rule matches)`, fix: 'Give every `decision` a `default` so control flow is total , no undefined branch between selection and execution.' };
91
+ return { verdict: 'absent', evidence: 'no decision-based control flow', fix: 'Model branching as a `decision` (FIRST-hit rules + default) so you own the loop, not a framework.' };
92
+ },
93
+ },
94
+ {
95
+ id: 'IL-12F-09', num: 9, name: 'Compact errors into context',
96
+ check(ast) {
97
+ const e = len(ast.errors), h = len(ast.handlers);
98
+ if (e && h) return { verdict: 'satisfied', evidence: `${e} named error(s) + ${h} handler(s) (compensate/notify)` };
99
+ if (e || h) return { verdict: 'partial', evidence: e ? 'errors named but no handlers' : 'handlers exist but no named errors', fix: 'Declare named `error`s AND `handler`s (compensate/notify) so failures are structured + recoverable, with bounded retries.' };
100
+ return { verdict: 'absent', evidence: 'no error model', fix: 'Name the failure modes as `error`s and add `handler`s so errors compact into a recoverable, bounded-retry path.' };
101
+ },
102
+ },
103
+ {
104
+ id: 'IL-12F-10', num: 10, name: 'Small, focused agents',
105
+ check(ast) {
106
+ const steps = len(ast.decisions) + len(ast.commands) + len(ast.handlers) + (ast.lifecycles || []).reduce((n, l) => n + len(l.states), 0);
107
+ if (steps === 0) return { verdict: 'partial', evidence: 'no executable steps to size', fix: 'Once operations exist, keep them within ~10 steps (20 max) so the agent stays focused.' };
108
+ if (steps <= 10) return { verdict: 'satisfied', evidence: `${steps} step(s) (target <= 10)` };
109
+ if (steps <= 20) return { verdict: 'partial', evidence: `${steps} step(s) (over the ~10 target, under the 20 cap)`, fix: 'Split into smaller missions; agents lose focus as step count grows.' };
110
+ return { verdict: 'absent', evidence: `${steps} step(s) (exceeds the ~20 hard cap)`, fix: 'Decompose this mission , at >20 steps the model loses coherence. One responsibility per mission.' };
111
+ },
112
+ },
113
+ {
114
+ id: 'IL-12F-11', num: 11, name: 'Trigger from anywhere',
115
+ check(ast) {
116
+ if (len(ast.events)) return { verdict: 'satisfied', evidence: `${len(ast.events)} event trigger(s) declared` };
117
+ if (len(ast.commands) || len(ast.apis)) return { verdict: 'partial', evidence: 'commands/APIs exist but no declared event triggers', fix: 'Declare `event`s so the mission can be triggered by webhooks/crons/other agents, not one entry point.' };
118
+ return { verdict: 'absent', evidence: 'no trigger surface declared', fix: 'Declare the `event`s/triggers that launch this mission (outer-loop + multi-channel).' };
119
+ },
120
+ },
121
+ {
122
+ id: 'IL-12F-12', num: 12, name: 'Stateless reducer',
123
+ check(ast) {
124
+ if (len(ast.decisions) || len(ast.lifecycles)) return { verdict: 'satisfied', evidence: 'behavior is a pure, replayable function (decisions/lifecycle run on IL’s deterministic runtime)' };
125
+ if (len(ast.guarantees)) return { verdict: 'partial', evidence: 'specified but nothing executable/pure to replay', fix: 'Express the logic as `decision`/`lifecycle` so it is a pure f(state)->next you can replay + test.' };
126
+ return { verdict: 'absent', evidence: 'nothing executable', fix: 'Model the step as a deterministic `decision`/`lifecycle` (pure reducer over the event thread).' };
127
+ },
128
+ },
129
+ {
130
+ id: 'IL-12F-13', num: 13, name: 'Pre-fetch context (appendix)',
131
+ check(ast) {
132
+ if (len(ast.inputs)) return { verdict: 'satisfied', evidence: `${len(ast.inputs)} input(s) declared up front` };
133
+ if (len(ast.decisions)) return { verdict: 'partial', evidence: 'decisions exist but no declared inputs to pre-fetch', fix: 'Declare the `input`s the decision needs so known data is fetched deterministically up front, not via an extra model turn.' };
134
+ return { verdict: 'absent', evidence: 'no declared inputs', fix: 'Declare `input`s so predictable data is present up front (pre-fetch), saving a round trip.' };
135
+ },
136
+ },
137
+ ];
138
+
139
+ /**
140
+ * Score an intent AST against the 13 factors. Deterministic; returns a stable report.
141
+ * Pass a parsed AST (from parseIntent). Accepts a mission or any profile intent.
142
+ */
143
+ export function twelveFactorReport(ast) {
144
+ const a = ast || {};
145
+ const factors = FACTORS.map((f) => {
146
+ const r = f.check(a) || { verdict: 'absent', evidence: '' };
147
+ const verdict = VERDICTS.includes(r.verdict) ? r.verdict : 'absent';
148
+ return { id: f.id, factor: f.num, name: f.name, verdict, evidence: r.evidence || '', ...(r.fix ? { fix: r.fix } : {}) };
149
+ });
150
+ const raw = factors.reduce((s, f) => s + WEIGHT[f.verdict], 0);
151
+ const score = Math.round((raw / FACTORS.length) * 100);
152
+ const counts = { satisfied: 0, partial: 0, absent: 0 };
153
+ for (const f of factors) counts[f.verdict]++;
154
+ // Advisory diagnostics for anything not fully satisfied (surface in scan / explain).
155
+ const diagnostics = factors
156
+ .filter((f) => f.verdict !== 'satisfied')
157
+ .map((f) => ({ level: f.verdict === 'absent' ? 'warning' : 'info', code: f.id, message: `Factor ${f.factor} (${f.name}): ${f.evidence}.${f.fix ? ' ' + f.fix : ''}` }));
158
+ return {
159
+ schemaVersion: TWELVE_FACTOR_SCHEMA,
160
+ subject: a.mission || a.title || null,
161
+ score, // 0..100
162
+ grade: score >= 85 ? 'strong' : score >= 60 ? 'partial' : 'weak',
163
+ counts,
164
+ factors,
165
+ diagnostics,
166
+ };
167
+ }
168
+
169
+ // Compact summary for the proof envelope / compileSource (no per-factor prose).
170
+ export function twelveFactorSummary(ast) {
171
+ const r = twelveFactorReport(ast);
172
+ return { schemaVersion: r.schemaVersion, score: r.score, grade: r.grade, counts: r.counts };
173
+ }
@@ -0,0 +1,105 @@
1
+ // Verify a code change against its intent (intent-verify-diff-v1) , the keystone of the AI loop.
2
+ // A human (or an agent) states intent; an AI proposes a code change; THIS proves, deterministically
3
+ // and with no AI, which of the intent's guarantees and never-rules the change upholds or breaks,
4
+ // and returns a gate verdict (PASS / BLOCK). It is honest: it does not claim to prove correctness
5
+ // (that needs tests + humans), but it catches the mechanical violations AI diffs actually ship ,
6
+ // a secret written to a log, a declared input dropped from a signature, a guarantee whose evidence
7
+ // the change removed.
8
+ //
9
+ // verifyDiff(intentText, { before?, after, language }) -> { verdict, ok, findings, blocking }
10
+ //
11
+ // The two signals that make this a DIFF check, not just a snapshot:
12
+ // 1. Regressions , a claim that held on `before` but is broken on `after` (the change's fault).
13
+ // 2. Guardrail hits , lines the change ADDED that match a never-rule's sensitive term reaching a
14
+ // sink (log/print/response/...). This is the active check that catches AI-introduced leaks.
15
+
16
+ import { parseIntent } from './parse.mjs';
17
+ import { checkDrift } from './drift.mjs';
18
+
19
+ export const VERIFY_DIFF_SCHEMA = 'intent-verify-diff-v1';
20
+
21
+ // A value reaching an output sink , where a leak would happen.
22
+ const SINK_RE = /\b(log|logger|logging|console|print|println|printf|echo|write|send|res|response|reply|render|fmt\.Print\w*|System\.out|puts|p\b)\b|\bconsole\.\w+|\bres\.\w+|\bresponse\.\w+/i;
23
+ // Sensitive nouns a never-rule might protect. If the rule names one and an added line sends it to
24
+ // a sink, that is a probable violation.
25
+ const SENSITIVE_TERMS = ['token', 'secret', 'password', 'passwd', 'credential', 'ssn', 'pii', 'card', 'cvv', 'apikey', 'api_key', 'privatekey', 'private_key', 'session', 'jwt', 'email', 'address', 'phone', 'dob', 'birthdate'];
26
+
27
+ function sensitiveTermsOf(statement) {
28
+ const s = String(statement).toLowerCase();
29
+ return SENSITIVE_TERMS.filter((t) => new RegExp(`\\b${t.replace('_', '[_ ]?')}`, 'i').test(s));
30
+ }
31
+
32
+ // Split a code line into identifier words, breaking camelCase and snake_case, so a term like
33
+ // "token" matches `paymentToken`, `payment_token`, and `paymenttoken` , the common ways an AI
34
+ // diff names a secret variable.
35
+ function identifierWords(text) {
36
+ return String(text)
37
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
38
+ .replace(/[^A-Za-z0-9]+/g, ' ')
39
+ .toLowerCase()
40
+ .split(/\s+/)
41
+ .filter(Boolean);
42
+ }
43
+ const lineHitsTerm = (text, terms) => {
44
+ const words = identifierWords(text);
45
+ return terms.some((term) => words.some((w) => w === term || w.endsWith(term)));
46
+ };
47
+
48
+ // Lines present in `after` but not in `before` (exact-text set diff , enough to spot an added
49
+ // sink line). When there is no `before`, every non-trivial line is "added" (verifying fresh code).
50
+ function addedLines(before, after) {
51
+ const afterLines = String(after ?? '').split(/\r?\n/);
52
+ if (before == null) return afterLines.map((text, i) => ({ line: i + 1, text })).filter((l) => l.text.trim());
53
+ const beforeSet = new Set(String(before).split(/\r?\n/).map((l) => l.trim()));
54
+ return afterLines.map((text, i) => ({ line: i + 1, text })).filter((l) => l.text.trim() && !beforeSet.has(l.text.trim()));
55
+ }
56
+
57
+ /**
58
+ * Verify a proposed code change against its intent. Returns a gate verdict + findings.
59
+ * @param {string} intentText the .intent source (the contract)
60
+ * @param {{ before?: string|null, after: string, language?: string }} change
61
+ */
62
+ export function verifyDiff(intentText, { before = null, after, language = 'typescript' } = {}) {
63
+ const ast = parseIntent(intentText);
64
+ const findings = [];
65
+
66
+ // 1. Contract drift on the AFTER code (guarantee/never/input support vs the intent).
67
+ const driftAfter = checkDrift(intentText, String(after ?? ''), { language });
68
+ // 2. What held BEFORE, so we can tell a pre-existing gap from a regression the change introduced.
69
+ const beforeKeys = before != null
70
+ ? new Set(checkDrift(intentText, before, { language }).findings.map((f) => `${f.code}|${f.message}`))
71
+ : null;
72
+ for (const f of driftAfter.findings) {
73
+ if (f.code === 'INTENT_DRIFT_NOT_APPROVED' || f.code === 'INTENT_DRIFT_NEW_BEHAVIOR') { findings.push({ ...f, regression: false }); continue; }
74
+ const regression = beforeKeys ? !beforeKeys.has(`${f.code}|${f.message}`) : false;
75
+ findings.push({ ...f, regression });
76
+ }
77
+
78
+ // 3. Guardrail scan: added lines that push a never-rule's sensitive term into a sink.
79
+ const added = addedLines(before, after);
80
+ for (const n of ast.neverRules || []) {
81
+ const terms = sensitiveTermsOf(n.statement);
82
+ if (!terms.length) continue;
83
+ const wantsSink = /\b(log|logs|logged|logging|expose|exposed|leak|print|return|respond|response|send|output)\b/i.test(n.statement);
84
+ for (const { line, text } of added) {
85
+ const hitsTerm = lineHitsTerm(text, terms);
86
+ if (hitsTerm && (SINK_RE.test(text) || wantsSink && /=|\(|:/.test(text))) {
87
+ findings.push({
88
+ level: 'error', code: 'INTENT_VERIFY_NEVER_VIOLATED', regression: true, line,
89
+ message: `Added code may violate never-rule "${n.statement}": ${text.trim().slice(0, 90)}`,
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ // Verdict: block on any guardrail violation, or any regression of a contract claim.
96
+ const blocking = findings.filter((f) => f.code === 'INTENT_VERIFY_NEVER_VIOLATED' || (f.regression && f.level === 'warning'));
97
+ return {
98
+ schema: VERIFY_DIFF_SCHEMA,
99
+ ok: blocking.length === 0,
100
+ verdict: blocking.length ? 'BLOCK' : 'PASS',
101
+ findings,
102
+ blocking: blocking.length,
103
+ summary: { verdict: blocking.length ? 'BLOCK' : 'PASS', findings: findings.length, blocking: blocking.length, regressions: findings.filter((f) => f.regression).length },
104
+ };
105
+ }
package/src/xml.mjs ADDED
@@ -0,0 +1,87 @@
1
+ // A tiny, dependency-free, deterministic XML parser , just enough to read the DMN/BPMN we
2
+ // emit and reasonably well-formed real-world files. Not a validating parser: it handles
3
+ // elements, attributes (single/double quoted), text, self-closing tags, comments, the XML
4
+ // declaration, and entity decoding. Namespaces are kept in the raw name; match by localName.
5
+
6
+ const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
7
+ export const decodeEntities = (s) =>
8
+ String(s).replace(/&(amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/g, (_, e) =>
9
+ e[0] === '#' ? String.fromCodePoint(e[1] === 'x' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10)) : ENTITIES[e]);
10
+
11
+ // Strip the namespace prefix: "dmn:decision" -> "decision".
12
+ export const localName = (name) => String(name).split(':').pop();
13
+
14
+ function parseAttrs(str) {
15
+ const attrs = {};
16
+ const re = /([^\s=/]+)\s*=\s*"([^"]*)"|([^\s=/]+)\s*=\s*'([^']*)'/g;
17
+ let m;
18
+ while ((m = re.exec(str))) {
19
+ if (m[1] !== undefined) attrs[m[1]] = decodeEntities(m[2]);
20
+ else attrs[m[3]] = decodeEntities(m[4]);
21
+ }
22
+ return attrs;
23
+ }
24
+
25
+ /** Parse an XML string into a tree of { name, attrs, children, text }. Root is a synthetic node. */
26
+ export function parseXml(input) {
27
+ const s = String(input ?? '');
28
+ const root = { name: '#root', attrs: {}, children: [], text: '' };
29
+ const stack = [root];
30
+ let i = 0;
31
+ while (i < s.length) {
32
+ if (s[i] === '<') {
33
+ if (s.startsWith('<!--', i)) { const e = s.indexOf('-->', i); i = e < 0 ? s.length : e + 3; continue; }
34
+ if (s.startsWith('<?', i)) { const e = s.indexOf('?>', i); i = e < 0 ? s.length : e + 2; continue; }
35
+ if (s.startsWith('<![CDATA[', i)) {
36
+ const e = s.indexOf(']]>', i);
37
+ stack[stack.length - 1].text += s.slice(i + 9, e < 0 ? s.length : e);
38
+ i = e < 0 ? s.length : e + 3; continue;
39
+ }
40
+ if (s.startsWith('<!', i)) { const e = s.indexOf('>', i); i = e < 0 ? s.length : e + 1; continue; }
41
+ const e = s.indexOf('>', i);
42
+ if (e < 0) break;
43
+ if (s[i + 1] === '/') { if (stack.length > 1) stack.pop(); i = e + 1; continue; }
44
+ let raw = s.slice(i + 1, e).trim();
45
+ const selfClose = raw.endsWith('/');
46
+ if (selfClose) raw = raw.slice(0, -1).trim();
47
+ const sp = raw.search(/\s/);
48
+ const name = sp < 0 ? raw : raw.slice(0, sp);
49
+ const node = { name, attrs: parseAttrs(sp < 0 ? '' : raw.slice(sp)), children: [], text: '' };
50
+ stack[stack.length - 1].children.push(node);
51
+ if (!selfClose) stack.push(node);
52
+ i = e + 1;
53
+ } else {
54
+ const e = s.indexOf('<', i);
55
+ const end = e < 0 ? s.length : e;
56
+ const t = decodeEntities(s.slice(i, end)).trim();
57
+ if (t) stack[stack.length - 1].text += t;
58
+ i = end;
59
+ }
60
+ }
61
+ return root;
62
+ }
63
+
64
+ // ── Tree walk helpers (all match by localName, ignoring namespace prefixes) ──
65
+
66
+ /** All descendant elements with the given localName (depth-first, document order). */
67
+ export function findAll(node, lname) {
68
+ const out = [];
69
+ const walk = (n) => {
70
+ for (const c of n.children || []) {
71
+ if (localName(c.name) === lname) out.push(c);
72
+ walk(c);
73
+ }
74
+ };
75
+ walk(node);
76
+ return out;
77
+ }
78
+
79
+ /** First descendant with the given localName, or null. */
80
+ export function find(node, lname) {
81
+ return findAll(node, lname)[0] || null;
82
+ }
83
+
84
+ /** Direct children with the given localName. */
85
+ export function childrenNamed(node, lname) {
86
+ return (node.children || []).filter((c) => localName(c.name) === lname);
87
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "name": "ThunderLang",
4
+ "scopeName": "source.intent",
5
+ "fileTypes": ["intent"],
6
+ "patterns": [
7
+ { "include": "#comment" },
8
+ { "include": "#string" },
9
+ { "include": "#block-keyword" },
10
+ { "include": "#field-type" },
11
+ { "include": "#operator" },
12
+ { "include": "#number" },
13
+ { "include": "#type" }
14
+ ],
15
+ "repository": {
16
+ "comment": {
17
+ "match": "#.*$",
18
+ "name": "comment.line.number-sign.intent"
19
+ },
20
+ "string": {
21
+ "name": "string.quoted.double.intent",
22
+ "begin": "\"",
23
+ "end": "\"",
24
+ "patterns": [{ "match": "\\\\.", "name": "constant.character.escape.intent" }]
25
+ },
26
+ "block-keyword": {
27
+ "comment": "A line-leading keyword introduces a block or a field (indentation-structured).",
28
+ "match": "^\\s*(mission|use|goal|why|because|requires|inputs?|outputs?|guarantees?|never|constraints?|assumptions?|risks|target|style|verify|errors|examples|service|api|event|database|architecture|selection|title|for|persona|customer|problem|evidence|opportunity|outcome_contract|outcome|metric|scope|include|exclude|non_goal|owner|unknown|question|assumption|experience|pattern|decision|rule|default|when|return|command|on|compensate|notify|preserve|lifecycle|state|transition|from|to|terminal|always|eventually|until|conflict|options|between|resolve_by|resolution|choose|approval|capability|interface|provides|slo|release|result|learning|component|artifact|test|case|scenario|given|expect|events|data|waiver|note|implements|covers|variant|token|classification|purpose|retention|basis|baseline|window|direction|includes|measures|value|kind|ref|version|status|date|reason|approved_by|by|at|decision|expires|before|idempotency_key|timeout|retry|backoff|confidence|source|recover|follows|actor|accessible|responsive|asked_of|resolve|validate)\\b",
29
+ "captures": { "1": { "name": "keyword.control.intent" } }
30
+ },
31
+ "field-type": {
32
+ "comment": "A typed field: name: Type (optionally List<Type>).",
33
+ "match": "\\b([a-z][A-Za-z0-9_.]*)\\s*(:)\\s*([A-Z][A-Za-z0-9]*(?:<[^>]+>)?)",
34
+ "captures": {
35
+ "1": { "name": "variable.other.member.intent" },
36
+ "2": { "name": "punctuation.separator.intent" },
37
+ "3": { "name": "entity.name.type.intent" }
38
+ }
39
+ },
40
+ "operator": {
41
+ "comment": "Expression operators used in decision conditions (when ...).",
42
+ "match": "(>=|<=|==|!=|>|<|=|\\+|\\-|\\*|/|%)|\\b(and|or|not|in)\\b",
43
+ "name": "keyword.operator.intent"
44
+ },
45
+ "number": {
46
+ "match": "\\b\\d+(?:\\.\\d+)?%?\\b",
47
+ "name": "constant.numeric.intent"
48
+ },
49
+ "type": {
50
+ "comment": "PascalCase identifiers are entities/types in ThunderLang.",
51
+ "match": "\\b([A-Z][A-Za-z0-9]*)(?:<[^>]+>)?\\b",
52
+ "name": "entity.name.type.intent"
53
+ }
54
+ }
55
+ }