progmune-runtime 2.1.5 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/strategy-planner.js +120 -19
- package/package.json +1 -1
package/dist/strategy-planner.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.selectCapabilityChains = selectCapabilityChains;
|
|
|
13
13
|
exports.formatChainHint = formatChainHint;
|
|
14
14
|
const utils_1 = require("./utils");
|
|
15
15
|
const feedback_1 = require("./feedback");
|
|
16
|
+
const semantic_topology_1 = require("./semantic-topology");
|
|
16
17
|
/** Build a capability graph from IR functions. */
|
|
17
18
|
function buildCapabilityGraph(ir) {
|
|
18
19
|
const graph = new Map();
|
|
@@ -25,60 +26,129 @@ function buildCapabilityGraph(ir) {
|
|
|
25
26
|
tags: f.tags || [],
|
|
26
27
|
requires: f.requires || [],
|
|
27
28
|
produces: f.produces || [],
|
|
29
|
+
useWhen: f.useWhen || [],
|
|
28
30
|
score: 0,
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
return graph;
|
|
32
34
|
}
|
|
33
|
-
/** Score a capability node against an intent.
|
|
35
|
+
/** Score a capability node against an intent.
|
|
36
|
+
* Returns 0 for irrelevant nodes (no keyword match at all). */
|
|
34
37
|
function scoreNode(node, intentLower, keywords) {
|
|
35
38
|
let score = 0;
|
|
39
|
+
let hasMatch = false;
|
|
36
40
|
// Name match
|
|
37
41
|
for (const kw of keywords) {
|
|
38
|
-
if (node.name.toLowerCase().includes(kw))
|
|
42
|
+
if (node.name.toLowerCase().includes(kw)) {
|
|
39
43
|
score += 1;
|
|
40
|
-
|
|
44
|
+
hasMatch = true;
|
|
45
|
+
}
|
|
46
|
+
const js = (0, utils_1.jaccardSimilarity)(node.name.toLowerCase(), kw);
|
|
47
|
+
if (js > 0.2) {
|
|
48
|
+
score += js;
|
|
49
|
+
hasMatch = true;
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
// Purpose match
|
|
43
53
|
const purposeLower = node.purpose.toLowerCase();
|
|
44
54
|
for (const kw of keywords) {
|
|
45
|
-
if (purposeLower.includes(kw))
|
|
55
|
+
if (purposeLower.includes(kw)) {
|
|
46
56
|
score += 2;
|
|
57
|
+
hasMatch = true;
|
|
58
|
+
}
|
|
47
59
|
}
|
|
48
60
|
// Tag match
|
|
49
61
|
for (const tag of node.tags) {
|
|
50
|
-
if (intentLower.includes(tag.toLowerCase()))
|
|
62
|
+
if (intentLower.includes(tag.toLowerCase())) {
|
|
51
63
|
score += 1.5;
|
|
64
|
+
hasMatch = true;
|
|
65
|
+
}
|
|
52
66
|
}
|
|
53
67
|
// Semantic word overlap in purpose
|
|
54
68
|
const intentWords = intentLower.split(/[\s,,]+/);
|
|
55
69
|
for (const w of intentWords) {
|
|
56
|
-
if (w.length > 2 && purposeLower.includes(w))
|
|
70
|
+
if (w.length > 2 && purposeLower.includes(w)) {
|
|
57
71
|
score += 0.5;
|
|
72
|
+
hasMatch = true;
|
|
73
|
+
}
|
|
58
74
|
}
|
|
75
|
+
// useWhen scenario match
|
|
76
|
+
if (node.useWhen) {
|
|
77
|
+
for (const scenario of node.useWhen) {
|
|
78
|
+
const scenarioWords = scenario.toLowerCase().split(/[\s,]+/);
|
|
79
|
+
const matchCount = scenarioWords.filter((w) => w.length > 3 && intentLower.includes(w)).length;
|
|
80
|
+
if (matchCount >= 2) {
|
|
81
|
+
score += 3.0;
|
|
82
|
+
hasMatch = true;
|
|
83
|
+
}
|
|
84
|
+
else if (matchCount === 1) {
|
|
85
|
+
score += 1.0;
|
|
86
|
+
hasMatch = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Require at least one match to be relevant
|
|
91
|
+
if (!hasMatch)
|
|
92
|
+
return 0;
|
|
59
93
|
// Dynamic Credit: multiply by actual success rate
|
|
60
94
|
const successRate = (0, feedback_1.getFailureAdjustedCredit)(node.name);
|
|
61
95
|
const creditFactor = 0.3 + successRate * 0.7;
|
|
62
96
|
return score * creditFactor;
|
|
63
97
|
}
|
|
64
|
-
/** Find all capability nodes that produce a given capability label.
|
|
65
|
-
|
|
98
|
+
/** Find all capability nodes that produce a given capability label.
|
|
99
|
+
* Falls back to topology similarity if no direct data-flow match. */
|
|
100
|
+
function findProducers(graph, capability, allNodes) {
|
|
66
101
|
const producers = [];
|
|
67
102
|
for (const node of graph.values()) {
|
|
68
103
|
if (node.produces.some(p => p === capability || capability.includes(p) || p.includes(capability))) {
|
|
69
104
|
producers.push(node);
|
|
70
105
|
}
|
|
71
106
|
}
|
|
107
|
+
// Topology fallback: find semantically related producers
|
|
108
|
+
if (producers.length === 0) {
|
|
109
|
+
try {
|
|
110
|
+
const topo = (0, semantic_topology_1.getTopology)();
|
|
111
|
+
for (const node of allNodes) {
|
|
112
|
+
if (node.produces.length > 0) {
|
|
113
|
+
for (const p of node.produces) {
|
|
114
|
+
if (topo.capabilityMatch(p, capability) && !producers.includes(node)) {
|
|
115
|
+
producers.push(node);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
}
|
|
72
124
|
return producers;
|
|
73
125
|
}
|
|
74
|
-
/** Find all capability nodes that require a given capability label.
|
|
75
|
-
|
|
126
|
+
/** Find all capability nodes that require a given capability label.
|
|
127
|
+
* Falls back to topology similarity if no direct data-flow match. */
|
|
128
|
+
function findConsumers(graph, capability, allNodes) {
|
|
76
129
|
const consumers = [];
|
|
77
130
|
for (const node of graph.values()) {
|
|
78
131
|
if (node.requires.some(r => r === capability || capability.includes(r) || r.includes(capability))) {
|
|
79
132
|
consumers.push(node);
|
|
80
133
|
}
|
|
81
134
|
}
|
|
135
|
+
// Topology fallback
|
|
136
|
+
if (consumers.length === 0) {
|
|
137
|
+
try {
|
|
138
|
+
const topo = (0, semantic_topology_1.getTopology)();
|
|
139
|
+
for (const node of allNodes) {
|
|
140
|
+
if (node.requires.length > 0) {
|
|
141
|
+
for (const r of node.requires) {
|
|
142
|
+
if (topo.capabilityMatch(capability, r) && !consumers.includes(node)) {
|
|
143
|
+
consumers.push(node);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch { }
|
|
151
|
+
}
|
|
82
152
|
return consumers;
|
|
83
153
|
}
|
|
84
154
|
/**
|
|
@@ -98,39 +168,70 @@ function selectCapabilityChains(intent, ir, maxChains = 5) {
|
|
|
98
168
|
for (const node of graph.values()) {
|
|
99
169
|
node.score = scoreNode(node, intentLower, keywords);
|
|
100
170
|
}
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
171
|
+
// Dynamic threshold: tighten for large IR to prevent score dilution
|
|
172
|
+
let dynamicThreshold = graph.size > 500 ? 2.0 : graph.size > 200 ? 1.5 : 1.0;
|
|
173
|
+
// Fallback: if no seeds found, halve threshold
|
|
174
|
+
let seeds = [...graph.values()]
|
|
175
|
+
.filter(n => n.score > dynamicThreshold && (n.produces.length > 0 || n.score > dynamicThreshold + 2))
|
|
176
|
+
.sort((a, b) => b.score - a.score);
|
|
177
|
+
if (seeds.length === 0 && dynamicThreshold > 0.5) {
|
|
178
|
+
dynamicThreshold *= 0.5;
|
|
179
|
+
seeds = [...graph.values()]
|
|
180
|
+
.filter(n => n.score > dynamicThreshold && (n.produces.length > 0 || n.score > dynamicThreshold + 1))
|
|
181
|
+
.sort((a, b) => b.score - a.score);
|
|
182
|
+
}
|
|
183
|
+
seeds = seeds.slice(0, graph.size > 500 ? 30 : 15);
|
|
184
|
+
const allNodes = [...graph.values()];
|
|
106
185
|
const chains = [];
|
|
107
186
|
for (const seed of seeds) {
|
|
108
187
|
// Build chain: seed → consumer → consumer...
|
|
109
188
|
const chain = [seed];
|
|
110
189
|
const visited = new Set([seed.name]);
|
|
111
190
|
let totalScore = seed.score;
|
|
112
|
-
// Forward trace:
|
|
191
|
+
// Forward trace: data flow → semantic leap
|
|
113
192
|
let current = seed;
|
|
114
193
|
let extended = true;
|
|
194
|
+
let leapDecay = 1.0; // weight decay for semantic leaps
|
|
115
195
|
while (extended && chain.length < 8) {
|
|
116
196
|
extended = false;
|
|
197
|
+
// Strategy 1: direct data flow (produces → requires)
|
|
117
198
|
for (const p of current.produces) {
|
|
118
|
-
const consumers = findConsumers(graph, p).filter(c => !visited.has(c.name));
|
|
199
|
+
const consumers = findConsumers(graph, p, allNodes).filter(c => !visited.has(c.name));
|
|
119
200
|
if (consumers.length > 0) {
|
|
120
|
-
// Pick best-scoring consumer
|
|
121
201
|
const bestConsumer = consumers.sort((a, b) => b.score - a.score)[0];
|
|
122
202
|
chain.push(bestConsumer);
|
|
123
203
|
visited.add(bestConsumer.name);
|
|
124
204
|
totalScore += bestConsumer.score;
|
|
125
205
|
current = bestConsumer;
|
|
126
206
|
extended = true;
|
|
207
|
+
leapDecay = 1.0; // reset decay on direct match
|
|
127
208
|
break;
|
|
128
209
|
}
|
|
129
210
|
}
|
|
211
|
+
// Strategy 2: semantic leap — use topology similarity
|
|
212
|
+
if (!extended) {
|
|
213
|
+
try {
|
|
214
|
+
const topo = (0, semantic_topology_1.getTopology)();
|
|
215
|
+
const similar = topo.findSimilar(current.name, 10)
|
|
216
|
+
.filter(s => !visited.has(s.name) && s.similarity > 0.2);
|
|
217
|
+
if (similar.length > 0) {
|
|
218
|
+
const bestMatch = graph.get(similar[0].name);
|
|
219
|
+
if (bestMatch && bestMatch.score > 0) {
|
|
220
|
+
chain.push(bestMatch);
|
|
221
|
+
visited.add(bestMatch.name);
|
|
222
|
+
totalScore += bestMatch.score * leapDecay; // decayed score
|
|
223
|
+
current = bestMatch;
|
|
224
|
+
extended = true;
|
|
225
|
+
leapDecay *= 0.7; // each semantic leap loses 30% weight
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
}
|
|
130
231
|
}
|
|
131
232
|
// Backward trace: does seed need something? Find producers.
|
|
132
233
|
if (seed.requires.length > 0) {
|
|
133
|
-
const producers = findProducers(graph, seed.requires[0])
|
|
234
|
+
const producers = findProducers(graph, seed.requires[0], allNodes)
|
|
134
235
|
.filter(p => !visited.has(p.name));
|
|
135
236
|
if (producers.length > 0) {
|
|
136
237
|
const bestProducer = producers.sort((a, b) => b.score - a.score)[0];
|