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
|
@@ -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
|