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.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/bin/flowscript +12 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +463 -0
- package/dist/cli.js.map +1 -0
- package/dist/errors/indentation-error.d.ts +11 -0
- package/dist/errors/indentation-error.d.ts.map +1 -0
- package/dist/errors/indentation-error.js +22 -0
- package/dist/errors/indentation-error.js.map +1 -0
- package/dist/grammar.ohm +132 -0
- package/dist/hash.d.ts +21 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +82 -0
- package/dist/hash.js.map +1 -0
- package/dist/indentation-scanner.d.ts +81 -0
- package/dist/indentation-scanner.d.ts.map +1 -0
- package/dist/indentation-scanner.js +290 -0
- package/dist/indentation-scanner.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/linter.d.ts +71 -0
- package/dist/linter.d.ts.map +1 -0
- package/dist/linter.js +122 -0
- package/dist/linter.js.map +1 -0
- package/dist/memory.d.ts +506 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +1802 -0
- package/dist/memory.js.map +1 -0
- package/dist/parser.d.ts +53 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1184 -0
- package/dist/parser.js.map +1 -0
- package/dist/query-engine.d.ts +320 -0
- package/dist/query-engine.d.ts.map +1 -0
- package/dist/query-engine.js +884 -0
- package/dist/query-engine.js.map +1 -0
- package/dist/rules/alternatives-without-decision.d.ts +24 -0
- package/dist/rules/alternatives-without-decision.d.ts.map +1 -0
- package/dist/rules/alternatives-without-decision.js +58 -0
- package/dist/rules/alternatives-without-decision.js.map +1 -0
- package/dist/rules/causal-cycles.d.ts +23 -0
- package/dist/rules/causal-cycles.d.ts.map +1 -0
- package/dist/rules/causal-cycles.js +83 -0
- package/dist/rules/causal-cycles.js.map +1 -0
- package/dist/rules/deep-nesting.d.ts +23 -0
- package/dist/rules/deep-nesting.d.ts.map +1 -0
- package/dist/rules/deep-nesting.js +55 -0
- package/dist/rules/deep-nesting.js.map +1 -0
- package/dist/rules/index.d.ts +15 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +29 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/invalid-syntax.d.ts +22 -0
- package/dist/rules/invalid-syntax.d.ts.map +1 -0
- package/dist/rules/invalid-syntax.js +52 -0
- package/dist/rules/invalid-syntax.js.map +1 -0
- package/dist/rules/long-causal-chains.d.ts +25 -0
- package/dist/rules/long-causal-chains.d.ts.map +1 -0
- package/dist/rules/long-causal-chains.js +75 -0
- package/dist/rules/long-causal-chains.js.map +1 -0
- package/dist/rules/missing-recommended-fields.d.ts +21 -0
- package/dist/rules/missing-recommended-fields.d.ts.map +1 -0
- package/dist/rules/missing-recommended-fields.js +45 -0
- package/dist/rules/missing-recommended-fields.js.map +1 -0
- package/dist/rules/missing-required-fields.d.ts +22 -0
- package/dist/rules/missing-required-fields.d.ts.map +1 -0
- package/dist/rules/missing-required-fields.js +46 -0
- package/dist/rules/missing-required-fields.js.map +1 -0
- package/dist/rules/orphaned-nodes.d.ts +22 -0
- package/dist/rules/orphaned-nodes.d.ts.map +1 -0
- package/dist/rules/orphaned-nodes.js +76 -0
- package/dist/rules/orphaned-nodes.js.map +1 -0
- package/dist/rules/unlabeled-tension.d.ts +20 -0
- package/dist/rules/unlabeled-tension.d.ts.map +1 -0
- package/dist/rules/unlabeled-tension.js +37 -0
- package/dist/rules/unlabeled-tension.js.map +1 -0
- package/dist/serializer.d.ts +40 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +368 -0
- package/dist/serializer.js.map +1 -0
- package/dist/tokenizer.d.ts +26 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +213 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +18 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +68 -0
- package/dist/validate.js.map +1 -0
- 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
|