progmune-runtime 2.1.2 → 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 CHANGED
@@ -143,33 +143,73 @@ 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
- // First pass: collect declared and referenced variables
148
- for (const action of actions) {
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 AND empty-default params
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
  }
162
- // Also detect empty defaults: these should become function params
163
- const isDefault = v === "" || v === 0 || v === false || v === null
164
- || (Array.isArray(v) && v.length === 0);
165
- if (isDefault && typeof arg === "object" && arg.name) {
166
- referenced.add(arg.name);
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);
167
174
  }
168
175
  }
169
176
  }
170
- }
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);
171
190
  // Inputs = referenced but not declared — add as typed parameters
172
- const inputs = [...referenced].filter(v => !declared.has(v) && v !== "");
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)];
173
213
  const inputTypes = new Map();
174
214
  for (const action of actions) {
175
215
  if (action.kind === "call" && action.args) {
@@ -177,42 +217,59 @@ function emitCode(actions, meta) {
177
217
  const v = typeof arg === "object" ? arg?.value : arg;
178
218
  const n = typeof arg === "object" ? arg?.name : undefined;
179
219
  const t = typeof arg === "object" ? arg?.type : "string";
180
- // Variable reference: value matches input name
181
- if (typeof v === "string" && inputs.includes(v) && t && t !== "any") {
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") {
182
225
  inputTypes.set(v, t);
183
226
  }
184
- // Empty default: param name matches input name
185
- const isDefault = v === "" || v === 0 || v === false || v === null
186
- || (Array.isArray(v) && v.length === 0);
187
- if (isDefault && n && inputs.includes(n)) {
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) {
188
231
  const cleanType = (t || "string").replace(/\[\]$/, "");
189
- if (!inputTypes.has(n))
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)) {
190
237
  inputTypes.set(n, cleanType);
238
+ }
191
239
  }
192
240
  }
193
241
  }
194
242
  }
195
- // If no inputs detected but functions have params with empty defaults,
196
- // create parameters from function signatures
197
- if (inputs.length === 0) {
198
- for (const action of actions) {
199
- if (action.kind === "call" && action.args && action.args.length > 0) {
200
- for (const arg of action.args) {
201
- const name = typeof arg === "object" ? arg.name : "param";
202
- const type = typeof arg === "object" ? (arg.type || "string") : "string";
203
- const val = typeof arg === "object" ? arg.value : arg;
204
- // Only add if value is empty default (not a real value)
205
- if (val === "" || val === 0 || val === false || val === null || (Array.isArray(val) && val.length === 0)) {
206
- if (!inputTypes.has(name)) {
207
- inputs.push(name);
208
- inputTypes.set(name, type.replace(/\[\]$/, ""));
209
- }
210
- }
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
+ });
211
264
  }
212
265
  }
213
266
  }
214
267
  }
215
- const paramList = inputs.map(v => `${v}: ${inputTypes.get(v) || "string"}`).join(", ");
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(", ");
216
273
  code += `\nexport function main(${paramList}) {\n`;
217
274
  const convert = (action, indent = " ") => {
218
275
  if (!action || !action.kind)
@@ -233,11 +290,21 @@ function emitCode(actions, meta) {
233
290
  return val;
234
291
  }
235
292
  // Empty default value → use function's parameter name (input parameter)
236
- const isDefault = val === "" || val === 0 || val === false || val === null
237
- || (Array.isArray(val) && val.length === 0);
238
- if (isDefault && a?.name && inputs.includes(a.name)) {
239
- declared.add(a.name);
240
- return a.name;
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
+ }
241
308
  }
242
309
  const paramType = meta?.params?.[i]?.type || "any";
243
310
  if (BASIC_TYPES.has(paramType)) {
@@ -328,7 +395,7 @@ function emitCode(actions, meta) {
328
395
  }
329
396
  code += "}\n";
330
397
  // Call main with params if it has inputs, otherwise no-arg call
331
- if (inputs.length > 0) {
398
+ if (uniqueInputs.length > 0) {
332
399
  // Don't call — the user provides the arguments
333
400
  // Just export the function for external use
334
401
  }
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, 60));
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
- * e.g. "failureStats formatFailureStats (FAILURE_STATS)" */
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.requires?.includes(p) && x.name !== f.name);
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
- chains.push(`${f.name}()→${c.name}() // ${p}`);
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 capability matching
714
+ // Capability Graph: semantic requires/produces matching
692
715
  if (f.produces) {
693
716
  for (const p of f.produces) {
694
- if (intentLower.includes(p.toLowerCase().replace(/_/g, " ")))
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
- if (intentLower.includes(r.toLowerCase().replace(/_/g, " ")))
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
- const compactFuncList = buildCompactFuncList(topFuncs, ir);
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
- svl: 1,
887
- violatedConstraint: "symbol_existence",
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
- currentPrompt = `可用函数:\n${compactFuncList}${protocolChainHint}\n\n需求:${userIntent}\n\n错误:${errorsFlat.join(";")}。请修正。\n${RETRY_HINT}\n只输出 JSON。`;
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
  ? {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "Progmune Runtime — Program Immunology: Constraint-Guided Program Synthesis Runtime",
5
5
  "main": "dist/mcp-server.mjs",
6
6
  "bin": {