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
@@ -0,0 +1,884 @@
1
+ "use strict";
2
+ /**
3
+ * FlowScript Query Engine
4
+ *
5
+ * Computational operations on FlowScript IR graphs that prove FlowScript is a
6
+ * "computable substrate" for cognitive partnership.
7
+ *
8
+ * This module implements five critical queries:
9
+ * 1. why(nodeId) - Causal ancestry (backward traversal)
10
+ * 2. whatIf(nodeId) - Impact analysis (forward traversal)
11
+ * 3. tensions() - Tradeoff mapping (tension extraction)
12
+ * 4. blocked() - Blocker tracking (state + dependencies)
13
+ * 5. alternatives(questionId) - Decision reconstruction
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.FlowScriptQueryEngine = void 0;
17
+ // ============================================================================
18
+ // FlowScript Query Engine
19
+ // ============================================================================
20
+ class FlowScriptQueryEngine {
21
+ constructor() {
22
+ this.nodeMap = new Map();
23
+ this.relationshipsFromSource = new Map();
24
+ this.relationshipsToTarget = new Map();
25
+ this.stateMap = new Map();
26
+ }
27
+ /**
28
+ * Load IR graph and build indexes for efficient querying
29
+ */
30
+ load(ir) {
31
+ this.ir = ir;
32
+ this.buildIndexes();
33
+ }
34
+ /**
35
+ * Build efficient indexes for O(1) lookups
36
+ */
37
+ buildIndexes() {
38
+ // Clear existing indexes
39
+ this.nodeMap.clear();
40
+ this.relationshipsFromSource.clear();
41
+ this.relationshipsToTarget.clear();
42
+ this.stateMap.clear();
43
+ // Node map: id -> Node
44
+ for (const node of this.ir.nodes) {
45
+ this.nodeMap.set(node.id, node);
46
+ }
47
+ // Relationship indexes: source -> [relationships], target -> [relationships]
48
+ for (const rel of this.ir.relationships) {
49
+ // From source
50
+ if (!this.relationshipsFromSource.has(rel.source)) {
51
+ this.relationshipsFromSource.set(rel.source, []);
52
+ }
53
+ this.relationshipsFromSource.get(rel.source).push(rel);
54
+ // To target
55
+ if (!this.relationshipsToTarget.has(rel.target)) {
56
+ this.relationshipsToTarget.set(rel.target, []);
57
+ }
58
+ this.relationshipsToTarget.get(rel.target).push(rel);
59
+ }
60
+ // State map: node_id -> State
61
+ for (const state of this.ir.states || []) {
62
+ this.stateMap.set(state.node_id, state);
63
+ }
64
+ }
65
+ /**
66
+ * Query 1: Trace causal ancestry (backward traversal)
67
+ *
68
+ * Traces backward through causal relationships to understand why a node exists.
69
+ * Returns causal chain from root causes to target node.
70
+ */
71
+ why(nodeId, options = {}) {
72
+ const format = options.format || 'chain';
73
+ const maxDepth = options.maxDepth;
74
+ const includeCorrelations = options.includeCorrelations || false;
75
+ // Build relationship types to follow
76
+ // Note: We follow both derives_from AND causes (backward) because:
77
+ // - "A <- B" (derives_from): A explicitly derives from B
78
+ // - "A -> B" (causes): B is caused by A, so B derives from A
79
+ const relationshipTypes = ['derives_from', 'causes'];
80
+ if (includeCorrelations) {
81
+ relationshipTypes.push('equivalent');
82
+ }
83
+ // Traverse backward to find all ancestors
84
+ const ancestors = this.traverseBackward(nodeId, relationshipTypes, maxDepth);
85
+ // Get target node
86
+ const targetNode = this.nodeMap.get(nodeId);
87
+ if (!targetNode) {
88
+ throw new Error(`Node not found: ${nodeId}`);
89
+ }
90
+ // Build causal chain from root to target
91
+ const { chain, rootCause } = this.buildCausalChain(nodeId, ancestors, relationshipTypes);
92
+ // Format based on options
93
+ if (format === 'minimal') {
94
+ return {
95
+ root_cause: rootCause.content,
96
+ chain: chain.map(n => n.content)
97
+ };
98
+ }
99
+ // Default: 'chain' format
100
+ return {
101
+ target: {
102
+ id: targetNode.id,
103
+ content: targetNode.content
104
+ },
105
+ causal_chain: chain.map((node, index) => ({
106
+ depth: chain.length - index,
107
+ id: node.id,
108
+ content: node.content,
109
+ relationship_type: node.relationshipType || 'derives_from'
110
+ })),
111
+ root_cause: {
112
+ id: rootCause.id,
113
+ content: rootCause.content,
114
+ is_root: true
115
+ },
116
+ metadata: {
117
+ total_ancestors: ancestors.length,
118
+ max_depth: chain.length,
119
+ has_multiple_paths: this.hasMultiplePaths(nodeId, relationshipTypes)
120
+ }
121
+ };
122
+ }
123
+ /**
124
+ * Query 2: Impact analysis (forward traversal)
125
+ *
126
+ * Traces forward through causal relationships to understand consequences.
127
+ * Returns impact tree with tensions in impact zone.
128
+ */
129
+ whatIf(nodeId, options = {}) {
130
+ const format = options.format || 'tree';
131
+ const maxDepth = options.maxDepth;
132
+ const includeCorrelations = options.includeCorrelations || false;
133
+ const includeTemporalConsequences = options.includeTemporalConsequences !== false;
134
+ // Build relationship types to follow
135
+ const relationshipTypes = ['causes'];
136
+ if (includeTemporalConsequences) {
137
+ relationshipTypes.push('temporal');
138
+ }
139
+ if (includeCorrelations) {
140
+ relationshipTypes.push('equivalent');
141
+ }
142
+ // Get source node
143
+ const sourceNode = this.nodeMap.get(nodeId);
144
+ if (!sourceNode) {
145
+ throw new Error(`Node not found: ${nodeId}`);
146
+ }
147
+ // Traverse forward to find all descendants
148
+ const descendants = this.traverseForward(nodeId, relationshipTypes, maxDepth);
149
+ // Build impact tree with depth information
150
+ const impactTree = this.buildImpactTree(nodeId, descendants, relationshipTypes);
151
+ // Find tensions in descendant subgraph
152
+ const descendantIds = new Set(descendants.map(d => d.id));
153
+ descendantIds.add(nodeId);
154
+ const tensions = this.findTensionsInSubgraph(descendantIds);
155
+ // Check if temporal consequences exist
156
+ const hasTemporalConsequences = descendants.some(d => d.relationshipType === 'temporal');
157
+ // Format: 'summary'
158
+ if (format === 'summary') {
159
+ return this.buildImpactSummary(sourceNode, descendants, tensions);
160
+ }
161
+ // Default: 'tree' or 'list' format
162
+ return {
163
+ source: {
164
+ id: sourceNode.id,
165
+ content: sourceNode.content
166
+ },
167
+ impact_tree: impactTree,
168
+ tensions_in_impact_zone: tensions,
169
+ metadata: {
170
+ total_descendants: descendants.length,
171
+ max_depth: Math.max(...descendants.map(d => d.depth || 0), 0),
172
+ tension_count: tensions.length,
173
+ has_temporal_consequences: hasTemporalConsequences
174
+ }
175
+ };
176
+ }
177
+ /**
178
+ * Query 3: Tradeoff mapping (tension extraction)
179
+ *
180
+ * Extracts all tension relationships, groups by axis or node.
181
+ * Returns systematic view of tradeoffs in the graph.
182
+ */
183
+ tensions(options = {}) {
184
+ const groupBy = options.groupBy || 'axis';
185
+ const filterByAxis = options.filterByAxis;
186
+ const includeContext = options.includeContext || false;
187
+ const scope = options.scope;
188
+ // Get all tension relationships
189
+ let tensionRels = this.ir.relationships.filter(rel => rel.type === 'tension');
190
+ // Filter by scope if provided
191
+ if (scope) {
192
+ const scopeNodeIds = new Set();
193
+ scopeNodeIds.add(scope);
194
+ // Get all descendants of scope node
195
+ const descendants = this.traverseForward(scope, ['causes', 'temporal', 'derives_from']);
196
+ descendants.forEach(d => scopeNodeIds.add(d.id));
197
+ // Filter tensions to those within scope
198
+ tensionRels = tensionRels.filter(rel => scopeNodeIds.has(rel.source) && scopeNodeIds.has(rel.target));
199
+ }
200
+ // Filter by axis if specified
201
+ if (filterByAxis && filterByAxis.length > 0) {
202
+ tensionRels = tensionRels.filter(rel => rel.axis_label && filterByAxis.includes(rel.axis_label));
203
+ }
204
+ // Build tension details
205
+ const tensionDetails = [];
206
+ for (const rel of tensionRels) {
207
+ const sourceNode = this.nodeMap.get(rel.source);
208
+ const targetNode = this.nodeMap.get(rel.target);
209
+ if (!sourceNode || !targetNode)
210
+ continue;
211
+ const detail = {
212
+ axis: rel.axis_label || 'unlabeled',
213
+ source: {
214
+ id: sourceNode.id,
215
+ content: sourceNode.content
216
+ },
217
+ target: {
218
+ id: targetNode.id,
219
+ content: targetNode.content
220
+ }
221
+ };
222
+ // Include context (parent nodes) if requested
223
+ if (includeContext) {
224
+ const context = [];
225
+ // Find parents of source node
226
+ const sourceParents = this.relationshipsToTarget.get(rel.source) || [];
227
+ for (const parentRel of sourceParents) {
228
+ if (parentRel.type !== 'tension') {
229
+ const parentNode = this.nodeMap.get(parentRel.source);
230
+ if (parentNode) {
231
+ context.push({
232
+ id: parentNode.id,
233
+ content: parentNode.content
234
+ });
235
+ }
236
+ }
237
+ }
238
+ if (context.length > 0) {
239
+ detail.context = context;
240
+ }
241
+ }
242
+ tensionDetails.push(detail);
243
+ }
244
+ // Calculate metadata
245
+ const uniqueAxes = Array.from(new Set(tensionDetails.map(t => t.axis)));
246
+ const axisCounts = new Map();
247
+ tensionDetails.forEach(t => {
248
+ axisCounts.set(t.axis, (axisCounts.get(t.axis) || 0) + 1);
249
+ });
250
+ let mostCommonAxis = null;
251
+ let maxCount = 0;
252
+ for (const [axis, count] of axisCounts) {
253
+ if (count > maxCount) {
254
+ mostCommonAxis = axis;
255
+ maxCount = count;
256
+ }
257
+ }
258
+ const metadata = {
259
+ total_tensions: tensionDetails.length,
260
+ unique_axes: uniqueAxes,
261
+ most_common_axis: mostCommonAxis
262
+ };
263
+ // Group by option
264
+ if (groupBy === 'axis') {
265
+ const byAxis = {};
266
+ tensionDetails.forEach(t => {
267
+ if (!byAxis[t.axis]) {
268
+ byAxis[t.axis] = [];
269
+ }
270
+ const { axis, ...detail } = t;
271
+ byAxis[t.axis].push(detail);
272
+ });
273
+ return {
274
+ tensions_by_axis: byAxis,
275
+ metadata
276
+ };
277
+ }
278
+ else if (groupBy === 'node') {
279
+ const byNode = {};
280
+ tensionDetails.forEach(t => {
281
+ const nodeId = t.source.id;
282
+ if (!byNode[nodeId]) {
283
+ byNode[nodeId] = [];
284
+ }
285
+ const { axis, ...detail } = t;
286
+ byNode[nodeId].push(detail);
287
+ });
288
+ return {
289
+ tensions_by_node: byNode,
290
+ metadata
291
+ };
292
+ }
293
+ else {
294
+ // groupBy === 'none' - flat array
295
+ const flatTensions = tensionDetails.map(t => {
296
+ const { axis, ...detail } = t;
297
+ return detail;
298
+ });
299
+ return {
300
+ tensions: flatTensions,
301
+ metadata
302
+ };
303
+ }
304
+ }
305
+ /**
306
+ * Query 4: Blocker tracking (state + dependencies)
307
+ *
308
+ * Finds all blocked nodes, calculates impact, shows transitive causes/effects.
309
+ * Returns priority-sorted list of blockers.
310
+ */
311
+ blocked(options = {}) {
312
+ const since = options.since;
313
+ const includeTransitiveCauses = options.includeTransitiveCauses !== false;
314
+ const includeTransitiveEffects = options.includeTransitiveEffects !== false;
315
+ // Find all blocked states
316
+ let blockedStates = (this.ir.states || []).filter(state => state.type === 'blocked');
317
+ // Filter by since date if provided
318
+ if (since) {
319
+ blockedStates = blockedStates.filter(state => {
320
+ const stateSince = state.fields?.since;
321
+ if (!stateSince)
322
+ return false;
323
+ return stateSince >= since;
324
+ });
325
+ }
326
+ // Build blocker details
327
+ const blockers = [];
328
+ const today = new Date();
329
+ for (const state of blockedStates) {
330
+ const node = this.nodeMap.get(state.node_id);
331
+ if (!node)
332
+ continue;
333
+ const reason = state.fields?.reason || 'unknown';
334
+ const sinceDate = state.fields?.since || '';
335
+ // Calculate days blocked
336
+ let daysBlocked = 0;
337
+ if (sinceDate) {
338
+ const sinceTime = new Date(sinceDate).getTime();
339
+ const todayTime = today.getTime();
340
+ daysBlocked = Math.floor((todayTime - sinceTime) / (1000 * 60 * 60 * 24));
341
+ }
342
+ const detail = {
343
+ node: {
344
+ id: node.id,
345
+ content: node.content
346
+ },
347
+ blocked_state: {
348
+ reason,
349
+ since: sinceDate,
350
+ days_blocked: daysBlocked
351
+ },
352
+ impact_score: 0
353
+ };
354
+ // Find transitive causes (what's blocking this blocker)
355
+ if (includeTransitiveCauses) {
356
+ const causes = this.traverseBackward(node.id, ['derives_from', 'causes']);
357
+ detail.transitive_causes = causes.map(c => ({
358
+ id: c.id,
359
+ content: c.content
360
+ }));
361
+ }
362
+ // Find transitive effects (what's blocked by this blocker)
363
+ if (includeTransitiveEffects) {
364
+ const effects = this.traverseForward(node.id, ['causes', 'temporal']);
365
+ detail.transitive_effects = effects.map(e => ({
366
+ id: e.id,
367
+ content: e.content
368
+ }));
369
+ detail.impact_score = effects.length;
370
+ }
371
+ blockers.push(detail);
372
+ }
373
+ // Sort by impact score (descending), then by days blocked (descending)
374
+ blockers.sort((a, b) => {
375
+ if (a.impact_score !== b.impact_score) {
376
+ return b.impact_score - a.impact_score;
377
+ }
378
+ return b.blocked_state.days_blocked - a.blocked_state.days_blocked;
379
+ });
380
+ // Calculate metadata
381
+ const totalBlockers = blockers.length;
382
+ const highPriorityCount = blockers.filter(b => b.impact_score > 0 || b.blocked_state.days_blocked > 7).length;
383
+ const avgDaysBlocked = totalBlockers > 0
384
+ ? blockers.reduce((sum, b) => sum + b.blocked_state.days_blocked, 0) / totalBlockers
385
+ : 0;
386
+ let oldestBlocker = null;
387
+ if (blockers.length > 0) {
388
+ const oldest = blockers.reduce((max, b) => b.blocked_state.days_blocked > max.blocked_state.days_blocked ? b : max);
389
+ oldestBlocker = {
390
+ id: oldest.node.id,
391
+ days: oldest.blocked_state.days_blocked
392
+ };
393
+ }
394
+ return {
395
+ blockers,
396
+ metadata: {
397
+ total_blockers: totalBlockers,
398
+ high_priority_count: highPriorityCount,
399
+ average_days_blocked: Math.round(avgDaysBlocked * 10) / 10,
400
+ oldest_blocker: oldestBlocker
401
+ }
402
+ };
403
+ }
404
+ /**
405
+ * Query 5: Decision reconstruction (alternatives + rationale)
406
+ *
407
+ * Reconstructs decision with all alternatives, showing which was chosen and why.
408
+ * Supports three formats: comparison (default), simple, tree.
409
+ */
410
+ alternatives(questionId, options = {}) {
411
+ const format = options.format || 'comparison';
412
+ const includeRationale = options.includeRationale !== false;
413
+ const includeConsequences = options.includeConsequences || false;
414
+ const showRejectedReasons = options.showRejectedReasons || false;
415
+ // Verify questionId is a question node
416
+ const questionNode = this.nodeMap.get(questionId);
417
+ if (!questionNode) {
418
+ throw new Error(`Node not found: ${questionId}`);
419
+ }
420
+ if (questionNode.type !== 'question') {
421
+ throw new Error(`Node ${questionId} is not a question (type: ${questionNode.type})`);
422
+ }
423
+ // Find all alternative relationships from question
424
+ const altRels = (this.relationshipsFromSource.get(questionId) || [])
425
+ .filter(rel => rel.type === 'alternative');
426
+ // Format-specific routing
427
+ switch (format) {
428
+ case 'simple': {
429
+ // Simple format: minimal summary (question + options + chosen + reason)
430
+ const alternatives = [];
431
+ for (const altRel of altRels) {
432
+ const altNode = this.nodeMap.get(altRel.target);
433
+ if (!altNode)
434
+ continue;
435
+ // Check if chosen
436
+ let isChosen = false;
437
+ let rationale;
438
+ for (const state of this.ir.states || []) {
439
+ if (state.type === 'decided') {
440
+ const stateNode = this.nodeMap.get(state.node_id);
441
+ if (stateNode && stateNode.content === altNode.content) {
442
+ isChosen = true;
443
+ if (includeRationale && state.fields) {
444
+ rationale = state.fields.rationale;
445
+ }
446
+ }
447
+ }
448
+ }
449
+ alternatives.push({
450
+ id: altNode.id,
451
+ content: altNode.content,
452
+ chosen: isChosen,
453
+ rationale
454
+ });
455
+ }
456
+ const chosenAlt = alternatives.find(a => a.chosen);
457
+ return {
458
+ format: 'simple',
459
+ question: questionNode.content,
460
+ options_considered: alternatives.map(a => a.content),
461
+ chosen: chosenAlt?.content || null,
462
+ reason: chosenAlt?.rationale || null
463
+ };
464
+ }
465
+ case 'tree': {
466
+ // Tree format: hierarchical structure with recursive children
467
+ const treeAlternatives = altRels.map(altRel => this.buildAlternativeTree(altRel.target, new Set(), showRejectedReasons));
468
+ return {
469
+ format: 'tree',
470
+ question: {
471
+ id: questionNode.id,
472
+ content: questionNode.content
473
+ },
474
+ alternatives: treeAlternatives
475
+ };
476
+ }
477
+ case 'comparison':
478
+ default: {
479
+ // Comparison format: full decision analysis with all details
480
+ const alternatives = [];
481
+ let chosenAlternative = null;
482
+ for (const altRel of altRels) {
483
+ const altNode = this.nodeMap.get(altRel.target);
484
+ if (!altNode)
485
+ continue;
486
+ // Check if this alternative was chosen (has decided state)
487
+ let isChosen = false;
488
+ let rationale;
489
+ let decidedOn;
490
+ // Check all states for decisions related to this alternative
491
+ for (const state of this.ir.states || []) {
492
+ if (state.type === 'decided') {
493
+ const stateNode = this.nodeMap.get(state.node_id);
494
+ if (stateNode && stateNode.content === altNode.content) {
495
+ isChosen = true;
496
+ if (includeRationale && state.fields) {
497
+ rationale = state.fields.rationale;
498
+ decidedOn = state.fields.on;
499
+ }
500
+ }
501
+ }
502
+ }
503
+ const detail = {
504
+ id: altNode.id,
505
+ content: altNode.content,
506
+ chosen: isChosen
507
+ };
508
+ if (isChosen && rationale) {
509
+ detail.rationale = rationale;
510
+ detail.decided_on = decidedOn;
511
+ }
512
+ // Extract rejection reasons from thought nodes (only for rejected alternatives)
513
+ if (showRejectedReasons && !isChosen) {
514
+ const reasons = this.extractRejectionReasons(altNode.id);
515
+ if (reasons.length > 0) {
516
+ detail.rejection_reasons = reasons;
517
+ }
518
+ }
519
+ // Get consequences (children of alternative)
520
+ if (includeConsequences) {
521
+ const consequences = (this.relationshipsFromSource.get(altNode.id) || [])
522
+ .filter(rel => rel.type === 'causes')
523
+ .map(rel => {
524
+ const childNode = this.nodeMap.get(rel.target);
525
+ return childNode ? {
526
+ id: childNode.id,
527
+ content: childNode.content
528
+ } : null;
529
+ })
530
+ .filter(c => c !== null);
531
+ if (consequences.length > 0) {
532
+ detail.consequences = consequences;
533
+ }
534
+ }
535
+ // Find tensions within this alternative
536
+ const altTensions = (this.relationshipsFromSource.get(altNode.id) || [])
537
+ .filter(rel => rel.type === 'tension')
538
+ .map(rel => {
539
+ const targetNode = this.nodeMap.get(rel.target);
540
+ if (!targetNode)
541
+ return null;
542
+ return {
543
+ axis: rel.axis_label || 'unlabeled',
544
+ source: {
545
+ id: altNode.id,
546
+ content: altNode.content
547
+ },
548
+ target: {
549
+ id: targetNode.id,
550
+ content: targetNode.content
551
+ }
552
+ };
553
+ })
554
+ .filter(t => t !== null);
555
+ if (altTensions.length > 0) {
556
+ detail.tensions = altTensions;
557
+ }
558
+ alternatives.push(detail);
559
+ if (isChosen) {
560
+ chosenAlternative = detail;
561
+ }
562
+ }
563
+ // Build decision summary
564
+ const rejected = alternatives
565
+ .filter(alt => !alt.chosen)
566
+ .map(alt => alt.content);
567
+ const keyFactors = [];
568
+ if (chosenAlternative?.tensions) {
569
+ keyFactors.push(...chosenAlternative.tensions.map(t => t.axis));
570
+ }
571
+ return {
572
+ format: 'comparison',
573
+ question: {
574
+ id: questionNode.id,
575
+ content: questionNode.content
576
+ },
577
+ alternatives,
578
+ decision_summary: {
579
+ chosen: chosenAlternative?.content || null,
580
+ rationale: chosenAlternative?.rationale || null,
581
+ rejected,
582
+ key_factors: Array.from(new Set(keyFactors))
583
+ }
584
+ };
585
+ }
586
+ }
587
+ }
588
+ // ==========================================================================
589
+ // Helper Methods
590
+ // ==========================================================================
591
+ /**
592
+ * Extract rejection reasons from thought: nodes under an alternative
593
+ *
594
+ * Convention: thought nodes that are children (via 'causes' relationships)
595
+ * of a rejected alternative are interpreted as rejection reasoning.
596
+ *
597
+ * @param altNodeId - The alternative node ID to extract rejection reasons from
598
+ * @returns Array of rejection reason strings (thought node contents)
599
+ */
600
+ extractRejectionReasons(altNodeId) {
601
+ const thoughts = (this.relationshipsFromSource.get(altNodeId) || [])
602
+ .filter(rel => rel.type === 'causes')
603
+ .map(rel => this.nodeMap.get(rel.target))
604
+ .filter(node => node !== undefined && node.type === 'thought')
605
+ .map(node => node.content);
606
+ return thoughts;
607
+ }
608
+ /**
609
+ * Build a recursive tree structure for an alternative and its consequence children
610
+ *
611
+ * Shows hierarchical structure of consequences (via 'causes' relationships).
612
+ * Includes cycle detection to handle potential graph cycles.
613
+ *
614
+ * @param nodeId - The node ID to start building from
615
+ * @param visited - Set of already visited node IDs for cycle detection
616
+ * @param includeRejectionReasons - Whether to include rejection reasons for rejected alternatives
617
+ * @returns TreeAlternative structure with recursive children
618
+ */
619
+ buildAlternativeTree(nodeId, visited = new Set(), includeRejectionReasons = false) {
620
+ // Cycle detection
621
+ if (visited.has(nodeId)) {
622
+ const node = this.nodeMap.get(nodeId);
623
+ return {
624
+ id: node.id,
625
+ content: node.content + ' [cycle detected]',
626
+ chosen: false,
627
+ children: []
628
+ };
629
+ }
630
+ visited.add(nodeId);
631
+ const node = this.nodeMap.get(nodeId);
632
+ // Check if this node is chosen (has decided state)
633
+ const isChosen = this.ir.states?.some(s => s.type === 'decided' && s.node_id === nodeId) || false;
634
+ // Build tree node
635
+ const treeNode = {
636
+ id: node.id,
637
+ content: node.content,
638
+ chosen: isChosen,
639
+ children: []
640
+ };
641
+ // Add rejection reasons if requested and not chosen
642
+ if (includeRejectionReasons && !isChosen) {
643
+ const reasons = this.extractRejectionReasons(nodeId);
644
+ if (reasons.length > 0) {
645
+ treeNode.rejection_reasons = reasons;
646
+ }
647
+ }
648
+ // Recursively build children (only consequence relationships via 'causes')
649
+ const childRels = (this.relationshipsFromSource.get(nodeId) || [])
650
+ .filter(rel => rel.type === 'causes');
651
+ for (const rel of childRels) {
652
+ // Pass copy of visited set to allow multiple paths to same node
653
+ const childTree = this.buildAlternativeTree(rel.target, new Set(visited), includeRejectionReasons);
654
+ treeNode.children.push(childTree);
655
+ }
656
+ return treeNode;
657
+ }
658
+ /**
659
+ * Traverse backward through relationships
660
+ */
661
+ traverseBackward(nodeId, relationshipTypes, maxDepth, visited = new Set(), currentDepth = 0) {
662
+ // Check depth limit
663
+ if (maxDepth !== undefined && currentDepth >= maxDepth) {
664
+ return [];
665
+ }
666
+ // Check cycles
667
+ if (visited.has(nodeId)) {
668
+ return [];
669
+ }
670
+ visited.add(nodeId);
671
+ const result = [];
672
+ // Get all incoming relationships to this node
673
+ const incomingRels = this.relationshipsToTarget.get(nodeId) || [];
674
+ // Filter by relationship types
675
+ const relevantRels = incomingRels.filter(rel => relationshipTypes.includes(rel.type));
676
+ // Traverse each parent
677
+ for (const rel of relevantRels) {
678
+ const parentNode = this.nodeMap.get(rel.source);
679
+ if (parentNode) {
680
+ // Add parent to result
681
+ result.push({
682
+ ...parentNode,
683
+ depth: currentDepth + 1,
684
+ relationshipType: rel.type
685
+ });
686
+ // Recursively traverse parent's ancestors
687
+ const ancestors = this.traverseBackward(rel.source, relationshipTypes, maxDepth, new Set(visited), currentDepth + 1);
688
+ result.push(...ancestors);
689
+ }
690
+ }
691
+ return result;
692
+ }
693
+ /**
694
+ * Traverse forward through relationships
695
+ */
696
+ traverseForward(nodeId, relationshipTypes, maxDepth, visited = new Set(), currentDepth = 0) {
697
+ // Check depth limit
698
+ if (maxDepth !== undefined && currentDepth >= maxDepth) {
699
+ return [];
700
+ }
701
+ // Check cycles
702
+ if (visited.has(nodeId)) {
703
+ return [];
704
+ }
705
+ visited.add(nodeId);
706
+ const result = [];
707
+ // Get all outgoing relationships from this node
708
+ const outgoingRels = this.relationshipsFromSource.get(nodeId) || [];
709
+ // Filter by relationship types
710
+ const relevantRels = outgoingRels.filter(rel => relationshipTypes.includes(rel.type));
711
+ // Traverse each child
712
+ for (const rel of relevantRels) {
713
+ const childNode = this.nodeMap.get(rel.target);
714
+ if (childNode) {
715
+ // Add child to result
716
+ result.push({
717
+ ...childNode,
718
+ depth: currentDepth + 1,
719
+ relationshipType: rel.type
720
+ });
721
+ // Recursively traverse child's descendants
722
+ const descendants = this.traverseForward(rel.target, relationshipTypes, maxDepth, new Set(visited), currentDepth + 1);
723
+ result.push(...descendants);
724
+ }
725
+ }
726
+ return result;
727
+ }
728
+ /**
729
+ * Build causal chain from root to target
730
+ */
731
+ buildCausalChain(targetId, ancestors, relationshipTypes) {
732
+ // If no ancestors, target is its own root
733
+ if (ancestors.length === 0) {
734
+ const targetNode = this.nodeMap.get(targetId);
735
+ return {
736
+ chain: [],
737
+ rootCause: targetNode
738
+ };
739
+ }
740
+ // Find root (deepest ancestor)
741
+ const maxDepth = Math.max(...ancestors.map(a => a.depth));
742
+ const roots = ancestors.filter(a => a.depth === maxDepth);
743
+ const root = roots[0];
744
+ // Build chain from root to target
745
+ const chain = [root];
746
+ let currentId = root.id;
747
+ // Walk from root back to target, only using nodes from ancestors
748
+ const ancestorIds = new Set(ancestors.map(a => a.id));
749
+ ancestorIds.add(targetId);
750
+ while (currentId !== targetId) {
751
+ // Find next node in chain (must be in ancestors or be the target)
752
+ const outgoingRels = this.relationshipsFromSource.get(currentId) || [];
753
+ const nextRel = outgoingRels.find(rel => relationshipTypes.includes(rel.type) &&
754
+ ancestorIds.has(rel.target));
755
+ if (!nextRel)
756
+ break;
757
+ const nextNode = this.nodeMap.get(nextRel.target);
758
+ if (!nextNode)
759
+ break;
760
+ // Don't add target to chain (chain is ancestors only)
761
+ if (nextRel.target === targetId) {
762
+ break;
763
+ }
764
+ // Only add if it's in our filtered ancestors (respects maxDepth)
765
+ const ancestorNode = ancestors.find(a => a.id === nextRel.target);
766
+ if (!ancestorNode)
767
+ break;
768
+ chain.push({
769
+ ...nextNode,
770
+ relationshipType: nextRel.type
771
+ });
772
+ currentId = nextRel.target;
773
+ }
774
+ return {
775
+ chain,
776
+ rootCause: root
777
+ };
778
+ }
779
+ /**
780
+ * Check if node has multiple causal paths
781
+ */
782
+ hasMultiplePaths(nodeId, relationshipTypes) {
783
+ const incomingRels = this.relationshipsToTarget.get(nodeId) || [];
784
+ const relevantRels = incomingRels.filter(rel => relationshipTypes.includes(rel.type));
785
+ return relevantRels.length > 1;
786
+ }
787
+ /**
788
+ * Build impact tree with direct and indirect consequences
789
+ */
790
+ buildImpactTree(_sourceId, descendants, _relationshipTypes) {
791
+ const direct = descendants.filter(d => d.depth === 1);
792
+ const indirect = descendants.filter(d => d.depth > 1);
793
+ // Check which nodes have tensions
794
+ const tensionNodeIds = new Set();
795
+ for (const rel of this.ir.relationships) {
796
+ if (rel.type === 'tension') {
797
+ tensionNodeIds.add(rel.source);
798
+ tensionNodeIds.add(rel.target);
799
+ }
800
+ }
801
+ return {
802
+ direct_consequences: direct.map(node => ({
803
+ id: node.id,
804
+ content: node.content,
805
+ relationship: node.relationshipType || 'causes',
806
+ depth: node.depth,
807
+ has_tension: tensionNodeIds.has(node.id)
808
+ })),
809
+ indirect_consequences: indirect.map(node => {
810
+ const consequence = {
811
+ id: node.id,
812
+ content: node.content,
813
+ relationship: node.relationshipType || 'causes',
814
+ depth: node.depth
815
+ };
816
+ // Check if this node is involved in a tension
817
+ const tensionRel = this.ir.relationships.find(rel => rel.type === 'tension' && (rel.source === node.id || rel.target === node.id));
818
+ if (tensionRel) {
819
+ consequence.tension_axis = tensionRel.axis_label || undefined;
820
+ }
821
+ return consequence;
822
+ })
823
+ };
824
+ }
825
+ /**
826
+ * Find all tensions in a subgraph
827
+ */
828
+ findTensionsInSubgraph(nodeIds) {
829
+ const tensions = [];
830
+ for (const rel of this.ir.relationships) {
831
+ if (rel.type === 'tension' && nodeIds.has(rel.source) && nodeIds.has(rel.target)) {
832
+ const sourceNode = this.nodeMap.get(rel.source);
833
+ const targetNode = this.nodeMap.get(rel.target);
834
+ if (sourceNode && targetNode) {
835
+ tensions.push({
836
+ axis: rel.axis_label || 'unlabeled',
837
+ source: {
838
+ id: sourceNode.id,
839
+ content: sourceNode.content
840
+ },
841
+ target: {
842
+ id: targetNode.id,
843
+ content: targetNode.content
844
+ }
845
+ });
846
+ }
847
+ }
848
+ }
849
+ return tensions;
850
+ }
851
+ /**
852
+ * Build impact summary format
853
+ */
854
+ buildImpactSummary(sourceNode, descendants, tensions) {
855
+ // Heuristic: classify consequences as benefits or risks
856
+ // This is a simple heuristic - can be improved later
857
+ const benefits = [];
858
+ const risks = [];
859
+ for (const desc of descendants) {
860
+ // Simple heuristic: check for positive/negative keywords
861
+ const content = desc.content.toLowerCase();
862
+ if (content.includes('risk') || content.includes('problem') ||
863
+ content.includes('issue') || content.includes('error') ||
864
+ content.includes('fail')) {
865
+ risks.push(desc.content);
866
+ }
867
+ else {
868
+ benefits.push(desc.content);
869
+ }
870
+ }
871
+ // Key tradeoff from first tension
872
+ const keyTradeoff = tensions.length > 0
873
+ ? `${tensions[0].axis} (${tensions[0].source.content} vs ${tensions[0].target.content})`
874
+ : null;
875
+ return {
876
+ impact_summary: `${sourceNode.content} affects ${descendants.length} downstream consideration${descendants.length === 1 ? '' : 's'}`,
877
+ benefits,
878
+ risks,
879
+ key_tradeoff: keyTradeoff
880
+ };
881
+ }
882
+ }
883
+ exports.FlowScriptQueryEngine = FlowScriptQueryEngine;
884
+ //# sourceMappingURL=query-engine.js.map