@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
package/src/parse.mjs ADDED
@@ -0,0 +1,733 @@
1
+ // ThunderLang parser (deterministic, no AI). Turns .intent source into an Intent AST.
2
+ // This is the MVP emit-stage parser: it covers the core constructs (mission, goal, why,
3
+ // requires, input, output, guarantees, never, constraints, assumptions, risks, target,
4
+ // style, verify) plus architecture blocks (service, api, event, database). Detail blocks
5
+ // `guarantee <text>` / `never <text>` attach `because` (rationale) and `verify` to a rule.
6
+ //
7
+ // Indentation defines structure: a keyword on its own line opens a block; deeper-indented
8
+ // lines are its children. We build an indent tree first, then interpret it, so nested
9
+ // architecture blocks (service -> owns/publishes/...) parse cleanly.
10
+
11
+ const firstWord = (s) => s.split(/\s+/)[0];
12
+ const rest = (s) => s.split(/\s+/).slice(1).join(' ').trim();
13
+
14
+ export function slug(text) {
15
+ return String(text)
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '')
19
+ .slice(0, 80) || 'unnamed';
20
+ }
21
+
22
+ // The primary subject of a file, so a non-mission root (a standalone service / api / event /
23
+ // capability) titles its graph and docs by its real name instead of a null "mission".
24
+ export function subjectName(ast) {
25
+ return ast?.mission || ast?.title
26
+ || ast?.services?.[0]?.name || ast?.apis?.[0]?.name || ast?.events?.[0]?.name
27
+ || ast?.capabilities?.[0]?.name || ast?.commands?.[0]?.name || ast?.decisions?.[0]?.name
28
+ || ast?.lifecycles?.[0]?.name || null;
29
+ }
30
+
31
+ /**
32
+ * The canonical cross-ecosystem intent reference id , the stable string every SkillsTech
33
+ * product puts in evidence-event-v1 / proof-bundle-v1 `intentReferences[]` so an evidence
34
+ * record or Ownership Proof cites the EXACT intent it supports. IL owns this id shape.
35
+ *
36
+ * - Subject-level (which intent): `intent:<mission-slug>` (stable across versions)
37
+ * - Version-pinned (which compile): `intent:<mission-slug>@<sha8>` (pass the proof `sourceHash`)
38
+ *
39
+ * Deterministic + browser-safe. Accepts an AST or a plain name string.
40
+ */
41
+ export function intentRefId(astOrName, { sourceHash } = {}) {
42
+ const name = typeof astOrName === 'string' ? astOrName : subjectName(astOrName);
43
+ const base = `intent:${slug(name || 'mission')}`;
44
+ if (!sourceHash) return base;
45
+ const short = String(sourceHash).replace(/^sha256:/, '').slice(0, 8);
46
+ return short ? `${base}@${short}` : base;
47
+ }
48
+
49
+ /**
50
+ * The canonical shared skill id. IL owns the `skill:` NAMESPACE (the id SHAPE, this function);
51
+ * SkillsTech Certified owns the CONTENT (which skills exist, aliases, cert->skill maps) , a
52
+ * founder decision (2026-07-14). Deterministic + browser-safe, so OT/RM/STT/Certified emit the
53
+ * SAME skill id for the same skill in evidence-event-v1 `skillIds[]`.
54
+ * skillRefId('TypeScript') -> 'skill:typescript'
55
+ * skillRefId('Distributed Systems') -> 'skill:distributed-systems'
56
+ * This is the id primitive only; the curated taxonomy list is STCE's and lands post-loop.
57
+ */
58
+ export function skillRefId(name) {
59
+ return `skill:${slug(name || 'unknown')}`;
60
+ }
61
+
62
+ // Strip comments (ignored), drop blank lines, keep indentation and 1-based line.
63
+ // A `#` comment is IGNORED by the compiler; `note <lens>:` blocks are compiled.
64
+ function toRows(source) {
65
+ const rows = [];
66
+ const lines = source.split(/\r?\n/);
67
+ for (let i = 0; i < lines.length; i++) {
68
+ let line = lines[i].replace(/^\s*#.*$/, '');
69
+ line = line.replace(/\s+#\s.*$/, '');
70
+ if (!line.trim()) continue;
71
+ rows.push({ indent: line.length - line.trimStart().length, text: line.trim(), line: i + 1 });
72
+ }
73
+ return rows;
74
+ }
75
+
76
+ // Build an indent tree: each node = { text, line, children: [] }.
77
+ function buildTree(rows) {
78
+ const root = { text: '__root__', line: 0, children: [] };
79
+ const stack = [{ indent: -1, node: root }];
80
+ for (const row of rows) {
81
+ const node = { text: row.text, line: row.line, children: [] };
82
+ while (stack.length > 1 && row.indent <= stack[stack.length - 1].indent) stack.pop();
83
+ stack[stack.length - 1].node.children.push(node);
84
+ stack.push({ indent: row.indent, node });
85
+ }
86
+ return root.children;
87
+ }
88
+
89
+ // Known IntentLens reader lenses. Unknown lenses warn (INTENT_NOTE_UNKNOWN_LENS).
90
+ export const KNOWN_LENSES = [
91
+ 'pm', 'beginner', 'qa', 'risk', 'security', 'support', 'reviewer', 'ops', 'non_goal', 'term',
92
+ ];
93
+
94
+ // Parse a `note <lens>:` node. Text is inline after the colon, or the child body.
95
+ function parseNoteNode(node) {
96
+ const after = node.text.replace(/^note\s+/i, '');
97
+ const m = after.match(/^([A-Za-z_]+)\s*:?\s*(.*)$/);
98
+ const lens = m ? m[1] : after.trim();
99
+ const inline = m ? m[2].trim() : '';
100
+ const body = node.children.map((c) => c.text).join(' ').trim();
101
+ return { lens, text: inline || body, line: node.line };
102
+ }
103
+
104
+ const isNote = (node) => firstWord(node.text).toLowerCase() === 'note';
105
+
106
+ const leafItems = (node) => node.children.map((c) => c.text);
107
+ const stripQuotes = (s) => String(s ?? '').trim().replace(/^"(.*)"$/s, '$1');
108
+ // key/value children: "classification observed" -> { classification: "observed" }.
109
+ const kvChildren = (node) => {
110
+ const o = {};
111
+ for (const c of node.children) { if (isNote(c)) continue; o[firstWord(c.text)] = rest(c.text); }
112
+ return o;
113
+ };
114
+ const childBlock = (node, kw) => node.children.find((c) => firstWord(c.text) === kw);
115
+
116
+ function parseFields(node) {
117
+ // "key: Type" lines. A field may carry indented modifiers and IntentLens notes.
118
+ return node.children.map((c) => {
119
+ const m = c.text.match(/^([A-Za-z_][\w]*)\s*:\s*(.+)$/);
120
+ const modifiers = [];
121
+ const notes = [];
122
+ for (const mc of c.children) {
123
+ if (isNote(mc)) notes.push(parseNoteNode(mc));
124
+ else modifiers.push(mc.text);
125
+ }
126
+ const base = m ? { name: m[1], type: m[2].trim() } : { name: c.text, type: null };
127
+ return { ...base, modifiers, notes, line: c.line };
128
+ });
129
+ }
130
+
131
+ // Roles that can contribute scoped constraints (Gap 1).
132
+ const ROLE_KEYWORDS = new Set(['product', 'experience', 'security', 'legal', 'operations', 'analytics', 'engineering', 'accessibility', 'business', 'design', 'qa', 'ux']);
133
+ const kids = (node) => node.children.filter((c) => !isNote(c));
134
+ const RECOVERY_RE = /\b(retry|recover|contact\s*support|contactsupport|retryprocessing|try\s*again|resume)\b/i;
135
+
136
+ // Experience Contract (intent-graph-v1 Section 7.3): UX behavior as a first-class contract.
137
+ function parseExperience(name, node) {
138
+ const exp = { name, actor: null, goal: '', enterWhen: [], journeys: [], states: [], responsive: [], accessible: { target: null, requirements: [] }, follows: [], line: node.line };
139
+ for (const c of kids(node)) {
140
+ const k = firstWord(c.text); const a = rest(c.text);
141
+ if (k === 'actor') exp.actor = a || null;
142
+ else if (k === 'goal') exp.goal = leafItems(c).map(stripQuotes).join(' ').trim();
143
+ else if (k === 'enter') exp.enterWhen.push(...leafItems(c)); // "enter when" + conditions
144
+ else if (k === 'journey') exp.journeys.push({ name: a || null, steps: leafItems(c) });
145
+ else if (k === 'state') {
146
+ const directives = kids(c).map((x) => x.text);
147
+ exp.states.push({
148
+ name: a || null, directives,
149
+ offers: directives.filter((x) => /^offer\b/i.test(x)).map((x) => x.replace(/^offer\s+/i, '')),
150
+ preserves: directives.some((x) => /^preserve\b/i.test(x)),
151
+ hasRecovery: directives.some((x) => RECOVERY_RE.test(x)),
152
+ line: c.line,
153
+ });
154
+ } else if (k === 'responsive') exp.responsive.push(...leafItems(c).map((x) => x.replace(/^support\s+/i, '')));
155
+ else if (k === 'accessible' || k === 'accessibility') {
156
+ for (const x of kids(c)) {
157
+ if (firstWord(x.text) === 'target') exp.accessible.target = rest(x.text);
158
+ else exp.accessible.requirements.push(x.text);
159
+ }
160
+ } else if (k === 'follows') exp.follows.push(a);
161
+ }
162
+ return exp;
163
+ }
164
+
165
+ // Decision / rules (intent-graph-v1 Gap 4).
166
+ function parseDecision(name, node) {
167
+ const dec = { name, inputs: [], rules: [], default: null, explanationRequired: false, owner: null, line: node.line };
168
+ for (const c of node.children.filter((x) => !isNote(x))) {
169
+ const k = firstWord(c.text); const a = rest(c.text);
170
+ if (k === 'inputs') dec.inputs.push(...leafItems(c));
171
+ else if (k === 'rule') {
172
+ const kv = {};
173
+ for (const ch of c.children.filter((x) => !isNote(x))) kv[firstWord(ch.text)] = rest(ch.text);
174
+ dec.rules.push({ name: a || null, when: kv.when || null, result: kv.return || null, priority: kv.priority || null, line: c.line });
175
+ } else if (k === 'default') {
176
+ const ret = c.children.find((x) => firstWord(x.text) === 'return');
177
+ dec.default = ret ? rest(ret.text) : (a || null);
178
+ } else if (k === 'explanation') dec.explanationRequired = /required/.test(a);
179
+ else if (k === 'owner') dec.owner = a || null;
180
+ }
181
+ return dec;
182
+ }
183
+
184
+ // Lifecycle state machine (intent-graph-v1 Gap 2).
185
+ function parseLifecycle(name, node) {
186
+ const lc = { name, states: [], transitions: [], terminals: [], line: node.line };
187
+ for (const c of node.children.filter((x) => !isNote(x))) {
188
+ const k = firstWord(c.text); const a = rest(c.text);
189
+ if (k === 'state') lc.states.push(a);
190
+ else if (k === 'transition') {
191
+ const kv = {};
192
+ for (const ch of c.children.filter((x) => !isNote(x))) kv[firstWord(ch.text)] = rest(ch.text);
193
+ lc.transitions.push({ name: a || null, from: kv.from || null, to: kv.to || null, within: kv.within || null });
194
+ } else if (k === 'terminal') lc.terminals.push(...a.split(',').map((s) => s.trim()).filter(Boolean));
195
+ }
196
+ return lc;
197
+ }
198
+
199
+ // Reusable experience pattern (Section 7.3).
200
+ function parsePattern(name, node) {
201
+ const p = { name, requires: [], accessible: [], line: node.line };
202
+ for (const c of kids(node)) {
203
+ const k = firstWord(c.text);
204
+ if (k === 'requires') p.requires.push(...leafItems(c));
205
+ else if (k === 'accessible' || k === 'accessibility') p.accessible.push(...leafItems(c));
206
+ }
207
+ return p;
208
+ }
209
+
210
+ // A global invariant , a system-wide law that must hold across features / services / data.
211
+ // `invariant Name` + statement / scope / applies_to / severity / because / verify.
212
+ function parseInvariant(name, node) {
213
+ const list = (kw) => { const b = childBlock(node, kw); return b ? leafItems(b) : []; };
214
+ const inv = { id: slug(name), name, statement: name, scope: 'global', appliesTo: list('applies_to'), severity: 'critical', because: null, verify: [], line: node.line };
215
+ for (const c of kids(node)) {
216
+ const k = firstWord(c.text);
217
+ if (k === 'verify') inv.verify.push(rest(c.text));
218
+ else if (k === 'because') inv.because = rest(c.text);
219
+ else if (k === 'statement') inv.statement = rest(c.text) || leafItems(c).join(' ');
220
+ else if (k === 'scope') inv.scope = (rest(c.text) || leafItems(c).join(' ')).toLowerCase();
221
+ else if (k === 'severity') inv.severity = (rest(c.text) || leafItems(c).join(' ')).toLowerCase();
222
+ }
223
+ inv.verify = inv.verify.filter(Boolean);
224
+ return inv;
225
+ }
226
+
227
+ function parseService(name, node) {
228
+ return {
229
+ id: slug(name), name,
230
+ owns: (childBlock(node, 'owns') && leafItems(childBlock(node, 'owns'))) || [],
231
+ consumes: (childBlock(node, 'consumes') && leafItems(childBlock(node, 'consumes'))) || [],
232
+ publishes: (childBlock(node, 'publishes') && leafItems(childBlock(node, 'publishes'))) || [],
233
+ database: (childBlock(node, 'database') && leafItems(childBlock(node, 'database'))[0]) || null,
234
+ owner: (childBlock(node, 'owner') && leafItems(childBlock(node, 'owner')).join(' ')) || null,
235
+ };
236
+ }
237
+
238
+ function parseApi(name, node) {
239
+ const one = (kw) => { const b = childBlock(node, kw); return b ? leafItems(b).join(' ') : null; };
240
+ const list = (kw) => { const b = childBlock(node, kw); return b ? leafItems(b) : []; };
241
+ return {
242
+ id: slug(name), name,
243
+ method: one('method'), path: one('path'),
244
+ requires: list('requires'), errors: list('errors'),
245
+ input: one('input'), output: one('output'),
246
+ };
247
+ }
248
+
249
+ function parseEvent(name, node) {
250
+ const list = (kw) => { const b = childBlock(node, kw); return b ? leafItems(b) : []; };
251
+ const one = (kw) => { const c = node.children.find((x) => firstWord(x.text) === kw); return c ? rest(c.text) : null; };
252
+ const payloadBlock = childBlock(node, 'payload');
253
+ return {
254
+ id: slug(name), name,
255
+ publishedBy: list('publishedBy'), consumedBy: list('consumedBy'),
256
+ payload: payloadBlock ? parseFields(payloadBlock) : [],
257
+ guarantees: list('guarantees'),
258
+ // Distributed delivery semantics (Gap 3): reuse the event, add fields when present.
259
+ delivery: one('delivery'), orderedBy: one('ordered_by'),
260
+ };
261
+ }
262
+
263
+ // Command with failure policy (Gap 3): idempotency / timeout / retry / backoff.
264
+ function parseCommand(name, node) {
265
+ const cmd = { name, idempotencyKey: null, timeout: null, retry: null, backoff: null, line: node.line };
266
+ for (const c of node.children.filter((x) => !isNote(x))) {
267
+ const k = firstWord(c.text); const a = rest(c.text);
268
+ if (k === 'idempotency_key') cmd.idempotencyKey = a;
269
+ else if (k === 'timeout') cmd.timeout = a;
270
+ else if (k === 'retry') {
271
+ cmd.retry = a; // e.g. "at_most 2"
272
+ const withBackoff = c.children.find((x) => /^with\b/.test(x.text));
273
+ if (withBackoff) cmd.backoff = rest(withBackoff.text);
274
+ }
275
+ }
276
+ return cmd;
277
+ }
278
+
279
+ // Failure / duplicate handler (Gap 3): "on <trigger>" + actions.
280
+ function parseHandler(trigger, node) {
281
+ const actions = node.children.filter((x) => !isNote(x)).map((c) => c.text.trim());
282
+ return {
283
+ trigger,
284
+ compensate: actions.filter((a) => /^compensate\b/.test(a)).map((a) => a.replace(/^compensate\s+/, '')),
285
+ notify: actions.filter((a) => /^notify\b/.test(a)).map((a) => a.replace(/^notify\s+/, '')),
286
+ preserve: actions.filter((a) => /^preserve\b/.test(a)).map((a) => a.replace(/^preserve\s+/, '')),
287
+ actions,
288
+ };
289
+ }
290
+
291
+ function upsertRule(list, statement, line) {
292
+ const id = slug(statement);
293
+ let r = list.find((x) => x.id === id);
294
+ if (!r) { r = { id, statement, because: null, verify: [], notes: [], line }; list.push(r); }
295
+ return r;
296
+ }
297
+
298
+ function applyDetail(rule, node) {
299
+ for (const c of node.children) {
300
+ const kw = firstWord(c.text);
301
+ if (kw === 'because') rule.because = rest(c.text) || (c.children[0] && c.children[0].text) || null;
302
+ else if (kw === 'verify') rule.verify.push(rest(c.text) || (c.children[0] && c.children[0].text) || '');
303
+ else if (isNote(c)) rule.notes.push(parseNoteNode(c));
304
+ }
305
+ rule.verify = rule.verify.filter(Boolean);
306
+ }
307
+
308
+ export function parseIntent(source) {
309
+ const ast = {
310
+ mission: null, goal: '', why: '',
311
+ requires: [], inputs: [], outputs: [],
312
+ guarantees: [], neverRules: [], constraints: [], invariants: [], assumptions: [], risks: [],
313
+ targets: [], style: [], verify: [], errors: [], examples: [],
314
+ services: [], apis: [], events: [], databases: [], architecture: [],
315
+ implementation: null, selection: [],
316
+ // Product / intent-graph profile (intent-graph-v1)
317
+ profiles: [], title: null, actor: null, problem: '', persona: null, customer: null,
318
+ evidence: [], outcomes: [], metrics: [],
319
+ scope: { include: [], exclude: [] }, nonGoals: [],
320
+ owner: null, approvals: [], unknowns: [], questions: [], assumptionDecls: [],
321
+ // Experience profile (intent-graph-v1)
322
+ experiences: [], patterns: [],
323
+ // Style intent , brand/visual language as a governed Experience-profile extension
324
+ styleIntents: [],
325
+ // Constraint composition + conflict resolution (Gap 1)
326
+ roleConstraints: [], conflicts: [],
327
+ // Temporal + lifecycle semantics (Gap 2)
328
+ lifecycles: [], always: [], eventually: [], until: [],
329
+ // Distributed + failure semantics (Gap 3)
330
+ commands: [], handlers: [],
331
+ // Decisions, rules, process (Gap 4)
332
+ decisions: [],
333
+ // Governance: waivers , governed exceptions to blocking diagnostics (Gap 5)
334
+ waivers: [],
335
+ // Data purpose + privacy , governed data elements (Gap 6)
336
+ dataElements: [],
337
+ // System profile , capabilities + system contracts (interfaces)
338
+ capabilities: [], interfaces: [],
339
+ // Design profile , design-system components + artifacts (mockups)
340
+ components: [], artifacts: [],
341
+ // Delivery profile , releases, outcome results, learnings
342
+ releases: [], results: [], learnings: [],
343
+ // Outcome contracts , executable commitments binding an outcome to a target
344
+ outcomeContracts: [],
345
+ // Skills the mission requires + the understanding a human must demonstrate (Ownership Graph seam)
346
+ skills: [], demonstrates: [],
347
+ // Tests , first-class cases that make a .intent file self-verifying
348
+ tests: [],
349
+ notes: [], diagnostics: [],
350
+ };
351
+ const missionNotes = [];
352
+ const items = (node) => node.children.filter((c) => !isNote(c));
353
+ for (const node of buildTree(toRows(source))) {
354
+ const kw = firstWord(node.text);
355
+ const arg = rest(node.text);
356
+ // Role-scoped constraints (Gap 1): "product requires", "security requires", ...
357
+ // Each role contributes constraints independently; IL composes them deterministically.
358
+ if (ROLE_KEYWORDS.has(kw) && /^requires\b/.test(arg)) {
359
+ for (const c of items(node)) ast.roleConstraints.push({ role: kw, statement: c.text.trim(), line: c.line });
360
+ continue;
361
+ }
362
+ switch (kw) {
363
+ case 'mission': ast.mission = arg || null; break;
364
+ case 'note': missionNotes.push(parseNoteNode(node)); break;
365
+ case 'goal': ast.goal = leafItems(node).join(' '); break;
366
+ case 'why': ast.why = leafItems(node).join(' '); break;
367
+ case 'requires': ast.requires.push(...leafItems(node)); break;
368
+ // Skills the mission requires (Ownership Graph seam). Inline "requires_skill A, B" or a block.
369
+ // Each is normalized to the shared `skill:<slug>` namespace (IL owns the id; STCE the content).
370
+ case 'requires_skill': case 'requires_skills': {
371
+ const names = (arg ? arg.split(',') : leafItems(node)).map((s) => s.trim()).filter(Boolean);
372
+ for (const name of names) ast.skills.push({ name, id: skillRefId(name), line: node.line });
373
+ break;
374
+ }
375
+ // What a human must be able to explain to own this mission (ties to Comprehension C0..C7).
376
+ case 'demonstrates': {
377
+ const stmts = (arg ? [arg] : leafItems(node)).map((s) => String(s).trim()).filter(Boolean);
378
+ for (const statement of stmts) ast.demonstrates.push({ statement, line: node.line });
379
+ break;
380
+ }
381
+ case 'input': ast.inputs.push(...parseFields(node)); break;
382
+ case 'output': ast.outputs.push(...parseFields(node)); break;
383
+ case 'guarantees': for (const c of items(node)) upsertRule(ast.guarantees, c.text, c.line); break;
384
+ case 'guarantee': applyDetail(upsertRule(ast.guarantees, arg, node.line), node); break;
385
+ case 'never':
386
+ if (arg) applyDetail(upsertRule(ast.neverRules, arg, node.line), node);
387
+ else for (const c of items(node)) upsertRule(ast.neverRules, c.text, c.line);
388
+ break;
389
+ case 'constraints': ast.constraints.push(...leafItems(node)); break;
390
+ case 'invariant': if (arg) ast.invariants.push(parseInvariant(arg, node)); break;
391
+ case 'assumptions': ast.assumptions.push(...leafItems(node)); break;
392
+ case 'risks': ast.risks.push(...leafItems(node)); break;
393
+ case 'target': ast.targets.push(...leafItems(node)); break;
394
+ case 'style': ast.style.push(...leafItems(node)); break;
395
+ case 'verify': ast.verify.push(...leafItems(node)); break;
396
+ // Named failure modes: PascalCase names -> a result/status union + per-error tests.
397
+ case 'errors':
398
+ for (const c of items(node)) ast.errors.push({ name: firstWord(c.text), line: c.line });
399
+ break;
400
+ // Executable examples: "given <input> -> expect <outcome>".
401
+ case 'examples':
402
+ for (const c of items(node)) {
403
+ const m = c.text.match(/^(?:given\s+)?(.*?)\s*->\s*(?:expect\s+)?(.*)$/i);
404
+ if (m) ast.examples.push({ given: m[1].trim(), expect: m[2].trim(), line: c.line });
405
+ else ast.examples.push({ given: c.text.trim(), expect: null, line: c.line });
406
+ }
407
+ break;
408
+ case 'service': ast.services.push(parseService(arg, node)); break;
409
+ case 'api': ast.apis.push(parseApi(arg, node)); break;
410
+ case 'event': ast.events.push(parseEvent(arg, node)); break;
411
+ case 'database': ast.databases.push({ id: slug(arg), name: arg, engine: leafItems(node)[0] || null }); break;
412
+ case 'architecture': ast.architecture.push(...leafItems(node)); break;
413
+ case 'selection': ast.selection.push(...leafItems(node)); break;
414
+ // ── Product / intent-graph profile (intent-graph-v1) ──
415
+ case 'use': if (arg) ast.profiles.push(arg); break;
416
+ case 'title': ast.title = stripQuotes(arg || leafItems(node).join(' ')); break;
417
+ case 'for': ast.actor = arg || null; break;
418
+ case 'persona': ast.persona = arg || null; break;
419
+ case 'customer': ast.customer = arg || null; break;
420
+ case 'problem': ast.problem = leafItems(node).map(stripQuotes).join(' ').trim(); break;
421
+ case 'evidence': {
422
+ const kv = kvChildren(node);
423
+ ast.evidence.push({ name: arg, classification: kv.classification || null, confidence: kv.confidence || null, source: kv.source || null, line: node.line });
424
+ break;
425
+ }
426
+ case 'outcome': ast.outcomes.push({ name: arg, description: stripQuotes(leafItems(node).join(' ')) || null, line: node.line }); break;
427
+ case 'metric': {
428
+ const kv = kvChildren(node);
429
+ ast.metrics.push({ name: arg, baseline: kv.baseline || null, target: kv.target || null, window: kv.window || null, line: node.line });
430
+ break;
431
+ }
432
+ case 'scope':
433
+ for (const c of items(node)) {
434
+ const k = firstWord(c.text);
435
+ if (k === 'include') ast.scope.include.push(rest(c.text));
436
+ else if (k === 'exclude') ast.scope.exclude.push(rest(c.text));
437
+ }
438
+ break;
439
+ case 'non_goal': ast.nonGoals.push(arg || leafItems(node).join(' ')); break;
440
+ case 'owner': ast.owner = arg || null; break;
441
+ case 'unknown': {
442
+ const kv = kvChildren(node);
443
+ ast.unknowns.push({ name: arg, owner: kv.owner || null, resolveBefore: (kv.resolve || '').replace(/^before\s+/, '') || null, blocks: kv.blocks || null, line: node.line });
444
+ break;
445
+ }
446
+ case 'question': {
447
+ const kv = kvChildren(node);
448
+ ast.questions.push({ name: arg, askedOf: kv.asked_of || null, blocks: kv.blocks || null, line: node.line });
449
+ break;
450
+ }
451
+ case 'assumption': {
452
+ const kv = kvChildren(node);
453
+ ast.assumptionDecls.push({ name: arg, confidence: kv.confidence || null, validateWith: (kv.validate || '').replace(/^with\s+/, '') || null, line: node.line });
454
+ break;
455
+ }
456
+ case 'experience': ast.experiences.push(parseExperience(arg, node)); break;
457
+ case 'pattern': ast.patterns.push(parsePattern(arg, node)); break;
458
+ case 'style_intent': {
459
+ // Brand + visual language as a governed Experience-profile extension. Tokens are
460
+ // (path value) pairs against a canonical address space; accessibility_target is
461
+ // always a PROPOSED claim (IL never labels it verified , that is OT's verdict).
462
+ const si = {
463
+ name: arg || null, appliesTo: null, purpose: null,
464
+ audience: [], surfaces: [], tokens: [], accessibilityTarget: null,
465
+ scope: null, line: node.line,
466
+ };
467
+ for (const c of kids(node)) {
468
+ const k = firstWord(c.text); const a = rest(c.text);
469
+ if (k === 'applies_to') si.appliesTo = a || null;
470
+ else if (k === 'purpose') si.purpose = stripQuotes(a || leafItems(c).join(' '));
471
+ else if (k === 'audience') si.audience.push(...(a ? a.split(',').map((s) => s.trim()).filter(Boolean) : leafItems(c)));
472
+ else if (k === 'surface' || k === 'surfaces') si.surfaces.push(...(a ? a.split(',').map((s) => s.trim()).filter(Boolean) : leafItems(c)));
473
+ else if (k === 'token') {
474
+ const bits = a.split(/\s+/);
475
+ const path = bits.shift();
476
+ if (path) si.tokens.push({ path, value: stripQuotes(bits.join(' ')) || null, line: c.line });
477
+ } else if (k === 'accessibility_target' || k === 'accessibility') si.accessibilityTarget = (a || '').trim() || null;
478
+ else if (k === 'scope') si.scope = a || null;
479
+ }
480
+ ast.styleIntents.push(si);
481
+ break;
482
+ }
483
+ case 'decision': ast.decisions.push(parseDecision(arg, node)); break;
484
+ // ── System profile ──
485
+ case 'capability': {
486
+ const cap = { name: arg, description: null, implements: [], line: node.line };
487
+ for (const c of kids(node)) {
488
+ const k = firstWord(c.text); const a = rest(c.text);
489
+ if (k === 'description') cap.description = stripQuotes(a || leafItems(c).join(' '));
490
+ else if (k === 'implements') cap.implements.push(...(a ? [a] : leafItems(c)));
491
+ }
492
+ ast.capabilities.push(cap);
493
+ break;
494
+ }
495
+ case 'interface': {
496
+ const iface = { name: arg, provides: [], requires: [], slo: null, line: node.line };
497
+ for (const c of kids(node)) {
498
+ const k = firstWord(c.text); const a = rest(c.text);
499
+ if (k === 'provides') iface.provides.push(...(a ? [a] : leafItems(c)));
500
+ else if (k === 'requires') iface.requires.push(...(a ? [a] : leafItems(c)));
501
+ else if (k === 'slo') iface.slo = stripQuotes(a || leafItems(c).join(' '));
502
+ }
503
+ ast.interfaces.push(iface);
504
+ break;
505
+ }
506
+ // ── Design profile (design-system mappings) ──
507
+ case 'component': {
508
+ const comp = { name: arg, description: null, variants: [], tokens: [], implements: [], line: node.line };
509
+ for (const c of kids(node)) {
510
+ const k = firstWord(c.text); const a = rest(c.text);
511
+ if (k === 'description') comp.description = stripQuotes(a || leafItems(c).join(' '));
512
+ else if (k === 'variant') comp.variants.push(...(a ? [a] : leafItems(c)));
513
+ else if (k === 'token') comp.tokens.push(...(a ? [a] : leafItems(c)));
514
+ else if (k === 'implements') comp.implements.push(...(a ? [a] : leafItems(c)));
515
+ }
516
+ ast.components.push(comp);
517
+ break;
518
+ }
519
+ case 'artifact': {
520
+ const art = { name: arg, kind: null, ref: null, covers: [], line: node.line };
521
+ for (const c of kids(node)) {
522
+ const k = firstWord(c.text); const a = rest(c.text);
523
+ if (k === 'kind') art.kind = a;
524
+ else if (k === 'ref' || k === 'url') art.ref = stripQuotes(a);
525
+ else if (k === 'covers') art.covers.push(...(a ? [a] : leafItems(c)));
526
+ }
527
+ ast.artifacts.push(art);
528
+ break;
529
+ }
530
+ case 'outcome_contract': {
531
+ // An executable commitment: outcome achieved when its metric hits target within the
532
+ // window. `direction` (higher|lower, default higher) sets which way is "better".
533
+ const kv = kvChildren(node);
534
+ ast.outcomeContracts.push({
535
+ name: arg,
536
+ outcome: kv.outcome || null,
537
+ metric: kv.metric || null,
538
+ baseline: kv.baseline ? stripQuotes(kv.baseline) : null,
539
+ target: kv.target ? stripQuotes(kv.target) : null,
540
+ direction: /lower/i.test(kv.direction || '') ? 'lower' : 'higher',
541
+ window: kv.window || null,
542
+ // Guardrails: what must NOT regress while the outcome improves (else the target is gameable).
543
+ guardrails: (childBlock(node, 'guardrails') && leafItems(childBlock(node, 'guardrails'))) || [],
544
+ // Attribution honesty: how the outcome is evidenced. Never assume causation from a metric move.
545
+ attribution: (kv.attribution || 'unknown').toLowerCase(),
546
+ owner: kv.owner || null,
547
+ line: node.line,
548
+ });
549
+ break;
550
+ }
551
+ // ── Delivery profile ──
552
+ case 'release': {
553
+ const kv = kvChildren(node);
554
+ const rel = { name: arg, version: kv.version ? stripQuotes(kv.version) : null, status: kv.status || 'planned', date: kv.date || null, includes: [], line: node.line };
555
+ for (const c of kids(node)) if (firstWord(c.text) === 'includes') rel.includes.push(...(rest(c.text) ? [rest(c.text)] : leafItems(c)));
556
+ ast.releases.push(rel);
557
+ break;
558
+ }
559
+ case 'result': {
560
+ const res = { name: arg, measures: null, metric: null, value: null, baseline: null, line: node.line };
561
+ for (const c of kids(node)) {
562
+ const k = firstWord(c.text); const a = rest(c.text);
563
+ if (k === 'measures') res.measures = a;
564
+ else if (k === 'metric') res.metric = a;
565
+ else if (k === 'value') res.value = stripQuotes(a);
566
+ else if (k === 'baseline') res.baseline = stripQuotes(a);
567
+ }
568
+ ast.results.push(res);
569
+ break;
570
+ }
571
+ case 'learning': {
572
+ const learn = { name: arg, description: null, from: null, line: node.line };
573
+ for (const c of kids(node)) {
574
+ const k = firstWord(c.text); const a = rest(c.text);
575
+ if (k === 'description') learn.description = stripQuotes(a || leafItems(c).join(' '));
576
+ else if (k === 'from') learn.from = a;
577
+ }
578
+ ast.learnings.push(learn);
579
+ break;
580
+ }
581
+ case 'test': {
582
+ // A first-class test block targeting a decision or lifecycle by name. Each `case`
583
+ // (decision) has `given <k> <v>` + `expect <result>`; each `scenario` (lifecycle)
584
+ // has `events a, b, c` + optional `expect <finalState>` / `valid` / `invalid`.
585
+ const t = { name: arg || null, cases: [], line: node.line };
586
+ for (const c of kids(node)) {
587
+ const ck = firstWord(c.text);
588
+ if (ck !== 'case' && ck !== 'scenario') continue;
589
+ const cs = { name: rest(c.text) || null, given: {}, expect: null, events: null, expectValid: null, line: c.line };
590
+ for (const cc of kids(c)) {
591
+ const k = firstWord(cc.text); const a = rest(cc.text);
592
+ if (k === 'given') {
593
+ for (const pair of a.split(',')) {
594
+ const bits = pair.trim().split(/\s+/);
595
+ const key = bits.shift();
596
+ if (key) cs.given[key] = bits.join(' ');
597
+ }
598
+ } else if (k === 'expect') cs.expect = a;
599
+ else if (k === 'events') cs.events = a.split(',').map((s) => s.trim()).filter(Boolean);
600
+ else if (k === 'valid') cs.expectValid = true;
601
+ else if (k === 'invalid') cs.expectValid = false;
602
+ }
603
+ t.cases.push(cs);
604
+ }
605
+ ast.tests.push(t);
606
+ break;
607
+ }
608
+ case 'data': {
609
+ // A governed data element (Gap 6). "data <path>" + classification / purpose /
610
+ // retention / basis (lawful basis). Purpose limitation is enforced by privacy.mjs.
611
+ const kv = kvChildren(node);
612
+ ast.dataElements.push({
613
+ id: `data.${slug(arg || String(ast.dataElements.length + 1))}`,
614
+ path: arg || null,
615
+ classification: kv.classification || null,
616
+ purpose: kv.purpose ? stripQuotes(kv.purpose) : null,
617
+ retention: kv.retention || null,
618
+ basis: kv.basis || null,
619
+ line: node.line,
620
+ });
621
+ break;
622
+ }
623
+ case 'waiver': {
624
+ // A governed exception to a blocking diagnostic (Gap 5). "waiver <CODE>" +
625
+ // reason / approved_by / scope / expires. Deterministic; expiry evaluated by caller.
626
+ const kv = kvChildren(node);
627
+ ast.waivers.push({
628
+ id: `waiver.${slug(arg || String(ast.waivers.length + 1))}`,
629
+ code: arg || null,
630
+ reason: kv.reason ? stripQuotes(kv.reason) : null,
631
+ approvedBy: kv.approved_by || kv.by || null,
632
+ scope: kv.scope || null,
633
+ expires: kv.expires || null,
634
+ line: node.line,
635
+ });
636
+ break;
637
+ }
638
+ case 'command': ast.commands.push(parseCommand(arg, node)); break;
639
+ case 'on': ast.handlers.push(parseHandler(arg, node)); break;
640
+ case 'lifecycle': ast.lifecycles.push(parseLifecycle(arg, node)); break;
641
+ case 'always': ast.always.push(...leafItems(node)); break;
642
+ case 'eventually': {
643
+ const lines = leafItems(node);
644
+ const within = (lines.find((l) => /^within\b/i.test(l)) || '').replace(/^within\s+/i, '') || null;
645
+ const statement = lines.filter((l) => !/^within\b/i.test(l)).join(' ').trim();
646
+ ast.eventually.push({ statement, within, line: node.line });
647
+ break;
648
+ }
649
+ case 'until': {
650
+ const lines = leafItems(node);
651
+ const restrict = lines.find((l) => /^restrict\b/i.test(l)) || null;
652
+ const condition = lines.filter((l) => !/^restrict\b/i.test(l)).join(' ').trim();
653
+ ast.until.push({ condition, restrict: restrict ? restrict.replace(/^restrict\s+/i, '') : null, line: node.line });
654
+ break;
655
+ }
656
+ case 'conflict': {
657
+ const c = { name: arg, between: [], options: [], resolveBy: [], before: null, resolution: null, line: node.line };
658
+ for (const ch of items(node)) {
659
+ const k = firstWord(ch.text);
660
+ if (k === 'between') c.between.push(...leafItems(ch));
661
+ else if (k === 'options') c.options.push(...leafItems(ch));
662
+ else if (k === 'resolve_by') c.resolveBy.push(...rest(ch.text).split(',').map((s) => s.trim()).filter(Boolean));
663
+ else if (k === 'before') c.before = rest(ch.text) || null;
664
+ else if (k === 'resolution') {
665
+ // A human's recorded choice (Studio Conflict Workspace write-back).
666
+ const kv = {};
667
+ for (const g of ch.children.filter((x) => !isNote(x))) kv[firstWord(g.text)] = rest(g.text);
668
+ c.resolution = { chosen: kv.choose || rest(ch.text) || null, by: kv.by || null, at: kv.at || null, decision: kv.decision || null };
669
+ }
670
+ }
671
+ ast.conflicts.push(c);
672
+ break;
673
+ }
674
+ // Intentionally deferred, AI-assisted implementation. "implement with ai [pending]".
675
+ case 'implement': {
676
+ if (/^with\s+ai\b/.test(arg)) {
677
+ const impl = { pending: /\bpending\b/.test(arg), line: node.line };
678
+ for (const c of items(node)) {
679
+ const k = firstWord(c.text).replace(/:$/, '');
680
+ if (k === 'may_modify') impl.mayModify = leafItems(c);
681
+ else if (k === 'must_not_modify') impl.mustNotModify = leafItems(c);
682
+ else impl[k] = rest(c.text);
683
+ }
684
+ ast.implementation = impl;
685
+ }
686
+ break;
687
+ }
688
+ // IntentLift inferred-draft metadata blocks: recognized, kept as metadata, not errors.
689
+ // `evidence`, `unknown`, and `assumption` are intentionally NOT listed here: each is a real
690
+ // top-level block handled by an earlier case in this switch (which wins), so listing them
691
+ // again was dead duplicate `case` labels. A lifted draft's evidence/unknown lines already
692
+ // parse via those earlier cases; behavior is unchanged.
693
+ case 'inferred': case 'maps_to':
694
+ case 'needs_review': case 'source': case 'confidence':
695
+ (ast.lift ||= {})[kw] = leafItems(node);
696
+ break;
697
+ case 'approval': {
698
+ // Product Mission form: "approval required from" + a list of approver roles.
699
+ if (/\bfrom\b/.test(arg) || /\brequired\b/.test(arg)) {
700
+ ast.approvals.push(...leafItems(node).map((s) => s.trim()).filter(Boolean));
701
+ break;
702
+ }
703
+ // Drift form: reviewed/by metadata block.
704
+ const a = {};
705
+ for (const c of node.children) a[firstWord(c.text)] = rest(c.text);
706
+ a.reviewed = a.reviewed === 'true';
707
+ ast.approval = a;
708
+ break;
709
+ }
710
+ default:
711
+ ast.diagnostics.push({ level: 'info', code: 'unknown-block', message: `Unrecognized top-level block: "${kw}"` });
712
+ }
713
+ }
714
+
715
+ // ── Assemble IntentLens notes with stable ids, target kinds, paths, spans ──
716
+ const mprefix = `mission.${ast.mission || 'unnamed'}`;
717
+ const pushNote = (raw, targetKind, targetPath) => {
718
+ if (!raw) return;
719
+ ast.notes.push({
720
+ id: `note_${String(ast.notes.length + 1).padStart(3, '0')}`,
721
+ lens: raw.lens, text: raw.text,
722
+ targetKind, targetPath,
723
+ sourceSpan: { line: raw.line, column: 1 },
724
+ });
725
+ };
726
+ for (const nt of missionNotes) pushNote(nt, 'mission', mprefix);
727
+ for (const f of ast.inputs) for (const nt of f.notes || []) pushNote(nt, 'input', `${mprefix}.input.${f.name}`);
728
+ for (const f of ast.outputs) for (const nt of f.notes || []) pushNote(nt, 'output', `${mprefix}.output.${f.name}`);
729
+ for (const g of ast.guarantees) for (const nt of g.notes || []) pushNote(nt, 'guarantee', `${mprefix}.guarantee.${g.id}`);
730
+ for (const n of ast.neverRules) for (const nt of n.notes || []) pushNote(nt, 'never', `${mprefix}.never.${n.id}`);
731
+
732
+ return ast;
733
+ }