flowscript-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/bin/flowscript +12 -0
  4. package/dist/cli.d.ts +12 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +463 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/errors/indentation-error.d.ts +11 -0
  9. package/dist/errors/indentation-error.d.ts.map +1 -0
  10. package/dist/errors/indentation-error.js +22 -0
  11. package/dist/errors/indentation-error.js.map +1 -0
  12. package/dist/grammar.ohm +132 -0
  13. package/dist/hash.d.ts +21 -0
  14. package/dist/hash.d.ts.map +1 -0
  15. package/dist/hash.js +82 -0
  16. package/dist/hash.js.map +1 -0
  17. package/dist/indentation-scanner.d.ts +81 -0
  18. package/dist/indentation-scanner.d.ts.map +1 -0
  19. package/dist/indentation-scanner.js +290 -0
  20. package/dist/indentation-scanner.js.map +1 -0
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +49 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/linter.d.ts +71 -0
  26. package/dist/linter.d.ts.map +1 -0
  27. package/dist/linter.js +122 -0
  28. package/dist/linter.js.map +1 -0
  29. package/dist/memory.d.ts +506 -0
  30. package/dist/memory.d.ts.map +1 -0
  31. package/dist/memory.js +1802 -0
  32. package/dist/memory.js.map +1 -0
  33. package/dist/parser.d.ts +53 -0
  34. package/dist/parser.d.ts.map +1 -0
  35. package/dist/parser.js +1184 -0
  36. package/dist/parser.js.map +1 -0
  37. package/dist/query-engine.d.ts +320 -0
  38. package/dist/query-engine.d.ts.map +1 -0
  39. package/dist/query-engine.js +884 -0
  40. package/dist/query-engine.js.map +1 -0
  41. package/dist/rules/alternatives-without-decision.d.ts +24 -0
  42. package/dist/rules/alternatives-without-decision.d.ts.map +1 -0
  43. package/dist/rules/alternatives-without-decision.js +58 -0
  44. package/dist/rules/alternatives-without-decision.js.map +1 -0
  45. package/dist/rules/causal-cycles.d.ts +23 -0
  46. package/dist/rules/causal-cycles.d.ts.map +1 -0
  47. package/dist/rules/causal-cycles.js +83 -0
  48. package/dist/rules/causal-cycles.js.map +1 -0
  49. package/dist/rules/deep-nesting.d.ts +23 -0
  50. package/dist/rules/deep-nesting.d.ts.map +1 -0
  51. package/dist/rules/deep-nesting.js +55 -0
  52. package/dist/rules/deep-nesting.js.map +1 -0
  53. package/dist/rules/index.d.ts +15 -0
  54. package/dist/rules/index.d.ts.map +1 -0
  55. package/dist/rules/index.js +29 -0
  56. package/dist/rules/index.js.map +1 -0
  57. package/dist/rules/invalid-syntax.d.ts +22 -0
  58. package/dist/rules/invalid-syntax.d.ts.map +1 -0
  59. package/dist/rules/invalid-syntax.js +52 -0
  60. package/dist/rules/invalid-syntax.js.map +1 -0
  61. package/dist/rules/long-causal-chains.d.ts +25 -0
  62. package/dist/rules/long-causal-chains.d.ts.map +1 -0
  63. package/dist/rules/long-causal-chains.js +75 -0
  64. package/dist/rules/long-causal-chains.js.map +1 -0
  65. package/dist/rules/missing-recommended-fields.d.ts +21 -0
  66. package/dist/rules/missing-recommended-fields.d.ts.map +1 -0
  67. package/dist/rules/missing-recommended-fields.js +45 -0
  68. package/dist/rules/missing-recommended-fields.js.map +1 -0
  69. package/dist/rules/missing-required-fields.d.ts +22 -0
  70. package/dist/rules/missing-required-fields.d.ts.map +1 -0
  71. package/dist/rules/missing-required-fields.js +46 -0
  72. package/dist/rules/missing-required-fields.js.map +1 -0
  73. package/dist/rules/orphaned-nodes.d.ts +22 -0
  74. package/dist/rules/orphaned-nodes.d.ts.map +1 -0
  75. package/dist/rules/orphaned-nodes.js +76 -0
  76. package/dist/rules/orphaned-nodes.js.map +1 -0
  77. package/dist/rules/unlabeled-tension.d.ts +20 -0
  78. package/dist/rules/unlabeled-tension.d.ts.map +1 -0
  79. package/dist/rules/unlabeled-tension.js +37 -0
  80. package/dist/rules/unlabeled-tension.js.map +1 -0
  81. package/dist/serializer.d.ts +40 -0
  82. package/dist/serializer.d.ts.map +1 -0
  83. package/dist/serializer.js +368 -0
  84. package/dist/serializer.js.map +1 -0
  85. package/dist/tokenizer.d.ts +26 -0
  86. package/dist/tokenizer.d.ts.map +1 -0
  87. package/dist/tokenizer.js +213 -0
  88. package/dist/tokenizer.js.map +1 -0
  89. package/dist/types.d.ts +96 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +50 -0
  92. package/dist/types.js.map +1 -0
  93. package/dist/validate.d.ts +18 -0
  94. package/dist/validate.d.ts.map +1 -0
  95. package/dist/validate.js +68 -0
  96. package/dist/validate.js.map +1 -0
  97. package/package.json +69 -0
package/dist/parser.js ADDED
@@ -0,0 +1,1184 @@
1
+ "use strict";
2
+ /**
3
+ * FlowScript PEG Parser (Ohm.js)
4
+ *
5
+ * Compiles FlowScript text → IR JSON using PEG grammar.
6
+ * Minimal working version - incrementally building up functionality.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.Parser = void 0;
43
+ const ohm = __importStar(require("ohm-js"));
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const hash_1 = require("./hash");
47
+ const indentation_scanner_1 = require("./indentation-scanner");
48
+ // Load grammar
49
+ const grammarPath = path.join(__dirname, 'grammar.ohm');
50
+ const grammarSource = fs.readFileSync(grammarPath, 'utf-8');
51
+ const grammar = ohm.grammar(grammarSource);
52
+ class Parser {
53
+ constructor(sourceFile) {
54
+ this.nodes = [];
55
+ this.relationships = [];
56
+ this.states = [];
57
+ this.currentModifiers = [];
58
+ this.currentSourceNode = null;
59
+ this.blockStartNodeIndex = null; // Track where block nodes start
60
+ this.blockPrimaryNode = null; // Cache first node in block
61
+ this.lineMap = null; // Maps transformed → original line numbers
62
+ this.sourceFile = sourceFile;
63
+ }
64
+ parse(input) {
65
+ // Preprocess: Transform indentation to explicit blocks
66
+ const scanner = new indentation_scanner_1.IndentationScanner();
67
+ const { transformed, lineMap } = scanner.process(input);
68
+ // Store lineMap for provenance mapping
69
+ this.lineMap = lineMap;
70
+ // Parse transformed source with Ohm
71
+ const match = grammar.match(transformed);
72
+ if (match.failed()) {
73
+ throw new Error(`Parse error: ${match.message}`);
74
+ }
75
+ // Reset state
76
+ this.nodes = [];
77
+ this.relationships = [];
78
+ this.states = [];
79
+ // Build IR using semantics
80
+ const semantics = this.createSemantics();
81
+ semantics(match).toIR();
82
+ // Link state markers to following nodes
83
+ this.linkStatesToNodes();
84
+ // Link questions to their alternatives
85
+ this.linkQuestionsToAlternatives();
86
+ // Populate hierarchical children arrays
87
+ this.populateChildrenArrays();
88
+ return {
89
+ version: '1.0.0',
90
+ nodes: this.nodes,
91
+ relationships: this.relationships,
92
+ states: this.states,
93
+ invariants: {
94
+ causal_acyclic: true,
95
+ all_nodes_reachable: true,
96
+ tension_axes_labeled: true,
97
+ state_fields_present: true
98
+ },
99
+ metadata: {
100
+ source_files: [this.sourceFile],
101
+ parsed_at: new Date().toISOString(),
102
+ parser: 'flowscript-peg-parser 1.0.0'
103
+ }
104
+ };
105
+ }
106
+ extractString(text) {
107
+ // Remove quotes if present
108
+ if (text.startsWith('"') && text.endsWith('"')) {
109
+ return text.slice(1, -1);
110
+ }
111
+ return text;
112
+ }
113
+ getProvenance(node) {
114
+ const interval = node.source;
115
+ const lineInfo = interval.getLineAndColumnMessage?.() || '';
116
+ const lineMatch = lineInfo.match(/Line (\d+)/);
117
+ const transformedLine = lineMatch ? parseInt(lineMatch[1]) : 1;
118
+ // Map transformed line number back to original line number
119
+ const originalLine = this.lineMap?.get(transformedLine) ?? transformedLine;
120
+ return {
121
+ source_file: this.sourceFile,
122
+ line_number: originalLine,
123
+ timestamp: new Date().toISOString()
124
+ };
125
+ }
126
+ createNode(type, content, modifiers, node) {
127
+ const result = {
128
+ id: (0, hash_1.hashContent)({ type, content, modifiers }),
129
+ type: type,
130
+ content,
131
+ provenance: this.getProvenance(node)
132
+ };
133
+ // Store modifiers at top level per ir.schema.json
134
+ if (modifiers.length > 0) {
135
+ result.modifiers = modifiers;
136
+ }
137
+ return result;
138
+ }
139
+ createRelationship(type, source, target, axisLabel, node) {
140
+ const rel = {
141
+ id: (0, hash_1.hashContent)({ type, source: source.id, target: target.id, axisLabel }),
142
+ type: type,
143
+ source: source.id,
144
+ target: target.id,
145
+ provenance: this.getProvenance(node)
146
+ };
147
+ // Always set axis_label (null for non-tension or tension without axis)
148
+ if (type === 'tension') {
149
+ rel.axis_label = axisLabel;
150
+ }
151
+ return rel;
152
+ }
153
+ createSemantics() {
154
+ const self = this;
155
+ const semantics = grammar.createSemantics();
156
+ semantics.addOperation('toIR', {
157
+ Document(lines) {
158
+ lines.toIR();
159
+ },
160
+ Line(content) {
161
+ content.toIR();
162
+ },
163
+ BlankLine(_space, _newline) {
164
+ // Skip blank lines
165
+ },
166
+ // Relationship Expressions
167
+ RelationshipExpression(firstRelNode, pairs) {
168
+ // Parse first node (can be Block or NodeText)
169
+ const firstNodeObj = firstRelNode.toIR();
170
+ // Set as current source for RelOpNodePair processing
171
+ self.currentSourceNode = firstNodeObj;
172
+ // Process each RelOpNodePair
173
+ const pairsList = pairs.children;
174
+ for (let i = 0; i < pairsList.length; i++) {
175
+ pairsList[i].toIR();
176
+ }
177
+ // Clear state
178
+ self.currentSourceNode = null;
179
+ return { type: 'relationship_expression' };
180
+ },
181
+ RelOpNodePair(operator, relNode) {
182
+ // Get current source from parser state
183
+ const currentSource = self.currentSourceNode;
184
+ // Parse target node (can be Block or NodeText)
185
+ const targetNode = relNode.toIR();
186
+ // Create relationship based on operator type
187
+ const relType = operator.toIR();
188
+ // Handle reverse causal (swap source and target)
189
+ let relationship;
190
+ if (relType.reverse) {
191
+ relationship = self.createRelationship(relType.type, targetNode, // target becomes source
192
+ currentSource, // source becomes target
193
+ relType.axisLabel, operator);
194
+ }
195
+ else {
196
+ relationship = self.createRelationship(relType.type, currentSource, targetNode, relType.axisLabel, operator);
197
+ }
198
+ self.relationships.push(relationship);
199
+ // Update current source for next pair (enables chaining)
200
+ self.currentSourceNode = targetNode;
201
+ return { type: 'relop_node_pair' };
202
+ },
203
+ RelNode(_ws1, content, _ws2) {
204
+ // content is Block, TypedRelTarget, or NodeText
205
+ const result = content.toIR();
206
+ // If it's a block, result will be { type: 'block', node: blockNode }
207
+ if (result && typeof result === 'object' && result.type === 'block') {
208
+ return result.node;
209
+ }
210
+ // If it's an alternative wrapper, extract the node
211
+ if (result && typeof result === 'object' && result.type === 'alternative') {
212
+ return result.node;
213
+ }
214
+ // If it's a Node (from TypedRelTarget), return directly
215
+ if (result && typeof result === 'object' && result.id) {
216
+ return result;
217
+ }
218
+ // It's NodeText - create a statement node
219
+ const text = content.sourceString.trim();
220
+ const node = self.createNode('statement', text, self.currentModifiers, content);
221
+ self.nodes.push(node);
222
+ return node;
223
+ },
224
+ // TypedRelTarget: delegates to specific typed target rules
225
+ TypedRelTarget(target) {
226
+ return target.toIR();
227
+ },
228
+ // Typed targets inside relationship expressions
229
+ // These are like Thought/Action/Question/Completion/Alternative but without
230
+ // RelOpNodePair* chaining — the outer expression handles that
231
+ ThoughtTarget(_marker, _space, text, block) {
232
+ const textContent = text.sourceString.trim();
233
+ const hasBlock = block.sourceString.trim().length > 0;
234
+ let node;
235
+ if (hasBlock) {
236
+ const blockResultArray = block.toIR();
237
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
238
+ ? blockResultArray[0] : null;
239
+ if (blockResult && blockResult.node) {
240
+ node = blockResult.node;
241
+ node.type = 'thought';
242
+ if (textContent)
243
+ node.content = textContent;
244
+ }
245
+ else {
246
+ node = self.createNode('thought', textContent, self.currentModifiers, this);
247
+ self.nodes.push(node);
248
+ }
249
+ }
250
+ else {
251
+ node = self.createNode('thought', textContent, self.currentModifiers, this);
252
+ self.nodes.push(node);
253
+ }
254
+ self.currentModifiers = [];
255
+ return node;
256
+ },
257
+ ActionTarget(_marker, _space, text, block) {
258
+ const textContent = text.sourceString.trim();
259
+ const hasBlock = block.sourceString.trim().length > 0;
260
+ let node;
261
+ if (hasBlock) {
262
+ const blockResultArray = block.toIR();
263
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
264
+ ? blockResultArray[0] : null;
265
+ if (blockResult && blockResult.node) {
266
+ node = blockResult.node;
267
+ node.type = 'action';
268
+ if (textContent)
269
+ node.content = textContent;
270
+ }
271
+ else {
272
+ node = self.createNode('action', textContent, self.currentModifiers, this);
273
+ self.nodes.push(node);
274
+ }
275
+ }
276
+ else {
277
+ node = self.createNode('action', textContent, self.currentModifiers, this);
278
+ self.nodes.push(node);
279
+ }
280
+ self.currentModifiers = [];
281
+ return node;
282
+ },
283
+ QuestionTarget(_marker, _space, text, block) {
284
+ const textContent = text.sourceString.trim();
285
+ const hasBlock = block.sourceString.trim().length > 0;
286
+ let node;
287
+ if (hasBlock) {
288
+ const blockResultArray = block.toIR();
289
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
290
+ ? blockResultArray[0] : null;
291
+ if (blockResult && blockResult.node) {
292
+ node = blockResult.node;
293
+ node.type = 'question';
294
+ if (textContent)
295
+ node.content = textContent;
296
+ }
297
+ else {
298
+ node = self.createNode('question', textContent, self.currentModifiers, this);
299
+ self.nodes.push(node);
300
+ }
301
+ }
302
+ else {
303
+ node = self.createNode('question', textContent, self.currentModifiers, this);
304
+ self.nodes.push(node);
305
+ }
306
+ self.currentModifiers = [];
307
+ return node;
308
+ },
309
+ CompletionTarget(_marker, _space, text, block) {
310
+ const textContent = text.sourceString.trim();
311
+ const hasBlock = block.sourceString.trim().length > 0;
312
+ let node;
313
+ if (hasBlock) {
314
+ const blockResultArray = block.toIR();
315
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
316
+ ? blockResultArray[0] : null;
317
+ if (blockResult && blockResult.node) {
318
+ node = blockResult.node;
319
+ node.type = 'completion';
320
+ if (textContent)
321
+ node.content = textContent;
322
+ }
323
+ else {
324
+ node = self.createNode('completion', textContent, self.currentModifiers, this);
325
+ self.nodes.push(node);
326
+ }
327
+ }
328
+ else {
329
+ node = self.createNode('completion', textContent, self.currentModifiers, this);
330
+ self.nodes.push(node);
331
+ }
332
+ self.currentModifiers = [];
333
+ return node;
334
+ },
335
+ AlternativeTarget(_marker, _space, text, block) {
336
+ const textContent = text.sourceString.trim();
337
+ const hasBlock = block.sourceString.trim().length > 0;
338
+ let node;
339
+ if (hasBlock) {
340
+ const blockResultArray = block.toIR();
341
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
342
+ ? blockResultArray[0] : null;
343
+ if (blockResult && blockResult.node) {
344
+ node = blockResult.node;
345
+ node.type = 'alternative';
346
+ if (textContent)
347
+ node.content = textContent;
348
+ }
349
+ else {
350
+ node = self.createNode('alternative', textContent, self.currentModifiers, this);
351
+ self.nodes.push(node);
352
+ }
353
+ }
354
+ else {
355
+ node = self.createNode('alternative', textContent, self.currentModifiers, this);
356
+ self.nodes.push(node);
357
+ }
358
+ self.currentModifiers = [];
359
+ return { type: 'alternative', node };
360
+ },
361
+ NodeText(chars) {
362
+ return this.sourceString;
363
+ },
364
+ // Continuation Relationship (block-scoped implicit source)
365
+ ContinuationRel(operator, _space, relNode) {
366
+ // Get or find the block's primary node (first node in block)
367
+ let sourceNode = self.blockPrimaryNode;
368
+ // Lazy evaluation: if primary node not cached, find it
369
+ if (!sourceNode && self.blockStartNodeIndex !== null) {
370
+ if (self.nodes.length > self.blockStartNodeIndex) {
371
+ sourceNode = self.nodes[self.blockStartNodeIndex];
372
+ self.blockPrimaryNode = sourceNode; // Cache for subsequent continuations
373
+ }
374
+ }
375
+ // If no source node available, skip relationship creation
376
+ if (!sourceNode) {
377
+ // Still parse the target node so it gets created
378
+ relNode.toIR();
379
+ return { type: 'continuation_no_source' };
380
+ }
381
+ // Parse target node
382
+ const targetNode = relNode.toIR();
383
+ // Get relationship type from operator
384
+ const relType = operator.toIR();
385
+ // Create relationship (handle reverse operators)
386
+ let relationship;
387
+ if (relType.reverse) {
388
+ // Reverse causal: target -> source (swap)
389
+ relationship = self.createRelationship(relType.type, targetNode, // target becomes source
390
+ sourceNode, // source becomes target
391
+ relType.axisLabel, operator);
392
+ }
393
+ else {
394
+ // Normal: source -> target
395
+ relationship = self.createRelationship(relType.type, sourceNode, targetNode, relType.axisLabel, operator);
396
+ }
397
+ self.relationships.push(relationship);
398
+ return { type: 'continuation_relationship' };
399
+ },
400
+ RelOp(op) {
401
+ return op.toIR();
402
+ },
403
+ bidirectional(_arrow) {
404
+ return { type: 'bidirectional', axisLabel: null, reverse: false };
405
+ },
406
+ causal(_arrow) {
407
+ return { type: 'causes', axisLabel: null, reverse: false };
408
+ },
409
+ reverseCausal(_arrow) {
410
+ return { type: 'derives_from', axisLabel: null, reverse: false };
411
+ },
412
+ temporal(_arrow) {
413
+ return { type: 'temporal', axisLabel: null, reverse: false };
414
+ },
415
+ tensionWithAxis(_open, axisLabel, _close) {
416
+ return { type: 'tension', axisLabel: axisLabel.sourceString, reverse: false };
417
+ },
418
+ tensionWithoutAxis(_marker) {
419
+ return { type: 'tension', axisLabel: null, reverse: false };
420
+ },
421
+ axisLabel(chars) {
422
+ return this.sourceString;
423
+ },
424
+ Element(modifiers, content) {
425
+ // Extract modifiers and store in parser state
426
+ self.currentModifiers = modifiers.children.map((m) => m.toIR());
427
+ // Call content semantic action WITHOUT passing modifiers
428
+ const result = content.toIR();
429
+ // Clear modifiers after use
430
+ self.currentModifiers = [];
431
+ return result;
432
+ },
433
+ Content(contentType) {
434
+ return contentType.toIR();
435
+ },
436
+ Modifier(marker) {
437
+ const text = this.sourceString;
438
+ const modMap = {
439
+ '!': 'urgent',
440
+ '++': 'strong_positive',
441
+ '*': 'high_confidence',
442
+ '~': 'low_confidence'
443
+ };
444
+ return modMap[text] || text;
445
+ },
446
+ // State markers
447
+ State(state) {
448
+ return state.toIR();
449
+ },
450
+ decidedWithFields(_open, fieldsNode, _close) {
451
+ const fields = fieldsNode.children.length > 0 ? fieldsNode.children[0].toIR() : {};
452
+ const state = {
453
+ id: (0, hash_1.hashContent)({ type: 'decided', fields }),
454
+ type: 'decided',
455
+ node_id: '',
456
+ fields,
457
+ provenance: self.getProvenance(this)
458
+ };
459
+ self.states.push(state);
460
+ return state;
461
+ },
462
+ decidedFields(firstField, _space1, _comma, _space2, restFields) {
463
+ const fields = {};
464
+ // Process first field
465
+ const firstFieldData = firstField.toIR();
466
+ Object.assign(fields, firstFieldData);
467
+ // Process rest of fields (iteration node)
468
+ const restFieldsList = restFields.children;
469
+ for (let i = 0; i < restFieldsList.length; i++) {
470
+ const fieldData = restFieldsList[i].toIR();
471
+ Object.assign(fields, fieldData);
472
+ }
473
+ return fields;
474
+ },
475
+ decidedField(field) {
476
+ return field.toIR();
477
+ },
478
+ rationalField(_key, _space, value) {
479
+ return { rationale: self.extractString(value.sourceString) };
480
+ },
481
+ onField(_key, _space, value) {
482
+ return { on: self.extractString(value.sourceString) };
483
+ },
484
+ decidedWithoutFields(_token) {
485
+ const fields = {};
486
+ const state = {
487
+ id: (0, hash_1.hashContent)({ type: 'decided', fields }),
488
+ type: 'decided',
489
+ node_id: '',
490
+ fields,
491
+ provenance: self.getProvenance(this)
492
+ };
493
+ self.states.push(state);
494
+ return state;
495
+ },
496
+ blockedWithFields(_open, fieldsNode, _close) {
497
+ const fields = fieldsNode.children.length > 0 ? fieldsNode.children[0].toIR() : {};
498
+ const state = {
499
+ id: (0, hash_1.hashContent)({ type: 'blocked', fields }),
500
+ type: 'blocked',
501
+ node_id: '',
502
+ fields,
503
+ provenance: self.getProvenance(this)
504
+ };
505
+ self.states.push(state);
506
+ return state;
507
+ },
508
+ blockedFields(firstField, _space1, _comma, _space2, restFields) {
509
+ const fields = {};
510
+ // Process first field
511
+ const firstFieldData = firstField.toIR();
512
+ Object.assign(fields, firstFieldData);
513
+ // Process rest of fields (iteration node)
514
+ const restFieldsList = restFields.children;
515
+ for (let i = 0; i < restFieldsList.length; i++) {
516
+ const fieldData = restFieldsList[i].toIR();
517
+ Object.assign(fields, fieldData);
518
+ }
519
+ return fields;
520
+ },
521
+ blockedField(field) {
522
+ return field.toIR();
523
+ },
524
+ reasonField(_key, _space, value) {
525
+ return { reason: self.extractString(value.sourceString) };
526
+ },
527
+ sinceField(_key, _space, value) {
528
+ return { since: self.extractString(value.sourceString) };
529
+ },
530
+ blockedWithoutFields(_token) {
531
+ const fields = {};
532
+ const state = {
533
+ id: (0, hash_1.hashContent)({ type: 'blocked', fields }),
534
+ type: 'blocked',
535
+ node_id: '',
536
+ fields,
537
+ provenance: self.getProvenance(this)
538
+ };
539
+ self.states.push(state);
540
+ return state;
541
+ },
542
+ exploring(_token) {
543
+ const state = {
544
+ id: (0, hash_1.hashContent)({ type: 'exploring', fields: {} }),
545
+ type: 'exploring',
546
+ node_id: '',
547
+ fields: {},
548
+ provenance: self.getProvenance(this)
549
+ };
550
+ self.states.push(state);
551
+ return state;
552
+ },
553
+ parkingWithFields(_open, fieldsNode, _close) {
554
+ const fields = fieldsNode.children.length > 0 ? fieldsNode.children[0].toIR() : {};
555
+ const state = {
556
+ id: (0, hash_1.hashContent)({ type: 'parking', fields }),
557
+ type: 'parking',
558
+ node_id: '',
559
+ fields,
560
+ provenance: self.getProvenance(this)
561
+ };
562
+ self.states.push(state);
563
+ return state;
564
+ },
565
+ parkingFields(firstField, _space1, _comma, _space2, restFields) {
566
+ const fields = {};
567
+ // Process first field
568
+ const firstFieldData = firstField.toIR();
569
+ Object.assign(fields, firstFieldData);
570
+ // Process rest of fields (iteration node)
571
+ const restFieldsList = restFields.children;
572
+ for (let i = 0; i < restFieldsList.length; i++) {
573
+ const fieldData = restFieldsList[i].toIR();
574
+ Object.assign(fields, fieldData);
575
+ }
576
+ return fields;
577
+ },
578
+ parkingField(field) {
579
+ return field.toIR();
580
+ },
581
+ whyField(_key, _space, value) {
582
+ return { why: self.extractString(value.sourceString) };
583
+ },
584
+ untilField(_key, _space, value) {
585
+ return { until: self.extractString(value.sourceString) };
586
+ },
587
+ parkingWithoutFields(_token) {
588
+ const fields = {};
589
+ const state = {
590
+ id: (0, hash_1.hashContent)({ type: 'parking', fields }),
591
+ type: 'parking',
592
+ node_id: '',
593
+ fields,
594
+ provenance: self.getProvenance(this)
595
+ };
596
+ self.states.push(state);
597
+ return state;
598
+ },
599
+ // Insights
600
+ Insight(insight) {
601
+ return insight.toIR();
602
+ },
603
+ Thought(_marker, _space, text, block, relPairs, _newline) {
604
+ // Handle three cases: text+block, just block, or just text
605
+ const hasText = text.sourceString.trim().length > 0;
606
+ const hasBlock = block.sourceString.trim().length > 0;
607
+ let node;
608
+ if (hasBlock) {
609
+ // Save modifiers before block parsing (block will clear them)
610
+ const savedModifiers = [...self.currentModifiers];
611
+ // Has a block (with or without text)
612
+ // block.toIR() returns an array because Block? is optional (iteration node)
613
+ const blockResultArray = block.toIR();
614
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
615
+ ? blockResultArray[0]
616
+ : null;
617
+ if (blockResult && blockResult.node) {
618
+ node = blockResult.node;
619
+ node.type = 'thought';
620
+ // Move modifiers from block's ext to root level
621
+ if (savedModifiers.length > 0) {
622
+ node.modifiers = savedModifiers;
623
+ // Remove from ext (block had them there)
624
+ if (node.ext?.modifiers) {
625
+ delete node.ext.modifiers;
626
+ }
627
+ }
628
+ // If there's also text, set it as the content
629
+ if (hasText) {
630
+ node.content = text.sourceString.trim();
631
+ }
632
+ else if (node.content === '' && node.ext?.children && Array.isArray(node.ext.children)) {
633
+ // No text provided - use first child's content as the thought content
634
+ const firstChild = node.ext.children[0];
635
+ if (firstChild && firstChild.content) {
636
+ node.content = firstChild.content;
637
+ }
638
+ }
639
+ }
640
+ else {
641
+ // Block parsing failed, fall back to text-only
642
+ const textContent = hasText ? text.sourceString.trim() : '';
643
+ node = self.createNode('thought', textContent, self.currentModifiers, this);
644
+ self.nodes.push(node);
645
+ }
646
+ }
647
+ else if (hasText) {
648
+ // Just text, no block
649
+ const textContent = text.sourceString.trim();
650
+ node = self.createNode('thought', textContent, self.currentModifiers, this);
651
+ self.nodes.push(node);
652
+ }
653
+ else {
654
+ // Neither text nor block - create empty thought
655
+ node = self.createNode('thought', '', self.currentModifiers, this);
656
+ self.nodes.push(node);
657
+ }
658
+ // If relationship pairs present, process them using existing RelOpNodePair logic
659
+ if (relPairs.children.length > 0) {
660
+ self.currentSourceNode = node;
661
+ relPairs.toIR();
662
+ self.currentSourceNode = null;
663
+ }
664
+ return node;
665
+ },
666
+ Action(_marker, _space, text, block, relPairs, _newline) {
667
+ // Handle three cases: text+block, just block, or just text
668
+ const hasText = text.sourceString.trim().length > 0;
669
+ const hasBlock = block.sourceString.trim().length > 0;
670
+ let node;
671
+ if (hasBlock) {
672
+ // Save modifiers before block parsing (block will clear them)
673
+ const savedModifiers = [...self.currentModifiers];
674
+ // Has a block (with or without text)
675
+ // block.toIR() returns an array because Block? is optional (iteration node)
676
+ const blockResultArray = block.toIR();
677
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
678
+ ? blockResultArray[0]
679
+ : null;
680
+ if (blockResult && blockResult.node) {
681
+ node = blockResult.node;
682
+ node.type = 'action';
683
+ // Move modifiers from block's ext to root level
684
+ if (savedModifiers.length > 0) {
685
+ node.modifiers = savedModifiers;
686
+ // Remove from ext (block had them there)
687
+ if (node.ext?.modifiers) {
688
+ delete node.ext.modifiers;
689
+ }
690
+ }
691
+ // If there's also text, set it as the content
692
+ if (hasText) {
693
+ node.content = text.sourceString.trim();
694
+ }
695
+ else if (node.content === '' && node.ext?.children && Array.isArray(node.ext.children)) {
696
+ // No text provided - use first child's content as the action content
697
+ const firstChild = node.ext.children[0];
698
+ if (firstChild && firstChild.content) {
699
+ node.content = firstChild.content;
700
+ }
701
+ }
702
+ }
703
+ else {
704
+ // Block parsing failed, fall back to text-only
705
+ const textContent = hasText ? text.sourceString.trim() : '';
706
+ node = self.createNode('action', textContent, self.currentModifiers, this);
707
+ self.nodes.push(node);
708
+ }
709
+ }
710
+ else if (hasText) {
711
+ // Just text, no block
712
+ const textContent = text.sourceString.trim();
713
+ node = self.createNode('action', textContent, self.currentModifiers, this);
714
+ self.nodes.push(node);
715
+ }
716
+ else {
717
+ // Neither text nor block - create empty action
718
+ node = self.createNode('action', '', self.currentModifiers, this);
719
+ self.nodes.push(node);
720
+ }
721
+ // If relationship pairs present, process them
722
+ if (relPairs.children.length > 0) {
723
+ self.currentSourceNode = node;
724
+ relPairs.toIR();
725
+ self.currentSourceNode = null;
726
+ }
727
+ return node;
728
+ },
729
+ Question(_marker, _space, text, block, _newline) {
730
+ // Handle three cases: text+block, just block, or just text
731
+ const hasText = text.sourceString.trim().length > 0;
732
+ const hasBlock = block.sourceString.trim().length > 0;
733
+ let node;
734
+ if (hasBlock) {
735
+ // Save modifiers before block parsing (block will clear them)
736
+ const savedModifiers = [...self.currentModifiers];
737
+ // Has a block (with or without text)
738
+ // block.toIR() returns an array because Block? is optional (iteration node)
739
+ const blockResultArray = block.toIR();
740
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
741
+ ? blockResultArray[0]
742
+ : null;
743
+ if (blockResult && blockResult.node) {
744
+ node = blockResult.node;
745
+ node.type = 'question';
746
+ // Move modifiers from block's ext to root level
747
+ if (savedModifiers.length > 0) {
748
+ node.modifiers = savedModifiers;
749
+ // Remove from ext (block had them there)
750
+ if (node.ext?.modifiers) {
751
+ delete node.ext.modifiers;
752
+ }
753
+ }
754
+ // If there's also text, set it as the content
755
+ if (hasText) {
756
+ node.content = text.sourceString.trim();
757
+ }
758
+ else if (node.content === '' && node.ext?.children && Array.isArray(node.ext.children)) {
759
+ // No text provided - use first child's content as the question content
760
+ const firstChild = node.ext.children[0];
761
+ if (firstChild && firstChild.content) {
762
+ node.content = firstChild.content;
763
+ }
764
+ }
765
+ }
766
+ else {
767
+ // Block parsing failed, fall back to text-only
768
+ const textContent = hasText ? text.sourceString.trim() : '';
769
+ node = self.createNode('question', textContent, self.currentModifiers, this);
770
+ self.nodes.push(node);
771
+ }
772
+ }
773
+ else if (hasText) {
774
+ // Just text, no block
775
+ const textContent = text.sourceString.trim();
776
+ node = self.createNode('question', textContent, self.currentModifiers, this);
777
+ self.nodes.push(node);
778
+ }
779
+ else {
780
+ // Neither text nor block - create empty question
781
+ node = self.createNode('question', '', self.currentModifiers, this);
782
+ self.nodes.push(node);
783
+ }
784
+ return node;
785
+ },
786
+ Completion(_marker, _space, text, block, _newline) {
787
+ // Handle three cases: text+block, just block, or just text
788
+ const hasText = text.sourceString.trim().length > 0;
789
+ const hasBlock = block.sourceString.trim().length > 0;
790
+ let node;
791
+ if (hasBlock) {
792
+ // Save modifiers before block parsing (block will clear them)
793
+ const savedModifiers = [...self.currentModifiers];
794
+ // Has a block (with or without text)
795
+ // block.toIR() returns an array because Block? is optional (iteration node)
796
+ const blockResultArray = block.toIR();
797
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
798
+ ? blockResultArray[0]
799
+ : null;
800
+ if (blockResult && blockResult.node) {
801
+ node = blockResult.node;
802
+ node.type = 'completion';
803
+ // Move modifiers from block's ext to root level
804
+ if (savedModifiers.length > 0) {
805
+ node.modifiers = savedModifiers;
806
+ // Remove from ext (block had them there)
807
+ if (node.ext?.modifiers) {
808
+ delete node.ext.modifiers;
809
+ }
810
+ }
811
+ // If there's also text, set it as the content
812
+ if (hasText) {
813
+ node.content = text.sourceString.trim();
814
+ }
815
+ else if (node.content === '' && node.ext?.children && Array.isArray(node.ext.children)) {
816
+ // No text provided - use first child's content as the completion content
817
+ const firstChild = node.ext.children[0];
818
+ if (firstChild && firstChild.content) {
819
+ node.content = firstChild.content;
820
+ }
821
+ }
822
+ }
823
+ else {
824
+ // Block parsing failed, fall back to text-only
825
+ const textContent = hasText ? text.sourceString.trim() : '';
826
+ node = self.createNode('completion', textContent, self.currentModifiers, this);
827
+ self.nodes.push(node);
828
+ }
829
+ }
830
+ else if (hasText) {
831
+ // Just text, no block
832
+ const textContent = text.sourceString.trim();
833
+ node = self.createNode('completion', textContent, self.currentModifiers, this);
834
+ self.nodes.push(node);
835
+ }
836
+ else {
837
+ // Neither text nor block - create empty completion
838
+ node = self.createNode('completion', '', self.currentModifiers, this);
839
+ self.nodes.push(node);
840
+ }
841
+ return node;
842
+ },
843
+ // Block (thought blocks)
844
+ Block(_lbrace, _ws1, blockContent, _ws2, _rbrace) {
845
+ // Save state for nested blocks
846
+ const savedStartIndex = self.blockStartNodeIndex;
847
+ const savedPrimaryNode = self.blockPrimaryNode;
848
+ // Save modifiers before parsing block contents (they'll be cleared during parsing)
849
+ const blockModifiers = [...self.currentModifiers];
850
+ self.currentModifiers = []; // Clear for child elements
851
+ // Track nodes and blocks before parsing block content
852
+ const nodesBefore = self.nodes.length;
853
+ // Set block start index
854
+ self.blockStartNodeIndex = nodesBefore;
855
+ // Set block primary node to the parent node (node immediately before block)
856
+ // This enables continuation relationships to reference the correct parent
857
+ // If no parent exists, set to null (standalone blocks will use first child as fallback)
858
+ self.blockPrimaryNode = nodesBefore > 0 ? self.nodes[nodesBefore - 1] : null;
859
+ // Parse block content (handles separators between lines)
860
+ // blockContent is optional (empty blocks have no content)
861
+ if (blockContent.sourceString.trim()) {
862
+ blockContent.toIR();
863
+ }
864
+ // Collect ALL nodes created since block started
865
+ const allNewNodes = self.nodes.slice(nodesBefore);
866
+ // Filter to get only DIRECT children (exclude nodes that are children of nested blocks)
867
+ const nestedBlocks = allNewNodes.filter(n => n.type === 'block');
868
+ const nestedBlockChildIds = new Set(nestedBlocks.flatMap(b => {
869
+ const children = b.ext?.children;
870
+ return Array.isArray(children) ? children.map((c) => c.id) : [];
871
+ }));
872
+ const directChildren = allNewNodes.filter(n => {
873
+ // Keep if it's not a child of a nested block
874
+ return !nestedBlockChildIds.has(n.id);
875
+ });
876
+ // Create block node (using saved modifiers)
877
+ const blockNode = {
878
+ id: (0, hash_1.hashContent)({ type: 'block', children: directChildren.map(c => c.id), modifiers: blockModifiers }),
879
+ type: 'block',
880
+ content: '', // Blocks have no direct content
881
+ provenance: self.getProvenance(this)
882
+ };
883
+ // Add children and modifiers to block node
884
+ if (directChildren.length > 0 || blockModifiers.length > 0) {
885
+ blockNode.ext = {};
886
+ if (directChildren.length > 0) {
887
+ blockNode.ext.children = directChildren;
888
+ }
889
+ if (blockModifiers.length > 0) {
890
+ blockNode.ext.modifiers = blockModifiers;
891
+ }
892
+ }
893
+ // Add block node to nodes list
894
+ self.nodes.push(blockNode);
895
+ // Restore state for nested blocks
896
+ self.blockStartNodeIndex = savedStartIndex;
897
+ self.blockPrimaryNode = savedPrimaryNode;
898
+ return { type: 'block', node: blockNode };
899
+ },
900
+ BlockLine(_ws, line) {
901
+ return line.toIR();
902
+ },
903
+ BlockContent(firstLine, separators, blockLines, _optionalSeparator) {
904
+ // Process first line
905
+ firstLine.toIR();
906
+ // Process remaining lines
907
+ // The iteration (separator BlockLine)* gets split into two arrays
908
+ const blockLinesList = blockLines.children || [];
909
+ for (const line of blockLinesList) {
910
+ line.toIR();
911
+ }
912
+ },
913
+ separator(_sep) {
914
+ // Separators are just delimiters - no IR needed
915
+ },
916
+ ws(_whitespace) {
917
+ // Whitespace is just formatting - no IR needed
918
+ },
919
+ BlockElement(modifiers, content) {
920
+ // Same as Element but for blocks
921
+ const mods = modifiers.children.map((m) => m.sourceString);
922
+ self.currentModifiers = mods;
923
+ return content.toIR();
924
+ },
925
+ BlockContent_inner(content) {
926
+ // Just pass through to the actual content type
927
+ return content.toIR();
928
+ },
929
+ BlockStatement(text) {
930
+ // Same as Statement but without trailing newline
931
+ const content = text.sourceString.trim();
932
+ const node = self.createNode('statement', content, self.currentModifiers, text);
933
+ self.nodes.push(node);
934
+ self.currentModifiers = [];
935
+ return node;
936
+ },
937
+ // Alternative
938
+ Alternative(_marker, _space, text, block, _newline) {
939
+ // Handle three cases: text+block, just block, or just text
940
+ const hasText = text.sourceString.trim().length > 0;
941
+ const hasBlock = block.sourceString.trim().length > 0;
942
+ let node;
943
+ if (hasBlock) {
944
+ // Save modifiers before block parsing (block will clear them)
945
+ const savedModifiers = [...self.currentModifiers];
946
+ // Has a block (with or without text)
947
+ // block.toIR() returns an array because Block? is optional (iteration node)
948
+ const blockResultArray = block.toIR();
949
+ const blockResult = Array.isArray(blockResultArray) && blockResultArray.length > 0
950
+ ? blockResultArray[0]
951
+ : null;
952
+ if (blockResult && blockResult.node) {
953
+ node = blockResult.node;
954
+ node.type = 'alternative';
955
+ // Move modifiers from block's ext to root level
956
+ if (savedModifiers.length > 0) {
957
+ node.modifiers = savedModifiers;
958
+ // Remove from ext (block had them there)
959
+ if (node.ext?.modifiers) {
960
+ delete node.ext.modifiers;
961
+ }
962
+ }
963
+ // If there's also text, set it as the content
964
+ if (hasText) {
965
+ node.content = text.sourceString.trim();
966
+ }
967
+ else if (node.content === '' && node.ext?.children && Array.isArray(node.ext.children)) {
968
+ // No text provided - use first child's content as the alternative content
969
+ const firstChild = node.ext.children[0];
970
+ if (firstChild && firstChild.content) {
971
+ node.content = firstChild.content;
972
+ }
973
+ }
974
+ }
975
+ else {
976
+ // Block parsing failed, fall back to text-only
977
+ const textContent = hasText ? text.sourceString.trim() : '';
978
+ node = self.createNode('alternative', textContent, self.currentModifiers, this);
979
+ self.nodes.push(node);
980
+ }
981
+ }
982
+ else if (hasText) {
983
+ // Just text, no block
984
+ const textContent = text.sourceString.trim();
985
+ node = self.createNode('alternative', textContent, self.currentModifiers, this);
986
+ self.nodes.push(node);
987
+ }
988
+ else {
989
+ // Neither text nor block - create empty alternative
990
+ node = self.createNode('alternative', '', self.currentModifiers, this);
991
+ self.nodes.push(node);
992
+ }
993
+ return { type: 'alternative', node };
994
+ },
995
+ // Statement (prose)
996
+ Statement(content, _newline) {
997
+ const text = content.sourceString.trim();
998
+ if (text.length > 0) {
999
+ const node = self.createNode('statement', text, self.currentModifiers, this);
1000
+ self.nodes.push(node);
1001
+ }
1002
+ },
1003
+ // Default handlers
1004
+ _terminal() {
1005
+ return this.sourceString;
1006
+ },
1007
+ _iter(...children) {
1008
+ return children.map(c => c.toIR());
1009
+ }
1010
+ });
1011
+ return semantics;
1012
+ }
1013
+ /**
1014
+ * Link state markers to following nodes.
1015
+ * States annotate the node that appears after them in source order.
1016
+ */
1017
+ linkStatesToNodes() {
1018
+ for (const state of this.states) {
1019
+ const stateLine = state.provenance.line_number;
1020
+ // Find first node at or after this line
1021
+ // Handle same-line case: [decided] Ship now (both on line N)
1022
+ const nextNode = this.nodes.find(node => node.provenance.line_number >= stateLine);
1023
+ if (nextNode) {
1024
+ state.node_id = nextNode.id;
1025
+ }
1026
+ // If no following node, leave node_id as empty string
1027
+ // (edge case: state at end of document with no following content)
1028
+ }
1029
+ }
1030
+ /**
1031
+ * Link questions to their alternatives.
1032
+ * Creates alternative relationships from question nodes to following || markers.
1033
+ */
1034
+ linkQuestionsToAlternatives() {
1035
+ for (let i = 0; i < this.nodes.length; i++) {
1036
+ const node = this.nodes[i];
1037
+ if (node.type !== 'question')
1038
+ continue;
1039
+ // Find all alternatives that follow this question (before next question or EOF)
1040
+ const questionLine = node.provenance.line_number;
1041
+ const alternatives = [];
1042
+ for (let j = i + 1; j < this.nodes.length; j++) {
1043
+ const candidate = this.nodes[j];
1044
+ // Stop if we hit another question (end of this question's scope)
1045
+ if (candidate.type === 'question')
1046
+ break;
1047
+ // Collect alternatives
1048
+ if (candidate.type === 'alternative') {
1049
+ alternatives.push(candidate);
1050
+ }
1051
+ }
1052
+ // Create alternative relationships
1053
+ for (const alt of alternatives) {
1054
+ const relationship = {
1055
+ id: (0, hash_1.hashContent)({ type: 'alternative', source: node.id, target: alt.id }),
1056
+ type: 'alternative',
1057
+ source: node.id,
1058
+ target: alt.id,
1059
+ provenance: alt.provenance // Use alternative's provenance (line where || appears)
1060
+ };
1061
+ this.relationships.push(relationship);
1062
+ }
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Populate hierarchical children arrays per spec.
1067
+ * Children represent syntactic nesting (who is indented under whom).
1068
+ *
1069
+ * Two-step process:
1070
+ * 1. Questions get children from alternative relationships
1071
+ * 2. Any node followed by a block inherits that block's children
1072
+ */
1073
+ populateChildrenArrays() {
1074
+ // Track redundant blocks (blocks whose children were assigned to a parent)
1075
+ const redundantBlockIds = new Set();
1076
+ // Build set of block IDs that are referenced in relationships
1077
+ // These blocks should NOT be removed even if their children are attached to a parent
1078
+ const blocksInRelationships = new Set();
1079
+ for (const rel of this.relationships) {
1080
+ const sourceNode = this.nodes.find(n => n.id === rel.source);
1081
+ const targetNode = this.nodes.find(n => n.id === rel.target);
1082
+ if (sourceNode?.type === 'block') {
1083
+ blocksInRelationships.add(sourceNode.id);
1084
+ }
1085
+ if (targetNode?.type === 'block') {
1086
+ blocksInRelationships.add(targetNode.id);
1087
+ }
1088
+ }
1089
+ // Step 1: Questions have children = their alternatives (from relationships)
1090
+ for (const rel of this.relationships) {
1091
+ if (rel.type === 'alternative') {
1092
+ const question = this.nodes.find(n => n.id === rel.source);
1093
+ if (question) {
1094
+ if (!question.children) {
1095
+ question.children = [];
1096
+ }
1097
+ question.children.push(rel.target);
1098
+ }
1099
+ }
1100
+ }
1101
+ // Step 2: For each block, find the node that precedes its first child
1102
+ // and assign the block's children to that node
1103
+ // (e.g., alternative followed by indented implications)
1104
+ for (const blockNode of this.nodes) {
1105
+ if (blockNode.type !== 'block' || !blockNode.ext?.children || !Array.isArray(blockNode.ext.children)) {
1106
+ continue;
1107
+ }
1108
+ const blockChildren = blockNode.ext.children;
1109
+ if (blockChildren.length === 0)
1110
+ continue;
1111
+ // Find the first non-block child in this block
1112
+ let firstChild = null;
1113
+ for (const child of blockChildren) {
1114
+ if (child.type !== 'block') {
1115
+ firstChild = child;
1116
+ break;
1117
+ }
1118
+ }
1119
+ if (!firstChild)
1120
+ continue;
1121
+ // Find the index of this first child in the main nodes array
1122
+ const firstChildIndex = this.nodes.findIndex(n => n.id === firstChild.id);
1123
+ if (firstChildIndex <= 0)
1124
+ continue; // No preceding node
1125
+ // The node right before the first child is the parent
1126
+ const parentNode = this.nodes[firstChildIndex - 1];
1127
+ // Skip if parent is a block
1128
+ if (parentNode.type === 'block')
1129
+ continue;
1130
+ // Get DIRECT children only (exclude nested blocks, don't flatten recursively)
1131
+ const directChildren = [];
1132
+ for (const child of blockChildren) {
1133
+ if (child.type !== 'block') {
1134
+ directChildren.push(child.id);
1135
+ }
1136
+ }
1137
+ if (directChildren.length > 0) {
1138
+ if (!parentNode.children) {
1139
+ parentNode.children = [];
1140
+ }
1141
+ // Append to existing children, but avoid duplicates
1142
+ // (e.g., question might already have alternatives from relationships)
1143
+ for (const childId of directChildren) {
1144
+ if (!parentNode.children.includes(childId)) {
1145
+ parentNode.children.push(childId);
1146
+ }
1147
+ }
1148
+ // Mark this block as redundant ONLY if it's not referenced in relationships
1149
+ if (!blocksInRelationships.has(blockNode.id)) {
1150
+ redundantBlockIds.add(blockNode.id);
1151
+ }
1152
+ }
1153
+ }
1154
+ // Step 3: Remove redundant blocks from nodes array
1155
+ // These blocks served their purpose (grouping indented content) but are no longer needed
1156
+ // Blocks that are referenced in relationships are kept (e.g., "main -> {block}")
1157
+ if (redundantBlockIds.size > 0) {
1158
+ this.nodes = this.nodes.filter(n => !redundantBlockIds.has(n.id));
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Recursively flatten block children to get all descendant node IDs.
1163
+ * Nested blocks are expanded to include their children.
1164
+ *
1165
+ * @param children - Array of child nodes from block.ext.children
1166
+ * @returns Array of node IDs (excludes block nodes themselves)
1167
+ */
1168
+ flattenBlockChildren(children) {
1169
+ const result = [];
1170
+ for (const child of children) {
1171
+ if (child.type === 'block' && child.ext?.children && Array.isArray(child.ext.children)) {
1172
+ // Recursively flatten nested blocks
1173
+ result.push(...this.flattenBlockChildren(child.ext.children));
1174
+ }
1175
+ else {
1176
+ // Regular node: add its ID
1177
+ result.push(child.id);
1178
+ }
1179
+ }
1180
+ return result;
1181
+ }
1182
+ }
1183
+ exports.Parser = Parser;
1184
+ //# sourceMappingURL=parser.js.map