progmune-runtime 2.1.1 → 2.1.3
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/emitter.js +118 -9
- package/dist/health-utils.js +40 -0
- package/dist/ledger-utils.js +40 -0
- package/dist/planner.js +143 -19
- package/dist/stdlib.js +205 -0
- package/dist/strategy-planner.js +168 -0
- package/package.json +1 -1
package/dist/emitter.js
CHANGED
|
@@ -143,40 +143,133 @@ function emitCode(actions, meta) {
|
|
|
143
143
|
// Pre-scan: find input variables (referenced but never assigned via call/assign)
|
|
144
144
|
const declared = new Set();
|
|
145
145
|
const referenced = new Set();
|
|
146
|
+
// Track param name → type for conflict detection
|
|
147
|
+
const paramTypes = new Map();
|
|
146
148
|
let counter = 0;
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
+
// Recursive scan: collect variables from actions including nested if/for
|
|
150
|
+
const scanAction = (action) => {
|
|
149
151
|
if (action.kind === "call" && action.assignTo) {
|
|
150
152
|
declared.add(action.assignTo);
|
|
151
153
|
}
|
|
152
154
|
else if (action.kind === "assign" && action.target) {
|
|
153
155
|
declared.add(action.target);
|
|
154
156
|
}
|
|
155
|
-
// Collect arg value references
|
|
156
157
|
if (action.kind === "call" && action.args) {
|
|
157
158
|
for (const arg of action.args) {
|
|
158
159
|
const v = typeof arg === "object" ? arg?.value : arg;
|
|
160
|
+
const n = typeof arg === "object" ? arg?.name : undefined;
|
|
161
|
+
const t = typeof arg === "object" ? arg?.type : "string";
|
|
159
162
|
if (typeof v === "string" && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v) && v !== "") {
|
|
160
163
|
referenced.add(v);
|
|
161
164
|
}
|
|
165
|
+
const isEmptyV = (val) => val === "" || val === 0 || val === false || val === null || (Array.isArray(val) && val.length === 0);
|
|
166
|
+
const isDefault = isEmptyV(v);
|
|
167
|
+
if (isDefault && n) {
|
|
168
|
+
// Track type for conflict detection
|
|
169
|
+
if (!paramTypes.has(n))
|
|
170
|
+
paramTypes.set(n, []);
|
|
171
|
+
if (!paramTypes.get(n).includes(t))
|
|
172
|
+
paramTypes.get(n).push(t);
|
|
173
|
+
referenced.add(n);
|
|
174
|
+
}
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
|
-
|
|
177
|
+
// Recursive: scan nested actions in if/for
|
|
178
|
+
if (action.kind === "if") {
|
|
179
|
+
(action.thenActions || []).forEach(scanAction);
|
|
180
|
+
(action.elseActions || []).forEach(scanAction);
|
|
181
|
+
}
|
|
182
|
+
else if (action.kind === "for") {
|
|
183
|
+
if (action.variable)
|
|
184
|
+
declared.add(action.variable);
|
|
185
|
+
(action.bodyActions || []).forEach(scanAction);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
for (const action of actions)
|
|
189
|
+
scanAction(action);
|
|
165
190
|
// Inputs = referenced but not declared — add as typed parameters
|
|
166
|
-
const
|
|
191
|
+
const rawInputs = [...referenced].filter(v => !declared.has(v) && v !== "");
|
|
192
|
+
// Resolve type conflicts: rename duplicate params with different types
|
|
193
|
+
const conflictNames = [...paramTypes.entries()]
|
|
194
|
+
.filter(([_, types]) => new Set(types).size > 1)
|
|
195
|
+
.map(([name]) => name);
|
|
196
|
+
const _resolved = [];
|
|
197
|
+
const renamed = new Map(); // oldName → newName
|
|
198
|
+
for (const name of rawInputs) {
|
|
199
|
+
if (conflictNames.includes(name) && paramTypes.get(name).length > 1) {
|
|
200
|
+
const types = [...new Set(paramTypes.get(name))]; // unique types
|
|
201
|
+
for (const t of types) {
|
|
202
|
+
const suffix = t.replace(/\[\]$/, "").toLowerCase();
|
|
203
|
+
const newName = `${name}_${suffix}`;
|
|
204
|
+
_resolved.push(newName);
|
|
205
|
+
renamed.set(name + ":" + t, newName);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
_resolved.push(name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const uniqueInputs = [...new Set(_resolved)];
|
|
167
213
|
const inputTypes = new Map();
|
|
168
214
|
for (const action of actions) {
|
|
169
215
|
if (action.kind === "call" && action.args) {
|
|
170
216
|
for (const arg of action.args) {
|
|
171
217
|
const v = typeof arg === "object" ? arg?.value : arg;
|
|
218
|
+
const n = typeof arg === "object" ? arg?.name : undefined;
|
|
172
219
|
const t = typeof arg === "object" ? arg?.type : "string";
|
|
173
|
-
|
|
220
|
+
// Check for renamed param (type conflict resolution)
|
|
221
|
+
const renameKey = n + ":" + t;
|
|
222
|
+
const resolvedName = renamed.get(renameKey) || n || "";
|
|
223
|
+
// Variable reference
|
|
224
|
+
if (typeof v === "string" && uniqueInputs.includes(v) && t && t !== "any") {
|
|
174
225
|
inputTypes.set(v, t);
|
|
175
226
|
}
|
|
227
|
+
// Empty default: use resolved name for type-conflicted params
|
|
228
|
+
const isEmptyV = (val) => val === "" || val === 0 || val === false || val === null || (Array.isArray(val) && val.length === 0);
|
|
229
|
+
const isDefault = isEmptyV(v);
|
|
230
|
+
if (isDefault && n) {
|
|
231
|
+
const cleanType = (t || "string").replace(/\[\]$/, "");
|
|
232
|
+
const targetName = (resolvedName !== n) ? resolvedName : n;
|
|
233
|
+
if (uniqueInputs.includes(targetName) && !inputTypes.has(targetName)) {
|
|
234
|
+
inputTypes.set(targetName, cleanType);
|
|
235
|
+
}
|
|
236
|
+
else if (uniqueInputs.includes(n) && !inputTypes.has(n)) {
|
|
237
|
+
inputTypes.set(n, cleanType);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
176
240
|
}
|
|
177
241
|
}
|
|
178
242
|
}
|
|
179
|
-
|
|
243
|
+
// Parameter bloat threshold: refuse if too many params
|
|
244
|
+
const MAX_PARAMS = 10;
|
|
245
|
+
if (uniqueInputs.length > MAX_PARAMS) {
|
|
246
|
+
return `// REFINEMENT_NEEDED: ${uniqueInputs.length} params detected (max ${MAX_PARAMS}). Split into smaller functions or use an options object.`;
|
|
247
|
+
}
|
|
248
|
+
// Refinement: collect empty defaults for Planner feedback
|
|
249
|
+
const emptyDefaults = [];
|
|
250
|
+
for (let i = 0; i < actions.length; i++) {
|
|
251
|
+
const action = actions[i];
|
|
252
|
+
if (action.kind === "call" && action.args) {
|
|
253
|
+
for (const arg of action.args) {
|
|
254
|
+
const v = typeof arg === "object" ? arg?.value : arg;
|
|
255
|
+
const isEmptyV = (val) => val === "" || val === 0 || val === false || val === null || (Array.isArray(val) && val.length === 0);
|
|
256
|
+
const isDefault = isEmptyV(v);
|
|
257
|
+
if (isDefault && typeof arg === "object" && arg.name) {
|
|
258
|
+
emptyDefaults.push({
|
|
259
|
+
actionIndex: i,
|
|
260
|
+
function: action.function || "unknown",
|
|
261
|
+
param: arg.name,
|
|
262
|
+
type: (typeof arg === "object" ? arg.type : "string") || "string",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Store for Planner feedback (accessible via global)
|
|
269
|
+
if (emptyDefaults.length > 0) {
|
|
270
|
+
globalThis.__progmune_empty_defaults = emptyDefaults;
|
|
271
|
+
}
|
|
272
|
+
const paramList = uniqueInputs.map(v => `${v}: ${inputTypes.get(v) || "string"}`).join(", ");
|
|
180
273
|
code += `\nexport function main(${paramList}) {\n`;
|
|
181
274
|
const convert = (action, indent = " ") => {
|
|
182
275
|
if (!action || !action.kind)
|
|
@@ -192,11 +285,27 @@ function emitCode(actions, meta) {
|
|
|
192
285
|
const val = a?.value;
|
|
193
286
|
if (typeof val === "string" && declared.has(val))
|
|
194
287
|
return val;
|
|
195
|
-
// If value looks like a variable name (valid JS identifier), pass it through
|
|
196
288
|
if (typeof val === "string" && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(val) && val !== "") {
|
|
197
289
|
declared.add(val);
|
|
198
290
|
return val;
|
|
199
291
|
}
|
|
292
|
+
// Empty default value → use function's parameter name (input parameter)
|
|
293
|
+
const isEmptyVal = (v) => v === "" || v === 0 || v === false || v === null || (Array.isArray(v) && v.length === 0);
|
|
294
|
+
const isDefault = isEmptyVal(val);
|
|
295
|
+
if (isDefault && a?.name) {
|
|
296
|
+
// Check for renamed param (type conflict resolution)
|
|
297
|
+
const paramType = meta?.params?.[i]?.type || "string";
|
|
298
|
+
const renameKey = a.name + ":" + paramType;
|
|
299
|
+
const resolvedName = renamed.get(renameKey) || a.name;
|
|
300
|
+
if (uniqueInputs.includes(resolvedName)) {
|
|
301
|
+
declared.add(resolvedName);
|
|
302
|
+
return resolvedName;
|
|
303
|
+
}
|
|
304
|
+
else if (uniqueInputs.includes(a.name)) {
|
|
305
|
+
declared.add(a.name);
|
|
306
|
+
return a.name;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
200
309
|
const paramType = meta?.params?.[i]?.type || "any";
|
|
201
310
|
if (BASIC_TYPES.has(paramType)) {
|
|
202
311
|
if (paramType === "string" || paramType === "str")
|
|
@@ -286,7 +395,7 @@ function emitCode(actions, meta) {
|
|
|
286
395
|
}
|
|
287
396
|
code += "}\n";
|
|
288
397
|
// Call main with params if it has inputs, otherwise no-arg call
|
|
289
|
-
if (
|
|
398
|
+
if (uniqueInputs.length > 0) {
|
|
290
399
|
// Don't call — the user provides the arguments
|
|
291
400
|
// Just export the function for external use
|
|
292
401
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeHealthScore = computeHealthScore;
|
|
4
|
+
exports.formatHealthLevel = formatHealthLevel;
|
|
5
|
+
exports.countSessionLedgers = countSessionLedgers;
|
|
6
|
+
/** Compute overall immune health score from failure and antibody data.
|
|
7
|
+
* @requires FAILURE_GENOME @produces HEALTH_SCORE
|
|
8
|
+
* @tags health, score, immune
|
|
9
|
+
*/
|
|
10
|
+
function computeHealthScore(failureGenome, antibodyStats) {
|
|
11
|
+
const totalFailures = failureGenome?.totalFailures || 0;
|
|
12
|
+
const totalHits = antibodyStats?.totalHits || 0;
|
|
13
|
+
const base = 100;
|
|
14
|
+
const failurePenalty = Math.min(totalFailures * 2, 40);
|
|
15
|
+
const antibodyBonus = Math.min(totalHits * 3, 20);
|
|
16
|
+
return Math.max(0, Math.min(100, base - failurePenalty + antibodyBonus));
|
|
17
|
+
}
|
|
18
|
+
/** Format a health score as a status level.
|
|
19
|
+
* @requires HEALTH_SCORE @produces HEALTH_STATUS
|
|
20
|
+
* @tags health, format
|
|
21
|
+
*/
|
|
22
|
+
function formatHealthLevel(score) {
|
|
23
|
+
if (score >= 90)
|
|
24
|
+
return "Excellent";
|
|
25
|
+
if (score >= 70)
|
|
26
|
+
return "Good";
|
|
27
|
+
if (score >= 50)
|
|
28
|
+
return "Fair";
|
|
29
|
+
return "Poor";
|
|
30
|
+
}
|
|
31
|
+
/** Validate a ledger and return pass/fail counts.
|
|
32
|
+
* @requires SESSION_LIST @produces VALIDATION_COUNTS
|
|
33
|
+
* @tags ledger, validation, audit
|
|
34
|
+
*/
|
|
35
|
+
function countSessionLedgers(sessions) {
|
|
36
|
+
const withLedger = sessions.filter((s) => {
|
|
37
|
+
return s.attempts?.some((a) => a.transitions?.length > 0);
|
|
38
|
+
}).length;
|
|
39
|
+
return { total: sessions.length, withLedger };
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.countTotalTransitions = countTotalTransitions;
|
|
4
|
+
exports.formatTransitionCount = formatTransitionCount;
|
|
5
|
+
exports.hasViolations = hasViolations;
|
|
6
|
+
exports.countSessionsWithViolations = countSessionsWithViolations;
|
|
7
|
+
/** Count total transitions across all session ledgers.
|
|
8
|
+
* @requires SESSION_LIST @produces TRANSITION_COUNT
|
|
9
|
+
* @tags ledger, count, statistics
|
|
10
|
+
*/
|
|
11
|
+
function countTotalTransitions(sessions) {
|
|
12
|
+
let count = 0;
|
|
13
|
+
for (const s of sessions) {
|
|
14
|
+
for (const a of (s.attempts || [])) {
|
|
15
|
+
count += (a.transitions || []).length;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return count;
|
|
19
|
+
}
|
|
20
|
+
/** Format a transition count as a summary string.
|
|
21
|
+
* @requires TRANSITION_COUNT @produces FORMATTED_COUNT
|
|
22
|
+
* @tags ledger, format
|
|
23
|
+
*/
|
|
24
|
+
function formatTransitionCount(count) {
|
|
25
|
+
return `${count} total transitions across all sessions`;
|
|
26
|
+
}
|
|
27
|
+
/** Check if a session has any protocol violations in its attempts.
|
|
28
|
+
* @requires SESSION_DATA @produces VIOLATION_CHECK
|
|
29
|
+
* @tags ledger, validation
|
|
30
|
+
*/
|
|
31
|
+
function hasViolations(session) {
|
|
32
|
+
return (session.attempts || []).some((a) => (a.violations || []).length > 0);
|
|
33
|
+
}
|
|
34
|
+
/** Count sessions that have violations.
|
|
35
|
+
* @requires SESSION_LIST @produces VIOLATION_COUNT
|
|
36
|
+
* @tags ledger, validation, statistics
|
|
37
|
+
*/
|
|
38
|
+
function countSessionsWithViolations(sessions) {
|
|
39
|
+
return sessions.filter(s => hasViolations(s)).length;
|
|
40
|
+
}
|
package/dist/planner.js
CHANGED
|
@@ -45,6 +45,7 @@ const memory_layer_1 = require("./memory-layer");
|
|
|
45
45
|
const ssg_validator_1 = require("./ssg-validator");
|
|
46
46
|
const protocol_registry_1 = require("./protocol-registry");
|
|
47
47
|
const semantic_snapshot_1 = require("./semantic-snapshot");
|
|
48
|
+
const strategy_planner_1 = require("./strategy-planner");
|
|
48
49
|
const fs = __importStar(require("fs"));
|
|
49
50
|
function enrichActions(actions, ir) {
|
|
50
51
|
return actions.map(a => {
|
|
@@ -104,10 +105,12 @@ function buildCompactFuncList(funcs, allFuncs) {
|
|
|
104
105
|
return def ? `${p.name}: ${def}` : `${p.name}: ${p.type}`;
|
|
105
106
|
}).join(",");
|
|
106
107
|
let line = `${f.name}(${params})->${f.returnType || "any"}`;
|
|
107
|
-
// Add capability metadata
|
|
108
|
+
// Add capability metadata + score
|
|
108
109
|
const meta = [];
|
|
110
|
+
if (f.score && f.score > 0)
|
|
111
|
+
meta.push(`★${f.score.toFixed(1)}`);
|
|
109
112
|
if (f.purpose)
|
|
110
|
-
meta.push(f.purpose.slice(0,
|
|
113
|
+
meta.push(f.purpose.slice(0, 50));
|
|
111
114
|
if (f.produces && f.produces.length > 0)
|
|
112
115
|
meta.push(`→${f.produces.join(",")}`);
|
|
113
116
|
if (meta.length > 0)
|
|
@@ -115,17 +118,37 @@ function buildCompactFuncList(funcs, allFuncs) {
|
|
|
115
118
|
return line;
|
|
116
119
|
}).join("\n");
|
|
117
120
|
}
|
|
121
|
+
/** Semantic matching: check if two capability labels are related.
|
|
122
|
+
* Uses exact match, substring, and Jaccard similarity. */
|
|
123
|
+
function semanticMatch(a, b) {
|
|
124
|
+
if (a === b)
|
|
125
|
+
return true;
|
|
126
|
+
if (a.includes(b) || b.includes(a))
|
|
127
|
+
return true;
|
|
128
|
+
// Fuzzy: shared word prefix (e.g. FAILURE_LIST ↔ FAILURE_DATA)
|
|
129
|
+
const aWords = a.split("_");
|
|
130
|
+
const bWords = b.split("_");
|
|
131
|
+
const shared = aWords.filter(w => bWords.some(bw => bw.includes(w) || w.includes(bw)));
|
|
132
|
+
return shared.length >= 1 && aWords.length <= 3 && bWords.length <= 3;
|
|
133
|
+
}
|
|
118
134
|
/** Build capability chain hints from IR: producer→consumer relationships.
|
|
119
|
-
*
|
|
135
|
+
* Uses semantic matching for fuzzy capability linking. */
|
|
120
136
|
function buildChainHints(funcs) {
|
|
121
137
|
const chains = [];
|
|
138
|
+
const seen = new Set();
|
|
122
139
|
for (const f of funcs) {
|
|
123
140
|
if (!f.produces)
|
|
124
141
|
continue;
|
|
125
142
|
for (const p of f.produces) {
|
|
126
|
-
const consumers = funcs.filter((x) => x.
|
|
143
|
+
const consumers = funcs.filter((x) => x.name !== f.name &&
|
|
144
|
+
(x.requires || []).some((r) => semanticMatch(p, r)));
|
|
127
145
|
for (const c of consumers) {
|
|
128
|
-
|
|
146
|
+
const key = `${f.name}→${c.name}`;
|
|
147
|
+
if (seen.has(key))
|
|
148
|
+
continue;
|
|
149
|
+
seen.add(key);
|
|
150
|
+
const matchedReq = (c.requires || []).find((r) => semanticMatch(p, r));
|
|
151
|
+
chains.push(`${f.name}()→${c.name}() // ${p} ≈ ${matchedReq || "?"}`);
|
|
129
152
|
}
|
|
130
153
|
}
|
|
131
154
|
}
|
|
@@ -688,17 +711,33 @@ async function plan(userIntent) {
|
|
|
688
711
|
score += 0.3;
|
|
689
712
|
}
|
|
690
713
|
}
|
|
691
|
-
// Capability Graph: requires/produces
|
|
714
|
+
// Capability Graph: semantic requires/produces matching
|
|
692
715
|
if (f.produces) {
|
|
693
716
|
for (const p of f.produces) {
|
|
694
|
-
|
|
717
|
+
const pText = p.toLowerCase().replace(/_/g, " ");
|
|
718
|
+
// Exact match
|
|
719
|
+
if (intentLower.includes(pText)) {
|
|
695
720
|
score += 1.5;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
// Semantic: word overlap
|
|
724
|
+
const pWords = pText.split(/\s+/);
|
|
725
|
+
const matchCount = pWords.filter((w) => intentLower.includes(w)).length;
|
|
726
|
+
if (matchCount > 0)
|
|
727
|
+
score += matchCount * 0.5;
|
|
696
728
|
}
|
|
697
729
|
}
|
|
698
730
|
if (f.requires) {
|
|
699
731
|
for (const r of f.requires) {
|
|
700
|
-
|
|
732
|
+
const rText = r.toLowerCase().replace(/_/g, " ");
|
|
733
|
+
if (intentLower.includes(rText)) {
|
|
701
734
|
score += 0.5;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const rWords = rText.split(/\s+/);
|
|
738
|
+
const matchCount = rWords.filter((w) => intentLower.includes(w)).length;
|
|
739
|
+
if (matchCount > 0)
|
|
740
|
+
score += matchCount * 0.2;
|
|
702
741
|
}
|
|
703
742
|
}
|
|
704
743
|
// Capability Graph: tag match
|
|
@@ -712,7 +751,23 @@ async function plan(userIntent) {
|
|
|
712
751
|
});
|
|
713
752
|
scored.sort((a, b) => b.score - a.score);
|
|
714
753
|
const topFuncs = scored.slice(0, 15);
|
|
715
|
-
|
|
754
|
+
// Strategy Layer: select capability chain (local, 0 LLM calls)
|
|
755
|
+
const chains = (0, strategy_planner_1.selectCapabilityChains)(userIntent, ir, 3);
|
|
756
|
+
const strategyHint = (0, strategy_planner_1.formatChainHint)(chains);
|
|
757
|
+
// Action Layer: filter functions to those in selected chains
|
|
758
|
+
let chainFuncs = topFuncs;
|
|
759
|
+
if (chains.length > 0) {
|
|
760
|
+
const chainNames = new Set();
|
|
761
|
+
for (const c of chains.slice(0, 2)) {
|
|
762
|
+
for (const n of c.nodes)
|
|
763
|
+
chainNames.add(n.name);
|
|
764
|
+
}
|
|
765
|
+
// Prioritize chain functions: keep them first, add others as fallback
|
|
766
|
+
const inChain = topFuncs.filter((f) => chainNames.has(f.name));
|
|
767
|
+
const outChain = topFuncs.filter((f) => !chainNames.has(f.name));
|
|
768
|
+
chainFuncs = [...inChain, ...outChain].slice(0, 15);
|
|
769
|
+
}
|
|
770
|
+
const compactFuncList = buildCompactFuncList(chainFuncs, ir);
|
|
716
771
|
const chainHints = buildChainHints(topFuncs);
|
|
717
772
|
// Known string-enum types: tell LLM these are strings, not objects
|
|
718
773
|
const STRING_ENUMS = {
|
|
@@ -734,7 +789,7 @@ async function plan(userIntent) {
|
|
|
734
789
|
}
|
|
735
790
|
const protocolChainHint = buildProtocolChainHint(protocols);
|
|
736
791
|
const userPrompt = `可用函数:
|
|
737
|
-
${compactFuncList}${protocolChainHint}${chainHints}${typeHints}
|
|
792
|
+
${compactFuncList}${protocolChainHint}${chainHints}${typeHints}${strategyHint}
|
|
738
793
|
|
|
739
794
|
需求:${userIntent}${antibodyHint}
|
|
740
795
|
|
|
@@ -874,20 +929,44 @@ ${RETRY_HINT}
|
|
|
874
929
|
const enriched = enrichActions(rawActions, ir);
|
|
875
930
|
const filtered = enriched.filter(a => !forbiddenFuncs.includes(a.kind === "call" ? a.function : ''));
|
|
876
931
|
let ssgTransitions = [];
|
|
932
|
+
// 0) Hard Constraint Pre-check: local scan before full validation
|
|
933
|
+
// Saves LLM tokens by detecting obvious violations first
|
|
934
|
+
const preCheckRules = new Map();
|
|
935
|
+
for (const p of protocols)
|
|
936
|
+
preCheckRules.set(p.function, p.protocol);
|
|
937
|
+
const preCheckErrors = [];
|
|
938
|
+
for (let ai = 0; ai < filtered.length; ai++) {
|
|
939
|
+
const a = filtered[ai];
|
|
940
|
+
if (a.kind !== "call" || !a.function)
|
|
941
|
+
continue;
|
|
942
|
+
const def = ir.find((f) => f.name === a.function);
|
|
943
|
+
if (!def) {
|
|
944
|
+
preCheckErrors.push(`${a.function}: 函数不存在于 IR`);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (def.params && a.args && a.args.length !== def.params.length) {
|
|
948
|
+
preCheckErrors.push(`${a.function}: 参数数量错误 (期望${def.params.length}, 实际${a.args.length})`);
|
|
949
|
+
}
|
|
950
|
+
// Protocol pre-check: verify function is callable in current namespace state
|
|
951
|
+
if (def.protocol && preCheckRules.size > 0) {
|
|
952
|
+
const ctx = { ledger: [], currentState: (0, ssg_validator_1.rebuildState)([], namespaceInitialStates) };
|
|
953
|
+
const { valid, rejection } = (0, ssg_validator_1.validateTransition)(ctx, a.function, ai, preCheckRules, namespaceInitialStates);
|
|
954
|
+
if (!valid && rejection) {
|
|
955
|
+
preCheckErrors.push(`${a.function}: 协议违规 — 需要先调用 ${rejection.fixPath?.join(" → ") || "?"}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
877
959
|
// 1) 基础序列校验
|
|
878
960
|
const seqResult = (0, validator_1.validateActionSequence)(filtered);
|
|
879
|
-
if (!seqResult.valid) {
|
|
880
|
-
const errorsFlat = seqResult.errors.flat();
|
|
961
|
+
if (!seqResult.valid || preCheckErrors.length > 0) {
|
|
962
|
+
const errorsFlat = [...preCheckErrors, ...seqResult.errors.flat()];
|
|
881
963
|
console.error("⚠️ 序列校验失败:", errorsFlat.join(", "));
|
|
882
964
|
// Use structured violations directly from validator
|
|
883
965
|
const violations = seqResult.violations.length > 0
|
|
884
966
|
? seqResult.violations
|
|
885
|
-
:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
actionIndex: 0,
|
|
889
|
-
description: errorsFlat.join("; "),
|
|
890
|
-
}];
|
|
967
|
+
: preCheckErrors.length > 0
|
|
968
|
+
? [{ svl: 1, violatedConstraint: "symbol_existence", actionIndex: 0, description: preCheckErrors.join("; ") }]
|
|
969
|
+
: [{ svl: 1, violatedConstraint: "symbol_existence", actionIndex: 0, description: errorsFlat.join("; ") }];
|
|
891
970
|
const primarySvl = `SVL-${violations[0].svl}`;
|
|
892
971
|
const attempt = {
|
|
893
972
|
id: (0, runtime_types_1.generateAttemptId)(),
|
|
@@ -918,7 +997,11 @@ ${RETRY_HINT}
|
|
|
918
997
|
plannerRetryTotal: maxRetries,
|
|
919
998
|
});
|
|
920
999
|
(0, memory_layer_1.recordEpisode)({ intent: userIntent, actions: filtered, success: false, svlViolated: primarySvl });
|
|
921
|
-
|
|
1000
|
+
// Build targeted retry prompt based on pre-check results
|
|
1001
|
+
const specificErrors = preCheckErrors.length > 0
|
|
1002
|
+
? `精确错误:\n${preCheckErrors.map(e => ` - ${e}`).join("\n")}`
|
|
1003
|
+
: `错误:${errorsFlat.join(";")}`;
|
|
1004
|
+
currentPrompt = `可用函数:\n${compactFuncList}${protocolChainHint}\n\n需求:${userIntent}\n\n${specificErrors}\n请修正上述问题。\n${RETRY_HINT}\n只输出 JSON。`;
|
|
922
1005
|
useSystem = false;
|
|
923
1006
|
(0, failure_corpus_1.saveCheckpoint)(userIntent, { attemptIndex: r + 1, sessionAttempts: session.attempts, currentPrompt, useSystem });
|
|
924
1007
|
continue;
|
|
@@ -1066,6 +1149,47 @@ ${RETRY_HINT}
|
|
|
1066
1149
|
(0, failure_corpus_1.saveCheckpoint)(userIntent, { attemptIndex: r + 1, sessionAttempts: session.attempts, currentPrompt, useSystem });
|
|
1067
1150
|
continue;
|
|
1068
1151
|
}
|
|
1152
|
+
// Phase 8: Refinement — detect empty args, ask LLM to fill meaningful values
|
|
1153
|
+
const emptyArgs = [];
|
|
1154
|
+
for (let ai = 0; ai < filtered.length; ai++) {
|
|
1155
|
+
const a = filtered[ai];
|
|
1156
|
+
if (a.kind === "call" && a.args) {
|
|
1157
|
+
for (const arg of a.args) {
|
|
1158
|
+
const v = typeof arg === "object" ? arg.value : arg;
|
|
1159
|
+
const isEmpty = (val) => val === "" || val === 0 || val === false || val === null || (Array.isArray(val) && val.length === 0);
|
|
1160
|
+
if (isEmpty(v)) {
|
|
1161
|
+
emptyArgs.push({ idx: ai, fn: a.function || "?", param: arg.name || "?", type: arg.type || "?" });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (emptyArgs.length > 0 && r < maxRetries - 1) {
|
|
1167
|
+
const argDetails = emptyArgs.map(e => ` ${e.fn}() 参数 "${e.param}" (${e.type}) 是空值`).join("\n");
|
|
1168
|
+
console.error(`🔍 检测到 ${emptyArgs.length} 个空参数,启动精炼...`);
|
|
1169
|
+
currentPrompt = `可用函数:\n${compactFuncList}${protocolChainHint}\n\n需求:${userIntent}\n\n上一次生成的 JSON 中以下参数为空值:\n${argDetails}\n\n请为这些参数填入有意义的示例值(字符串用描述性值,数字用合理数值,对象用 {} as Type)。\n${RETRY_HINT}\n只输出 JSON。`;
|
|
1170
|
+
useSystem = false;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
// 4D Scoring Self-Verification: compare LLM choices against scores
|
|
1174
|
+
const chosenFuncs = filtered.filter(a => a.kind === "call").map(a => a.function);
|
|
1175
|
+
const scoredFuncs = new Map(topFuncs.map((f) => [f.name, f.score || 0]));
|
|
1176
|
+
let scoreViolations = 0;
|
|
1177
|
+
for (const fn of chosenFuncs) {
|
|
1178
|
+
const actualScore = scoredFuncs.get(fn) || 0;
|
|
1179
|
+
const allScores = [...scoredFuncs.values()].filter((s) => typeof s === "number" && s > 0);
|
|
1180
|
+
const maxScore = allScores.length > 0 ? Math.max(...allScores) : 0;
|
|
1181
|
+
if (maxScore > 0 && actualScore < maxScore * 0.3 && maxScore > 1) {
|
|
1182
|
+
scoreViolations++;
|
|
1183
|
+
const better = [...scoredFuncs.entries()]
|
|
1184
|
+
.filter(([_, s]) => typeof s === "number" && s > actualScore * 2)
|
|
1185
|
+
.sort((a, b) => b[1] - a[1])
|
|
1186
|
+
.slice(0, 3).map(([n, s]) => `${n}(${s.toFixed(1)})`).join(", ");
|
|
1187
|
+
console.error(`⚠️ 评分偏低: ${fn}(${actualScore.toFixed(1)}) 被选中, 但更高分函数可用: ${better}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (scoreViolations > 0) {
|
|
1191
|
+
console.error(`📊 评分自省: ${scoreViolations}/${chosenFuncs.length} 个函数评分偏低 (阈值: 最高分30%)`);
|
|
1192
|
+
}
|
|
1069
1193
|
// 校验通过:构建成功 Attempt
|
|
1070
1194
|
const successAntibodyHit = antibodyHint
|
|
1071
1195
|
? {
|
package/dist/stdlib.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Progmune Standard Library — general-purpose utilities for external tasks.
|
|
4
|
+
* Each function has @requires/@produces for Capability Graph integration.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.isValidEmail = isValidEmail;
|
|
8
|
+
exports.truncate = truncate;
|
|
9
|
+
exports.camelToSnake = camelToSnake;
|
|
10
|
+
exports.capitalizeWords = capitalizeWords;
|
|
11
|
+
exports.countSubstring = countSubstring;
|
|
12
|
+
exports.removeWhitespace = removeWhitespace;
|
|
13
|
+
exports.mostFrequent = mostFrequent;
|
|
14
|
+
exports.unique = unique;
|
|
15
|
+
exports.chunk = chunk;
|
|
16
|
+
exports.arrayDiff = arrayDiff;
|
|
17
|
+
exports.average = average;
|
|
18
|
+
exports.median = median;
|
|
19
|
+
exports.roundTo = roundTo;
|
|
20
|
+
exports.isPrime = isPrime;
|
|
21
|
+
exports.randomInt = randomInt;
|
|
22
|
+
exports.deepClone = deepClone;
|
|
23
|
+
exports.pick = pick;
|
|
24
|
+
exports.deepMerge = deepMerge;
|
|
25
|
+
exports.hasRequiredFields = hasRequiredFields;
|
|
26
|
+
exports.isPlainObject = isPlainObject;
|
|
27
|
+
exports.parseSemver = parseSemver;
|
|
28
|
+
exports.formatDuration = formatDuration;
|
|
29
|
+
exports.formatFileSize = formatFileSize;
|
|
30
|
+
exports.toQueryString = toQueryString;
|
|
31
|
+
exports.retry = retry;
|
|
32
|
+
exports.debounce = debounce;
|
|
33
|
+
// ── String ──
|
|
34
|
+
/** @requires STRING @produces VALIDATION_RESULT @tags string, email, validation */
|
|
35
|
+
function isValidEmail(str) {
|
|
36
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
|
|
37
|
+
}
|
|
38
|
+
/** @requires STRING @produces TRUNCATED_STRING @tags string, format */
|
|
39
|
+
function truncate(str, maxLen, ellipsis = "...") {
|
|
40
|
+
return str.length <= maxLen ? str : str.slice(0, maxLen - ellipsis.length) + ellipsis;
|
|
41
|
+
}
|
|
42
|
+
/** @requires STRING @produces FORMATTED_STRING @tags string, case, format */
|
|
43
|
+
function camelToSnake(str) {
|
|
44
|
+
return str.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
|
|
45
|
+
}
|
|
46
|
+
/** @requires STRING @produces FORMATTED_STRING @tags string, case, format */
|
|
47
|
+
function capitalizeWords(str) {
|
|
48
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
49
|
+
}
|
|
50
|
+
/** @requires STRING @produces COUNT @tags string, count */
|
|
51
|
+
function countSubstring(str, sub) {
|
|
52
|
+
if (!sub)
|
|
53
|
+
return 0;
|
|
54
|
+
let count = 0, pos = 0;
|
|
55
|
+
while ((pos = str.indexOf(sub, pos)) !== -1) {
|
|
56
|
+
count++;
|
|
57
|
+
pos += sub.length;
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
/** @requires STRING @produces CLEANED_STRING @tags string, format */
|
|
62
|
+
function removeWhitespace(str) {
|
|
63
|
+
return str.replace(/\s+/g, "");
|
|
64
|
+
}
|
|
65
|
+
// ── Array ──
|
|
66
|
+
/** @requires ARRAY @produces ELEMENT @tags array, statistics */
|
|
67
|
+
function mostFrequent(arr) {
|
|
68
|
+
if (arr.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const counts = new Map();
|
|
71
|
+
for (const item of arr)
|
|
72
|
+
counts.set(item, (counts.get(item) || 0) + 1);
|
|
73
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
74
|
+
}
|
|
75
|
+
/** @requires ARRAY @produces ARRAY @tags array, dedupe */
|
|
76
|
+
function unique(arr) { return [...new Set(arr)]; }
|
|
77
|
+
/** @requires ARRAY @produces ARRAY @tags array, chunk */
|
|
78
|
+
function chunk(arr, size) {
|
|
79
|
+
const result = [];
|
|
80
|
+
for (let i = 0; i < arr.length; i += size)
|
|
81
|
+
result.push(arr.slice(i, i + size));
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/** @requires ARRAY @produces ARRAY @tags array, difference */
|
|
85
|
+
function arrayDiff(a, b) {
|
|
86
|
+
const setB = new Set(b);
|
|
87
|
+
return a.filter(x => !setB.has(x));
|
|
88
|
+
}
|
|
89
|
+
// ── Math ──
|
|
90
|
+
/** @requires NUMBERS @produces AVERAGE @tags math, statistics */
|
|
91
|
+
function average(nums) {
|
|
92
|
+
return nums.length === 0 ? 0 : nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
93
|
+
}
|
|
94
|
+
/** @requires NUMBERS @produces MEDIAN @tags math, statistics */
|
|
95
|
+
function median(nums) {
|
|
96
|
+
if (nums.length === 0)
|
|
97
|
+
return 0;
|
|
98
|
+
const sorted = [...nums].sort((a, b) => a - b);
|
|
99
|
+
const mid = Math.floor(sorted.length / 2);
|
|
100
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
101
|
+
}
|
|
102
|
+
/** @requires NUMBER @produces ROUNDED_NUMBER @tags math, format */
|
|
103
|
+
function roundTo(num, decimals) {
|
|
104
|
+
const factor = Math.pow(10, decimals);
|
|
105
|
+
return Math.round(num * factor) / factor;
|
|
106
|
+
}
|
|
107
|
+
/** @requires NUMBER @produces PRIME_CHECK @tags math, validation */
|
|
108
|
+
function isPrime(n) {
|
|
109
|
+
if (n < 2)
|
|
110
|
+
return false;
|
|
111
|
+
for (let i = 2; i <= Math.sqrt(n); i++)
|
|
112
|
+
if (n % i === 0)
|
|
113
|
+
return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/** @requires NUMBERS @produces RANDOM_INT @tags math, random */
|
|
117
|
+
function randomInt(min, max) {
|
|
118
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
119
|
+
}
|
|
120
|
+
// ── Object ──
|
|
121
|
+
/** @requires OBJECT @produces CLONED_OBJECT @tags object, clone */
|
|
122
|
+
function deepClone(obj) {
|
|
123
|
+
return JSON.parse(JSON.stringify(obj));
|
|
124
|
+
}
|
|
125
|
+
/** @requires OBJECT @produces PICKED_OBJECT @tags object, filter */
|
|
126
|
+
function pick(obj, keys) {
|
|
127
|
+
const result = {};
|
|
128
|
+
for (const k of keys)
|
|
129
|
+
if (k in obj)
|
|
130
|
+
result[k] = obj[k];
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/** @requires OBJECTS @produces MERGED_OBJECT @tags object, merge */
|
|
134
|
+
function deepMerge(...objects) {
|
|
135
|
+
return objects.reduce((acc, obj) => {
|
|
136
|
+
for (const key of Object.keys(obj || {})) {
|
|
137
|
+
acc[key] = typeof obj[key] === "object" && !Array.isArray(obj[key])
|
|
138
|
+
? deepMerge(acc[key] || {}, obj[key]) : obj[key];
|
|
139
|
+
}
|
|
140
|
+
return acc;
|
|
141
|
+
}, {});
|
|
142
|
+
}
|
|
143
|
+
// ── Validation ──
|
|
144
|
+
/** @requires OBJECT @produces VALIDATION_RESULT @tags validation, schema */
|
|
145
|
+
function hasRequiredFields(obj, fields) {
|
|
146
|
+
return fields.every(f => obj && obj[f] !== undefined && obj[f] !== null);
|
|
147
|
+
}
|
|
148
|
+
/** @requires ANY @produces VALIDATION_RESULT @tags validation, type */
|
|
149
|
+
function isPlainObject(val) {
|
|
150
|
+
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
151
|
+
}
|
|
152
|
+
/** @requires STRING @produces PARSED_VERSION @tags validation, semver */
|
|
153
|
+
function parseSemver(version) {
|
|
154
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
155
|
+
if (!match)
|
|
156
|
+
return null;
|
|
157
|
+
return { major: +match[1], minor: +match[2], patch: +match[3] };
|
|
158
|
+
}
|
|
159
|
+
// ── Formatting ──
|
|
160
|
+
/** @requires NUMBER @produces FORMATTED_STRING @tags format, duration */
|
|
161
|
+
function formatDuration(ms) {
|
|
162
|
+
if (ms < 1000)
|
|
163
|
+
return `${ms}ms`;
|
|
164
|
+
if (ms < 60000)
|
|
165
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
166
|
+
const mins = Math.floor(ms / 60000);
|
|
167
|
+
const secs = Math.round((ms % 60000) / 1000);
|
|
168
|
+
return `${mins}m ${secs}s`;
|
|
169
|
+
}
|
|
170
|
+
/** @requires NUMBER @produces FORMATTED_STRING @tags format, file */
|
|
171
|
+
function formatFileSize(bytes) {
|
|
172
|
+
if (bytes < 1024)
|
|
173
|
+
return `${bytes}B`;
|
|
174
|
+
if (bytes < 1048576)
|
|
175
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
176
|
+
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
177
|
+
}
|
|
178
|
+
/** @requires OBJECT @produces QUERY_STRING @tags web, format */
|
|
179
|
+
function toQueryString(obj) {
|
|
180
|
+
return Object.entries(obj)
|
|
181
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
182
|
+
.join("&");
|
|
183
|
+
}
|
|
184
|
+
// ── Async ──
|
|
185
|
+
/** @requires FUNCTION @produces RETRY_RESULT @tags async, retry */
|
|
186
|
+
async function retry(fn, maxRetries = 3) {
|
|
187
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
188
|
+
try {
|
|
189
|
+
return await fn();
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (i === maxRetries - 1)
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
throw new Error("unreachable");
|
|
197
|
+
}
|
|
198
|
+
/** @requires FUNCTION @produces DEBOUNCED_FUNCTION @tags async, debounce */
|
|
199
|
+
function debounce(fn, delay) {
|
|
200
|
+
let timer;
|
|
201
|
+
return ((...args) => {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 8: Multi-Level Planning — Strategy Layer
|
|
4
|
+
*
|
|
5
|
+
* Selects capability chains from the IR graph WITHOUT calling LLM.
|
|
6
|
+
* Pure graph search: intent → capability matching → ordered chain.
|
|
7
|
+
*
|
|
8
|
+
* The Action Layer (planner.ts) then uses this chain to constrain
|
|
9
|
+
* the LLM's function selection space.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.selectCapabilityChains = selectCapabilityChains;
|
|
13
|
+
exports.formatChainHint = formatChainHint;
|
|
14
|
+
const utils_1 = require("./utils");
|
|
15
|
+
/** Build a capability graph from IR functions. */
|
|
16
|
+
function buildCapabilityGraph(ir) {
|
|
17
|
+
const graph = new Map();
|
|
18
|
+
for (const f of ir) {
|
|
19
|
+
if (!f.exported)
|
|
20
|
+
continue;
|
|
21
|
+
graph.set(f.name, {
|
|
22
|
+
name: f.name,
|
|
23
|
+
purpose: f.purpose || "",
|
|
24
|
+
tags: f.tags || [],
|
|
25
|
+
requires: f.requires || [],
|
|
26
|
+
produces: f.produces || [],
|
|
27
|
+
score: 0,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return graph;
|
|
31
|
+
}
|
|
32
|
+
/** Score a capability node against an intent. */
|
|
33
|
+
function scoreNode(node, intentLower, keywords) {
|
|
34
|
+
let score = 0;
|
|
35
|
+
// Name match
|
|
36
|
+
for (const kw of keywords) {
|
|
37
|
+
if (node.name.toLowerCase().includes(kw))
|
|
38
|
+
score += 1;
|
|
39
|
+
score += (0, utils_1.jaccardSimilarity)(node.name.toLowerCase(), kw);
|
|
40
|
+
}
|
|
41
|
+
// Purpose match
|
|
42
|
+
const purposeLower = node.purpose.toLowerCase();
|
|
43
|
+
for (const kw of keywords) {
|
|
44
|
+
if (purposeLower.includes(kw))
|
|
45
|
+
score += 2;
|
|
46
|
+
}
|
|
47
|
+
// Tag match
|
|
48
|
+
for (const tag of node.tags) {
|
|
49
|
+
if (intentLower.includes(tag.toLowerCase()))
|
|
50
|
+
score += 1.5;
|
|
51
|
+
}
|
|
52
|
+
// Semantic word overlap in purpose
|
|
53
|
+
const intentWords = intentLower.split(/[\s,,]+/);
|
|
54
|
+
for (const w of intentWords) {
|
|
55
|
+
if (w.length > 2 && purposeLower.includes(w))
|
|
56
|
+
score += 0.5;
|
|
57
|
+
}
|
|
58
|
+
return score;
|
|
59
|
+
}
|
|
60
|
+
/** Find all capability nodes that produce a given capability label. */
|
|
61
|
+
function findProducers(graph, capability) {
|
|
62
|
+
const producers = [];
|
|
63
|
+
for (const node of graph.values()) {
|
|
64
|
+
if (node.produces.some(p => p === capability || capability.includes(p) || p.includes(capability))) {
|
|
65
|
+
producers.push(node);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return producers;
|
|
69
|
+
}
|
|
70
|
+
/** Find all capability nodes that require a given capability label. */
|
|
71
|
+
function findConsumers(graph, capability) {
|
|
72
|
+
const consumers = [];
|
|
73
|
+
for (const node of graph.values()) {
|
|
74
|
+
if (node.requires.some(r => r === capability || capability.includes(r) || r.includes(capability))) {
|
|
75
|
+
consumers.push(node);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return consumers;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Select the best capability chain for an intent.
|
|
82
|
+
*
|
|
83
|
+
* Algorithm:
|
|
84
|
+
* 1. Score all nodes against intent
|
|
85
|
+
* 2. Find top producers (nodes whose produces matches intent keywords)
|
|
86
|
+
* 3. For each producer, trace forward: producer → consumer → consumer...
|
|
87
|
+
* 4. Score each chain and return top N
|
|
88
|
+
*/
|
|
89
|
+
function selectCapabilityChains(intent, ir, maxChains = 5) {
|
|
90
|
+
const intentLower = intent.toLowerCase();
|
|
91
|
+
const keywords = (0, utils_1.extractKeywords)(intent);
|
|
92
|
+
const graph = buildCapabilityGraph(ir);
|
|
93
|
+
// Score all nodes
|
|
94
|
+
for (const node of graph.values()) {
|
|
95
|
+
node.score = scoreNode(node, intentLower, keywords);
|
|
96
|
+
}
|
|
97
|
+
// Find seed nodes: highest-scoring nodes that produce something
|
|
98
|
+
const seeds = [...graph.values()]
|
|
99
|
+
.filter(n => n.produces.length > 0 && n.score > 0)
|
|
100
|
+
.sort((a, b) => b.score - a.score)
|
|
101
|
+
.slice(0, 10);
|
|
102
|
+
const chains = [];
|
|
103
|
+
for (const seed of seeds) {
|
|
104
|
+
// Build chain: seed → consumer → consumer...
|
|
105
|
+
const chain = [seed];
|
|
106
|
+
const visited = new Set([seed.name]);
|
|
107
|
+
let totalScore = seed.score;
|
|
108
|
+
// Forward trace: for each produce of the last node, find consumers
|
|
109
|
+
let current = seed;
|
|
110
|
+
let extended = true;
|
|
111
|
+
while (extended && chain.length < 8) {
|
|
112
|
+
extended = false;
|
|
113
|
+
for (const p of current.produces) {
|
|
114
|
+
const consumers = findConsumers(graph, p).filter(c => !visited.has(c.name));
|
|
115
|
+
if (consumers.length > 0) {
|
|
116
|
+
// Pick best-scoring consumer
|
|
117
|
+
const bestConsumer = consumers.sort((a, b) => b.score - a.score)[0];
|
|
118
|
+
chain.push(bestConsumer);
|
|
119
|
+
visited.add(bestConsumer.name);
|
|
120
|
+
totalScore += bestConsumer.score;
|
|
121
|
+
current = bestConsumer;
|
|
122
|
+
extended = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Backward trace: does seed need something? Find producers.
|
|
128
|
+
if (seed.requires.length > 0) {
|
|
129
|
+
const producers = findProducers(graph, seed.requires[0])
|
|
130
|
+
.filter(p => !visited.has(p.name));
|
|
131
|
+
if (producers.length > 0) {
|
|
132
|
+
const bestProducer = producers.sort((a, b) => b.score - a.score)[0];
|
|
133
|
+
chain.unshift(bestProducer);
|
|
134
|
+
totalScore += bestProducer.score;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (chain.length >= 1) {
|
|
138
|
+
chains.push({
|
|
139
|
+
nodes: chain,
|
|
140
|
+
score: totalScore / chain.length, // average score
|
|
141
|
+
explanation: chain.map(n => n.name).join(" → "),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Sort by score, deduplicate, return top N
|
|
146
|
+
chains.sort((a, b) => b.score - a.score);
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
const unique = [];
|
|
149
|
+
for (const c of chains) {
|
|
150
|
+
const key = c.explanation;
|
|
151
|
+
if (!seen.has(key)) {
|
|
152
|
+
seen.add(key);
|
|
153
|
+
unique.push(c);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return unique.slice(0, maxChains);
|
|
157
|
+
}
|
|
158
|
+
/** Format chains as a hint for the LLM prompt. */
|
|
159
|
+
function formatChainHint(chains) {
|
|
160
|
+
if (chains.length === 0)
|
|
161
|
+
return "";
|
|
162
|
+
const lines = ["\n推荐能力链 (Strategy Layer):"];
|
|
163
|
+
for (let i = 0; i < Math.min(chains.length, 3); i++) {
|
|
164
|
+
const c = chains[i];
|
|
165
|
+
lines.push(` ${i + 1}. ${c.explanation} (★${c.score.toFixed(1)})`);
|
|
166
|
+
}
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|